@theia/mini-browser 1.45.1 → 1.46.0-next.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -45
- package/lib/browser/environment/mini-browser-environment-module.d.ts +3 -3
- package/lib/browser/environment/mini-browser-environment-module.js +24 -24
- package/lib/browser/environment/mini-browser-environment.d.ts +25 -25
- package/lib/browser/environment/mini-browser-environment.js +95 -95
- package/lib/browser/environment/mini-browser-environment.js.map +1 -1
- package/lib/browser/location-mapper-service.d.ts +58 -58
- package/lib/browser/location-mapper-service.js +140 -140
- package/lib/browser/mini-browser-content-style.d.ts +17 -17
- package/lib/browser/mini-browser-content-style.js +36 -36
- package/lib/browser/mini-browser-content.d.ts +177 -177
- package/lib/browser/mini-browser-content.js +554 -554
- package/lib/browser/mini-browser-frontend-module.d.ts +4 -4
- package/lib/browser/mini-browser-frontend-module.js +70 -70
- package/lib/browser/mini-browser-frontend-security-warnings.d.ts +11 -11
- package/lib/browser/mini-browser-frontend-security-warnings.js +73 -73
- package/lib/browser/mini-browser-open-handler.d.ts +76 -76
- package/lib/browser/mini-browser-open-handler.js +292 -292
- package/lib/browser/mini-browser.d.ts +25 -25
- package/lib/browser/mini-browser.js +118 -118
- package/lib/common/mini-browser-endpoint.d.ts +12 -12
- package/lib/common/mini-browser-endpoint.js +31 -31
- package/lib/common/mini-browser-service.d.ts +14 -14
- package/lib/common/mini-browser-service.js +20 -20
- package/lib/electron-browser/environment/electron-mini-browser-environment-module.d.ts +3 -3
- package/lib/electron-browser/environment/electron-mini-browser-environment-module.js +25 -25
- package/lib/electron-browser/environment/electron-mini-browser-environment.d.ts +9 -9
- package/lib/electron-browser/environment/electron-mini-browser-environment.js +60 -60
- package/lib/electron-main/mini-browser-electron-main-contribution.d.ts +12 -12
- package/lib/electron-main/mini-browser-electron-main-contribution.js +54 -54
- package/lib/node/mini-browser-backend-module.d.ts +3 -3
- package/lib/node/mini-browser-backend-module.js +41 -41
- package/lib/node/mini-browser-backend-security-warnings.d.ts +5 -5
- package/lib/node/mini-browser-backend-security-warnings.js +51 -51
- package/lib/node/mini-browser-endpoint.d.ts +97 -97
- package/lib/node/mini-browser-endpoint.js +268 -268
- package/lib/node/mini-browser-endpoint.js.map +1 -1
- package/lib/node/mini-browser-ws-validator.d.ts +12 -12
- package/lib/node/mini-browser-ws-validator.js +69 -69
- package/lib/package.spec.js +18 -18
- package/package.json +5 -6
- package/src/browser/environment/mini-browser-environment-module.ts +24 -24
- package/src/browser/environment/mini-browser-environment.ts +87 -87
- package/src/browser/location-mapper-service.ts +150 -150
- package/src/browser/mini-browser-content-style.ts +32 -32
- package/src/browser/mini-browser-content.ts +630 -630
- package/src/browser/mini-browser-frontend-module.ts +86 -86
- package/src/browser/mini-browser-frontend-security-warnings.ts +59 -59
- package/src/browser/mini-browser-open-handler.ts +312 -312
- package/src/browser/mini-browser.ts +110 -110
- package/src/browser/pdfobject.d.ts +99 -99
- package/src/browser/style/index.css +157 -157
- package/src/browser/style/mini-browser.svg +17 -17
- package/src/common/mini-browser-endpoint.ts +28 -28
- package/src/common/mini-browser-service.ts +29 -29
- package/src/electron-browser/environment/electron-mini-browser-environment-module.ts +25 -25
- package/src/electron-browser/environment/electron-mini-browser-environment.ts +53 -53
- package/src/electron-main/mini-browser-electron-main-contribution.ts +42 -42
- package/src/node/mini-browser-backend-module.ts +41 -41
- package/src/node/mini-browser-backend-security-warnings.ts +45 -45
- package/src/node/mini-browser-endpoint.ts +315 -315
- package/src/node/mini-browser-ws-validator.ts +56 -56
- package/src/package.spec.ts +21 -21
|
@@ -1,315 +1,315 @@
|
|
|
1
|
-
// *****************************************************************************
|
|
2
|
-
// Copyright (C) 2018 TypeFox and others.
|
|
3
|
-
//
|
|
4
|
-
// This program and the accompanying materials are made available under the
|
|
5
|
-
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
-
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
-
//
|
|
8
|
-
// This Source Code may also be made available under the following Secondary
|
|
9
|
-
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
-
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
-
// with the GNU Classpath Exception which is available at
|
|
12
|
-
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
-
//
|
|
14
|
-
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
-
// *****************************************************************************
|
|
16
|
-
|
|
17
|
-
const vhost = require('vhost');
|
|
18
|
-
import express = require('@theia/core/shared/express');
|
|
19
|
-
import * as fs from '@theia/core/shared/fs-extra';
|
|
20
|
-
import { lookup } from 'mime-types';
|
|
21
|
-
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
|
22
|
-
import { Application, Request, Response } from '@theia/core/shared/express';
|
|
23
|
-
import { FileUri } from '@theia/core/lib/
|
|
24
|
-
import { ILogger } from '@theia/core/lib/common/logger';
|
|
25
|
-
import { MaybePromise } from '@theia/core/lib/common/types';
|
|
26
|
-
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
|
27
|
-
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
|
28
|
-
import { MiniBrowserService } from '../common/mini-browser-service';
|
|
29
|
-
import { MiniBrowserEndpoint as MiniBrowserEndpointNS } from '../common/mini-browser-endpoint';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* The return type of the `FileSystem#resolveContent` method.
|
|
33
|
-
*/
|
|
34
|
-
export interface FileStatWithContent {
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* The file stat.
|
|
38
|
-
*/
|
|
39
|
-
readonly stat: fs.Stats & { uri: string };
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The content of the file as a UTF-8 encoded string.
|
|
43
|
-
*/
|
|
44
|
-
readonly content: string;
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Endpoint handler contribution for the `MiniBrowserEndpoint`.
|
|
50
|
-
*/
|
|
51
|
-
export const MiniBrowserEndpointHandler = Symbol('MiniBrowserEndpointHandler');
|
|
52
|
-
export interface MiniBrowserEndpointHandler {
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Returns with or resolves to the file extensions supported by the current `mini-browser` endpoint handler.
|
|
56
|
-
* The file extension must not start with the leading `.` (dot). For instance; `'html'` or `['jpg', 'jpeg']`.
|
|
57
|
-
* The file extensions are case insensitive.
|
|
58
|
-
*/
|
|
59
|
-
supportedExtensions(): MaybePromise<string | string[]>;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Returns a number representing the priority between all the available handlers for the same file extension.
|
|
63
|
-
*/
|
|
64
|
-
priority(): number;
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Responds back to the sender.
|
|
68
|
-
*/
|
|
69
|
-
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response>;
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
@injectable()
|
|
74
|
-
export class MiniBrowserEndpoint implements BackendApplicationContribution, MiniBrowserService {
|
|
75
|
-
|
|
76
|
-
private attachRequestHandlerPromise: Promise<void>;
|
|
77
|
-
|
|
78
|
-
@inject(ILogger)
|
|
79
|
-
protected readonly logger: ILogger;
|
|
80
|
-
|
|
81
|
-
@inject(ContributionProvider)
|
|
82
|
-
@named(MiniBrowserEndpointHandler)
|
|
83
|
-
protected readonly contributions: ContributionProvider<MiniBrowserEndpointHandler>;
|
|
84
|
-
|
|
85
|
-
protected readonly handlers: Map<string, MiniBrowserEndpointHandler> = new Map();
|
|
86
|
-
|
|
87
|
-
configure(app: Application): void {
|
|
88
|
-
this.attachRequestHandlerPromise = this.attachRequestHandler(app);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async onStart(): Promise<void> {
|
|
92
|
-
await Promise.all(Array.from(this.getContributions(), async handler => {
|
|
93
|
-
const extensions = await handler.supportedExtensions();
|
|
94
|
-
for (const extension of (Array.isArray(extensions) ? extensions : [extensions]).map(e => e.toLocaleLowerCase())) {
|
|
95
|
-
const existingHandler = this.handlers.get(extension);
|
|
96
|
-
if (!existingHandler || handler.priority > existingHandler.priority) {
|
|
97
|
-
this.handlers.set(extension, handler);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}));
|
|
101
|
-
await this.attachRequestHandlerPromise;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async supportedFileExtensions(): Promise<Readonly<{ extension: string, priority: number }>[]> {
|
|
105
|
-
return Array.from(this.handlers.entries(), ([extension, handler]) => ({ extension, priority: handler.priority() }));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
protected async attachRequestHandler(app: Application): Promise<void> {
|
|
109
|
-
const miniBrowserApp = express();
|
|
110
|
-
miniBrowserApp.get('*', async (request, response) => this.response(await this.getUri(request), response));
|
|
111
|
-
app.use(MiniBrowserEndpointNS.PATH, vhost(await this.getVirtualHostRegExp(), miniBrowserApp));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
protected async response(uri: string, response: Response): Promise<Response> {
|
|
115
|
-
const exists = await fs.pathExists(FileUri.fsPath(uri));
|
|
116
|
-
if (!exists) {
|
|
117
|
-
return this.missingResourceHandler()(uri, response);
|
|
118
|
-
}
|
|
119
|
-
const statWithContent = await this.readContent(uri);
|
|
120
|
-
try {
|
|
121
|
-
if (!statWithContent.stat.isDirectory()) {
|
|
122
|
-
const extension = uri.split('.').pop();
|
|
123
|
-
if (!extension) {
|
|
124
|
-
return this.defaultHandler()(statWithContent, response);
|
|
125
|
-
}
|
|
126
|
-
const handler = this.handlers.get(extension.toString().toLocaleLowerCase());
|
|
127
|
-
if (!handler) {
|
|
128
|
-
return this.defaultHandler()(statWithContent, response);
|
|
129
|
-
}
|
|
130
|
-
return handler.respond(statWithContent, response);
|
|
131
|
-
}
|
|
132
|
-
} catch (e) {
|
|
133
|
-
return this.errorHandler()(e, uri, response);
|
|
134
|
-
}
|
|
135
|
-
return this.defaultHandler()(statWithContent, response);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
protected getContributions(): MiniBrowserEndpointHandler[] {
|
|
139
|
-
return this.contributions.getContributions();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
protected getUri(request: Request): MaybePromise<string> {
|
|
143
|
-
return FileUri.create(request.path).toString(true);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
protected async readContent(uri: string): Promise<FileStatWithContent> {
|
|
147
|
-
const fsPath = FileUri.fsPath(uri);
|
|
148
|
-
const [stat, content] = await Promise.all([fs.stat(fsPath), fs.readFile(fsPath, 'utf8')]);
|
|
149
|
-
return { stat: Object.assign(stat, { uri }), content };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
-
protected errorHandler(): (error: any, uri: string, response: Response) => MaybePromise<Response> {
|
|
154
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
155
|
-
return async (error: any, uri: string, response: Response) => {
|
|
156
|
-
const details = error.toString ? error.toString() : error;
|
|
157
|
-
this.logger.error(`Error occurred while handling request for ${uri}. Details: ${details}`);
|
|
158
|
-
if (error instanceof Error) {
|
|
159
|
-
let message = error.message;
|
|
160
|
-
if (error.stack) {
|
|
161
|
-
message += `\n${error.stack}`;
|
|
162
|
-
}
|
|
163
|
-
this.logger.error(message);
|
|
164
|
-
} else if (typeof error === 'string') {
|
|
165
|
-
this.logger.error(error);
|
|
166
|
-
} else {
|
|
167
|
-
this.logger.error(`${error}`);
|
|
168
|
-
}
|
|
169
|
-
return response.send(500);
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
protected missingResourceHandler(): (uri: string, response: Response) => MaybePromise<Response> {
|
|
174
|
-
return async (uri: string, response: Response) => {
|
|
175
|
-
this.logger.error(`Cannot handle missing resource. URI: ${uri}.`);
|
|
176
|
-
return response.sendStatus(404);
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
protected defaultHandler(): (statWithContent: FileStatWithContent, response: Response) => MaybePromise<Response> {
|
|
181
|
-
return async (statWithContent: FileStatWithContent, response: Response) => {
|
|
182
|
-
const { content } = statWithContent;
|
|
183
|
-
const mimeType = lookup(FileUri.fsPath(statWithContent.stat.uri));
|
|
184
|
-
if (!mimeType) {
|
|
185
|
-
this.logger.warn(`Cannot handle unexpected resource. URI: ${statWithContent.stat.uri}.`);
|
|
186
|
-
response.contentType('application/octet-stream');
|
|
187
|
-
} else {
|
|
188
|
-
response.contentType(mimeType);
|
|
189
|
-
}
|
|
190
|
-
return response.send(content);
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
protected async getVirtualHostRegExp(): Promise<RegExp> {
|
|
195
|
-
const pattern = process.env[MiniBrowserEndpointNS.HOST_PATTERN_ENV] || MiniBrowserEndpointNS.HOST_PATTERN_DEFAULT;
|
|
196
|
-
const vhostRe = pattern
|
|
197
|
-
.replace(/\./g, '\\.')
|
|
198
|
-
.replace('{{uuid}}', '.+')
|
|
199
|
-
.replace('{{hostname}}', '.+');
|
|
200
|
-
return new RegExp(vhostRe, 'i');
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// See `EditorManager#canHandle`.
|
|
205
|
-
const CODE_EDITOR_PRIORITY = 100;
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Endpoint handler contribution for HTML files.
|
|
209
|
-
*/
|
|
210
|
-
@injectable()
|
|
211
|
-
export class HtmlHandler implements MiniBrowserEndpointHandler {
|
|
212
|
-
|
|
213
|
-
supportedExtensions(): string[] {
|
|
214
|
-
return ['html', 'xhtml', 'htm'];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
priority(): number {
|
|
218
|
-
// Prefer Code Editor over Mini Browser
|
|
219
|
-
// https://github.com/eclipse-theia/theia/issues/2051
|
|
220
|
-
return 1;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
|
224
|
-
response.contentType('text/html');
|
|
225
|
-
return response.send(statWithContent.content);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Handler for JPG resources.
|
|
232
|
-
*/
|
|
233
|
-
@injectable()
|
|
234
|
-
export class ImageHandler implements MiniBrowserEndpointHandler {
|
|
235
|
-
|
|
236
|
-
supportedExtensions(): string[] {
|
|
237
|
-
return ['jpg', 'jpeg', 'png', 'bmp', 'gif'];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
priority(): number {
|
|
241
|
-
return CODE_EDITOR_PRIORITY + 1;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
|
245
|
-
fs.readFile(FileUri.fsPath(statWithContent.stat.uri), (error, data) => {
|
|
246
|
-
if (error) {
|
|
247
|
-
throw error;
|
|
248
|
-
}
|
|
249
|
-
response.contentType('image/jpeg');
|
|
250
|
-
response.send(data);
|
|
251
|
-
});
|
|
252
|
-
return response;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* PDF endpoint handler.
|
|
259
|
-
*/
|
|
260
|
-
@injectable()
|
|
261
|
-
export class PdfHandler implements MiniBrowserEndpointHandler {
|
|
262
|
-
|
|
263
|
-
supportedExtensions(): string {
|
|
264
|
-
return 'pdf';
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
priority(): number {
|
|
268
|
-
return CODE_EDITOR_PRIORITY + 1;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
|
272
|
-
// https://stackoverflow.com/questions/11598274/display-pdf-in-browser-using-express-js
|
|
273
|
-
const encodeRFC5987ValueChars = (input: string) =>
|
|
274
|
-
encodeURIComponent(input).
|
|
275
|
-
// Note that although RFC3986 reserves "!", RFC5987 does not, so we do not need to escape it.
|
|
276
|
-
replace(/['()]/g, escape). // i.e., %27 %28 %29
|
|
277
|
-
replace(/\*/g, '%2A').
|
|
278
|
-
// The following are not required for percent-encoding per RFC5987, so we can allow for a little better readability over the wire: |`^.
|
|
279
|
-
replace(/%(?:7C|60|5E)/g, unescape);
|
|
280
|
-
|
|
281
|
-
const fileName = FileUri.create(statWithContent.stat.uri).path.base;
|
|
282
|
-
fs.readFile(FileUri.fsPath(statWithContent.stat.uri), (error, data) => {
|
|
283
|
-
if (error) {
|
|
284
|
-
throw error;
|
|
285
|
-
}
|
|
286
|
-
// Change `inline` to `attachment` if you would like to force downloading the PDF instead of previewing in the browser.
|
|
287
|
-
response.setHeader('Content-disposition', `inline; filename*=UTF-8''${encodeRFC5987ValueChars(fileName)}`);
|
|
288
|
-
response.contentType('application/pdf');
|
|
289
|
-
response.send(data);
|
|
290
|
-
});
|
|
291
|
-
return response;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Endpoint contribution for SVG resources.
|
|
298
|
-
*/
|
|
299
|
-
@injectable()
|
|
300
|
-
export class SvgHandler implements MiniBrowserEndpointHandler {
|
|
301
|
-
|
|
302
|
-
supportedExtensions(): string {
|
|
303
|
-
return 'svg';
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
priority(): number {
|
|
307
|
-
return 1;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
|
311
|
-
response.contentType('image/svg+xml');
|
|
312
|
-
return response.send(statWithContent.content);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
}
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2018 TypeFox and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
const vhost = require('vhost');
|
|
18
|
+
import express = require('@theia/core/shared/express');
|
|
19
|
+
import * as fs from '@theia/core/shared/fs-extra';
|
|
20
|
+
import { lookup } from 'mime-types';
|
|
21
|
+
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
|
22
|
+
import { Application, Request, Response } from '@theia/core/shared/express';
|
|
23
|
+
import { FileUri } from '@theia/core/lib/common/file-uri';
|
|
24
|
+
import { ILogger } from '@theia/core/lib/common/logger';
|
|
25
|
+
import { MaybePromise } from '@theia/core/lib/common/types';
|
|
26
|
+
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
|
27
|
+
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
|
28
|
+
import { MiniBrowserService } from '../common/mini-browser-service';
|
|
29
|
+
import { MiniBrowserEndpoint as MiniBrowserEndpointNS } from '../common/mini-browser-endpoint';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The return type of the `FileSystem#resolveContent` method.
|
|
33
|
+
*/
|
|
34
|
+
export interface FileStatWithContent {
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The file stat.
|
|
38
|
+
*/
|
|
39
|
+
readonly stat: fs.Stats & { uri: string };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The content of the file as a UTF-8 encoded string.
|
|
43
|
+
*/
|
|
44
|
+
readonly content: string;
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Endpoint handler contribution for the `MiniBrowserEndpoint`.
|
|
50
|
+
*/
|
|
51
|
+
export const MiniBrowserEndpointHandler = Symbol('MiniBrowserEndpointHandler');
|
|
52
|
+
export interface MiniBrowserEndpointHandler {
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Returns with or resolves to the file extensions supported by the current `mini-browser` endpoint handler.
|
|
56
|
+
* The file extension must not start with the leading `.` (dot). For instance; `'html'` or `['jpg', 'jpeg']`.
|
|
57
|
+
* The file extensions are case insensitive.
|
|
58
|
+
*/
|
|
59
|
+
supportedExtensions(): MaybePromise<string | string[]>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns a number representing the priority between all the available handlers for the same file extension.
|
|
63
|
+
*/
|
|
64
|
+
priority(): number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Responds back to the sender.
|
|
68
|
+
*/
|
|
69
|
+
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response>;
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@injectable()
|
|
74
|
+
export class MiniBrowserEndpoint implements BackendApplicationContribution, MiniBrowserService {
|
|
75
|
+
|
|
76
|
+
private attachRequestHandlerPromise: Promise<void>;
|
|
77
|
+
|
|
78
|
+
@inject(ILogger)
|
|
79
|
+
protected readonly logger: ILogger;
|
|
80
|
+
|
|
81
|
+
@inject(ContributionProvider)
|
|
82
|
+
@named(MiniBrowserEndpointHandler)
|
|
83
|
+
protected readonly contributions: ContributionProvider<MiniBrowserEndpointHandler>;
|
|
84
|
+
|
|
85
|
+
protected readonly handlers: Map<string, MiniBrowserEndpointHandler> = new Map();
|
|
86
|
+
|
|
87
|
+
configure(app: Application): void {
|
|
88
|
+
this.attachRequestHandlerPromise = this.attachRequestHandler(app);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async onStart(): Promise<void> {
|
|
92
|
+
await Promise.all(Array.from(this.getContributions(), async handler => {
|
|
93
|
+
const extensions = await handler.supportedExtensions();
|
|
94
|
+
for (const extension of (Array.isArray(extensions) ? extensions : [extensions]).map(e => e.toLocaleLowerCase())) {
|
|
95
|
+
const existingHandler = this.handlers.get(extension);
|
|
96
|
+
if (!existingHandler || handler.priority > existingHandler.priority) {
|
|
97
|
+
this.handlers.set(extension, handler);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}));
|
|
101
|
+
await this.attachRequestHandlerPromise;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async supportedFileExtensions(): Promise<Readonly<{ extension: string, priority: number }>[]> {
|
|
105
|
+
return Array.from(this.handlers.entries(), ([extension, handler]) => ({ extension, priority: handler.priority() }));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
protected async attachRequestHandler(app: Application): Promise<void> {
|
|
109
|
+
const miniBrowserApp = express();
|
|
110
|
+
miniBrowserApp.get('*', async (request, response) => this.response(await this.getUri(request), response));
|
|
111
|
+
app.use(MiniBrowserEndpointNS.PATH, vhost(await this.getVirtualHostRegExp(), miniBrowserApp));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
protected async response(uri: string, response: Response): Promise<Response> {
|
|
115
|
+
const exists = await fs.pathExists(FileUri.fsPath(uri));
|
|
116
|
+
if (!exists) {
|
|
117
|
+
return this.missingResourceHandler()(uri, response);
|
|
118
|
+
}
|
|
119
|
+
const statWithContent = await this.readContent(uri);
|
|
120
|
+
try {
|
|
121
|
+
if (!statWithContent.stat.isDirectory()) {
|
|
122
|
+
const extension = uri.split('.').pop();
|
|
123
|
+
if (!extension) {
|
|
124
|
+
return this.defaultHandler()(statWithContent, response);
|
|
125
|
+
}
|
|
126
|
+
const handler = this.handlers.get(extension.toString().toLocaleLowerCase());
|
|
127
|
+
if (!handler) {
|
|
128
|
+
return this.defaultHandler()(statWithContent, response);
|
|
129
|
+
}
|
|
130
|
+
return handler.respond(statWithContent, response);
|
|
131
|
+
}
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return this.errorHandler()(e, uri, response);
|
|
134
|
+
}
|
|
135
|
+
return this.defaultHandler()(statWithContent, response);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
protected getContributions(): MiniBrowserEndpointHandler[] {
|
|
139
|
+
return this.contributions.getContributions();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
protected getUri(request: Request): MaybePromise<string> {
|
|
143
|
+
return FileUri.create(request.path).toString(true);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
protected async readContent(uri: string): Promise<FileStatWithContent> {
|
|
147
|
+
const fsPath = FileUri.fsPath(uri);
|
|
148
|
+
const [stat, content] = await Promise.all([fs.stat(fsPath), fs.readFile(fsPath, 'utf8')]);
|
|
149
|
+
return { stat: Object.assign(stat, { uri }), content };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
153
|
+
protected errorHandler(): (error: any, uri: string, response: Response) => MaybePromise<Response> {
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
155
|
+
return async (error: any, uri: string, response: Response) => {
|
|
156
|
+
const details = error.toString ? error.toString() : error;
|
|
157
|
+
this.logger.error(`Error occurred while handling request for ${uri}. Details: ${details}`);
|
|
158
|
+
if (error instanceof Error) {
|
|
159
|
+
let message = error.message;
|
|
160
|
+
if (error.stack) {
|
|
161
|
+
message += `\n${error.stack}`;
|
|
162
|
+
}
|
|
163
|
+
this.logger.error(message);
|
|
164
|
+
} else if (typeof error === 'string') {
|
|
165
|
+
this.logger.error(error);
|
|
166
|
+
} else {
|
|
167
|
+
this.logger.error(`${error}`);
|
|
168
|
+
}
|
|
169
|
+
return response.send(500);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
protected missingResourceHandler(): (uri: string, response: Response) => MaybePromise<Response> {
|
|
174
|
+
return async (uri: string, response: Response) => {
|
|
175
|
+
this.logger.error(`Cannot handle missing resource. URI: ${uri}.`);
|
|
176
|
+
return response.sendStatus(404);
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
protected defaultHandler(): (statWithContent: FileStatWithContent, response: Response) => MaybePromise<Response> {
|
|
181
|
+
return async (statWithContent: FileStatWithContent, response: Response) => {
|
|
182
|
+
const { content } = statWithContent;
|
|
183
|
+
const mimeType = lookup(FileUri.fsPath(statWithContent.stat.uri));
|
|
184
|
+
if (!mimeType) {
|
|
185
|
+
this.logger.warn(`Cannot handle unexpected resource. URI: ${statWithContent.stat.uri}.`);
|
|
186
|
+
response.contentType('application/octet-stream');
|
|
187
|
+
} else {
|
|
188
|
+
response.contentType(mimeType);
|
|
189
|
+
}
|
|
190
|
+
return response.send(content);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
protected async getVirtualHostRegExp(): Promise<RegExp> {
|
|
195
|
+
const pattern = process.env[MiniBrowserEndpointNS.HOST_PATTERN_ENV] || MiniBrowserEndpointNS.HOST_PATTERN_DEFAULT;
|
|
196
|
+
const vhostRe = pattern
|
|
197
|
+
.replace(/\./g, '\\.')
|
|
198
|
+
.replace('{{uuid}}', '.+')
|
|
199
|
+
.replace('{{hostname}}', '.+');
|
|
200
|
+
return new RegExp(vhostRe, 'i');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// See `EditorManager#canHandle`.
|
|
205
|
+
const CODE_EDITOR_PRIORITY = 100;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Endpoint handler contribution for HTML files.
|
|
209
|
+
*/
|
|
210
|
+
@injectable()
|
|
211
|
+
export class HtmlHandler implements MiniBrowserEndpointHandler {
|
|
212
|
+
|
|
213
|
+
supportedExtensions(): string[] {
|
|
214
|
+
return ['html', 'xhtml', 'htm'];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
priority(): number {
|
|
218
|
+
// Prefer Code Editor over Mini Browser
|
|
219
|
+
// https://github.com/eclipse-theia/theia/issues/2051
|
|
220
|
+
return 1;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
|
224
|
+
response.contentType('text/html');
|
|
225
|
+
return response.send(statWithContent.content);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Handler for JPG resources.
|
|
232
|
+
*/
|
|
233
|
+
@injectable()
|
|
234
|
+
export class ImageHandler implements MiniBrowserEndpointHandler {
|
|
235
|
+
|
|
236
|
+
supportedExtensions(): string[] {
|
|
237
|
+
return ['jpg', 'jpeg', 'png', 'bmp', 'gif'];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
priority(): number {
|
|
241
|
+
return CODE_EDITOR_PRIORITY + 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
|
245
|
+
fs.readFile(FileUri.fsPath(statWithContent.stat.uri), (error, data) => {
|
|
246
|
+
if (error) {
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
response.contentType('image/jpeg');
|
|
250
|
+
response.send(data);
|
|
251
|
+
});
|
|
252
|
+
return response;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* PDF endpoint handler.
|
|
259
|
+
*/
|
|
260
|
+
@injectable()
|
|
261
|
+
export class PdfHandler implements MiniBrowserEndpointHandler {
|
|
262
|
+
|
|
263
|
+
supportedExtensions(): string {
|
|
264
|
+
return 'pdf';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
priority(): number {
|
|
268
|
+
return CODE_EDITOR_PRIORITY + 1;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
|
272
|
+
// https://stackoverflow.com/questions/11598274/display-pdf-in-browser-using-express-js
|
|
273
|
+
const encodeRFC5987ValueChars = (input: string) =>
|
|
274
|
+
encodeURIComponent(input).
|
|
275
|
+
// Note that although RFC3986 reserves "!", RFC5987 does not, so we do not need to escape it.
|
|
276
|
+
replace(/['()]/g, escape). // i.e., %27 %28 %29
|
|
277
|
+
replace(/\*/g, '%2A').
|
|
278
|
+
// The following are not required for percent-encoding per RFC5987, so we can allow for a little better readability over the wire: |`^.
|
|
279
|
+
replace(/%(?:7C|60|5E)/g, unescape);
|
|
280
|
+
|
|
281
|
+
const fileName = FileUri.create(statWithContent.stat.uri).path.base;
|
|
282
|
+
fs.readFile(FileUri.fsPath(statWithContent.stat.uri), (error, data) => {
|
|
283
|
+
if (error) {
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
// Change `inline` to `attachment` if you would like to force downloading the PDF instead of previewing in the browser.
|
|
287
|
+
response.setHeader('Content-disposition', `inline; filename*=UTF-8''${encodeRFC5987ValueChars(fileName)}`);
|
|
288
|
+
response.contentType('application/pdf');
|
|
289
|
+
response.send(data);
|
|
290
|
+
});
|
|
291
|
+
return response;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Endpoint contribution for SVG resources.
|
|
298
|
+
*/
|
|
299
|
+
@injectable()
|
|
300
|
+
export class SvgHandler implements MiniBrowserEndpointHandler {
|
|
301
|
+
|
|
302
|
+
supportedExtensions(): string {
|
|
303
|
+
return 'svg';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
priority(): number {
|
|
307
|
+
return 1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
|
311
|
+
response.contentType('image/svg+xml');
|
|
312
|
+
return response.send(statWithContent.content);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
}
|