summaryrefslogtreecommitdiff
path: root/cgithub/src
diff options
context:
space:
mode:
Diffstat (limited to 'cgithub/src')
-rw-r--r--cgithub/src/app.test.ts68
-rw-r--r--cgithub/src/app.ts26
-rw-r--r--cgithub/src/scraper.test.ts32
-rw-r--r--cgithub/src/scraper.ts28
4 files changed, 148 insertions, 6 deletions
diff --git a/cgithub/src/app.test.ts b/cgithub/src/app.test.ts
index 94bf205..e28c375 100644
--- a/cgithub/src/app.test.ts
+++ b/cgithub/src/app.test.ts
@@ -1,6 +1,6 @@
import assert from "node:assert";
import path from "node:path";
-import { describe, it } from "node:test";
+import { describe, it, type TestContext } from "node:test";
import { Eta } from "eta";
import { createApp } from "./app.ts";
@@ -100,12 +100,30 @@ describe("blob view", () => {
language: null,
size: "815 KB",
image: false,
+ renderFileType: null,
textLines: null,
htmlLines: null,
htmlContent: null,
};
- // GitHub previews neither the text nor the bytes of most binary files.
+ it("should embed a PDF", () => {
+ const html = eta.render("blob.eta", {
+ ...blob,
+ path: "test/pdfs/basicapi.pdf",
+ renderFileType: "pdf",
+ });
+
+ assert.match(html, /<object[^>]*type="application\/pdf"/);
+ assert.match(html, /data="\/justrajdeep\/fonts\/embed\/pdf\/master\/test\/pdfs\/basicapi.pdf"/);
+ });
+
+ it("should not embed a file type it has no viewer for", () => {
+ const html = eta.render("blob.eta", { ...blob, renderFileType: "stl" });
+
+ assert.doesNotMatch(html, /<object/);
+ assert.match(html, /This file cannot be displayed\./);
+ });
+
it("should offer the raw file when there is nothing to display", () => {
const html = eta.render("blob.eta", blob);
@@ -114,6 +132,52 @@ describe("blob view", () => {
});
});
+describe("embedded files", () => {
+ function stubGitHub(t: TestContext, body: string, contentType: string, status = 200) {
+ t.mock.method(
+ globalThis,
+ "fetch",
+ async () => new Response(body, { status, headers: { "Content-Type": contentType } }),
+ );
+ }
+
+ it("should retype a PDF GitHub sends as a download", async (t) => {
+ stubGitHub(t, "%PDF-1.6", "application/octet-stream");
+
+ const res = await app.request("http://cgithub.example/mozilla/pdf.js/embed/pdf/master/doc.pdf");
+
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.headers.get("Content-Type"), "application/pdf");
+ assert.strictEqual(await res.text(), "%PDF-1.6");
+ });
+
+ it("should pass on a type GitHub already got right", async (t) => {
+ stubGitHub(t, "%PDF-1.6", "application/pdf");
+
+ const res = await app.request("http://cgithub.example/mozilla/pdf.js/embed/pdf/master/doc.pdf");
+
+ assert.strictEqual(res.headers.get("Content-Type"), "application/pdf");
+ });
+
+ it("should not serve a file type it has no viewer for", async (t) => {
+ stubGitHub(t, "<script>", "text/html");
+
+ const res = await app.request("http://cgithub.example/mozilla/pdf.js/embed/html/master/x.html");
+
+ assert.strictEqual(res.status, 404);
+ });
+
+ it("should pass on the status of a file GitHub does not have", async (t) => {
+ stubGitHub(t, "404: Not Found", "text/plain", 404);
+
+ const res = await app.request(
+ "http://cgithub.example/mozilla/pdf.js/embed/pdf/master/nope.pdf",
+ );
+
+ assert.strictEqual(res.status, 404);
+ });
+});
+
describe("redirects to GitHub", () => {
// An HTTP redirect would be caught by the extension's declarativeNetRequest
// rules and bounced straight back here.
diff --git a/cgithub/src/app.ts b/cgithub/src/app.ts
index 9781a98..33b408e 100644
--- a/cgithub/src/app.ts
+++ b/cgithub/src/app.ts
@@ -10,6 +10,7 @@ import {
getGitHubLatestCommit,
getGitHubOwner,
getGitHubPulls,
+ getGitHubRaw,
getGitHubRefs,
getGitHubRelease,
getGitHubReleases,
@@ -120,6 +121,31 @@ export function createApp(eta: Eta) {
);
});
+ // This is not a native GitHub route, but instead it lets us render blob
+ // content whose raw headers prevent hotlinking.
+ app.get("/:owner/:repo/embed/:type/:branch/:path{.*}", async (c) => {
+ const { owner, repo, type, branch, path } = c.req.param();
+
+ if (type !== "pdf") {
+ return c.notFound();
+ }
+
+ let response: Response;
+ try {
+ response = await getGitHubRaw(owner, repo, branch, path);
+ } catch (e) {
+ c.status(e instanceof GitHubHTTPError ? (e.status as StatusCode) : 500);
+ return c.body(null);
+ }
+
+ let contentType = response.headers.get("Content-Type");
+ if (contentType === null || contentType === "application/octet-stream") {
+ contentType = "application/pdf";
+ }
+
+ return new Response(response.body, { headers: { "Content-Type": contentType } });
+ });
+
app.get("/:owner/:repo/issues", async (c) => {
const { owner, repo } = c.req.param();
const q = c.req.query("q");
diff --git a/cgithub/src/scraper.test.ts b/cgithub/src/scraper.test.ts
index 836235e..88e9271 100644
--- a/cgithub/src/scraper.test.ts
+++ b/cgithub/src/scraper.test.ts
@@ -7,6 +7,7 @@ import {
getGitHubSidebar,
getGitHubLatestCommit,
getGitHubPulls,
+ getGitHubRaw,
getGitHubRefs,
getGitHubRelease,
getGitHubReleases,
@@ -328,11 +329,41 @@ describe("GitHub scraper", () => {
assert.strictEqual(data.size, "1.26 KB");
assert.strictEqual(data.language, null);
assert.strictEqual(data.image, true);
+ assert.strictEqual(data.renderFileType, null);
assert.strictEqual(data.textLines, null);
assert.strictEqual(data.htmlLines, null);
assert.strictEqual(data.htmlContent, null);
});
+ // GitHub renders a PDF with a viewer of its own, so the page carries no
+ // content for it beyond the file type.
+ it("should support PDF", async () => {
+ const data = await getGitHubBlob("mozilla", "pdf.js", "master", "test/pdfs/basicapi.pdf");
+
+ assert.strictEqual(data.repo.owner, "mozilla");
+ assert.strictEqual(data.repo.name, "pdf.js");
+ assert.strictEqual(data.branch, "master");
+ assert.strictEqual(data.path, "test/pdfs/basicapi.pdf");
+
+ assert.strictEqual(data.size, "103 KB");
+ assert.strictEqual(data.language, null);
+ assert.strictEqual(data.image, false);
+ assert.strictEqual(data.renderFileType, "pdf");
+ assert.strictEqual(data.textLines, null);
+ assert.strictEqual(data.htmlLines, null);
+ assert.strictEqual(data.htmlContent, null);
+ });
+
+ // Why the embed route exists rather than the blob view pointing a viewer
+ // at GitHub: as application/octet-stream a browser downloads the file
+ // instead of displaying it.
+ it("should fetch a raw PDF that GitHub types as a download", async () => {
+ const response = await getGitHubRaw("mozilla", "pdf.js", "master", "test/pdfs/basicapi.pdf");
+
+ assert.strictEqual(response.headers.get("Content-Type"), "application/octet-stream");
+ assert.ok((await response.text()).startsWith("%PDF-"));
+ });
+
// A binary file GitHub has no preview for: no content of any kind, and the
// path exercises escaping of a space.
it("should support an unpreviewable binary file", async () => {
@@ -346,6 +377,7 @@ describe("GitHub scraper", () => {
assert.strictEqual(data.size, "815 KB");
assert.strictEqual(data.language, null);
assert.strictEqual(data.image, false);
+ assert.strictEqual(data.renderFileType, null);
assert.strictEqual(data.textLines, null);
assert.strictEqual(data.htmlLines, null);
assert.strictEqual(data.htmlContent, null);
diff --git a/cgithub/src/scraper.ts b/cgithub/src/scraper.ts
index 4220dc4..1a67d05 100644
--- a/cgithub/src/scraper.ts
+++ b/cgithub/src/scraper.ts
@@ -141,6 +141,7 @@ interface CodeViewBlobLayoutRouteStyledBlob {
interface CodeViewBlobRoute {
richText: string | null;
+ renderedFileInfo?: { renderFileType: string | null } | null;
}
interface SidebarAbout {
@@ -250,6 +251,7 @@ export interface GitHubBlob extends GitHubNav {
language: string | null;
size: string;
image: boolean;
+ renderFileType: string | null;
textLines: string[] | null;
htmlLines: string[] | null;
htmlContent: string | null;
@@ -378,15 +380,19 @@ export interface GitHubOwner {
repos: OwnerRepo[];
}
-async function fetchGitHubResponse(path: string, accept: string): Promise<Response> {
- // GitHub throttles/blocks requests without realistic browser headers.
- // These headers make the request appear as a standard browser visit.
- const headers = {
+// GitHub throttles/blocks requests without realistic browser headers.
+// These headers make the request appear as a standard browser visit.
+function browserHeaders(accept: string): Record<string, string> {
+ return {
"User-Agent":
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
Accept: accept,
"Accept-Language": "en-US,en;q=0.5",
};
+}
+
+async function fetchGitHubResponse(path: string, accept: string): Promise<Response> {
+ const headers = browserHeaders(accept);
const response = await fetch(`https://github.com/${path}`, { headers, redirect: "manual" });
if (response.status === 301 || response.status === 302) {
const location = response.headers.get("location");
@@ -546,12 +552,26 @@ export async function getGitHubBlob(
language: blob.language || null,
size,
image: blob.image,
+ renderFileType: payload?.codeViewBlobRoute?.renderedFileInfo?.renderFileType ?? null,
textLines: styledBlob?.rawLines ?? null,
htmlLines,
htmlContent: payload?.codeViewBlobRoute?.richText ?? null,
});
}
+// For proxying embeds.
+export async function getGitHubRaw(
+ owner: string,
+ repo: string,
+ branch: string,
+ path: string,
+): Promise<Response> {
+ const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${path}`;
+ const response = await fetch(url, { headers: browserHeaders("*/*") });
+ if (!response.ok) throw new GitHubHTTPError(response.status, response.statusText);
+ return response;
+}
+
export async function getGitHubIssues(
owner: string,
repo: string,