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('
| ' + (line.oldNo || "") + " | " + '' + (line.newNo || "") + " | " + '' + (line.kind === "add" ? "+" : line.kind === "remove" ? "-" : line.kind === "hunk" ? "@" : "") + " | " + '' + esc(line.text) + " | " + "
.
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