Skip to content

Commit b703b0b

Browse files
authored
Enable extension to work on chrome-rendered PDFs (#50)
1 parent 6d58dad commit b703b0b

File tree

5 files changed

+121
-2
lines changed

5 files changed

+121
-2
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mindstudio-chrome-extension",
3-
"version": "0.7.11",
3+
"version": "0.7.12",
44
"engines": {
55
"node": ">= 14.0.0",
66
"npm": ">= 6.0.0"

src/content/launcher/index.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class LauncherService {
7070

7171
// Listen for requests for page data launch variables and respond to them
7272
// (only used when invoking new runs)
73-
runtime.listen('remote/request_launch_variables', () => {
73+
runtime.listen('remote/request_launch_variables', async () => {
7474
console.info(
7575
'[MindStudio][Launcher] Side panel requested launch variables',
7676
);
@@ -79,12 +79,24 @@ export class LauncherService {
7979
const fullText = page.getCleanTextContent();
8080
const metadata = page.getMetadataBundle();
8181

82+
// If it's a PDF, we need to rehost it and pass the rehosted path to the
83+
// launch variables for extraction
84+
let rehostedPdfPath: string | undefined = undefined;
85+
if (
86+
window.location.href.endsWith('.pdf') ||
87+
document.querySelector('embed')?.type.includes('pdf')
88+
) {
89+
console.info('Rehosting PDF for extraction');
90+
rehostedPdfPath = await page.getRehostedPdfSecurePath();
91+
}
92+
8293
const launchVariables: LaunchVariables = {
8394
url: window.location.href,
8495
userSelection,
8596
rawHtml,
8697
fullText,
8798
metadata,
99+
_rehostedPdfPath: rehostedPdfPath,
88100
};
89101

90102
console.info(

src/shared/services/api.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,86 @@ class ApiClient {
7474
enabledSites: app.extensionSupportedSites,
7575
}));
7676
}
77+
78+
async getSignedUploadUrl(): Promise<{
79+
fields: { [index: string]: string };
80+
url: string;
81+
path: string;
82+
}> {
83+
const organizationId = await storage.get('SELECTED_ORGANIZATION');
84+
if (!organizationId) {
85+
throw new Error();
86+
}
87+
88+
const url = `${ApiUrl}/v1/cdn/upload-private`;
89+
const headers = await this.getHeaders();
90+
91+
const request = await fetch(url, {
92+
method: 'POST',
93+
headers: {
94+
...Object.fromEntries(headers.entries()),
95+
'X-Organization-Id': organizationId,
96+
},
97+
body: JSON.stringify({
98+
extension: 'pdf',
99+
appId: '_extensionTemp',
100+
}),
101+
});
102+
103+
if (!request.ok) {
104+
throw new Error(`API request failed: ${request.statusText}`);
105+
}
106+
107+
const response = await request.json();
108+
109+
return response;
110+
}
111+
112+
uploadFile(
113+
route: string,
114+
file: FormData,
115+
onProgress?: (progress: number) => void,
116+
) {
117+
return new Promise((resolve, reject) => {
118+
const req = new XMLHttpRequest();
119+
req.open('post', route);
120+
121+
req.addEventListener(
122+
'load',
123+
() => {
124+
if (req.status >= 200 && req.status <= 299) {
125+
resolve(req.response);
126+
} else {
127+
reject();
128+
}
129+
},
130+
false,
131+
);
132+
133+
req.addEventListener(
134+
'error',
135+
() => {
136+
reject();
137+
},
138+
false,
139+
);
140+
141+
req.upload.addEventListener(
142+
'progress',
143+
(e) => {
144+
try {
145+
const { loaded, total } = e;
146+
const progress = Math.floor((loaded / total) * 100) / 100;
147+
onProgress?.(progress);
148+
} catch {}
149+
},
150+
false,
151+
);
152+
153+
// Send file
154+
req.send(file);
155+
});
156+
}
77157
}
78158

79159
export const api = ApiClient.getInstance();

src/shared/types/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ export interface LaunchVariables {
8383
fullText: string;
8484
metadata: string;
8585
userSelection: string | null;
86+
_rehostedPdfPath?: string;
8687
}
8788

8889
export const getEmptyLaunchVariables = (): LaunchVariables => ({

src/shared/utils/page.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,36 @@
1+
import { api } from '../services/api';
12
import { isMindStudioElement } from './dom';
23

34
/**
45
* Utilities for interacting with the current web page.
56
* These utilities only work in the content script context.
67
*/
78
export const page = {
9+
/**
10+
* The way chrome renders PDFs makes them impossible to grab content inside,
11+
* because they're actually rendering in a separate private extension. So
12+
* if the current page is a PDF, we call fetch('') on the current URL and get
13+
* the PDF as a blob, upload it to MindStudio, and then pass that URL (in case
14+
* the URL is not publicly accessible) to MindStudio
15+
*/
16+
async getRehostedPdfSecurePath(): Promise<string> {
17+
const request = await fetch('');
18+
const blob = await request.blob();
19+
20+
// Get a signed upload URL
21+
const { fields, path, url } = await api.getSignedUploadUrl();
22+
23+
const data = new FormData();
24+
Object.keys(fields).forEach((key) => {
25+
data.append(key, fields[key]);
26+
});
27+
data.append('file', blob);
28+
29+
await api.uploadFile(url, data);
30+
31+
return path;
32+
},
33+
834
/**
935
* Gets the currently selected content from the page.
1036
* Works with both text selections and input fields.

0 commit comments

Comments
 (0)