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/:path*?", { templateUrl: "/partials/pullRequest.htm", controller: "pullRequestController", title: "Anonymous pull request – Anonymous GitHub", reloadOnUrl: false, }) .when("/gist/:gistId/:path*?", { 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, """); } // Escape a value for safe interpolation into a single-quoted // AngularJS expression string (e.g. ng-click="openFolder('...')") // that itself sits inside a double-quoted HTML attribute which is // later $compile()d. Backslash/quote are escaped at the Angular // string level; &<>" are HTML-encoded for the attribute. Without // this a file name like `');$emit(...)//` would break out of the // expression string and execute (DOM XSS, CWE-79). function escapeNgString(str) { return String(str) .replace(/\\/g, "\\\\") .replace(/'/g, "\\'") .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"); } const ngPath = escapeNgString(path); 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(""); // notebook.render() turns notebook JSON (markdown cells, cell // outputs) into HTML without sanitising it — a malicious // notebook could embed