angular .module("anonymous-github", [ "ngRoute", "ngSanitize", "ui.ace", "ngPDFViewer", "pascalprecht.translate", "admin", ]) .config([ "$routeProvider", "$locationProvider", "$translateProvider", function ($routeProvider, $locationProvider, $translateProvider) { $translateProvider.useStaticFilesLoader({ prefix: "/i18n/locale-", suffix: ".json", }); $translateProvider.preferredLanguage("en"); $routeProvider .when("/", { templateUrl: "/partials/home.htm", controller: "homeController", title: "Anonymous GitHub – Share the code, not the author", }) .when("/dashboard", { templateUrl: "/partials/dashboard.htm", controller: "unifiedDashboardController", title: "Your anonymizations – Anonymous GitHub", }) .when("/pr-dashboard", { redirectTo: "/dashboard", }) .when("/anonymize/:repoId?", { templateUrl: "/partials/anonymize.htm", controller: "anonymizeController", title: "New anonymization – Anonymous GitHub", }) .when("/pull-request-anonymize/:pullRequestId?", { templateUrl: "/partials/anonymize.htm", controller: "anonymizeController", title: "Anonymize a pull request – Anonymous GitHub", }) .when("/gist-anonymize/:gistId?", { templateUrl: "/partials/anonymize.htm", controller: "anonymizeController", title: "Anonymize a gist – Anonymous GitHub", }) .when("/status/:repoId", { templateUrl: "/partials/status.htm", controller: "statusController", title: "Repository status – Anonymous GitHub", }) .when("/conferences", { templateUrl: "/partials/conferences.htm", controller: "conferencesController", title: "Your conferences – Anonymous GitHub", }) .when("/conference/new", { templateUrl: "/partials/newConference.htm", controller: "newConferenceController", title: "New conference – Anonymous GitHub", }) .when("/conference/:conferenceId/edit", { templateUrl: "/partials/newConference.htm", controller: "newConferenceController", title: "Edit conference – Anonymous GitHub", }) .when("/conference/:conferenceId", { templateUrl: "/partials/conference.htm", controller: "conferenceController", title: "Conference – Anonymous GitHub", }) .when("/faq", { templateUrl: "/partials/faq.htm", controller: "faqController", title: "FAQ – Anonymous GitHub", }) .when("/profile", { templateUrl: "/partials/profile.htm", controller: "profileController", title: "Your settings – Anonymous GitHub", }) .when("/claim", { templateUrl: "/partials/claim.htm", controller: "claimController", title: "Claim an anonymization – Anonymous GitHub", }) .when("/pr/:pullRequestId", { templateUrl: "/partials/pullRequest.htm", controller: "pullRequestController", title: "Anonymous pull request – Anonymous GitHub", reloadOnUrl: false, }) .when("/gist/:gistId", { templateUrl: "/partials/gist.htm", controller: "gistController", title: "Anonymous gist – Anonymous GitHub", reloadOnUrl: false, }) .when("/r/:repoId/:path*?", { templateUrl: "/partials/explorer.htm", controller: "exploreController", title: "Anonymous repository – Anonymous GitHub", reloadOnUrl: false, }) .when("/repository/:repoId/:path*?", { templateUrl: "/partials/explorer.htm", controller: "exploreController", title: "Anonymous repository – Anonymous GitHub", reloadOnUrl: false, }) .when("/admin/", { templateUrl: "/partials/admin/overview.htm", controller: "overviewAdminController", title: "Admin · Overview – Anonymous GitHub", }) .when("/admin/repositories", { templateUrl: "/partials/admin/repositories.htm", controller: "repositoriesAdminController", title: "Admin · Repositories – Anonymous GitHub", }) .when("/admin/users", { templateUrl: "/partials/admin/users.htm", controller: "usersAdminController", title: "Admin · Users – Anonymous GitHub", }) .when("/admin/users/:username", { templateUrl: "/partials/admin/user.htm", controller: "userAdminController", title: "Admin · User details – Anonymous GitHub", }) .when("/admin/conferences", { templateUrl: "/partials/admin/conferences.htm", controller: "conferencesAdminController", title: "Admin · Conferences – Anonymous GitHub", }) .when("/admin/queues", { templateUrl: "/partials/admin/queues.htm", controller: "queuesAdminController", title: "Admin · Queues – Anonymous GitHub", }) .when("/admin/errors", { templateUrl: "/partials/admin/errors.htm", controller: "errorsAdminController", title: "Admin · Errors – Anonymous GitHub", }) .when("/404", { templateUrl: "/partials/404.htm", title: "Page not found – Anonymous GitHub", }) .otherwise({ templateUrl: "/partials/404.htm", title: "Page not found – Anonymous GitHub", }); $locationProvider.html5Mode(true); }, ]) .filter("humanFileSize", function () { return humanFileSize; }) .filter("bigNum", function () { return function bigNum(v) { const n = Number(v) || 0; const abs = Math.abs(n); if (abs < 1000) return String(n); if (abs < 10000) return (n / 1000).toFixed(1).replace(/\.0$/, "") + "k"; if (abs < 1000000) return Math.round(n / 1000) + "k"; if (abs < 10000000) return (n / 1000000).toFixed(1).replace(/\.0$/, "") + "M"; return Math.round(n / 1000000) + "M"; }; }) .filter("humanTime", function () { return function humanTime(seconds) { if (!seconds) { return "never"; } if (seconds instanceof Date) seconds = Math.round((Date.now() - seconds) / 1000); if (typeof seconds == "string" || typeof seconds == "number") seconds = Math.round((Date.now() - new Date(seconds)) / 1000); var suffix = seconds < 0 ? "from now" : "ago"; // more than 2 days ago display Date if (Math.abs(seconds) > 2 * 60 * 60 * 24) { const now = new Date(); now.setSeconds(now.getSeconds() - seconds); return "on " + now.toLocaleDateString(); } seconds = Math.abs(seconds); var times = [ seconds / 60 / 60 / 24 / 365, // years seconds / 60 / 60 / 24 / 30, // months seconds / 60 / 60 / 24 / 7, // weeks seconds / 60 / 60 / 24, // days seconds / 60 / 60, // hours seconds / 60, // minutes seconds, // seconds ]; var names = ["year", "month", "week", "day", "hour", "minute", "second"]; for (var i = 0; i < names.length; i++) { var time = Math.floor(times[i]); var name = names[i]; if (time > 1) name += "s"; if (time >= 1) return time + " " + name + " " + suffix; } return "0 seconds " + suffix; }; }) .filter("title", function () { return function (str) { if (!str) return str; str = str.toLowerCase(); var words = str.split(" "); var capitalized = words.map(function (word) { return word.charAt(0).toUpperCase() + word.substring(1, word.length); }); return capitalized.join(" "); }; }) .filter("statusMsg", function () { return function (msg) { if (!msg) return msg; var m = msg.match(/^rate_limited:(\d+)$/); if (m) { var remaining = Math.max(0, Math.ceil((parseInt(m[1], 10) - Date.now()) / 1000)); if (remaining <= 0) return "Rate limited — resuming soon"; var min = Math.floor(remaining / 60); var sec = remaining % 60; return "Rate limited — retrying in " + (min > 0 ? min + "m " + sec + "s" : sec + "s"); } return msg; }; }) .filter("diff", [ "$sce", function ($sce) { const esc = (s) => s.replace(/&/g, "&").replace(//g, ">"); function flushFile(out, file) { if (!file) return; const headerName = file.newPath && file.newPath !== "/dev/null" ? file.newPath : file.oldPath || ""; const status = file.oldPath === "/dev/null" ? "added" : file.newPath === "/dev/null" ? "deleted" : file.oldPath && file.newPath && file.oldPath !== file.newPath ? "renamed" : "modified"; out.push('
'); out.push( '
' + '' + esc(headerName) + "" + '' + status + "
" ); if (file.lines.length) { out.push(''); for (const line of file.lines) { out.push( '' + '" + '" + '" + '" + "" ); } out.push("
' + (line.oldNo || "") + "' + (line.newNo || "") + "' + (line.kind === "add" ? "+" : line.kind === "remove" ? "-" : line.kind === "hunk" ? "@" : "") + "' + esc(line.text) + "
"); } out.push("
"); } return function (str) { if (!str) return str; const out = []; let file = null; let oldNo = 0; let newNo = 0; const ensureFile = () => { if (!file) file = { oldPath: "", newPath: "", lines: [] }; return file; }; const startNewFileIfNeeded = () => { if (file && (file.lines.length || file.oldPath || file.newPath)) { flushFile(out, file); file = null; } }; const lines = str.split("\n"); for (let i = 0; i < lines.length; i++) { const ln = lines[i]; if (ln.startsWith("diff --git")) { startNewFileIfNeeded(); ensureFile(); continue; } if (ln.startsWith("--- ")) { // New file boundary if the previous file already had lines. if (file && file.lines.length) startNewFileIfNeeded(); ensureFile().oldPath = ln.replace(/^--- (a\/)?/, "").trim(); continue; } if (ln.startsWith("+++ ")) { ensureFile().newPath = ln.replace(/^\+\+\+ (b\/)?/, "").trim(); continue; } if ( ln.startsWith("index ") || ln.startsWith("similarity index") || ln.startsWith("rename ") || ln.startsWith("new file mode") || ln.startsWith("deleted file mode") || ln.startsWith("Binary files") ) { continue; } if (ln.startsWith("@@")) { const m = ln.match(/@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/); if (m) { oldNo = parseInt(m[1], 10); newNo = parseInt(m[2], 10); } ensureFile().lines.push({ kind: "hunk", oldNo: "", newNo: "", text: ln }); continue; } if (!file) continue; if (ln.startsWith("+")) { file.lines.push({ kind: "add", oldNo: "", newNo: newNo, text: ln.slice(1) }); newNo++; } else if (ln.startsWith("-")) { file.lines.push({ kind: "remove", oldNo: oldNo, newNo: "", text: ln.slice(1) }); oldNo++; } else { file.lines.push({ kind: "ctx", oldNo: oldNo, newNo: newNo, text: ln.startsWith(" ") ? ln.slice(1) : ln }); oldNo++; newNo++; } } flushFile(out, file); return $sce.trustAsHtml(out.join("")); }; }, ]) .directive("gistFile", [ "$location", "$timeout", "$sce", function ($location, $timeout, $sce) { // Map GitHub `language` and file extensions to Prism aliases. Prism // only ships a handful of grammars (js/py/r/julia/markup); unknown // classes still render as readable
.
      const langAliases = {
        javascript: "javascript",
        js: "javascript",
        typescript: "javascript",
        ts: "javascript",
        jsx: "javascript",
        tsx: "javascript",
        python: "python",
        py: "python",
        ipynb: "json",
        r: "r",
        julia: "julia",
        html: "markup",
        xml: "markup",
        svg: "markup",
        json: "json",
        yaml: "yaml",
        yml: "yaml",
        bash: "bash",
        sh: "bash",
        shell: "bash",
        css: "css",
        scss: "css",
        c: "c",
        "c++": "cpp",
        cpp: "cpp",
        java: "java",
        go: "go",
        rust: "rust",
        ruby: "ruby",
        php: "php",
        sql: "sql",
        diff: "diff",
      };
      function ext(filename) {
        const i = (filename || "").lastIndexOf(".");
        return i < 0 ? "" : filename.slice(i + 1).toLowerCase();
      }
      function langFor(file) {
        const fromLang =
          file && file.language && langAliases[file.language.toLowerCase()];
        if (fromLang) return fromLang;
        const fromExt = langAliases[ext(file && file.filename)];
        return fromExt || "none";
      }
      function kind(file) {
        const e = ext(file && file.filename);
        if (e === "md" || e === "markdown" || (file && file.language === "Markdown"))
          return "md";
        return "code";
      }
      return {
        restrict: "E",
        scope: { file: "=", terms: "=", options: "=" },
        template:
          '
' + '
', link: function (scope, elem) { function update() { if (!scope.file) return; scope.kind = kind(scope.file); scope.prismClass = "language-" + langFor(scope.file); // Re-run Prism after the new lands in the DOM. $timeout(() => { const codes = elem[0].querySelectorAll("pre code"); codes.forEach((c) => { if (window.Prism) Prism.highlightElement(c); }); }, 50); } scope.$watch("file", update); scope.$watch("file.content", update); scope.$watch("terms", update); scope.$watch("options", update, true); }, }; }, ]) .directive("markdown", [ "$location", function ($location) { return { restrict: "E", scope: { terms: "=", options: "=", content: "=", }, link: function (scope, elem, attrs) { function update() { elem.html(renderMD(scope.content, $location.url() + "/../")); } scope.$watch(attrs.terms, update); scope.$watch("terms", update); scope.$watch("options", update); scope.$watch("content", update); }, }; }, ]) .directive("tree", [ function () { return { restrict: "E", scope: { file: "=", parent: "@", searchQuery: "=", searchResults: "=" }, controller: [ "$element", "$scope", "$routeParams", "$compile", function ($element, $scope, $routeParams, $compile) { $scope.repoId = document.location.pathname.split("/")[2]; $scope.opens = {}; if ($routeParams.path) { let accumulatedPath = ""; $routeParams.path.split("/").forEach((f) => { $scope.opens[accumulatedPath + "/" + f] = true; accumulatedPath = accumulatedPath + "/" + f; }); } const toArray = function (arr) { const output = []; const keys = { "": { child: output } }; function ensurePath(path) { if (keys[path]) return; const segments = path.split("/"); let acc = ""; for (let i = 0; i < segments.length; i++) { const parent = acc; acc = acc ? acc + "/" + segments[i] : segments[i]; if (!keys[acc]) { const dir = { name: segments[i], child: [] }; keys[acc] = dir; keys[parent].child.push(dir); } } } for (let file of arr) { if (file.path && !keys[file.path]) { ensurePath(file.path); } let current = keys[file.path || ""].child; let fPath = `${file.path}/${file.name}`; if (fPath.startsWith("/")) { fPath = fPath.substring(1); } if (file.size != null) { current.push({ name: file.name, size: file.size, sha: file.sha, }); } else { if (!keys[fPath]) { const dir = { name: file.name, child: [], }; keys[fPath] = dir; current.push(dir); } } } return output; }; const sortFiles = (f1, f2) => { const f1d = !!f1.child; const f2d = !!f2.child; if (f1d && f2d) { return f1.name.localeCompare(f2.name); } if (f1d) { return -1; } if (f2d) { return 1; } return f1.name.localeCompare(f2.name); }; function getFileCount(folderPath) { const counts = $scope.$parent.fileCounts; if (!counts) return 0; const normalized = folderPath.startsWith("/") ? folderPath.substring(1) : folderPath; return counts[normalized] || 0; } function isTruncated(folderPath) { const truncated = ($scope.$parent.options && $scope.$parent.options.truncatedFolders) || []; if (!truncated.length) return false; const normalized = folderPath.startsWith("/") ? folderPath.substring(1) : folderPath; return truncated.indexOf(normalized) !== -1; } function escapeHtml(str) { return str.replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } function buildSearchFilter() { const results = $scope.searchResults; if (!results || !results.length) return null; const matchPaths = new Set(); const matchFolders = new Set(); for (const f of results) { const full = f.path ? `${f.path}/${f.name}` : f.name; matchPaths.add(full); // Also collect all ancestor folders if (f.path) { const segments = f.path.split("/").filter(Boolean); let acc = ""; for (const seg of segments) { acc = acc ? `${acc}/${seg}` : seg; matchFolders.add(acc); } } } return { paths: matchPaths, folders: matchFolders }; } function nodeMatchesFilter(node, parentPath, filterSet) { if (!filterSet) return true; const path = parentPath ? `${parentPath}/${node.name}` : node.name; if (!node.child) { return filterSet.paths.has(path); } // Show folder if it's an ancestor of a match or contains matches if (filterSet.folders.has(path)) return true; return node.child.some((c) => nodeMatchesFilter(c, path, filterSet) ); } function generate(current, parentPath, filterSet) { if (!current) return ""; current = current.sort(sortFiles); let output = "
    "; for (let f of current) { if (filterSet && !nodeMatchesFilter(f, parentPath ? parentPath.substring(1) : "", filterSet)) { continue; } let dir = !!f.child; let name = f.name; let size = f.size; let collapsed = f; if (dir) { let test = name; let inner = f.child; while (inner && inner.length == 1) { test += "/" + inner[0].name; size = inner[0].size; inner = inner[0].child; } name = test; collapsed = inner ? { child: inner } : f; if (size != null && size >= 0) { dir = false; } } const sizeTitle = size != null ? `Size: ${humanFileSize(size || 0)}` : ""; const path = `${parentPath}/${name}`; const fileCount = dir ? getFileCount(path) : 0; const isOpen = filterSet ? ($scope.opens[path] !== false) : $scope.opens[path]; const cssClasses = ["file"]; if (dir) { cssClasses.push("folder"); } if (isOpen) { cssClasses.push("open"); } if ($scope.isActive(path)) { cssClasses.push("active"); } const truncated = dir && isTruncated(path); if (truncated) { cssClasses.push("truncated"); } output += `
  • `; if (dir) { output += `${escapeHtml(name)}`; if (truncated) { output += ``; } if (fileCount > 0) { output += `${fileCount}`; } output += ``; } else { const needsSpacer = parentPath !== ""; output += `${needsSpacer ? '' : ''}${escapeHtml(name)}`; } if (isOpen && collapsed.child) { const children = collapsed.child; if (children.length > 1) { output += generate(children, path, filterSet); } else if (dir) { let inner = children; while (inner && inner.length == 1) { inner = inner[0].child; } output += generate(inner, path, filterSet); } } output += "
  • "; } return output + "
"; } function display() { $element.html(""); const filterSet = $scope.searchQuery ? buildSearchFilter() : null; let output; if (filterSet !== null && filterSet.paths.size === 0) { output = '
No files found
'; } else { output = generate(toArray($scope.file).sort(sortFiles), "", filterSet); } $compile(output)($scope, (clone) => { $element.append(clone); restoreFocus(); }); } function expandAllFolders(nodes, parentPath) { if (!nodes) return; for (const f of nodes) { if (!f.child || f.child.length === 0) continue; const path = `${parentPath}/${f.name}`; if (!(path in $scope.opens)) { $scope.opens[path] = true; } expandAllFolders(f.child, path); } } $scope.$watch( "file", (newValue) => { if (newValue == null) return; if (newValue.length == 0) { return $element.html("Empty repository"); } expandAllFolders(toArray(newValue), ""); display(); }, true ); $scope.$watch("searchResults", (newVal, oldVal) => { if (newVal === oldVal) return; if ($scope.file && $scope.file.length) { display(); } }); $scope.$watch("searchQuery", (newVal, oldVal) => { if (newVal === oldVal) return; if (!newVal && $scope.file && $scope.file.length) { display(); } }); $scope.isActive = function (name) { return $routeParams.path == name.substring(1); }; $scope.openFolder = function (folder, event) { var currentlyOpen = $scope.opens[folder]; if (currentlyOpen === undefined && $scope.searchQuery) { currentlyOpen = true; } $scope.opens[folder] = !currentlyOpen; const li = event.target.closest("li"); const childUl = li ? li.querySelector(":scope > ul") : null; const needsLoad = childUl == null || childUl.children.length === 0; if (needsLoad) { $scope.$parent.getFiles(folder.substring(1)); } }; var focusedPath = $routeParams.path ? "/" + $routeParams.path : null; function getVisibleLinks() { return Array.from($element[0].querySelectorAll("li > a")); } function getFocusedLink() { return $element[0].querySelector("a.tree-focused"); } function getLinkPath(link) { if (!link) return null; var href = link.getAttribute("href"); if (href) { var prefix = "/r/" + $scope.repoId; return href.indexOf(prefix) === 0 ? decodeURIComponent(href.substring(prefix.length)) : null; } var onclick = link.getAttribute("ng-click"); if (onclick) { var m = onclick.match(/openFolder\('([^']+)'/); return m ? m[1] : null; } return null; } function findLinkByPath(path) { if (!path) return null; var links = getVisibleLinks(); for (var i = 0; i < links.length; i++) { if (getLinkPath(links[i]) === path) return links[i]; } return null; } function setFocus(link) { var prev = getFocusedLink(); if (prev) prev.classList.remove("tree-focused"); if (link) { link.classList.add("tree-focused"); link.scrollIntoView({ block: "nearest" }); focusedPath = getLinkPath(link); } else { focusedPath = null; } } function restoreFocus() { if (!focusedPath) return; var link = findLinkByPath(focusedPath); if (link) { link.classList.add("tree-focused"); $element[0].focus(); } } $element[0].setAttribute("tabindex", "0"); $element[0].addEventListener("keydown", function (e) { var links = getVisibleLinks(); if (!links.length) return; var focused = getFocusedLink(); var idx = focused ? links.indexOf(focused) : -1; if (e.key === "ArrowDown") { e.preventDefault(); var next = idx < links.length - 1 ? idx + 1 : 0; setFocus(links[next]); } else if (e.key === "ArrowUp") { e.preventDefault(); var prev = idx > 0 ? idx - 1 : links.length - 1; setFocus(links[prev]); } else if (e.key === "ArrowRight") { e.preventDefault(); if (!focused) return; var li = focused.closest("li"); if (li && li.classList.contains("folder")) { if (!li.classList.contains("open")) { focused.click(); } else { var childLink = li.querySelector(":scope > ul > li > a"); if (childLink) setFocus(childLink); } } } else if (e.key === "ArrowLeft") { e.preventDefault(); if (!focused) return; var li = focused.closest("li"); if (li && li.classList.contains("folder") && li.classList.contains("open")) { focused.click(); } else { var parentLi = li && li.parentElement ? li.parentElement.closest("li.folder") : null; if (parentLi) { var parentLink = parentLi.querySelector(":scope > a"); if (parentLink) setFocus(parentLink); } } } else if (e.key === "Enter") { e.preventDefault(); if (focused) focused.click(); } }); $element[0].addEventListener("click", function (e) { var link = e.target.closest("a"); if (link && $element[0].contains(link)) { setFocus(link); } }); }, ], }; }, ]) .directive("notebook", [ function () { return { restrict: "E", scope: { file: "=" }, controller: [ "$element", "$scope", "$http", function ($element, $scope, $http) { function renderNotebookJSON(json) { const notebook = nb.parse(json); try { $element.html(""); $element.append(notebook.render()); Prism.highlightAll(); } catch (error) { $element.html("Unable to render the notebook."); } } function render() { if ($scope.$parent.content) { try { renderNotebookJSON(JSON.parse($scope.$parent.content)); } catch (error) { $element.html( "Unable to render the notebook invalid notebook format." ); } } else if ($scope.file) { $http .get($scope.file.download_url) .then((res) => renderNotebookJSON(res.data)); } } $scope.$watch("file", (v) => { render(); }); render(); }, ], }; }, ]) .directive("loc", [ function () { return { restrict: "E", scope: { stats: "=" }, template: "
", controller: [ "$scope", function ($scope) { function render() { $scope.elements = []; $scope.total = 0; for (let lang in $scope.stats) { const loc = $scope.stats[lang].code; if (!loc) { continue; } $scope.total += loc; $scope.elements.push({ lang, loc, color: langColors[lang], }); } setTimeout(() => { $('[data-toggle="tooltip"]').tooltip(); }, 100); } $scope.$watch("stats", (v) => { render(); }); render(); }, ], }; }, ]) .controller("mainController", [ "$scope", "$http", "$location", "$timeout", function ($scope, $http, $location, $timeout) { $scope.title = "Main"; $scope.user = { status: "connection" }; $scope.site_options; $scope.toasts = []; $scope.removeToast = function (toast) { const index = $scope.toasts.indexOf(toast); if (index === -1) return; $scope.toasts.splice(index, 1); }; // Auto-dismiss toasts after a fixed delay so they don't pile up across // navigations (e.g. the "README not found" toast re-fired every time the // edit screen was reopened — see #246). Long-running operations that // mutate the toast (remove/refresh) will simply disappear once the // delay elapses; users can re-check status from the dashboard. $scope.addToast = function (toast) { $scope.toasts.push(toast); $timeout(function () { $scope.removeToast(toast); }, 8000); return toast; }; $scope.path = $location.url(); $scope.paths = $location.path().substring(1).split("/"); $scope.darkMode = function (on) { localStorage.setItem("darkMode", on); $scope.isDarkMode = on; const darkPrismLink = "/css/prism-okaidia.css"; const lightPrismLink = "/css/prism.css"; if (on) { $("body").addClass("dark-mode"); let link = document.createElement("link"); link.href = darkPrismLink; link.rel = "stylesheet"; document.head.append(link); $(`link[href='${lightPrismLink}']`).remove(); } else { $("body").removeClass("dark-mode"); let link = document.createElement("link"); link.href = lightPrismLink; link.rel = "stylesheet"; document.head.append(link); $(`link[href='${darkPrismLink}']`).remove(); } // Update Ko-fi floating button to match theme var kofiBtn = document.querySelector(".floatingchat-container-wrap-mo498 .floating-chat-kofi-text-container-wrap"); if (kofiBtn) { kofiBtn.style.backgroundColor = on ? "#FAF9F6" : "#1A1815"; kofiBtn.style.color = on ? "#1A1815" : "#FAF9F6"; } $scope.$broadcast("dark-mode", on); }; $scope.darkMode(localStorage.getItem("darkMode") == "true"); function getUser() { $http.get("/api/user").then( (res) => { if (res) $scope.user = res.data; }, () => { $scope.user = null; } ); } getUser(); function getOptions() { $http.get("/api/options").then( (res) => { if (res) $scope.site_options = res.data; }, () => { $scope.site_options = null; } ); } getOptions(); function getMessage() { $http.get("/api/message").then( (res) => { if (res) $scope.generalMessage = res.data; }, () => { $scope.generalMessage = null; } ); } getMessage(); function changedUrl(_, current) { if (current) { $scope.title = current.title; } $scope.path = $location.url(); $scope.paths = $location.path().substring(1).split("/"); } $scope.$on("$routeChangeSuccess", changedUrl); $scope.$on("$routeUpdate", changedUrl); }, ]) .controller("faqController", ["$scope", "$http", function ($scope, $http) {}]) .controller("profileController", [ "$scope", "$http", function ($scope, $http) { $scope.terms = ""; $scope.options = { expirationMode: "remove", update: false, image: true, pdf: true, notebook: true, loc: true, link: true, }; function getDefault() { $http.get("/api/user/default").then((res) => { const data = res.data; if (data.terms) { $scope.terms = data.terms.join("\n"); } $scope.option = Object.assign({}, $scope.option, data.options); }); } getDefault(); $scope.saveDefault = () => { const params = { terms: $scope.terms.trim().split("\n"), options: $scope.options, }; $http.post("/api/user/default", params).then( () => { getDefault(); $scope.message = "Saved"; }, (error) => { $translate("ERRORS." + error.data.error).then((translation) => { $scope.error = translation; }, console.error); } ); }; }, ]) .controller("claimController", [ "$scope", "$http", "$location", function ($scope, $http, $location) { $scope.repoId = null; $scope.repoUrl = null; $scope.claim = () => { $http .post("/api/repo/claim", { repoId: $scope.repoId, repoUrl: $scope.repoUrl, }) .then( (res) => { $location.url("/dashboard"); }, (err) => { $scope.error = err.data; $scope.claimForm.repoUrl.$setValidity("not_found", false); $scope.claimForm.repoId.$setValidity("not_found", false); } ); }; }, ]) .controller("homeController", [ "$scope", "$http", "$location", function ($scope, $http, $location) { if ($scope.user && !$scope.user.status) { $location.url("/dashboard"); } $scope.$watch("user.status", () => { if ($scope.user && !$scope.user.status) { $location.url("/dashboard"); } }); $scope.cards = [ { key: "repositories", total: 0, label: "repositories anonymized" }, { key: "users", total: 0, label: "researchers" }, { key: "pageViews", total: 0, label: "page views" }, { key: "pullRequests", total: 0, label: "pull requests" }, ]; function getStat() { $http.get("/api/stat/").then((res) => { $scope.stat = res.data; $scope.cards[0].total = res.data.nbRepositories; $scope.cards[1].total = res.data.nbUsers; $scope.cards[2].total = res.data.nbPageViews; $scope.cards[3].total = res.data.nbPullRequests; }); } getStat(); function buildSeriesView(series) { const view = { series: series, bars: [], viewW: 100, deltaToday: 0, pctChange: 0, pctAbs: 0, isUp: true, }; if (!series || series.length < 2) return view; // Bars represent the *daily increment* (today - yesterday), not the // cumulative total. The big number above the chart shows the total. const deltas = new Array(series.length - 1); for (let i = 1; i < series.length; i++) { deltas[i - 1] = series[i] - series[i - 1]; } const n = deltas.length; const max = Math.max.apply(null, deltas); const min = Math.min.apply(null, deltas); // Anchor scale to zero so visually small days look small even when all // deltas are positive; only fall back to min when there are negatives. const base = Math.min(0, min); const range = max - base || 1; view.viewW = n * 2; view.bars = new Array(n); for (let i = 0; i < n; i++) { const norm = (deltas[i] - base) / range; const h = Math.max(1.5, norm * 34); view.bars[i] = { x: (i * 2 + 0.25).toFixed(2), y: (36 - h).toFixed(2), w: "1.5", h: h.toFixed(2), }; } view.deltaToday = deltas[n - 1]; if (n >= 2) { const prior = deltas[n - 2]; if (prior) { view.pctChange = ((view.deltaToday - prior) / prior) * 100; } } view.pctAbs = Math.round(Math.abs(view.pctChange)); view.isUp = view.pctChange >= 0; return view; } $scope.history = { repositories: buildSeriesView([]), users: buildSeriesView([]), pageViews: buildSeriesView([]), pullRequests: buildSeriesView([]), }; $http.get("/api/stat/history?days=60").then((res) => { const rows = res.data || []; $scope.history = { repositories: buildSeriesView(rows.map((r) => r.nbRepositories || 0)), users: buildSeriesView(rows.map((r) => r.nbUsers || 0)), pageViews: buildSeriesView(rows.map((r) => r.nbPageViews || 0)), pullRequests: buildSeriesView(rows.map((r) => r.nbPullRequests || 0)), }; }); }, ]) .controller("unifiedDashboardController", [ "$scope", "$http", "$location", function ($scope, $http, $location) { $scope.$on("$routeChangeStart", function () { $('[data-toggle="tooltip"]').tooltip("dispose"); }); $scope.$watch("user.status", () => { if ($scope.user == null) { $location.url("/"); } }); if ($scope.user == null) { $location.url("/"); } setTimeout(() => { $('[data-toggle="tooltip"]').tooltip(); }, 250); $scope.items = []; $scope.search = ""; const dashboardPrefsKey = "dashboard.filterPrefs"; const dashboardPrefDefaults = { typeFilter: "all", filters: { status: { ready: true, expired: true, removed: false } }, orderBy: "-anonymizeDate", }; const savedDashboardPrefs = loadFilterPrefs(dashboardPrefsKey) || {}; $scope.typeFilter = savedDashboardPrefs.typeFilter || dashboardPrefDefaults.typeFilter; $scope.filters = { status: Object.assign( {}, dashboardPrefDefaults.filters.status, (savedDashboardPrefs.filters && savedDashboardPrefs.filters.status) || {} ), }; $scope.orderBy = savedDashboardPrefs.orderBy || dashboardPrefDefaults.orderBy; $scope.$watchGroup( ["typeFilter", "orderBy"], () => { saveFilterPrefs(dashboardPrefsKey, { typeFilter: $scope.typeFilter, filters: $scope.filters, orderBy: $scope.orderBy, }); } ); $scope.$watch( "filters", () => { saveFilterPrefs(dashboardPrefsKey, { typeFilter: $scope.typeFilter, filters: $scope.filters, orderBy: $scope.orderBy, }); }, true ); function getQuota() { $http.get("/api/user/quota").then((res) => { $scope.quota = res.data; $scope.quota.storage.percent = $scope.quota.storage.total ? ($scope.quota.storage.used * 100) / $scope.quota.storage.total : 100; $scope.quota.file.percent = $scope.quota.file.total ? ($scope.quota.file.used * 100) / $scope.quota.file.total : 100; $scope.quota.repository.percent = $scope.quota.repository.total ? ($scope.quota.repository.used * 100) / $scope.quota.repository.total : 100; }, console.error); } getQuota(); let loadedRepos = null; let loadedPRs = null; let loadedGists = null; function mergeItems() { $scope.items = (loadedRepos || []) .concat(loadedPRs || []) .concat(loadedGists || []); } function loadAll() { loadedRepos = null; loadedPRs = null; loadedGists = null; $http.get("/api/user/anonymized_repositories").then( (res) => { loadedRepos = res.data.map((repo) => { if (!repo.pageView) repo.pageView = 0; if (!repo.lastView) repo.lastView = ""; repo.options.terms = repo.options.terms.filter((f) => f); repo._type = "repo"; repo._id = repo.repoId; repo._name = repo.repoId; repo._source = repo.source.fullName; repo._editUrl = "/anonymize/" + repo.repoId; repo._viewUrl = "/r/" + repo.repoId + "/"; return repo; }); mergeItems(); }, (err) => { console.error(err); } ); $http.get("/api/user/anonymized_pull_requests").then( (res2) => { loadedPRs = res2.data.map((pr) => { if (!pr.pageView) pr.pageView = 0; if (!pr.lastView) pr.lastView = ""; pr.options.terms = pr.options.terms.filter((f) => f); pr._type = "pr"; pr._id = pr.pullRequestId; pr._name = pr.pullRequestId; pr._source = pr.source.repositoryFullName + "#" + pr.source.pullRequestId; pr._editUrl = "/pull-request-anonymize/" + pr.pullRequestId; pr._viewUrl = "/pr/" + pr.pullRequestId + "/"; return pr; }); mergeItems(); }, (err) => { console.error(err); } ); $http.get("/api/user/anonymized_gists").then( (res3) => { loadedGists = res3.data.map((g) => { if (!g.pageView) g.pageView = 0; if (!g.lastView) g.lastView = ""; g.options.terms = (g.options.terms || []).filter((f) => f); g._type = "gist"; g._id = g.gistId; g._name = g.gistId; g._source = g.source.gistId; g._editUrl = "/gist-anonymize/" + g.gistId; g._viewUrl = "/gist/" + g.gistId + "/"; return g; }); mergeItems(); }, (err) => { console.error(err); } ); } loadAll(); function waitRepoToBeReady(repoId, callback) { $http.get("/api/repo/" + repoId).then((res) => { for (const item of $scope.items) { if (item._type === "repo" && item.repoId == repoId) { item.status = res.data.status; break; } } if ( res.data.status == "ready" || res.data.status == "error" || res.data.status == "removed" || res.data.status == "expired" ) { callback(res.data); return; } setTimeout(() => waitRepoToBeReady(repoId, callback), 2500); }); } const labelOf = (t) => t === "repo" ? "repository" : t === "gist" ? "gist" : "pull request"; const apiBaseOf = (t) => t === "repo" ? "/api/repo" : t === "gist" ? "/api/gist" : "/api/pr"; $scope.removeItem = (item) => { const label = labelOf(item._type); if (confirm(`Are you sure that you want to remove the ${label} ${item._id}?`)) { const toast = { title: `Removing ${item._id}...`, date: new Date(), body: `The ${label} ${item._id} is going to be removed.`, }; $scope.addToast(toast); const endpoint = `${apiBaseOf(item._type)}/${item._id}`; $http.delete(endpoint).then( () => { if (item._type === "repo") { waitRepoToBeReady(item._id, () => { toast.title = `${item._id} is removed.`; toast.body = `The ${label} ${item._id} is removed.`; $scope.$apply(); }); } else { toast.title = `${item._id} is removed.`; toast.body = `The ${label} ${item._id} is removed.`; loadAll(); } }, (error) => { toast.title = `Error during the removal of ${item._id}.`; toast.body = error.body; loadAll(); } ); } }; $scope.refreshItem = (item) => { const label = labelOf(item._type); const toast = { title: `Refreshing ${item._id}...`, date: new Date(), body: `The ${label} ${item._id} is going to be refreshed.`, }; $scope.addToast(toast); const endpoint = `${apiBaseOf(item._type)}/${item._id}/refresh`; $http.post(endpoint).then( () => { if (item._type === "repo") { waitRepoToBeReady(item._id, () => { toast.title = `${item._id} is refreshed.`; toast.body = `The ${label} ${item._id} is refreshed.`; $scope.$apply(); }); } else { toast.title = `${item._id} is refreshed.`; toast.body = `The ${label} ${item._id} is refreshed.`; loadAll(); } }, (error) => { toast.title = `Error during the refresh of ${item._id}.`; toast.body = error.body; loadAll(); } ); }; $scope.itemFilter = (item) => { if ($scope.typeFilter !== "all" && item._type !== $scope.typeFilter) return false; if ($scope.filters.status[item.status] == false) return false; if ($scope.search.trim().length == 0) return true; if (item._source && item._source.indexOf($scope.search) > -1) return true; if (item._id.indexOf($scope.search) > -1) return true; return false; }; }, ]) .controller("dashboardController", [ "$scope", "$location", function ($scope, $location) { $location.url("/dashboard"); }, ]) .controller("prDashboardController", [ "$scope", "$location", function ($scope, $location) { $location.url("/dashboard"); }, ]) .controller("statusController", [ "$scope", "$http", "$routeParams", function ($scope, $http, $routeParams) { $scope.repoId = $routeParams.repoId; $scope.repo = null; $scope.progress = 0; $scope.rateLimitResetAt = 0; $scope.rateLimitCountdown = ""; var countdownTimer = null; function startRateLimitCountdown(resetAt) { $scope.rateLimitResetAt = resetAt; if (countdownTimer) clearInterval(countdownTimer); function tick() { var remaining = Math.max(0, Math.ceil((resetAt - Date.now()) / 1000)); if (remaining <= 0) { $scope.rateLimitCountdown = ""; $scope.rateLimitResetAt = 0; clearInterval(countdownTimer); countdownTimer = null; } else { var min = Math.floor(remaining / 60); var sec = remaining % 60; $scope.rateLimitCountdown = min > 0 ? min + "m " + sec + "s" : sec + "s"; } $scope.$applyAsync(); } tick(); countdownTimer = setInterval(tick, 1000); } $scope.$on("$destroy", function () { if (countdownTimer) clearInterval(countdownTimer); }); function parseStatusMessage(msg) { if (!msg) return msg; var m = msg.match(/^rate_limited:(\d+)$/); if (m) { startRateLimitCountdown(parseInt(m[1], 10)); return null; } $scope.rateLimitResetAt = 0; return msg; } $scope.getStatus = () => { $http .get("/api/repo/" + $scope.repoId, { repoId: $scope.repoId, repoUrl: $scope.repoUrl, }) .then( (res) => { $scope.repo = res.data; if (res.data.rateLimitResetAt) { startRateLimitCountdown(res.data.rateLimitResetAt); } else { $scope.repo.statusMessage = parseStatusMessage($scope.repo.statusMessage); } if ($scope.repo.status == "ready") { $scope.progress = 100; } else if ($scope.repo.status == "queue") { $scope.progress = 10; } else if ($scope.repo.status == "downloaded") { $scope.progress = 50; } else if ($scope.repo.status == "download") { $scope.progress = 25; } else if ($scope.repo.status == "preparing") { $scope.progress = 25; } else if ($scope.repo.status == "anonymizing") { $scope.progress = 75; } var shouldPoll = $scope.repo.status != "ready"; if ($scope.repo.status == "error" && !$scope.rateLimitResetAt) { shouldPoll = false; } if (shouldPoll) { setTimeout($scope.getStatus, 2000); } }, (err) => { $scope.error = err.data.error; } ); }; $scope.getStatus(); }, ]) .controller("anonymizeController", [ "$scope", "$http", "$sce", "$routeParams", "$location", "$translate", "$timeout", function ($scope, $http, $sce, $routeParams, $location, $translate, $timeout) { // Unified state $scope.sourceUrl = ""; $scope.detectedType = null; // 'repo' | 'pr' | 'gist' $scope.repoId = ""; $scope.pullRequestId = ""; $scope.gistId = ""; $scope.terms = ""; $scope.defaultTerms = ""; $scope.branches = []; $scope.source = { branch: "", commit: "" }; $scope.options = { expirationMode: "remove", expirationDate: new Date(), update: false, image: true, pdf: true, notebook: true, link: true, body: true, title: true, origin: false, diff: true, content: true, comments: true, username: true, date: true, }; $scope.options.expirationDate.setDate( $scope.options.expirationDate.getDate() + 90 ); $scope.anonymize_readme = ""; $scope.readme = ""; $scope.html_readme = ""; $scope.isUpdate = false; function getDefault(cb) { $http.get("/api/user/default").then((res) => { const data = res.data; if (data.terms) { $scope.defaultTerms = data.terms.join("\n"); } $scope.options = Object.assign({}, $scope.options, data.options); $scope.options.expirationDate = new Date( $scope.options.expirationDate ); $scope.options.expirationDate.setDate( $scope.options.expirationDate.getDate() + 90 ); if (cb) cb(); }); } // Helper to safely set validity on form fields function setValidity(field, key, value) { if ($scope.anonymize && $scope.anonymize[field]) { $scope.anonymize[field].$setValidity(key, value); } } getDefault(() => { // Edit mode: repo if ($routeParams.repoId && $routeParams.repoId != "") { $scope.isUpdate = true; $scope.detectedType = "repo"; $scope.repoId = $routeParams.repoId; $http.get("/api/repo/" + $scope.repoId).then( async (res) => { $scope.sourceUrl = "https://github.com/" + res.data.source.fullName; $scope.terms = res.data.options.terms.filter((f) => f).join("\n"); $scope.source = res.data.source; $scope.role = res.data.role || "owner"; $scope.coauthors = res.data.coauthors || []; // Remember the saved branch so the source.branch watcher knows // not to bump source.commit to GitHub HEAD on edit-page load // (#360). Without this, just opening the Edit form silently // pulled in any new commits and saving — even to toggle a // checkbox — picked them up. $scope._originalBranch = res.data.source.branch; $scope.options = Object.assign({}, $scope.options, res.data.options); $scope.conference = res.data.conference; $scope.repositoryID = res.data.source.repositoryID; if (res.data.options.expirationDate) { $scope.options.expirationDate = new Date(res.data.options.expirationDate); } await Promise.all([getRepoDetails(), getReadme()]); anonymizeReadme(); $scope.$apply(); }, () => { $location.url("/404"); } ); $scope.$watch("anonymize", () => { if ($scope.anonymize.repoId) $scope.anonymize.repoId.$$element[0].disabled = true; if ($scope.anonymize.sourceUrl) $scope.anonymize.sourceUrl.$$element[0].disabled = true; }); } // Edit mode: PR if ($routeParams.pullRequestId && $routeParams.pullRequestId != "") { $scope.isUpdate = true; $scope.detectedType = "pr"; $scope.pullRequestId = $routeParams.pullRequestId; $http.get("/api/pr/" + $scope.pullRequestId).then( async (res) => { $scope.sourceUrl = "https://github.com/" + res.data.source.repositoryFullName + "/pull/" + res.data.source.pullRequestId; $scope.terms = res.data.options.terms.filter((f) => f).join("\n"); $scope.source = res.data.source; $scope.options = Object.assign({}, $scope.options, res.data.options); $scope.conference = res.data.conference; if (res.data.options.expirationDate) { $scope.options.expirationDate = new Date(res.data.options.expirationDate); } try { $scope.details = (await $http.get(`/api/pr/${res.data.source.repositoryFullName}/${res.data.source.pullRequestId}`)).data; } catch (error) { const code = error && error.data && error.data.error; if (code) { $translate("ERRORS." + code).then((translation) => { $scope.addToast({ title: "Error", date: new Date(), body: translation }); $scope.error = translation; }, console.error); displayErrorMessage(code); } } $scope.$apply(); }, () => { $location.url("/404"); } ); $scope.$watch("anonymize", () => { if ($scope.anonymize.pullRequestId) $scope.anonymize.pullRequestId.$$element[0].disabled = true; if ($scope.anonymize.sourceUrl) $scope.anonymize.sourceUrl.$$element[0].disabled = true; }); } // Edit mode: Gist if ($routeParams.gistId && $routeParams.gistId != "") { $scope.isUpdate = true; $scope.detectedType = "gist"; $scope.gistId = $routeParams.gistId; $http.get("/api/gist/" + $scope.gistId).then( async (res) => { $scope.sourceUrl = "https://gist.github.com/" + res.data.source.gistId; $scope.terms = res.data.options.terms.filter((f) => f).join("\n"); $scope.source = res.data.source; $scope.options = Object.assign({}, $scope.options, res.data.options); $scope.conference = res.data.conference; if (res.data.options.expirationDate) { $scope.options.expirationDate = new Date(res.data.options.expirationDate); } $scope.details = (await $http.get(`/api/gist/source/${res.data.source.gistId}`)).data; $scope.$apply(); }, () => { $location.url("/404"); } ); $scope.$watch("anonymize", () => { if ($scope.anonymize.gistId) $scope.anonymize.gistId.$$element[0].disabled = true; if ($scope.anonymize.sourceUrl) $scope.anonymize.sourceUrl.$$element[0].disabled = true; }); } }); // URL change handler - auto-detect type $scope.urlSelected = async () => { $scope.terms = $scope.defaultTerms; $scope.repoId = ""; $scope.pullRequestId = ""; $scope.gistId = ""; $scope.details = null; $scope.branches = []; $scope.source = { type: "GitHubStream", branch: "", commit: "" }; $scope.anonymize_readme = ""; $scope.readme = ""; $scope.html_readme = ""; $scope.detectedType = null; let o; try { o = parseGithubUrl($scope.sourceUrl); } catch (error) { setValidity("sourceUrl", "github", false); return; } setValidity("sourceUrl", "github", true); try { if (o.gistId && !o.repo) { $scope.detectedType = "gist"; $scope.source = { gistId: o.gistId }; await getGistDetails(); } else if (o.pullRequestId) { $scope.detectedType = "pr"; $scope.source = { repositoryFullName: o.owner + "/" + o.repo, pullRequestId: o.pullRequestId }; await getPrDetails(); } else { $scope.detectedType = "repo"; await Promise.all([getRepoDetails(), getReadme()]); anonymizeReadme(); } } catch (error) { return; } $scope.$apply(); $('[data-toggle="tooltip"]').tooltip(); }; $('[data-toggle="tooltip"]').tooltip(); // ========== REPO LOGIC ========== $scope.$watch("options.update", (v) => { if ($scope.detectedType !== "repo") return; if ($scope.anonymize && $scope.anonymize.commit) { $scope.anonymize.commit.$$element[0].disabled = !!v; } }); $scope.$watch("source.branch", async () => { if ($scope.detectedType !== "repo") return; const selected = $scope.branches.filter((f) => f.name == $scope.source.branch)[0]; if (!selected) return; // In update mode, preserve the saved commit while the branch is // unchanged — see #360. Saving the form (e.g. to turn off // auto-update) used to bump the commit to GitHub HEAD because this // watcher overwrote it on edit-page load. const keepSavedCommit = $scope.isUpdate && $scope._originalBranch === $scope.source.branch && !!$scope.source.commit; if (!keepSavedCommit) { $scope.source.commit = selected.commit; } $scope.readme = selected.readme; await getReadme(); anonymizeReadme(); $scope.$apply(); }); $scope.getBranches = async (force) => { const o = parseGithubUrl($scope.sourceUrl); try { const branches = await $http.get(`/api/repo/${o.owner}/${o.repo}/branches`, { params: { force: force === true ? "1" : "0", repositoryID: $scope.repositoryID }, }); $scope.branches = branches.data; $scope.sourceUnreachable = false; if (!$scope.source.branch) { $scope.source.branch = $scope.details.defaultBranch; } const selected = $scope.branches.filter((b) => b.name == $scope.source.branch); if (selected.length > 0) { // When the user explicitly clicks refresh (force=true), always // update the commit to the latest on the branch. Only preserve // the saved commit on the initial edit-page load (#360). const keepSavedCommit = !force && $scope.isUpdate && !$scope.options.update && $scope._originalBranch === $scope.source.branch && !!$scope.source.commit; if (!keepSavedCommit) { $scope.source.commit = selected[0].commit; } $scope.readme = selected[0].readme; await getReadme(force); } } catch (error) { $scope.branches = []; $scope.sourceUnreachable = error && (error.status === 404 || (error.data && error.data.error === "repo_not_found")); const code = (error && error.data && error.data.error) || (error && error.status === 404 ? "repo_not_found" : "unknown_error"); $translate("ERRORS." + code).then((translation) => { $scope.toasts = $scope.toasts || []; $scope.addToast({ title: "Error", date: new Date(), body: translation }); $scope.error = translation; }, console.error); if (typeof setValidity === "function") { setValidity("sourceUrl", "missing", false); } } $scope.$apply(); }; async function getRepoDetails() { const o = parseGithubUrl($scope.sourceUrl); try { resetValidity(); // force=1 so newly enabled features (e.g. GitHub Pages — see // #364) are reflected without waiting for the cached metadata to // expire. The endpoint hits the GitHub API once. const res = await $http.get(`/api/repo/${o.owner}/${o.repo}/`, { params: { repositoryID: $scope.repositoryID, force: "1" }, }); $scope.details = res.data; if (!$scope.repoId) { $scope.repoId = $scope.details.repo + "-" + generateRandomId(4); } await $scope.getBranches(); } catch (error) { if (error.data) { $translate("ERRORS." + error.data.error).then((translation) => { $scope.addToast({ title: "Error", date: new Date(), body: translation }); $scope.error = translation; }, console.error); displayErrorMessage(error.data.error); } setValidity("sourceUrl", "missing", false); throw error; } } async function getReadme(force) { if ($scope.readme && !force) return $scope.readme; const o = parseGithubUrl($scope.sourceUrl); try { const res = await $http.get(`/api/repo/${o.owner}/${o.repo}/readme`, { params: { force: force === true ? "1" : "0", branch: $scope.source.branch, repositoryID: $scope.repositoryID }, }); $scope.readme = res.data; } catch (error) { $scope.readme = ""; } } // Both anonymizeReadme() and anonymizePrContent() used to reimplement // ContentAnonimizer client-side, which drifted from the backend (term // boundary fixes, accent matching, custom replacements all only landed // in the server). Send the snippets to /api/anonymize-preview instead so // the preview matches what reviewers see byte-for-byte. Calls are // debounced and the in-flight request is dropped on the next change so // typing in the form stays responsive. function previewOptions() { const opts = { terms: $scope.terms ? $scope.terms.split("\n") : [], image: !!$scope.options.image, link: !!$scope.options.link, repoId: $scope.repoId, }; if ($scope.source && $scope.source.branch) { opts.branchName = $scope.source.branch; } try { const o = parseGithubUrl($scope.sourceUrl); opts.repoName = `${o.owner}/${o.repo}`; } catch (_) { /* sourceUrl not yet parseable */ } return opts; } // Single-flight + debounced wrapper. Returns a promise that resolves // with the latest server result; intermediate calls are coalesced. function makePreviewBatcher(buildBody, applyResult) { let pendingTimer = null; let inflightToken = 0; return function schedule() { if (pendingTimer) $timeout.cancel(pendingTimer); pendingTimer = $timeout(() => { pendingTimer = null; const myToken = ++inflightToken; const body = buildBody(); if (!body) return; $http.post("/api/anonymize-preview", body).then( (res) => { if (myToken !== inflightToken) return; // stale applyResult(res.data); }, () => { /* ignore preview errors; no UI feedback needed */ } ); }, 200); }; } const scheduleReadmePreview = makePreviewBatcher( () => { if (!$scope.readme) return null; return { content: $scope.readme, options: previewOptions() }; }, (data) => { $scope.anonymize_readme = data.content || ""; let baseUrl = ""; try { const o = parseGithubUrl($scope.sourceUrl); // Fall back to the repo's default branch when source.branch // hasn't loaded yet — without this, relative // resolved against a baseUrl like ".../raw//" (no branch // segment), so the browser fetched ".../raw/X" and 404'd // (#407). const branch = $scope.source.branch || ($scope.details && $scope.details.defaultBranch) || "main"; baseUrl = `https://github.com/${o.owner}/${o.repo}/raw/${branch}/`; } catch (_) { /* fall through with empty base */ } const html = renderMD($scope.anonymize_readme, baseUrl); $scope.html_readme = $sce.trustAsHtml(html); $timeout(Prism.highlightAll, 150); } ); function anonymizeReadme() { if (!$scope.anonymize || !$scope.anonymize.terms) return; // The "regex characters detected" hint is informational, not a blocker // — IP addresses, escaped chars, etc. are all legitimate terms (#430). $scope.termsRegexWarning = !!$scope.terms && !!$scope.terms.match(/[-[\]{}()*+?.,\\^$|#]/g); scheduleReadmePreview(); } // ========== PR LOGIC ========== async function getPrDetails() { const o = parseGithubUrl($scope.sourceUrl); try { resetValidity(); const res = await $http.get(`/api/pr/${o.owner}/${o.repo}/${o.pullRequestId}`); $scope.details = res.data; if (!$scope.pullRequestId) { $scope.pullRequestId = o.repo + "-PR" + o.pullRequestId + "-" + generateRandomId(4); } } catch (error) { if (error.data) { $translate("ERRORS." + error.data.error).then((translation) => { $scope.addToast({ title: "Error", date: new Date(), body: translation }); $scope.error = translation; }, console.error); displayErrorMessage(error.data.error); } setValidity("sourceUrl", "missing", false); throw error; } } // Angular templates evaluate this synchronously, so we keep a // {original -> anonymized} cache populated by a debounced batch call to // /api/anonymize-preview whenever the PR details, terms, or options // change. anonymizePrContent() returns the cached value if known and // falls back to the original until the next cycle resolves. let _prAnonCache = new Map(); let _prSeenContents = new Set(); function collectPrContents() { const out = new Set(); const d = $scope.details && $scope.details.pullRequest; if (!d) return out; if (typeof d.title === "string") out.add(d.title); if (typeof d.body === "string") out.add(d.body); if (typeof d.diff === "string") out.add(d.diff); const comments = ($scope.details && $scope.details.comments) || []; for (const c of comments) { if (typeof c.author === "string") out.add(c.author); if (typeof c.body === "string") out.add(c.body); } return out; } const refreshPrPreview = makePreviewBatcher( () => { const seen = collectPrContents(); _prSeenContents = seen; const list = Array.from(seen); if (list.length === 0) return null; return { contents: list, options: previewOptions() }; }, (data) => { if (!data || !Array.isArray(data.contents)) return; const seen = Array.from(_prSeenContents); const next = new Map(); for (let i = 0; i < seen.length && i < data.contents.length; i++) { next.set(seen[i], data.contents[i]); } _prAnonCache = next; } ); $scope.anonymizePrContent = function (content) { if (!content) return content; if (_prAnonCache.has(content)) return _prAnonCache.get(content); if (!_prSeenContents.has(content)) { refreshPrPreview(); } return content; }; // ========== GIST LOGIC ========== async function getGistDetails() { const o = parseGithubUrl($scope.sourceUrl); try { resetValidity(); const res = await $http.get(`/api/gist/source/${o.gistId}`); $scope.details = res.data; if (!$scope.gistId) { $scope.gistId = "gist-" + o.gistId.substring(0, 6) + "-" + generateRandomId(4); } } catch (error) { if (error.data) { $translate("ERRORS." + error.data.error).then((translation) => { $scope.addToast({ title: "Error", date: new Date(), body: translation }); $scope.error = translation; }, console.error); displayErrorMessage(error.data.error); } setValidity("sourceUrl", "missing", false); throw error; } } let _gistAnonCache = new Map(); let _gistSeenContents = new Set(); let _gistCacheVersion = 0; function collectGistContents() { const out = new Set(); const d = $scope.details && $scope.details.gist; if (!d) return out; if (typeof d.description === "string") out.add(d.description); if (typeof d.ownerLogin === "string") out.add(d.ownerLogin); const files = (d.files) || []; for (const f of files) { if (typeof f.filename === "string") out.add(f.filename); if (typeof f.content === "string") out.add(f.content); } const comments = d.comments || []; for (const c of comments) { if (typeof c.author === "string") out.add(c.author); if (typeof c.body === "string") out.add(c.body); } return out; } const refreshGistPreview = makePreviewBatcher( () => { const seen = collectGistContents(); _gistSeenContents = seen; const list = Array.from(seen); if (list.length === 0) return null; return { contents: list, options: previewOptions() }; }, (data) => { if (!data || !Array.isArray(data.contents)) return; const seen = Array.from(_gistSeenContents); const next = new Map(); for (let i = 0; i < seen.length && i < data.contents.length; i++) { next.set(seen[i], data.contents[i]); } _gistAnonCache = next; _gistCacheVersion++; rebuildPreviewGistFiles(); } ); $scope.anonymizeGistContent = function (content) { if (!content) return content; if (_gistAnonCache.has(content)) return _gistAnonCache.get(content); if (!_gistSeenContents.has(content)) { refreshGistPreview(); } return content; }; // Precomputed file objects for the preview pane so 's // two-way binding has a stable reference. Recomputes when the source // files change OR when the anonymization cache turns over. $scope.previewGistFiles = []; function rebuildPreviewGistFiles() { const files = ($scope.details && $scope.details.gist && $scope.details.gist.files) || []; $scope.previewGistFiles = files.map((f) => ({ filename: $scope.anonymizeGistContent(f.filename), content: $scope.anonymizeGistContent(f.content), language: f.language, })); } // _prAnonCache turns over inside refreshGistPreview's applyResult; the // simplest signal we have is the digest cycle, so re-derive each digest. // Cheap when _gistAnonCache hits. $scope.$watch("details.gist.files", rebuildPreviewGistFiles, true); $scope.$watch("terms", rebuildPreviewGistFiles); // ========== SHARED LOGIC ========== function getConference() { if (!$scope.conference) return; $http.get("/api/conferences/" + $scope.conference).then( (res) => { $scope.conference_data = res.data; $scope.conference_data.startDate = new Date($scope.conference_data.startDate); $scope.conference_data.endDate = new Date($scope.conference_data.endDate); $scope.options.expirationDate = new Date($scope.conference_data.endDate); $scope.options.expirationMode = "remove"; $scope.options.update = $scope.conference_data.options.update; $scope.options.image = $scope.conference_data.options.image; $scope.options.pdf = $scope.conference_data.options.pdf; $scope.options.notebook = $scope.conference_data.options.notebook; $scope.options.link = $scope.conference_data.options.link; }, () => { $scope.conference_data = null; } ); } function resetValidity() { setValidity("repoId", "used", true); setValidity("repoId", "format", true); setValidity("pullRequestId", "used", true); setValidity("pullRequestId", "format", true); setValidity("gistId", "used", true); setValidity("gistId", "format", true); setValidity("sourceUrl", "used", true); setValidity("sourceUrl", "missing", true); setValidity("sourceUrl", "access", true); setValidity("sourceUrl", "github", true); setValidity("commit", "exists", true); setValidity("conference", "activated", true); setValidity("terms", "format", true); $scope.termsRegexWarning = false; } function displayErrorMessage(message) { const idField = $scope.detectedType === "pr" ? "pullRequestId" : $scope.detectedType === "gist" ? "gistId" : "repoId"; switch (message) { case "repoId_already_used": setValidity(idField, "used", false); break; case "invalid_repoId": setValidity(idField, "format", false); break; case "options_not_provided": setValidity(idField, "format", false); break; case "repo_already_anonymized": setValidity("sourceUrl", "used", false); break; case "invalid_terms_format": setValidity("terms", "format", false); break; case "repo_not_found": setValidity("sourceUrl", "missing", false); break; case "repo_not_accessible": setValidity("sourceUrl", "access", false); break; case "commit_not_found": setValidity("commit", "exists", false); break; case "conf_not_activated": setValidity("conference", "activated", false); break; } } // ========== CO-AUTHORS ========== $scope.coauthors = $scope.coauthors || []; $scope.coauthorResults = []; $scope.coauthorError = ""; $scope.searchCoauthors = () => { const q = ($scope.coauthorSearch || "").trim(); $scope.coauthorError = ""; if (q.length < 2) { $scope.coauthorResults = []; return; } $http.get("/api/user/search/github-users", { params: { q } }).then( (res) => { const existing = new Set( ($scope.coauthors || []).map((c) => (c.username || "").toLowerCase()) ); $scope.coauthorResults = (res.data || []).filter( (u) => !existing.has((u.username || "").toLowerCase()) ); }, () => { $scope.coauthorResults = []; } ); }; $scope.addCoauthor = (u, event) => { if (event) event.preventDefault(); if (!u || !u.username) return; $http .post("/api/repo/" + $scope.repoId + "/coauthors", { username: u.username, }) .then( (res) => { $scope.coauthors = res.data || []; $scope.coauthorResults = []; $scope.coauthorSearch = ""; $scope.coauthorError = ""; }, (err) => { const code = (err && err.data && err.data.error) || "unknown_error"; $scope.coauthorError = code; } ); }; $scope.removeCoauthor = (c) => { if (!c || !c.username) return; if (!confirm("Remove co-author " + c.username + "?")) return; $http .delete( "/api/repo/" + $scope.repoId + "/coauthors/" + encodeURIComponent(c.username) ) .then( (res) => { $scope.coauthors = res.data || []; }, (err) => { const code = (err && err.data && err.data.error) || "unknown_error"; $scope.coauthorError = code; } ); }; // Submit: repo $scope.anonymizeRepo = (event) => { event.target.disabled = true; const o = parseGithubUrl($scope.sourceUrl); const payload = { repoId: $scope.repoId, terms: $scope.terms.trim().split("\n").filter((f) => f), fullName: `${o.owner}/${o.repo}`, repository: $scope.sourceUrl, options: $scope.options, source: $scope.source, conference: $scope.conference, }; if ($scope.details) payload.options.pageSource = $scope.details.pageSource; resetValidity(); const url = $scope.isUpdate ? "/api/repo/" + $scope.repoId : "/api/repo/"; $http.post(url, payload, { headers: { "Content-Type": "application/json" } }).then( () => { window.location.href = "/status/" + $scope.repoId; }, (error) => { if (error.data) { $translate("ERRORS." + error.data.error).then((t) => { $scope.error = t; }, console.error); displayErrorMessage(error.data.error); } } ).finally(() => { event.target.disabled = false; $scope.$apply(); }); }; // Submit: Gist $scope.anonymizeGist = (event) => { event.target.disabled = true; const o = parseGithubUrl($scope.sourceUrl); const payload = { gistId: $scope.gistId, terms: $scope.terms.trim().split("\n").filter((f) => f), source: { gistId: o.gistId }, options: $scope.options, conference: $scope.conference, }; resetValidity(); const url = $scope.isUpdate ? "/api/gist/" + $scope.gistId : "/api/gist/"; $http.post(url, payload, { headers: { "Content-Type": "application/json" } }).then( () => { window.location.href = "/gist/" + $scope.gistId; }, (error) => { if (error.data) { $translate("ERRORS." + error.data.error).then((t) => { $scope.error = t; }, console.error); displayErrorMessage(error.data.error); } } ).finally(() => { event.target.disabled = false; $scope.$apply(); }); }; // Submit: PR $scope.anonymizePullRequest = (event) => { event.target.disabled = true; const o = parseGithubUrl($scope.sourceUrl); const payload = { pullRequestId: $scope.pullRequestId, terms: $scope.terms.trim().split("\n").filter((f) => f), source: { repositoryFullName: `${o.owner}/${o.repo}`, pullRequestId: o.pullRequestId }, options: $scope.options, conference: $scope.conference, }; resetValidity(); const url = $scope.isUpdate ? "/api/pr/" + $scope.pullRequestId : "/api/pr/"; $http.post(url, payload, { headers: { "Content-Type": "application/json" } }).then( () => { window.location.href = "/pr/" + $scope.pullRequestId; }, (error) => { if (error.data) { $translate("ERRORS." + error.data.error).then((t) => { $scope.error = t; }, console.error); displayErrorMessage(error.data.error); } } ).finally(() => { event.target.disabled = false; $scope.$apply(); }); }; $scope.$watch("conference", () => { getConference(); }); $scope.$watch("terms", () => { if ($scope.detectedType === "repo") anonymizeReadme(); if ($scope.detectedType === "pr") refreshPrPreview(); if ($scope.detectedType === "gist") refreshGistPreview(); }); $scope.$watch("options.image", () => { if ($scope.detectedType === "repo") anonymizeReadme(); if ($scope.detectedType === "pr") refreshPrPreview(); if ($scope.detectedType === "gist") refreshGistPreview(); }); $scope.$watch("options.link", () => { if ($scope.detectedType === "repo") anonymizeReadme(); if ($scope.detectedType === "pr") refreshPrPreview(); if ($scope.detectedType === "gist") refreshGistPreview(); }); $scope.$watch("details", () => { if ($scope.detectedType === "pr") refreshPrPreview(); if ($scope.detectedType === "gist") refreshGistPreview(); }, true); }, ]) .controller("exploreController", [ "$scope", "$http", "$location", "$routeParams", "$sce", "$q", "PDFViewerService", function ($scope, $http, $location, $routeParams, $sce, $q, PDFViewerService) { $scope.files = []; $scope.isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent); $scope.fileSearchQuery = ""; $scope.fileSearchResults = null; $scope.fileSearchLoading = false; document.addEventListener("keydown", function (e) { if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); var input = document.querySelector(".tree-search-input"); if (input) { input.focus(); input.select(); } } }); var searchCanceller = null; $scope.onFileSearchChange = function () { // Cancel any in-flight search request if (searchCanceller) { searchCanceller.resolve(); searchCanceller = null; } const query = $scope.fileSearchQuery; if (!query || query.length < 2) { $scope.fileSearchResults = null; $scope.fileSearchLoading = false; return; } $scope.fileSearchLoading = true; searchCanceller = $q.defer(); $http.get( `/api/repo/${$scope.repoId}/files/search?q=${encodeURIComponent(query)}`, { timeout: searchCanceller.promise } ).then(function (res) { searchCanceller = null; $scope.fileSearchLoading = false; // Merge search results into $scope.files so the tree can render them. // Ancestor folders must appear before their children for toArray() to work. var existing = {}; $scope.files.forEach(function(f) { existing[(f.path || "") + "/" + f.name] = true; }); // First pass: collect ancestor folders (shallow to deep) var foldersToAdd = []; var folderSeen = {}; for (var i = 0; i < res.data.length; i++) { var f = res.data[i]; if (f.path) { var segments = f.path.split("/"); var acc = ""; for (var j = 0; j < segments.length; j++) { var parent = acc; acc = acc ? acc + "/" + segments[j] : segments[j]; var folderKey = parent + "/" + segments[j]; if (!existing[folderKey] && !folderSeen[folderKey]) { folderSeen[folderKey] = true; foldersToAdd.push({ name: segments[j], path: parent }); } } } } // Sort folders by depth (shallow first) foldersToAdd.sort(function(a, b) { return (a.path || "").split("/").length - (b.path || "").split("/").length; }); // Add folders first, then files if (foldersToAdd.length > 0) { $scope.files.push.apply($scope.files, foldersToAdd); } var filesToAdd = []; for (var k = 0; k < res.data.length; k++) { var rf = res.data[k]; var key = (rf.path || "") + "/" + rf.name; if (!existing[key] && rf.size != null) { filesToAdd.push(rf); existing[key] = true; } } if (filesToAdd.length > 0) { $scope.files.push.apply($scope.files, filesToAdd); } $scope.fileSearchResults = res.data; }, function () { // Only clear loading if this wasn't a cancellation if (!searchCanceller) { $scope.fileSearchLoading = false; $scope.fileSearchResults = []; } }); }; const extensionModes = { yml: "yaml", txt: "text", py: "python", js: "javascript", ts: "typescript", }; const textFiles = ["license", "txt"]; const imageFiles = [ "png", "jpg", "jpeg", "gif", "svg", "ico", "bmp", "tiff", "tif", "webp", "avif", "heif", "heic", ]; const audioFiles = ["wav", "mp3", "ogg", "wma", "flac", "aac", "m4a"]; const mediaFiles = [ "mp4", "avi", "webm", "mov", "mpg", "mpeg", "mkv", "flv", "wmv", "3gp", "3g2", "m4v", "f4v", "f4p", "f4a", "f4b", ]; $scope.$on("$routeUpdate", function (event, current) { if (($routeParams.path || "") == $scope.filePath) { return; } $scope.filePath = $routeParams.path || ""; $scope.paths = $scope.filePath .split("/") .filter((f) => f && f.trim().length > 0); if ($scope.repoId != $routeParams.repoId) { return init(); } updateContent(); // #510 — if we navigated into a subdirectory whose file listing // hasn't been fetched, lazy-load the parent directories in the // background so getSelectedFile() can populate $scope.file with the // right sha for the next interaction. Done after updateContent so // the request fires immediately (getContent falls back to sha "0"). for (let i = 0; i < $scope.paths.length - 1; i++) { const dirPath = i > 0 ? $scope.paths.slice(0, i).join("/") : ""; const alreadyLoaded = $scope.files.some((f) => f.path === dirPath); if (!alreadyLoaded) { $scope.getFiles(dirPath); } } }); function selectFile() { if ($scope.paths[0] != "") { return; } const readmePriority = [ "readme.md", "readme.txt", "readme.org", "readme.1st", "readme", ]; const readmeCandidates = {}; for (const file of $scope.files) { if (file.name.toLowerCase().indexOf("readme") > -1) { readmeCandidates[file.name.toLowerCase()] = file.name; } } let best_match = null; for (const p of readmePriority) { if (readmeCandidates[p]) { best_match = p; break; } } if (!best_match && Object.keys(readmeCandidates).length > 0) best_match = Object.keys(readmeCandidates)[0]; if (best_match) { let uri = $location.url(); if (uri[uri.length - 1] != "/") { uri += "/"; } // redirect to readme $location.url( uri + encodePathForUrl(readmeCandidates[best_match]) ); } } $scope.fileCounts = null; $scope.getFiles = function (path) { return $http.get( `/api/repo/${$scope.repoId}/files/?path=${encodeURIComponent(path)}&v=${$scope.options.lastUpdateDate}` ).then(function (res) { const normalized = path || ""; $scope.files = $scope.files.filter((f) => f.path !== normalized); $scope.files.push(...res.data); return res.data; }, function (err) { $scope.type = "error"; $scope.content = (err && err.data && err.data.error) || "unknown_error"; $scope.files = []; }); }; function fetchFileCounts() { $http.get( `/api/repo/${$scope.repoId}/files/counts` ).then(function (res) { $scope.fileCounts = res.data; }, function () { $scope.fileCounts = {}; }); } function getSelectedFile() { return $scope.files.filter( (f) => f.name == $scope.paths[$scope.paths.length - 1] && f.path == $scope.paths.slice(0, $scope.paths.length - 1).join("/") )[0]; } var rlCountdownTimer = null; $scope.$on("$destroy", function () { if (rlCountdownTimer) clearInterval(rlCountdownTimer); }); function getOptions(callback) { $http.get(`/api/repo/${$scope.repoId}/options`).then( (res) => { $scope.options = res.data; if ($scope.options.url) { window.location = $scope.options.url; return; } if (callback) { callback(res.data); } }, (err) => { var data = err.data || {}; if (data.error === "rate_limited" && data.resetAt) { $scope.type = "rate_limited"; $scope.rateLimitResetAt = data.resetAt; if (rlCountdownTimer) clearInterval(rlCountdownTimer); function rlTick() { var remaining = Math.max(0, Math.ceil(($scope.rateLimitResetAt - Date.now()) / 1000)); if (remaining <= 0) { $scope.rateLimitCountdown = ""; $scope.rateLimitResetAt = 0; if (rlCountdownTimer) { clearInterval(rlCountdownTimer); rlCountdownTimer = null; } getOptions(callback); } else { var min = Math.floor(remaining / 60); var sec = remaining % 60; $scope.rateLimitCountdown = min > 0 ? min + "m " + sec + "s" : sec + "s"; } $scope.$applyAsync(); } rlTick(); rlCountdownTimer = setInterval(rlTick, 1000); } else if (data.error === "repository_not_ready") { $scope.type = "loading"; setTimeout(function () { getOptions(callback); }, 3000); } else { $scope.type = "error"; $scope.content = data.error; } } ); } function getMode(extension) { if (extensionModes[extension]) { return extensionModes[extension]; } return extension; } function getType(extension) { if (extension == "pdf") { $scope.instance = PDFViewerService.Instance("viewer"); return "pdf"; } if (extension == "md") { return "md"; } if (extension == "org") { return "org"; } if (extension == "ipynb") { return "IPython"; } if (textFiles.indexOf(extension) > -1) { return "text"; } if (imageFiles.indexOf(extension) > -1) { return "image"; } if (mediaFiles.indexOf(extension) > -1) { return "media"; } if (audioFiles.indexOf(extension) > -1) { return "audio"; } return "code"; } function getContent(path, fileInfo) { if (!path) { $scope.type = "error"; $scope.content = "no_file_selected"; return; } const originalType = $scope.type; $scope.type = "loading"; $scope.content = "loading"; // fileInfo can be undefined when the user navigates (e.g. clicks a // markdown link into a subdir whose file list hasn't loaded yet) — // see #510. Fall back to "0" so the request still goes through; the // server returns a fresh ETag on first hit either way. const sha = (fileInfo && fileInfo.sha) || "0"; $http .get( `/api/repo/${$scope.repoId}/file/${encodePathForUrl(path)}?v=` + sha, { transformResponse: (data) => { return data; }, } ) .then( (res) => { $scope.type = originalType; $scope.content = res.data; if ($scope.content == "") { $scope.content = null; } if ($scope.type == "md") { $scope.content = $sce.trustAsHtml( renderMD(res.data, $location.url() + "/../") ); $scope.type = "html"; } if ($scope.type == "org") { const content = contentAbs2Relative(res.data); const orgParser = new Org.Parser(); const orgDocument = orgParser.parse(content); var orgHTMLDocument = orgDocument.convert(Org.ConverterHTML, { headerOffset: 1, exportFromLineNumber: false, suppressSubScriptHandling: true, suppressAutoLink: false, }); $scope.content = $sce.trustAsHtml(orgHTMLDocument.toString()); $scope.type = "html"; } if ( $scope.type == "code" && res.headers("content-type") == "application/octet-stream" ) { $scope.type = "binary"; $scope.content = "binary"; } setTimeout(() => { Prism.highlightAll(); }, 50); }, (err) => { $scope.type = "error"; $scope.content = "unknown_error"; try { err.data = JSON.parse(err.data); if (err.data.error) { $scope.content = err.data.error; } else { $scope.content = err.data; } } catch (ignore) { console.log(err); if (err.status == -1) { $scope.content = "request_error"; } else if (err.status == 502) { // cloudflare error $scope.content = "unreachable"; } } } ); } function updateContent() { $scope.content = ""; $scope.file = getSelectedFile(); let fileVersion = "0"; if ($scope.file && $scope.file.sha) { fileVersion = $scope.file.sha; } $scope.url = `/api/repo/${$scope.repoId}/file/${encodePathForUrl( $scope.filePath )}?v=${fileVersion}`; let extension = $scope.filePath.toLowerCase(); const extensionIndex = extension.lastIndexOf("."); if (extensionIndex > -1) { extension = extension.substring(extensionIndex + 1); } $scope.aceOption = { readOnly: true, useWrapMode: true, showGutter: true, theme: "chrome", useSoftTab: true, showPrintMargin: true, tabSize: 2, highlightSelectedWord: true, fontSize: 15, keyBinding: "vscode", fullLineSelection: true, highlightActiveLine: false, highlightGutterLine: false, cursor: "hide", showInvisibles: false, showIndentGuides: true, showPrintMargin: false, highlightSelectedWord: false, enableBehaviours: true, fadeFoldWidgets: false, mode: getMode(extension), onLoad: function (_editor) { const Range = ace.require("ace/range").Range; let activeLineMarker = null; function highlightLines(from, to) { if (activeLineMarker !== null) { _editor.session.removeMarker(activeLineMarker); activeLineMarker = null; } if (from === null || from === undefined) return; activeLineMarker = _editor.session.addMarker( new Range(from, 0, to, 1), "highlighted-line", "fullLine" ); } function applyHashFromUrl(scroll) { const m = window.location.hash.match(/^#L(\d+)(?:-L(\d+))?/); if (!m) { highlightLines(null); return; } const from = parseInt(m[1]) - 1; const to = m[2] ? parseInt(m[2]) - 1 : from; highlightLines(from, to); if (scroll) { setTimeout(() => { _editor.scrollToLine(from, true, true, function () {}); }, 100); } } applyHashFromUrl(true); // #392 — clicking a gutter line updates the URL to #L and // shift-clicking extends to #L-L so the user can copy // a stable link to a specific line. Use replaceState to avoid // polluting history with every click. let anchorRow = null; _editor.on("guttermousedown", function (e) { const row = e.getDocumentPosition().row; const shift = e.domEvent && e.domEvent.shiftKey; let from = row; let to = row; if (shift && anchorRow !== null) { from = Math.min(anchorRow, row); to = Math.max(anchorRow, row); } else { anchorRow = row; } const hash = from === to ? `#L${from + 1}` : `#L${from + 1}-L${to + 1}`; const url = window.location.pathname + window.location.search + hash; window.history.replaceState(null, "", url); highlightLines(from, to); e.stop(); }); window.addEventListener("hashchange", () => applyHashFromUrl(false)); _editor.setFontSize($scope.aceOption.fontSize); _editor.setReadOnly($scope.aceOption.readOnly); _editor.setKeyboardHandler($scope.aceOption.keyBinding); _editor.setSelectionStyle( $scope.aceOption.fullLineSelection ? "line" : "text" ); _editor.setOption("displayIndentGuides", true); _editor.setHighlightActiveLine( $scope.aceOption.highlightActiveLine ); if ($scope.aceOption.cursor == "hide") { _editor.renderer.$cursorLayer.element.style.display = "none"; } _editor.setHighlightGutterLine( $scope.aceOption.highlightGutterLine ); _editor.setShowInvisibles($scope.aceOption.showInvisibles); _editor.setDisplayIndentGuides($scope.aceOption.showIndentGuides); _editor.renderer.setShowPrintMargin( $scope.aceOption.showPrintMargin ); _editor.setHighlightSelectedWord( $scope.aceOption.highlightSelectedWord ); _editor.session.setUseSoftTabs($scope.aceOption.useSoftTab); _editor.session.setTabSize($scope.aceOption.tabSize); _editor.setBehavioursEnabled($scope.aceOption.enableBehaviours); _editor.setFadeFoldWidgets($scope.aceOption.fadeFoldWidgets); }, }; $scope.$on("dark-mode", (event, on) => { if (on) { $scope.aceOption.theme = "nord_dark"; } else { $scope.aceOption.theme = "chrome"; } }); if ($scope.isDarkMode) { $scope.aceOption.theme = "nord_dark"; } $scope.type = getType(extension); getContent($scope.filePath, $scope.file); } function init() { $scope.repoId = $routeParams.repoId; $scope.type = "loading"; $scope.filePath = $routeParams.path || ""; $scope.paths = $scope.filePath.split("/"); getOptions(function (options) { fetchFileCounts(); var chain = $q.resolve(); for (let i = 0; i < $scope.paths.length; i++) { const path = i > 0 ? $scope.paths.slice(0, i).join("/") : ""; chain = chain.then(function () { return $scope.getFiles(path); }).then(function () { if ($scope.type === "error") { return $q.reject("error"); } }); } chain.then(function () { if ($scope.files.length == 1 && $scope.files[0].name == "") { $scope.files = []; $scope.type = "empty"; } else { selectFile(); updateContent(); } }); }); } init(); }, ]) // anonymizePullRequestController removed - unified into anonymizeController .controller("pullRequestController", [ "$scope", "$http", "$location", "$routeParams", "$sce", function ($scope, $http, $location, $routeParams, $sce) { async function getOption(callback) { $http.get(`/api/pr/${$scope.pullRequestId}/options`).then( (res) => { $scope.options = res.data; if ($scope.options.url) { // the repository is expired with redirect option window.location = $scope.options.url; return; } if (callback) { callback(res.data); } }, (err) => { $scope.type = "error"; $scope.content = err.data.error; } ); } async function getPullRequest(callback) { $http.get(`/api/pr/${$scope.pullRequestId}/content`).then( (res) => { $scope.details = res.data; if (callback) { callback(res.data); } }, (err) => { $scope.type = "error"; $scope.content = err.data.error; } ); } function init() { $scope.pullRequestId = $routeParams.pullRequestId; $scope.type = "loading"; getOption((_) => { getPullRequest(); }); } init(); }, ]) .controller("gistController", [ "$scope", "$http", "$location", "$routeParams", "$sce", function ($scope, $http, $location, $routeParams, $sce) { async function getOption(callback) { $http.get(`/api/gist/${$scope.gistId}/options`).then( (res) => { $scope.options = res.data; if ($scope.options.url) { window.location = $scope.options.url; return; } if (callback) callback(res.data); }, (err) => { $scope.type = "error"; $scope.content = err.data.error; } ); } async function getGist(callback) { $http.get(`/api/gist/${$scope.gistId}/content`).then( (res) => { $scope.details = res.data; if (callback) callback(res.data); }, (err) => { $scope.type = "error"; $scope.content = err.data.error; } ); } function init() { $scope.gistId = $routeParams.gistId; $scope.type = "loading"; getOption(() => { getGist(); }); } init(); }, ]) .controller("conferencesController", [ "$scope", "$http", "$location", function ($scope, $http, $location) { $scope.$watch("user.status", () => { if ($scope.user == null) { $location.url("/"); } }); if ($scope.user == null) { $location.url("/"); } $scope.conferences = []; $scope.search = ""; const conferencesPrefsKey = "conferences.filterPrefs"; const conferencesPrefDefaults = { filters: { status: { ready: true, expired: false, removed: false } }, orderBy: "name", }; const savedConferencesPrefs = loadFilterPrefs(conferencesPrefsKey) || {}; $scope.filters = { status: Object.assign( {}, conferencesPrefDefaults.filters.status, (savedConferencesPrefs.filters && savedConferencesPrefs.filters.status) || {} ), }; $scope.orderBy = savedConferencesPrefs.orderBy || conferencesPrefDefaults.orderBy; $scope.$watch("orderBy", () => { saveFilterPrefs(conferencesPrefsKey, { filters: $scope.filters, orderBy: $scope.orderBy, }); }); $scope.$watch( "filters", () => { saveFilterPrefs(conferencesPrefsKey, { filters: $scope.filters, orderBy: $scope.orderBy, }); }, true ); $scope.removeConference = function (conf) { if ( confirm( `Are you sure that you want to remove the conference ${conf.name}? All the repositories linked to this conference will expire.` ) ) { const toast = { title: `Removing ${conf.name}...`, date: new Date(), body: `The conference ${conf.name} is going to be removed.`, }; $scope.addToast(toast); $http.delete(`/api/conferences/${conf.conferenceID}`).then(() => { toast.title = `${conf.name} is removed.`; toast.body = `The conference ${conf.name} is removed.`; getConferences(); }); } }; function getConferences() { $http.get("/api/conferences/").then( (res) => { $scope.conferences = res.data || []; }, (err) => { console.error(err); } ); } getConferences(); $scope.conferenceFilter = (conference) => { if ($scope.filters.status[conference.status] == false) return false; if ($scope.search.trim().length == 0) return true; if (conference.name.indexOf($scope.search) > -1) return true; if (conference.conferenceID.indexOf($scope.search) > -1) return true; return false; }; }, ]) .controller("newConferenceController", [ "$scope", "$http", "$location", "$routeParams", function ($scope, $http, $location, $routeParams) { $scope.$watch("user.status", () => { if ($scope.user == null) { $location.url("/"); } }); if ($scope.user == null) { $location.url("/"); } $scope.plans = []; $scope.editionMode = false; function getConference() { $http .get("/api/conferences/" + $routeParams.conferenceId) .then((res) => { $scope.options = res.data; $scope.options.startDate = new Date($scope.options.startDate); $scope.options.endDate = new Date($scope.options.endDate); }); } if ($routeParams.conferenceId) { $scope.editionMode = true; getConference(); } function getPlans() { $http.get("/api/conferences/plans").then((res) => { $scope.plans = res.data; $scope.plan = $scope.plans.filter( (f) => f.id == $scope.options.plan.planID )[0]; }); } getPlans(); const start = new Date(); start.setDate(1); start.setMonth(start.getMonth() + 1); const end = new Date(); end.setMonth(start.getMonth() + 7, 0); $scope.options = { startDate: start, endDate: end, plan: { planID: "free_conference", }, options: { link: true, image: true, pdf: true, notebook: true, update: true, page: true, }, }; $scope.plan = null; $scope.$watch("options.plan.planID", () => { $scope.plan = $scope.plans.filter( (f) => f.id == $scope.options.plan.planID )[0]; }); function resetValidity() { $scope.conference.name.$setValidity("required", true); $scope.conference.conferenceID.$setValidity("pattern", true); $scope.conference.conferenceID.$setValidity("required", true); $scope.conference.conferenceID.$setValidity("used", true); $scope.conference.startDate.$setValidity("required", true); $scope.conference.startDate.$setValidity("invalid", true); $scope.conference.endDate.$setValidity("required", true); $scope.conference.endDate.$setValidity("invalid", true); $scope.conference.$setValidity("error", true); } function displayErrorMessage(message) { switch (message) { case "conf_name_missing": $scope.conference.name.$setValidity("required", false); break; case "conf_id_missing": $scope.conference.conferenceID.$setValidity("required", false); break; case "conf_id_format": $scope.conference.conferenceID.$setValidity("pattern", false); break; case "conf_id_used": $scope.conference.conferenceID.$setValidity("used", false); break; case "conf_start_date_missing": $scope.conference.startDate.$setValidity("required", false); break; case "conf_end_date_missing": $scope.conference.endDate.$setValidity("required", false); break; case "conf_start_date_invalid": $scope.conference.startDate.$setValidity("invalid", false); break; case "conf_end_date_invalid": $scope.conference.endDate.$setValidity("invalid", false); break; default: $scope.conference.$setValidity("error", false); break; } } $scope.submit = function () { const toast = { title: `Creating ${$scope.options.name}...`, date: new Date(), body: `The conference ${$scope.options.conferenceID} is in creation.`, }; if ($scope.editionMode) { toast.title = `Updating ${$scope.options.name}...`; toast.body = `The conference '${$scope.options.conferenceID}' is updating.`; } $scope.addToast(toast); resetValidity(); $http .post( "/api/conferences/" + ($scope.editionMode ? $scope.options.conferenceID : ""), $scope.options ) .then( () => { if (!$scope.editionMode) { toast.title = `${$scope.options.name} created`; toast.body = `The conference '${$scope.options.conferenceID}' is created.`; } else { toast.title = `${$scope.options.name} updated`; toast.body = `The conference '${$scope.options.conferenceID}' is updated.`; } $location.url("/conference/" + $scope.options.conferenceID); }, (error) => { displayErrorMessage(error.data.error); $scope.removeToast(toast); } ); }; }, ]) .controller("conferenceController", [ "$scope", "$http", "$location", "$routeParams", function ($scope, $http, $location, $routeParams) { $scope.$watch("user.status", () => { if ($scope.user == null) { $location.url("/"); } }); if ($scope.user == null) { $location.url("/"); } $scope.conference = null; $scope.search = ""; $scope.filters = { status: { ready: true, expired: false, removed: false }, }; $scope.orderBy = "-anonymizeDate"; $scope.repoFiler = (repo) => { if ($scope.filters.status[repo.status] == false) return false; if ($scope.search.trim().length == 0) return true; if (repo.source.fullName.indexOf($scope.search) > -1) return true; if (repo.repoId.indexOf($scope.search) > -1) return true; return false; }; function getConference() { $http .get("/api/conferences/" + $routeParams.conferenceId) .then((res) => { $scope.conference = res.data; }); } getConference(); }, ]); $(document).on("click", "#navbarSupportedContent .nav-link", function (e) { if ($(this).attr("data-toggle") === "dropdown") return; var $collapse = $("#navbarSupportedContent"); if ($collapse.hasClass("show")) { $collapse.collapse("hide"); } });