diff --git a/public/partials/pullRequest.htm b/public/partials/pullRequest.htm
new file mode 100644
index 0000000..aff5d01
--- /dev/null
+++ b/public/partials/pullRequest.htm
@@ -0,0 +1,118 @@
+
+
+
+
+
+ Last Update: {{details.anonymizeDate|date}}
+
+
+
+
+
+ {{details.title}}
+
+ {{details.merged?"merged":details.state | title}}
+
+
+
+
+
Pull Request on {{details.baseRepositoryFullName}}
+
+
+
+
+
+
+
+
+
diff --git a/public/script/app.js b/public/script/app.js
index ba4367f..48e7c6a 100644
--- a/public/script/app.js
+++ b/public/script/app.js
@@ -34,11 +34,21 @@ angular
controller: "dashboardController",
title: "Dashboard - Anonymous GitHub",
})
+ .when("/pr-dashboard", {
+ templateUrl: "/partials/pr-dashboard.htm",
+ controller: "prDashboardController",
+ title: "Pull Request Dashboard - Anonymous GitHub",
+ })
.when("/anonymize/:repoId?", {
templateUrl: "/partials/anonymize.htm",
controller: "anonymizeController",
title: "Anonymize - Anonymous GitHub",
})
+ .when("/pull-request-anonymize/:pullRequestId?", {
+ templateUrl: "/partials/anonymizePullRequest.htm",
+ controller: "anonymizePullRequestController",
+ title: "Anonymize - Anonymous GitHub",
+ })
.when("/status/:repoId", {
templateUrl: "/partials/status.htm",
controller: "statusController",
@@ -79,6 +89,12 @@ angular
controller: "claimController",
title: "Claim repository - Anonymous GitHub",
})
+ .when("/pr/:pullRequestId", {
+ templateUrl: "/partials/pullRequest.htm",
+ controller: "pullRequestController",
+ title: "Anonymized Pull Request - Anonymous GitHub",
+ reloadOnUrl: false,
+ })
.when("/r/:repoId/:path*?", {
templateUrl: "/partials/explorer.htm",
controller: "exploreController",
@@ -205,6 +221,53 @@ angular
return capitalized.join(" ");
};
})
+ .filter("diff", function ($sce) {
+ return function (str) {
+ if (!str) return str;
+ const lines = str.split("\n");
+ const o = [];
+ for (let i = 1; i < lines.length; i++) {
+ if (lines[i].startsWith("+++")) {
+ o.push(`
${lines[i]}`);
+ } else if (lines[i].startsWith("---")) {
+ o.push(`
${lines[i]}`);
+ } else if (lines[i].startsWith("@@")) {
+ o.push(`
${lines[i]}`);
+ } else if (lines[i].startsWith("index")) {
+ o.push(`
${lines[i]}`);
+ } else if (lines[i].startsWith("+")) {
+ o.push(`
${lines[i]}`);
+ } else if (lines[i].startsWith("-")) {
+ o.push(`
${lines[i]}`);
+ } else {
+ o.push(`
${lines[i]}`);
+ }
+ }
+ return $sce.trustAsHtml(o.join("\n"));
+ };
+ })
+ .directive("markdown", [
+ "$location",
+ function ($location) {
+ return {
+ restrict: "E",
+ scope: {
+ terms: "=",
+ options: "=",
+ content: "=",
+ },
+ link: function (scope, elem, attrs) {
+ function update() {
+ elem.html(marked(scope.content, { baseUrl: $location.url() }));
+ }
+ scope.$watch(attrs.terms, update);
+ scope.$watch("terms", update);
+ scope.$watch("options", update);
+ scope.$watch("content", update);
+ },
+ };
+ },
+ ])
.directive("tree", [
function () {
return {
@@ -748,6 +811,123 @@ angular
};
},
])
+ .controller("prDashboardController", [
+ "$scope",
+ "$http",
+ "$location",
+ function ($scope, $http, $location) {
+ $scope.$on("$routeChangeStart", function () {
+ // remove tooltip
+ $('[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.pullRequests = [];
+ $scope.search = "";
+ $scope.filters = {
+ status: { ready: true, expired: true, removed: false },
+ };
+ $scope.orderBy = "-anonymizeDate";
+
+ function getPullRequests() {
+ $http.get("/api/user/anonymized_pull_requests").then(
+ (res) => {
+ $scope.pullRequests = res.data;
+ for (const pr of $scope.pullRequests) {
+ if (!pr.pageView) {
+ pr.pageView = 0;
+ }
+ if (!pr.lastView) {
+ pr.lastView = "";
+ }
+ pr.options.terms = pr.options.terms.filter((f) => f);
+ }
+ },
+ (err) => {
+ console.error(err);
+ }
+ );
+ }
+ getPullRequests();
+
+ $scope.removePullRequest = (pr) => {
+ if (
+ confirm(
+ `Are you sure that you want to remove the pull request ${pr.pullRequestId}?`
+ )
+ ) {
+ const toast = {
+ title: `Removing ${pr.pullRequestId}...`,
+ date: new Date(),
+ body: `The pull request ${pr.pullRequestId} is going to be removed.`,
+ };
+ $scope.toasts.push(toast);
+ $http.delete(`/api/pr/${pr.pullRequestId}`).then(
+ () => {
+ toast.title = `${pr.pullRequestId} is removed.`;
+ toast.body = `The pull request ${pr.pullRequestId} is removed.`;
+
+ getPullRequests();
+ },
+ (error) => {
+ toast.title = `Error during the removal of ${pr.pullRequestId}.`;
+ toast.body = error.body;
+
+ getPullRequests();
+ }
+ );
+ }
+ };
+
+ $scope.updatePullRequest = (pr) => {
+ const toast = {
+ title: `Refreshing ${pr.pullRequestId}...`,
+ date: new Date(),
+ body: `The pull request ${pr.pullRequestId} is going to be refreshed.`,
+ };
+ $scope.toasts.push(toast);
+
+ $http.post(`/api/pr/${pr.pullRequestId}/refresh`).then(
+ () => {
+ toast.title = `${pr.pullRequestId} is refreshed.`;
+ toast.body = `The pull request ${pr.pullRequestId} is refreshed.`;
+ getPullRequests();
+ },
+ (error) => {
+ toast.title = `Error during the refresh of ${pr.pullRequestId}.`;
+ toast.body = error.body;
+
+ getPullRequests();
+ }
+ );
+ };
+
+ $scope.pullRequestFilter = (pr) => {
+ if ($scope.filters.status[pr.status] == false) return false;
+
+ if ($scope.search.trim().length == 0) return true;
+
+ if ((pr.source.pullRequestId + "").indexOf($scope.search) > -1)
+ return true;
+ if (pr.source.repositoryFullName.indexOf($scope.search) > -1)
+ return true;
+ if (pr.pullRequestId.indexOf($scope.search) > -1) return true;
+
+ return false;
+ };
+ },
+ ])
.controller("statusController", [
"$scope",
"$http",
@@ -778,7 +958,10 @@ angular
} else if ($scope.repo.status == "anonymizing") {
$scope.progress = 75;
}
- if ($scope.repo.status != "ready" && $scope.repo.status != "error") {
+ if (
+ $scope.repo.status != "ready" &&
+ $scope.repo.status != "error"
+ ) {
setTimeout($scope.getStatus, 2000);
}
},
@@ -1267,11 +1450,25 @@ angular
ts: "typescript",
};
const textFiles = ["license", "txt"];
- const imageFiles = ["png", "jpg", "jpeg", "gif", "svg", "ico", "bmp", "tiff", "tif", "webp", "avif", "heif", "heic"];
+ const imageFiles = [
+ "png",
+ "jpg",
+ "jpeg",
+ "gif",
+ "svg",
+ "ico",
+ "bmp",
+ "tiff",
+ "tif",
+ "webp",
+ "avif",
+ "heif",
+ "heic",
+ ];
$scope.$on("$routeUpdate", function (event, current) {
if (($routeParams.path || "") == $scope.filePath) {
- return
+ return;
}
$scope.filePath = $routeParams.path || "";
$scope.paths = $scope.filePath.split("/");
@@ -1409,7 +1606,9 @@ angular
if ($scope.type == "md") {
const md = contentAbs2Relative(res.data);
- $scope.content = $sce.trustAsHtml(marked(md, { baseUrl: $location.url() }));
+ $scope.content = $sce.trustAsHtml(
+ marked(md, { baseUrl: $location.url() })
+ );
$scope.type = "html";
}
if ($scope.type == "org") {
@@ -1481,7 +1680,7 @@ angular
if (window.location.hash && window.location.hash.match(/^#L\d+/)) {
let from = 0;
let to = 0;
- if (window.location.hash.indexOf('-') > -1) {
+ if (window.location.hash.indexOf("-") > -1) {
const match = window.location.hash.match(/^#L(\d+)-L(\d+)/);
from = parseInt(match[1]) - 1;
to = parseInt(match[2]) - 1;
@@ -1489,9 +1688,13 @@ angular
from = parseInt(window.location.hash.substring(2)) - 1;
to = from;
}
-
- const Range = ace.require('ace/range').Range;
- _editor.session.addMarker(new Range(from, 0, to, 1), "highlighted-line", "fullLine");
+
+ const Range = ace.require("ace/range").Range;
+ _editor.session.addMarker(
+ new Range(from, 0, to, 1),
+ "highlighted-line",
+ "fullLine"
+ );
setTimeout(() => {
_editor.scrollToLine(from, true, true, function () {});
}, 100);
@@ -1559,6 +1762,375 @@ angular
init();
},
])
+ .controller("anonymizePullRequestController", [
+ "$scope",
+ "$http",
+ "$sce",
+ "$routeParams",
+ "$location",
+ "$translate",
+ function ($scope, $http, $sce, $routeParams, $location, $translate) {
+ $scope.pullRequestUrl = "";
+ $scope.pullRequestId = "";
+ $scope.terms = "";
+ $scope.defaultTerms = "";
+ $scope.options = {
+ expirationMode: "remove",
+ expirationDate: new Date(),
+ update: false,
+ image: true,
+ link: true,
+ body: true,
+ title: true,
+ origin: false,
+ diff: true,
+ comments: true,
+ username: true,
+ date: true,
+ };
+ $scope.options.expirationDate.setMonth(
+ $scope.options.expirationDate.getMonth() + 4
+ );
+ $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();
+ });
+ }
+
+ getDefault(() => {
+ if ($routeParams.pullRequestId && $routeParams.pullRequestId != "") {
+ $scope.isUpdate = true;
+ $scope.pullRequestId = $routeParams.pullRequestId;
+ $http.get("/api/pr/" + $scope.pullRequestId).then(
+ async (res) => {
+ $scope.pullRequestUrl =
+ "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 = res.data.options;
+ $scope.conference = res.data.conference;
+ if (res.data.options.expirationDate) {
+ $scope.options.expirationDate = new Date(
+ res.data.options.expirationDate
+ );
+ } else {
+ $scope.options.expirationDate = new Date();
+ $scope.options.expirationDate.setDate(
+ $scope.options.expirationDate.getDate() + 90
+ );
+ }
+
+ $scope.details = (
+ await $http.get(
+ `/api/pr/${res.data.source.repositoryFullName}/${res.data.source.pullRequestId}`
+ )
+ ).data;
+ $scope.$apply();
+ },
+ (err) => {
+ $location.url("/404");
+ }
+ );
+ $scope.$watch("anonymize", () => {
+ $scope.anonymizeForm.pullRequestId.$$element[0].disabled = true;
+ $scope.anonymizeForm.pullRequestUrl.$$element[0].disabled = true;
+ });
+ }
+ });
+
+ $scope.pullRequestSelected = async () => {
+ $scope.terms = $scope.defaultTerms;
+ $scope.pullRequestId = "";
+ $scope.source = {};
+
+ try {
+ const o = parseGithubUrl($scope.pullRequestUrl);
+ if (!o.pullRequestId) {
+ $scope.anonymizeForm.pullRequestUrl.$setValidity("github", false);
+ return;
+ }
+ $scope.anonymizeForm.pullRequestUrl.$setValidity("github", true);
+ } catch (error) {
+ $scope.anonymizeForm.pullRequestUrl.$setValidity("github", false);
+ return;
+ }
+ try {
+ await getDetails();
+ } catch (error) {}
+ $scope.$apply();
+ $('[data-toggle="tooltip"]').tooltip();
+ };
+ $('[data-toggle="tooltip"]').tooltip();
+
+ $scope.$watch("options.update", (v) => {});
+
+ async function getDetails() {
+ const o = parseGithubUrl($scope.pullRequestUrl);
+ try {
+ resetValidity();
+ const res = await $http.get(
+ `/api/pr/${o.owner}/${o.repo}/${o.pullRequestId}`
+ );
+ $scope.details = res.data;
+ if ($scope.options.origin) {
+ $scope.pullRequestId = o.repo + "-" + generateRandomId(4);
+ } else {
+ $scope.pullRequestId = generateRandomId(4);
+ }
+ } catch (error) {
+ if (error.data) {
+ $translate("ERRORS." + error.data.error).then((translation) => {
+ $scope.error = translation;
+ }, console.error);
+ displayErrorMessage(error.data.error);
+ }
+ $scope.anonymizeForm.pullRequestUrl.$setValidity("missing", false);
+ throw error;
+ }
+ }
+
+ 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;
+ },
+ (err) => {
+ $scope.conference_data = null;
+ }
+ );
+ }
+
+ $scope.anonymize = function (content) {
+ const urlRegex =
+ /\b((https?|ftp|file):\/\/)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]\b\/?>?/g;
+
+ if (!$scope.options.image) {
+ // remove images
+ content = content.replace(
+ /!\[[^\]]*\]\((?
.*?)(?=\"|\))(?\".*\")?\)/g,
+ ""
+ );
+ }
+ if (!$scope.options.link) {
+ content = content.replace(
+ urlRegex,
+ $scope.site_options.ANONYMIZATION_MASK
+ );
+ }
+ const terms = $scope.terms.split("\n");
+ for (let i = 0; i < terms.length; i++) {
+ const term = terms[i];
+ if (term.trim() == "") {
+ continue;
+ }
+ // remove whole url if it contains the term
+ content = content.replace(urlRegex, (match) => {
+ if (new RegExp(`\\b${term}\\b`, "gi").test(match))
+ return $scope.site_options.ANONYMIZATION_MASK + "-" + (i + 1);
+ return match;
+ });
+
+ // remove the term in the text
+ content = content.replace(
+ new RegExp(`\\b${term}\\b`, "gi"),
+ $scope.site_options.ANONYMIZATION_MASK + "-" + (i + 1)
+ );
+ }
+ return content;
+ };
+
+ function resetValidity() {
+ $scope.anonymizeForm.pullRequestId.$setValidity("used", true);
+ $scope.anonymizeForm.pullRequestId.$setValidity("format", true);
+ $scope.anonymizeForm.pullRequestUrl.$setValidity("used", true);
+ $scope.anonymizeForm.pullRequestUrl.$setValidity("missing", true);
+ $scope.anonymizeForm.pullRequestUrl.$setValidity("access", true);
+ $scope.anonymizeForm.conference.$setValidity("activated", true);
+ $scope.anonymizeForm.terms.$setValidity("format", true);
+ $scope.anonymizeForm.terms.$setValidity("format", true);
+ }
+
+ function displayErrorMessage(message) {
+ switch (message) {
+ case "repoId_already_used":
+ $scope.anonymizeForm.repoId.$setValidity("used", false);
+ break;
+ case "invalid_repoId":
+ $scope.anonymizeForm.repoId.$setValidity("format", false);
+ break;
+ case "options_not_provided":
+ $scope.anonymizeForm.repoId.$setValidity("format", false);
+ break;
+ case "repo_already_anonymized":
+ $scope.anonymizeForm.repoUrl.$setValidity("used", false);
+ break;
+ case "invalid_terms_format":
+ $scope.anonymizeForm.terms.$setValidity("format", false);
+ break;
+ case "invalid_terms_format":
+ $scope.anonymizeForm.terms.$setValidity("format", false);
+ break;
+ case "repo_not_found":
+ $scope.anonymizeForm.repoUrl.$setValidity("missing", false);
+ break;
+ case "repo_not_accessible":
+ $scope.anonymizeForm.repoUrl.$setValidity("access", false);
+ break;
+ case "conf_not_activated":
+ $scope.anonymizeForm.conference.$setValidity("activated", false);
+ break;
+ default:
+ $scope.anonymizeForm.$setValidity("error", false);
+ break;
+ }
+ }
+
+ function getPullRequest() {
+ const o = parseGithubUrl($scope.pullRequestUrl);
+ return {
+ 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,
+ };
+ }
+
+ async function sendPullRequest(url) {
+ resetValidity();
+ try {
+ const newPR = getPullRequest();
+ await $http.post(url, newPR, {
+ headers: { "Content-Type": "application/json" },
+ });
+ window.location.href = "/pr/" + $scope.pullRequestId;
+ } catch (error) {
+ if (error.data) {
+ $translate("ERRORS." + error.data.error).then((translation) => {
+ $scope.error = translation;
+ }, console.error);
+ displayErrorMessage(error.data.error);
+ } else {
+ console.error(error);
+ }
+ }
+ }
+
+ $scope.anonymizePullRequest = (event) => {
+ event.target.disabled = true;
+ sendPullRequest("/api/pr/").finally(() => {
+ event.target.disabled = false;
+ $scope.$apply();
+ });
+ };
+
+ $scope.updatePullRequest = async (event) => {
+ event.target.disabled = true;
+ sendPullRequest("/api/pr/" + $scope.pullRequestId).finally(() => {
+ event.target.disabled = false;
+ $scope.$apply();
+ });
+ };
+
+ $scope.$watch("conference", async (v) => {
+ getConference();
+ });
+ },
+ ])
+ .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("conferencesController", [
"$scope",
"$http",
diff --git a/public/script/utils.js b/public/script/utils.js
index b96d609..3fda749 100644
--- a/public/script/utils.js
+++ b/public/script/utils.js
@@ -8,7 +8,7 @@ function urlRel2abs(url) {
return url; //Url is already absolute
}
var base_url = location.href.match(/^(.+)\/?(?:#.+)?$/)[0] + "/";
-
+
if (url.substring(0, 2) == "//") return location.protocol + url;
else if (url.charAt(0) == "/")
return location.protocol + "//" + location.host + url;
@@ -28,7 +28,7 @@ function urlRel2abs(url) {
.replace(/'/g, "%27")
.replace(//g, "%3E");
-
+
return url;
}
@@ -93,11 +93,17 @@ function generateRandomId(length) {
}
function parseGithubUrl(url) {
- var matches = url.replace(".git", "").match(/.*?github.com\/([\w-\._]+)\/([\w-\._]+)/);
- if (matches && matches.length == 3) {
+ if (!url) throw "Invalid url";
+ const matches = url
+ .replace(".git", "")
+ .match(
+ /.*?github.com\/(?[\w-\._]+)\/(?[\w-\._]+)(\/pull\/(?[0-9]+))?/
+ );
+ if (matches && matches.groups.owner && matches.groups.repo) {
return {
- owner: matches[1],
- repo: matches[2],
+ owner: matches.groups.owner,
+ repo: matches.groups.repo,
+ pullRequestId: matches.groups.PR,
};
} else {
throw "Invalid url";
diff --git a/src/PullRequest.ts b/src/PullRequest.ts
new file mode 100644
index 0000000..be379f8
--- /dev/null
+++ b/src/PullRequest.ts
@@ -0,0 +1,311 @@
+import { RepositoryStatus, Source, Tree, TreeElement, TreeFile } from "./types";
+import User from "./User";
+import { anonymizeContent, anonymizePath } from "./anonymize-utils";
+import UserModel from "./database/users/users.model";
+import Conference from "./Conference";
+import ConferenceModel from "./database/conference/conferences.model";
+import AnonymousError from "./AnonymousError";
+import { IAnonymizedPullRequestDocument } from "./database/anonymizedPullRequests/anonymizedPullRequests.types";
+import config from "../config";
+import { Octokit } from "@octokit/rest";
+import got from "got";
+
+export default class PullRequest {
+ private _model: IAnonymizedPullRequestDocument;
+ owner: User;
+
+ constructor(data: IAnonymizedPullRequestDocument) {
+ this._model = data;
+ this.owner = new User(new UserModel({ _id: data.owner }));
+ }
+
+ getToken() {
+ if (this.owner && this.owner.accessToken) {
+ return this.owner.accessToken;
+ }
+ if (this._model.source.accessToken) {
+ try {
+ return this._model.source.accessToken;
+ } catch (error) {
+ console.debug("[ERROR] Token is invalid", this.pullRequestId);
+ }
+ }
+ return config.GITHUB_TOKEN;
+ }
+
+ async download() {
+ console.debug("[INFO] Downloading pull request", this.pullRequestId);
+ const auth = this.getToken();
+ const octokit = new Octokit({ auth });
+ const [owner, repo] = this._model.source.repositoryFullName.split("/");
+ const pull_number = this._model.source.pullRequestId;
+ const prInfo = await octokit.rest.pulls.get({
+ owner,
+ repo,
+ pull_number,
+ });
+
+ prInfo.data.updated_at;
+ prInfo.data.draft;
+ prInfo.data.merged;
+ prInfo.data.merged_at;
+ prInfo.data.state;
+ prInfo.data.base.repo.full_name;
+ prInfo.data.head.repo.full_name;
+ const comments = await octokit.rest.issues.listComments({
+ owner,
+ repo,
+ issue_number: pull_number,
+ per_page: 100,
+ });
+ // const commits = await octokit.rest.pulls.listCommits({
+ // owner,
+ // repo,
+ // pull_number,
+ // per_page: 100,
+ // });
+ // const files = await octokit.rest.pulls.listFiles({
+ // owner,
+ // repo,
+ // pull_number,
+ // per_page: 100,
+ // });
+ const diff = await got(prInfo.data.diff_url);
+ this._model.pullRequest = {
+ diff: diff.body,
+ title: prInfo.data.title,
+ body: prInfo.data.body,
+ creationDate: new Date(prInfo.data.created_at),
+ updatedDate: new Date(prInfo.data.updated_at),
+ draft: prInfo.data.draft,
+ merged: prInfo.data.merged,
+ mergedDate: prInfo.data.merged_at
+ ? new Date(prInfo.data.merged_at)
+ : null,
+ state: prInfo.data.state,
+ baseRepositoryFullName: prInfo.data.base.repo.full_name,
+ headRepositoryFullName: prInfo.data.head.repo.full_name,
+ comments: comments.data.map((comment) => ({
+ body: comment.body,
+ creationDate: new Date(comment.created_at),
+ updatedDate: new Date(comment.updated_at),
+ author: comment.user.login,
+ })),
+ };
+ }
+
+ /**
+ * Check the status of the pullRequest
+ */
+ check() {
+ if (
+ this._model.options.expirationMode !== "never" &&
+ this.status == "ready"
+ ) {
+ if (this._model.options.expirationDate <= new Date()) {
+ this.expire();
+ }
+ }
+ if (
+ this.status == "expired" ||
+ this.status == "expiring" ||
+ this.status == "removing" ||
+ this.status == "removed"
+ ) {
+ throw new AnonymousError("pullRequest_expired", {
+ object: this,
+ httpStatus: 410,
+ });
+ }
+ const fiveMinuteAgo = new Date();
+ fiveMinuteAgo.setMinutes(fiveMinuteAgo.getMinutes() - 5);
+
+ if (
+ this.status == "preparing" ||
+ (this.status == "download" && this._model.statusDate > fiveMinuteAgo)
+ ) {
+ throw new AnonymousError("pullRequest_not_ready", {
+ object: this,
+ });
+ }
+ }
+
+ /**
+ * Update the pullRequest if a new commit exists
+ *
+ * @returns void
+ */
+ async updateIfNeeded(opt?: { force: boolean }): Promise {
+ const yesterday = new Date();
+ yesterday.setDate(yesterday.getDate() - 1);
+ if (
+ opt?.force ||
+ (this._model.options.update && this._model.lastView < yesterday)
+ ) {
+ await this.download();
+ this._model.lastView = new Date();
+ await this._model.save();
+ }
+ }
+ /**
+ * Download the require state for the pullRequest to work
+ *
+ * @returns void
+ */
+ async anonymize() {
+ if (this.status == "ready") return;
+ await this.updateStatus("preparing");
+ await this.updateIfNeeded({ force: true });
+ return this.updateStatus("ready");
+ }
+
+ /**
+ * Update the last view and view count
+ */
+ async countView() {
+ this._model.lastView = new Date();
+ this._model.pageView = (this._model.pageView || 0) + 1;
+ return this._model.save();
+ }
+
+ /**
+ * Update the status of the pullRequest
+ * @param status the new status
+ * @param errorMessage a potential error message to display
+ */
+ async updateStatus(status: RepositoryStatus, statusMessage?: string) {
+ this._model.status = status;
+ this._model.statusDate = new Date();
+ this._model.statusMessage = statusMessage;
+ return this._model.save();
+ }
+
+ /**
+ * Expire the pullRequest
+ */
+ async expire() {
+ await this.updateStatus("expiring");
+ await this.resetSate();
+ await this.updateStatus("expired");
+ }
+
+ /**
+ * Remove the pullRequest
+ */
+ async remove() {
+ await this.updateStatus("removing");
+ await this.resetSate();
+ await this.updateStatus("removed");
+ }
+
+ /**
+ * Reset/delete the state of the pullRequest
+ */
+ async resetSate(status?: RepositoryStatus, statusMessage?: string) {
+ if (status) this._model.status = status;
+ if (statusMessage) this._model.statusMessage = statusMessage;
+ // remove cache
+ this._model.pullRequest = null;
+ return Promise.all([this._model.save()]);
+ }
+
+ /**
+ * Returns the conference of the pullRequest
+ *
+ * @returns conference of the pullRequest
+ */
+ async conference(): Promise {
+ if (!this._model.conference) {
+ return null;
+ }
+ const conference = await ConferenceModel.findOne({
+ conferenceID: this._model.conference,
+ });
+ if (conference) return new Conference(conference);
+ return null;
+ }
+
+ /***** Getters ********/
+
+ get pullRequestId() {
+ return this._model.pullRequestId;
+ }
+
+ get options() {
+ return this._model.options;
+ }
+
+ get source() {
+ return this._model.source;
+ }
+
+ get model() {
+ return this._model;
+ }
+
+ get status() {
+ return this._model.status;
+ }
+
+ content() {
+ const output: any = {
+ anonymizeDate: this._model.anonymizeDate,
+ merged: this._model.pullRequest.merged,
+ mergedDate: this._model.pullRequest.mergedDate,
+ state: this._model.pullRequest.state,
+ draft: this._model.pullRequest.draft,
+ };
+ if (this.options.title) {
+ output.title = anonymizeContent(this._model.pullRequest.title, this);
+ }
+ if (this.options.body) {
+ output.body = anonymizeContent(this._model.pullRequest.body, this);
+ }
+ if (this.options.comments) {
+ output.comments = this._model.pullRequest.comments.map((comment) => {
+ const o: any = {};
+ if (this.options.body) o.body = anonymizeContent(comment.body, this);
+ if (this.options.username)
+ o.author = anonymizeContent(comment.author, this);
+ if (this.options.date) {
+ o.updatedDate = comment.updatedDate;
+ o.creationDate = comment.creationDate;
+ }
+ return o;
+ });
+ }
+ if (this.options.diff) {
+ output.diff = anonymizeContent(this._model.pullRequest.diff, this);
+ }
+ if (this.options.origin) {
+ output.baseRepositoryFullName =
+ this._model.pullRequest.baseRepositoryFullName;
+ }
+ if (this.options.date) {
+ output.updatedDate = this.model.pullRequest.updatedDate;
+ output.creationDate = this.model.pullRequest.creationDate;
+ }
+ return output;
+ }
+
+ toJSON() {
+ return {
+ pullRequestId: this._model.pullRequestId,
+ options: this._model.options,
+ conference: this._model.conference,
+ anonymizeDate: this._model.anonymizeDate,
+ status: this._model.status,
+ state: this.model.pullRequest.state,
+ merged: this.model.pullRequest.merged,
+ mergedDate: this.model.pullRequest.mergedDate,
+ statusMessage: this._model.statusMessage,
+ source: {
+ pullRequestId: this._model.source.pullRequestId,
+ repositoryFullName: this._model.source.repositoryFullName,
+ },
+ pullRequest: this._model.pullRequest,
+ lastView: this._model.lastView,
+ pageView: this._model.pageView,
+ };
+ }
+}
diff --git a/src/User.ts b/src/User.ts
index 147a7d6..26cd1a6 100644
--- a/src/User.ts
+++ b/src/User.ts
@@ -4,6 +4,8 @@ import RepositoryModel from "./database/repositories/repositories.model";
import { IUserDocument } from "./database/users/users.types";
import Repository from "./Repository";
import { GitHubRepository } from "./source/GitHubRepository";
+import PullRequest from "./PullRequest";
+import AnonymizedPullRequestModel from "./database/anonymizedPullRequests/anonymizedPullRequests.model";
/**
* Model for a user
@@ -136,6 +138,31 @@ export default class User {
await Promise.all(promises);
return repositories;
}
+ /**
+ * Get the lost of anonymized repositories
+ * @returns the list of anonymized repositories
+ */
+ async getPullRequests() {
+ const pullRequests = (
+ await AnonymizedPullRequestModel.find({
+ owner: this.id,
+ }).exec()
+ ).map((d) => new PullRequest(d));
+ const promises = [];
+ for (let repo of pullRequests) {
+ if (
+ repo.status == "ready" &&
+ repo.options.expirationMode != "never" &&
+ repo.options.expirationDate != null &&
+ repo.options.expirationDate < new Date()
+ ) {
+ // expire the repository
+ promises.push(repo.expire());
+ }
+ }
+ await Promise.all(promises);
+ return pullRequests;
+ }
get model() {
return this._model;
diff --git a/src/anonymize-utils.ts b/src/anonymize-utils.ts
index 46d58fb..fc70e5d 100644
--- a/src/anonymize-utils.ts
+++ b/src/anonymize-utils.ts
@@ -72,7 +72,24 @@ export function anonymizeStream(filename: string, repository: Repository) {
return ts;
}
-export function anonymizeContent(content: string, repository: Repository) {
+interface Anonymizationptions {
+ repoId?: string;
+ source?: {};
+ options: {
+ terms: string[];
+ image: boolean;
+ link: boolean;
+ pageSource?: {
+ branch: string;
+ path: string;
+ };
+ };
+}
+
+export function anonymizeContent(
+ content: string,
+ repository: Anonymizationptions
+) {
if (repository.options?.image === false) {
// remove image in markdown
content = content.replace(
diff --git a/src/database/anonymizedPullRequests/anonymizedPullRequests.model.ts b/src/database/anonymizedPullRequests/anonymizedPullRequests.model.ts
new file mode 100644
index 0000000..9c1e2f5
--- /dev/null
+++ b/src/database/anonymizedPullRequests/anonymizedPullRequests.model.ts
@@ -0,0 +1,14 @@
+import { model } from "mongoose";
+
+import AnonymizedPullRequestSchema from "./anonymizedPullRequests.schema";
+import {
+ IAnonymizedPullRequestDocument,
+ IAnonymizedPullRequestModel,
+} from "./anonymizedPullRequests.types";
+
+const AnonymizedPullRequestModel = model(
+ "AnonymizedPullRequest",
+ AnonymizedPullRequestSchema
+) as IAnonymizedPullRequestModel;
+
+export default AnonymizedPullRequestModel;
diff --git a/src/database/anonymizedPullRequests/anonymizedPullRequests.schema.ts b/src/database/anonymizedPullRequests/anonymizedPullRequests.schema.ts
new file mode 100644
index 0000000..290a2e1
--- /dev/null
+++ b/src/database/anonymizedPullRequests/anonymizedPullRequests.schema.ts
@@ -0,0 +1,66 @@
+import { Schema } from "mongoose";
+
+const AnonymizedPullRequestSchema = new Schema({
+ pullRequestId: {
+ type: String,
+ index: { unique: true },
+ },
+ status: {
+ type: String,
+ default: "preparing",
+ },
+ statusDate: Date,
+ statusMessage: String,
+ anonymizeDate: Date,
+ lastView: Date,
+ pageView: Number,
+ owner: Schema.Types.ObjectId,
+ conference: String,
+ source: {
+ pullRequestId: Number,
+ repositoryFullName: String,
+ accessToken: String,
+ },
+ options: {
+ terms: [String],
+ expirationMode: { type: String },
+ expirationDate: Date,
+ update: Boolean,
+ image: Boolean,
+ link: Boolean,
+ title: Boolean,
+ body: Boolean,
+ comments: Boolean,
+ diff: Boolean,
+ origin: Boolean,
+ username: Boolean,
+ date: Boolean,
+ },
+ dateOfEntry: {
+ type: Date,
+ default: new Date(),
+ },
+ pullRequest: {
+ diff: String,
+ title: String,
+ body: String,
+ creationDate: Date,
+ updatedDate: Date,
+ draft: Boolean,
+ merged: Boolean,
+ mergedDate: Date,
+ state: String,
+ baseRepositoryFullName: String,
+ headRepositoryFullName: String,
+ comments: [
+ {
+ body: String,
+ creationDate: Date,
+ updatedDate: Date,
+ author: String,
+ },
+ ],
+ },
+});
+
+export default AnonymizedPullRequestSchema;
diff --git a/src/database/anonymizedPullRequests/anonymizedPullRequests.types.ts b/src/database/anonymizedPullRequests/anonymizedPullRequests.types.ts
new file mode 100644
index 0000000..17e0c8b
--- /dev/null
+++ b/src/database/anonymizedPullRequests/anonymizedPullRequests.types.ts
@@ -0,0 +1,61 @@
+import { Document, Model } from "mongoose";
+import { RepositoryStatus } from "../../types";
+
+export interface IAnonymizedPullRequest {
+ pullRequestId: string;
+ status?: RepositoryStatus;
+ statusMessage?: string;
+ statusDate: Date;
+ anonymizeDate: Date;
+ source: {
+ pullRequestId: number;
+ repositoryFullName?: string;
+ accessToken?: string;
+ };
+ owner: string;
+ conference: string;
+ options: {
+ terms: string[];
+ expirationMode: "never" | "redirect" | "remove";
+ expirationDate?: Date;
+ update: boolean;
+ image: boolean;
+ link: boolean;
+ title: boolean;
+ body: boolean;
+ comments: boolean;
+ diff: boolean;
+ origin: boolean;
+ username: boolean;
+ date: boolean;
+ };
+ pageView: number;
+ lastView: Date;
+ pullRequest: {
+ diff: string;
+ title: string;
+ body: string;
+ creationDate: Date;
+ updatedDate: Date;
+ draft?: boolean;
+ merged?: boolean;
+ mergedDate?: Date;
+ state?: string;
+ baseRepositoryFullName?: string;
+ headRepositoryFullName?: string;
+ comments?: {
+ body: string;
+ creationDate: Date;
+ updatedDate: Date;
+ author: string;
+ }[];
+ };
+}
+
+export interface IAnonymizedPullRequestDocument
+ extends IAnonymizedPullRequest,
+ Document {
+ setLastUpdated: (this: IAnonymizedPullRequestDocument) => Promise;
+}
+export interface IAnonymizedPullRequestModel
+ extends Model {}
diff --git a/src/database/database.ts b/src/database/database.ts
index 5e52d2f..3972302 100644
--- a/src/database/database.ts
+++ b/src/database/database.ts
@@ -3,6 +3,8 @@ import Repository from "../Repository";
import config from "../../config";
import AnonymizedRepositoryModel from "./anonymizedRepositories/anonymizedRepositories.model";
import AnonymousError from "../AnonymousError";
+import AnonymizedPullRequestModel from "./anonymizedPullRequests/anonymizedPullRequests.model";
+import PullRequest from "../PullRequest";
const MONGO_URL = `mongodb://${config.DB_USERNAME}:${config.DB_PASSWORD}@${config.DB_HOSTNAME}:27017/`;
@@ -17,7 +19,7 @@ export async function connect() {
}
export async function getRepository(repoId: string) {
- if (!repoId || repoId == 'undefined') {
+ if (!repoId || repoId == "undefined") {
throw new AnonymousError("repo_not_found", {
object: repoId,
httpStatus: 404,
@@ -31,3 +33,20 @@ export async function getRepository(repoId: string) {
});
return new Repository(data);
}
+export async function getPullRequest(pullRequestId: string) {
+ if (!pullRequestId || pullRequestId == "undefined") {
+ throw new AnonymousError("pull_request_not_found", {
+ object: pullRequestId,
+ httpStatus: 404,
+ });
+ }
+ const data = await AnonymizedPullRequestModel.findOne({
+ pullRequestId,
+ });
+ if (!data)
+ throw new AnonymousError("pull_request_not_found", {
+ object: pullRequestId,
+ httpStatus: 404,
+ });
+ return new PullRequest(data);
+}
diff --git a/src/routes/index.ts b/src/routes/index.ts
index 3e0174e..7cf0980 100644
--- a/src/routes/index.ts
+++ b/src/routes/index.ts
@@ -1,3 +1,5 @@
+import pullRequestPrivate from "./pullRequest-private";
+import pullRequestPublic from "./pullRequest-public";
import repositoryPrivate from "./repository-private";
import repositoryPublic from "./repository-public";
import conference from "./conference";
@@ -8,6 +10,8 @@ import option from "./option";
import admin from "./admin";
export default {
+ pullRequestPrivate,
+ pullRequestPublic,
repositoryPrivate,
repositoryPublic,
file,
diff --git a/src/routes/pullRequest-private.ts b/src/routes/pullRequest-private.ts
new file mode 100644
index 0000000..b9dde8d
--- /dev/null
+++ b/src/routes/pullRequest-private.ts
@@ -0,0 +1,245 @@
+import * as express from "express";
+import { ensureAuthenticated } from "./connection";
+
+import {
+ getPullRequest,
+ getUser,
+ handleError,
+ isOwnerOrAdmin,
+} from "./route-utils";
+import AnonymousError from "../AnonymousError";
+import { IAnonymizedPullRequestDocument } from "../database/anonymizedPullRequests/anonymizedPullRequests.types";
+import PullRequest from "../PullRequest";
+import AnonymizedPullRequestModel from "../database/anonymizedPullRequests/anonymizedPullRequests.model";
+
+const router = express.Router();
+
+// user needs to be connected for all user API
+router.use(ensureAuthenticated);
+
+// refresh pullRequest
+router.post(
+ "/:pullRequestId/refresh",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const pullRequest = await getPullRequest(req, res, { nocheck: true });
+ if (!pullRequest) return;
+
+ if (
+ pullRequest.status == "preparing" ||
+ pullRequest.status == "removing" ||
+ pullRequest.status == "expiring"
+ )
+ return;
+
+ const user = await getUser(req);
+ isOwnerOrAdmin([pullRequest.owner.id], user);
+ await pullRequest.anonymize()
+ res.json({ status: pullRequest.status });
+ } catch (error) {
+ handleError(error, res, req);
+ }
+ }
+);
+
+// delete a pullRequest
+router.delete(
+ "/:pullRequestId/",
+ async (req: express.Request, res: express.Response) => {
+ const pullRequest = await getPullRequest(req, res, { nocheck: true });
+ if (!pullRequest) return;
+ try {
+ if (pullRequest.status == "removed")
+ throw new AnonymousError("is_removed", {
+ object: req.params.pullRequestId,
+ httpStatus: 410,
+ });
+ const user = await getUser(req);
+ isOwnerOrAdmin([pullRequest.owner.id], user);
+ await pullRequest.remove();
+ return res.json({ status: pullRequest.status });
+ } catch (error) {
+ handleError(error, res, req);
+ }
+ }
+);
+
+router.get(
+ "/:owner/:repository/:pullRequestId",
+ async (req: express.Request, res: express.Response) => {
+ const user = await getUser(req);
+ try {
+ const pullRequest = new PullRequest(
+ new AnonymizedPullRequestModel({
+ owner: user.id,
+ source: {
+ pullRequestId: parseInt(req.params.pullRequestId),
+ repositoryFullName: `${req.params.owner}/${req.params.repository}`,
+ },
+ })
+ );
+ await pullRequest.download();
+ res.json(pullRequest.toJSON());
+ } catch (error) {
+ handleError(error, res, req);
+ }
+ }
+);
+
+// get pullRequest information
+router.get(
+ "/:pullRequestId/",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const pullRequest = await getPullRequest(req, res, { nocheck: true });
+ if (!pullRequest) return;
+
+ const user = await getUser(req);
+ isOwnerOrAdmin([pullRequest.owner.id], user);
+ res.json(pullRequest.toJSON());
+ } catch (error) {
+ handleError(error, res, req);
+ }
+ }
+);
+
+function validateNewPullRequest(pullRequestUpdate): void {
+ const validCharacters = /^[0-9a-zA-Z\-\_]+$/;
+ if (
+ !pullRequestUpdate.pullRequestId.match(validCharacters) ||
+ pullRequestUpdate.pullRequestId.length < 3
+ ) {
+ throw new AnonymousError("invalid_pullRequestId", {
+ object: pullRequestUpdate,
+ httpStatus: 400,
+ });
+ }
+ if (!pullRequestUpdate.source.repositoryFullName) {
+ throw new AnonymousError("repository_not_specified", {
+ object: pullRequestUpdate,
+ httpStatus: 400,
+ });
+ }
+ if (!pullRequestUpdate.source.pullRequestId) {
+ throw new AnonymousError("pullRequestId_not_specified", {
+ object: pullRequestUpdate,
+ httpStatus: 400,
+ });
+ }
+ if (
+ parseInt(pullRequestUpdate.source.pullRequestId) !=
+ pullRequestUpdate.source.pullRequestId
+ ) {
+ throw new AnonymousError("pullRequestId_is_not_a_number", {
+ object: pullRequestUpdate,
+ httpStatus: 400,
+ });
+ }
+ if (!pullRequestUpdate.options) {
+ throw new AnonymousError("options_not_provided", {
+ object: pullRequestUpdate,
+ httpStatus: 400,
+ });
+ }
+ if (!Array.isArray(pullRequestUpdate.terms)) {
+ throw new AnonymousError("invalid_terms_format", {
+ object: pullRequestUpdate,
+ httpStatus: 400,
+ });
+ }
+}
+
+function updatePullRequestModel(
+ model: IAnonymizedPullRequestDocument,
+ pullRequestUpdate: any
+) {
+ model.options = {
+ terms: pullRequestUpdate.terms,
+ expirationMode: pullRequestUpdate.options.expirationMode,
+ expirationDate: pullRequestUpdate.options.expirationDate
+ ? new Date(pullRequestUpdate.options.expirationDate)
+ : null,
+ update: pullRequestUpdate.options.update,
+ image: pullRequestUpdate.options.image,
+ link: pullRequestUpdate.options.link,
+ body: pullRequestUpdate.options.body,
+ title: pullRequestUpdate.options.title,
+ username: pullRequestUpdate.options.username,
+ origin: pullRequestUpdate.options.origin,
+ diff: pullRequestUpdate.options.diff,
+ comments: pullRequestUpdate.options.comments,
+ date: pullRequestUpdate.options.date,
+ };
+}
+
+// update a pullRequest
+router.post(
+ "/:pullRequestId/",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const pullRequest = await getPullRequest(req, res, { nocheck: true });
+ if (!pullRequest) return;
+ const user = await getUser(req);
+
+ isOwnerOrAdmin([pullRequest.owner.id], user);
+ const pullRequestUpdate = req.body;
+ validateNewPullRequest(pullRequestUpdate);
+ pullRequest.model.anonymizeDate = new Date();
+
+ updatePullRequestModel(pullRequest.model, pullRequestUpdate);
+ // TODO handle conference
+ pullRequest.model.conference = pullRequestUpdate.conference;
+ await pullRequest.updateIfNeeded({ force: true });
+ res.json(pullRequest.toJSON());
+ } catch (error) {
+ return handleError(error, res, req);
+ }
+ }
+);
+
+// add pullRequest
+router.post("/", async (req: express.Request, res: express.Response) => {
+ const user = await getUser(req);
+ const pullRequestUpdate = req.body;
+
+ try {
+ validateNewPullRequest(pullRequestUpdate);
+
+ const pullRequest = new PullRequest(
+ new AnonymizedPullRequestModel({
+ owner: user.id,
+ options: pullRequestUpdate.options,
+ })
+ );
+
+ pullRequest.model.pullRequestId = pullRequestUpdate.pullRequestId;
+ pullRequest.model.anonymizeDate = new Date();
+ pullRequest.model.owner = user.id;
+
+ updatePullRequestModel(pullRequest.model, pullRequestUpdate);
+ pullRequest.source.accessToken = user.accessToken;
+ pullRequest.source.pullRequestId = pullRequestUpdate.source.pullRequestId;
+ pullRequest.source.repositoryFullName =
+ pullRequestUpdate.source.repositoryFullName;
+
+ pullRequest.conference = pullRequestUpdate.conference;
+
+ await pullRequest.anonymize()
+ res.send(pullRequest.toJSON());
+ } catch (error) {
+ if (error.message?.indexOf(" duplicate key") > -1) {
+ return handleError(
+ new AnonymousError("pullRequestId_already_used", {
+ httpStatus: 400,
+ cause: error,
+ object: pullRequestUpdate,
+ }),
+ res,
+ req
+ );
+ }
+ return handleError(error, res, req);
+ }
+});
+
+export default router;
diff --git a/src/routes/pullRequest-public.ts b/src/routes/pullRequest-public.ts
new file mode 100644
index 0000000..8224358
--- /dev/null
+++ b/src/routes/pullRequest-public.ts
@@ -0,0 +1,84 @@
+import * as express from "express";
+
+import { getPullRequest, handleError } from "./route-utils";
+import AnonymousError from "../AnonymousError";
+
+const router = express.Router();
+
+router.get(
+ "/:pullRequestId/options",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ res.header("Cache-Control", "no-cache");
+ const pr = await getPullRequest(req, res, { nocheck: true });
+ if (!pr) return;
+ let redirectURL = null;
+ if (pr.status == "expired" && pr.options.expirationMode == "redirect") {
+ redirectURL = `https://github.com/${pr.source.repositoryFullName}/pull/${pr.source.pullRequestId}`;
+ } else {
+ if (
+ pr.status == "expired" ||
+ pr.status == "expiring" ||
+ pr.status == "removing" ||
+ pr.status == "removed"
+ ) {
+ throw new AnonymousError("pull_request_expired", {
+ object: pr,
+ httpStatus: 410,
+ });
+ }
+
+ const fiveMinuteAgo = new Date();
+ fiveMinuteAgo.setMinutes(fiveMinuteAgo.getMinutes() - 5);
+ if (pr.status != "ready") {
+ if (
+ pr.model.statusDate < fiveMinuteAgo
+ // && repo.status != "preparing"
+ ) {
+ await pr.updateIfNeeded({ force: true });
+ }
+ if (pr.status == "error") {
+ throw new AnonymousError(
+ pr.model.statusMessage
+ ? pr.model.statusMessage
+ : "pull_request_not_available",
+ {
+ object: pr,
+ httpStatus: 500,
+ }
+ );
+ }
+ throw new AnonymousError("pull_request_not_ready", {
+ httpStatus: 404,
+ object: pr,
+ });
+ }
+
+ await pr.updateIfNeeded();
+ }
+
+ res.json({
+ url: redirectURL,
+ lastUpdateDate: pr.model.statusDate,
+ });
+ } catch (error) {
+ handleError(error, res, req);
+ }
+ }
+);
+router.get(
+ "/:pullRequestId/content",
+ async (req: express.Request, res: express.Response) => {
+ const pullRequest = await getPullRequest(req, res);
+ if (!pullRequest) return;
+ try {
+ await pullRequest.countView();
+ res.header("Cache-Control", "no-cache");
+ res.json(pullRequest.content());
+ } catch (error) {
+ handleError(error, res, req);
+ }
+ }
+);
+
+export default router;
diff --git a/src/routes/repository-public.ts b/src/routes/repository-public.ts
index 519bd68..f88c0ba 100644
--- a/src/routes/repository-public.ts
+++ b/src/routes/repository-public.ts
@@ -60,11 +60,10 @@ router.get(
router.get(
"/:repoId/files",
async (req: express.Request, res: express.Response) => {
+ res.header("Cache-Control", "no-cache");
const repo = await getRepo(req, res);
if (!repo) return;
try {
- res.header("Cache-Control", "no-cache");
-
res.json(await repo.anonymizedFiles({ includeSha: false }));
} catch (error) {
handleError(error, res, req);
@@ -76,6 +75,7 @@ router.get(
"/:repoId/options",
async (req: express.Request, res: express.Response) => {
try {
+ res.header("Cache-Control", "no-cache");
const repo = await getRepo(req, res, { nocheck: true });
if (!repo) return;
let redirectURL = null;
@@ -146,7 +146,6 @@ router.get(
download = true;
}
- res.header("Cache-Control", "no-cache");
res.json({
url: redirectURL,
download,
diff --git a/src/routes/route-utils.ts b/src/routes/route-utils.ts
index 2092bc3..9c97500 100644
--- a/src/routes/route-utils.ts
+++ b/src/routes/route-utils.ts
@@ -5,6 +5,35 @@ import UserModel from "../database/users/users.model";
import User from "../User";
import * as io from "@pm2/io";
+export async function getPullRequest(
+ req: express.Request,
+ res: express.Response,
+ opt?: { nocheck?: boolean }
+) {
+ try {
+ const pullRequest = await db.getPullRequest(req.params.pullRequestId);
+ if (opt?.nocheck == true) {
+ } else {
+ // redirect if the repository is expired
+ if (
+ pullRequest.status == "expired" &&
+ pullRequest.options.expirationMode == "redirect"
+ ) {
+ res.redirect(
+ `http://github.com/${pullRequest.source.repositoryFullName}/pull/${pullRequest.source.pullRequestId}`
+ );
+ return null;
+ }
+
+ pullRequest.check();
+ }
+ return pullRequest;
+ } catch (error) {
+ handleError(error, res, req);
+ return null;
+ }
+}
+
export async function getRepo(
req: express.Request,
res: express.Response,
@@ -50,7 +79,7 @@ function printError(error: any, req?: express.Request) {
if (req) {
message += ` ${req.originalUrl}`;
// ignore common error
- if (req.originalUrl === '/api/repo/undefined/options') return
+ if (req.originalUrl === "/api/repo/undefined/options") return;
}
console.error(message);
} else if (error instanceof Error) {
diff --git a/src/routes/user.ts b/src/routes/user.ts
index 73af85a..e2ee748 100644
--- a/src/routes/user.ts
+++ b/src/routes/user.ts
@@ -97,6 +97,21 @@ router.get(
}
}
);
+router.get(
+ "/anonymized_pull_requests",
+ async (req: express.Request, res: express.Response) => {
+ try {
+ const user = await getUser(req);
+ res.json(
+ (await user.getPullRequests()).map((x) => {
+ return x.toJSON();
+ })
+ );
+ } catch (error) {
+ handleError(error, res, req);
+ }
+ }
+);
router.get(
"/all_repositories",
diff --git a/src/routes/webview.ts b/src/routes/webview.ts
index 21d5462..59fff13 100644
--- a/src/routes/webview.ts
+++ b/src/routes/webview.ts
@@ -6,7 +6,7 @@ import GitHubDownload from "../source/GitHubDownload";
import AnonymousError from "../AnonymousError";
import { TreeElement } from "../types";
import * as marked from "marked";
-import { anonymizeContent, streamToString } from "../anonymize-utils";
+import { streamToString } from "../anonymize-utils";
const router = express.Router();
@@ -103,7 +103,7 @@ async function webView(req: express.Request, res: express.Response) {
}
if ((await f.extension()) == "md") {
const content = await streamToString(await f.anonymizedContent());
- res.send(marked.marked(content));
+ res.contentType("html").send(marked.marked(content));
} else {
f.send(res);
}
diff --git a/src/server.ts b/src/server.ts
index 30b3333..7ca46d2 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -90,6 +90,8 @@ export default async function start() {
apiRouter.use("/repo", router.repositoryPublic);
apiRouter.use("/repo", speedLimiter, router.file);
apiRouter.use("/repo", speedLimiter, router.repositoryPrivate);
+ apiRouter.use("/pr", speedLimiter, router.pullRequestPrivate);
+ apiRouter.use("/pr", speedLimiter, router.pullRequestPublic);
apiRouter.get("/message", async (_, res) => {
if (ofs.existsSync("./message.txt")) {