deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/mini-browser/.eslintrc.js
Normal file
10
packages/mini-browser/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
46
packages/mini-browser/README.md
Normal file
46
packages/mini-browser/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - MINI-BROWSER EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/mini-browser` extension provides a browser widget with the corresponding backend endpoints.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `THEIA_MINI_BROWSER_HOST_PATTERN`
|
||||
|
||||
A string pattern possibly containing `{{uuid}}` and `{{hostname}}` which will be replaced. This is the host for which the `mini-browser` will serve.
|
||||
It is a good practice to host the `mini-browser` handlers on a sub-domain as it is more secure.
|
||||
Defaults to `{{uuid}}.mini-browser.{{hostname}}`.
|
||||
|
||||
## Security Warnings
|
||||
|
||||
- Potentially Insecure Host Pattern
|
||||
|
||||
When you change the host pattern via the `THEIA_MINI_BROWSER_HOST_PATTERN` environment variable warnings will be emitted both from the frontend and from the backend.
|
||||
You can disable those warnings by setting `warnOnPotentiallyInsecureHostPattern: false` in the appropriate application configurations in your application's `package.json`.
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/mini-browser`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_mini-browser.html)
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
58
packages/mini-browser/package.json
Normal file
58
packages/mini-browser/package.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@theia/mini-browser",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - Mini-Browser Extension",
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0",
|
||||
"@theia/filesystem": "1.68.0",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"mime-types": "^2.1.18",
|
||||
"pdfobject": "^2.0.201604172",
|
||||
"tslib": "^2.6.2",
|
||||
"vhost": "^3.0.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"backend": "lib/node/mini-browser-backend-module",
|
||||
"frontend": "lib/browser/mini-browser-frontend-module"
|
||||
},
|
||||
{
|
||||
"frontend": "lib/browser/environment/mini-browser-environment-module",
|
||||
"frontendElectron": "lib/electron-browser/environment/electron-mini-browser-environment-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { MiniBrowserEnvironment } from './mini-browser-environment';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(MiniBrowserEnvironment).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(MiniBrowserEnvironment);
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Endpoint, FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
|
||||
import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { generateUuid } from '@theia/core/lib/common/uuid';
|
||||
import { MiniBrowserEndpoint } from '../../common/mini-browser-endpoint';
|
||||
|
||||
/**
|
||||
* Fetch values from the backend's environment and caches them locally.
|
||||
* Helps with deploying various mini-browser endpoints.
|
||||
*/
|
||||
@injectable()
|
||||
export class MiniBrowserEnvironment implements FrontendApplicationContribution {
|
||||
|
||||
protected _hostPatternPromise: Promise<string>;
|
||||
protected _hostPattern?: string;
|
||||
|
||||
@inject(EnvVariablesServer)
|
||||
protected environment: EnvVariablesServer;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this._hostPatternPromise = this.getHostPattern()
|
||||
.then(pattern => this._hostPattern = pattern);
|
||||
}
|
||||
|
||||
get hostPatternPromise(): Promise<string> {
|
||||
return this._hostPatternPromise;
|
||||
}
|
||||
|
||||
get hostPattern(): string | undefined {
|
||||
return this._hostPattern;
|
||||
}
|
||||
|
||||
async onStart(): Promise<void> {
|
||||
await this._hostPatternPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws if `hostPatternPromise` is not yet resolved.
|
||||
*/
|
||||
getEndpoint(uuid: string, hostname?: string): Endpoint {
|
||||
if (this._hostPattern === undefined) {
|
||||
throw new Error('MiniBrowserEnvironment is not finished initializing');
|
||||
}
|
||||
return new Endpoint({
|
||||
path: MiniBrowserEndpoint.PATH,
|
||||
host: this._hostPattern
|
||||
.replace('{{uuid}}', uuid)
|
||||
.replace('{{hostname}}', hostname || this.getDefaultHostname()),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws if `hostPatternPromise` is not yet resolved.
|
||||
*/
|
||||
getRandomEndpoint(): Endpoint {
|
||||
return this.getEndpoint(generateUuid());
|
||||
}
|
||||
|
||||
protected async getHostPattern(): Promise<string> {
|
||||
return environment.electron.is()
|
||||
? MiniBrowserEndpoint.HOST_PATTERN_DEFAULT
|
||||
: this.environment.getValue(MiniBrowserEndpoint.HOST_PATTERN_ENV)
|
||||
.then(envVar => envVar?.value || MiniBrowserEndpoint.HOST_PATTERN_DEFAULT);
|
||||
}
|
||||
|
||||
protected getDefaultHostname(): string {
|
||||
return self.location.host;
|
||||
}
|
||||
}
|
||||
150
packages/mini-browser/src/browser/location-mapper-service.ts
Normal file
150
packages/mini-browser/src/browser/location-mapper-service.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { Endpoint } from '@theia/core/lib/browser';
|
||||
import { MaybePromise, Prioritizeable } from '@theia/core/lib/common/types';
|
||||
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import { MiniBrowserEnvironment } from './environment/mini-browser-environment';
|
||||
|
||||
/**
|
||||
* Contribution for the `LocationMapperService`.
|
||||
*/
|
||||
export const LocationMapper = Symbol('LocationMapper');
|
||||
export interface LocationMapper {
|
||||
|
||||
/**
|
||||
* Should return with a positive number if the current contribution can handle the given location.
|
||||
* The number indicates the priority of the location mapper. If it is not a positive number, it means, the
|
||||
* contribution cannot handle the location.
|
||||
*/
|
||||
canHandle(location: string): MaybePromise<number>;
|
||||
|
||||
/**
|
||||
* Maps the given location.
|
||||
*/
|
||||
map(location: string): MaybePromise<string>;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Location mapper service.
|
||||
*/
|
||||
@injectable()
|
||||
export class LocationMapperService {
|
||||
|
||||
@inject(ContributionProvider)
|
||||
@named(LocationMapper)
|
||||
protected readonly contributions: ContributionProvider<LocationMapper>;
|
||||
|
||||
async map(location: string): Promise<string> {
|
||||
const contributions = await this.prioritize(location);
|
||||
if (contributions.length === 0) {
|
||||
return this.defaultMapper()(location);
|
||||
}
|
||||
return contributions[0].map(location);
|
||||
}
|
||||
|
||||
protected defaultMapper(): (location: string) => MaybePromise<string> {
|
||||
return location => `${new Endpoint().httpScheme}//${location}`;
|
||||
}
|
||||
|
||||
protected async prioritize(location: string): Promise<LocationMapper[]> {
|
||||
const prioritized = await Prioritizeable.prioritizeAll(this.getContributions(), contribution => contribution.canHandle(location));
|
||||
return prioritized.map(p => p.value);
|
||||
}
|
||||
|
||||
protected getContributions(): LocationMapper[] {
|
||||
return this.contributions.getContributions();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP location mapper.
|
||||
*/
|
||||
@injectable()
|
||||
export class HttpLocationMapper implements LocationMapper {
|
||||
|
||||
canHandle(location: string): MaybePromise<number> {
|
||||
return location.startsWith('http://') ? 1 : 0;
|
||||
}
|
||||
|
||||
map(location: string): MaybePromise<string> {
|
||||
return location;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTPS location mapper.
|
||||
*/
|
||||
@injectable()
|
||||
export class HttpsLocationMapper implements LocationMapper {
|
||||
|
||||
canHandle(location: string): MaybePromise<number> {
|
||||
return location.startsWith('https://') ? 1 : 0;
|
||||
}
|
||||
|
||||
map(location: string): MaybePromise<string> {
|
||||
return location;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Location mapper for locations without a scheme.
|
||||
*/
|
||||
@injectable()
|
||||
export class LocationWithoutSchemeMapper implements LocationMapper {
|
||||
|
||||
canHandle(location: string): MaybePromise<number> {
|
||||
return new URI(location).scheme === '' ? 1 : 0;
|
||||
}
|
||||
|
||||
map(location: string): MaybePromise<string> {
|
||||
return `http://${location}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* `file` URI location mapper.
|
||||
*/
|
||||
@injectable()
|
||||
export class FileLocationMapper implements LocationMapper {
|
||||
|
||||
@inject(MiniBrowserEnvironment)
|
||||
protected miniBrowserEnvironment: MiniBrowserEnvironment;
|
||||
|
||||
canHandle(location: string): MaybePromise<number> {
|
||||
return location.startsWith('file://') ? 1 : 0;
|
||||
}
|
||||
|
||||
async map(location: string): Promise<string> {
|
||||
const uri = new URI(location);
|
||||
if (uri.scheme !== 'file') {
|
||||
throw new Error(`Only URIs with 'file' scheme can be mapped to an URL. URI was: ${uri}.`);
|
||||
}
|
||||
let rawLocation = uri.path.toString();
|
||||
if (rawLocation.charAt(0) === '/') {
|
||||
rawLocation = rawLocation.substring(1);
|
||||
}
|
||||
return this.miniBrowserEnvironment.getRandomEndpoint().getRestUrl().resolve(rawLocation).toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2019 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
export namespace MiniBrowserContentStyle {
|
||||
export const MINI_BROWSER = 'theia-mini-browser';
|
||||
export const TOOLBAR = 'theia-mini-browser-toolbar';
|
||||
export const TOOLBAR_READ_ONLY = 'theia-mini-browser-toolbar-read-only';
|
||||
export const PRE_LOAD = 'theia-mini-browser-load-indicator';
|
||||
export const FADE_OUT = 'theia-fade-out';
|
||||
export const CONTENT_AREA = 'theia-mini-browser-content-area';
|
||||
export const PDF_CONTAINER = 'theia-mini-browser-pdf-container';
|
||||
export const PREVIOUS = 'theia-mini-browser-previous';
|
||||
export const NEXT = 'theia-mini-browser-next';
|
||||
export const REFRESH = 'theia-mini-browser-refresh';
|
||||
export const OPEN = 'theia-mini-browser-open';
|
||||
export const BUTTON = 'theia-mini-browser-button';
|
||||
export const DISABLED = 'theia-mini-browser-button-disabled';
|
||||
export const TRANSPARENT_OVERLAY = 'theia-transparent-overlay';
|
||||
export const ERROR_BAR = 'theia-mini-browser-error-bar';
|
||||
}
|
||||
630
packages/mini-browser/src/browser/mini-browser-content.ts
Normal file
630
packages/mini-browser/src/browser/mini-browser-content.ts
Normal file
@@ -0,0 +1,630 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as PDFObject from 'pdfobject';
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Message } from '@theia/core/shared/@lumino/messaging';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { Emitter } from '@theia/core/lib/common/event';
|
||||
import { KeybindingRegistry } from '@theia/core/lib/browser/keybinding';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { parseCssTime, Key, KeyCode } from '@theia/core/lib/browser';
|
||||
import { DisposableCollection, Disposable } from '@theia/core/lib/common/disposable';
|
||||
import { BaseWidget, addEventListener, codiconArray } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { LocationMapperService } from './location-mapper-service';
|
||||
import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker';
|
||||
|
||||
import debounce = require('@theia/core/shared/lodash.debounce');
|
||||
import { MiniBrowserContentStyle } from './mini-browser-content-style';
|
||||
import { FileService } from '@theia/filesystem/lib/browser/file-service';
|
||||
import { FileChangesEvent, FileChangeType } from '@theia/filesystem/lib/common/files';
|
||||
|
||||
/**
|
||||
* Initializer properties for the embedded browser widget.
|
||||
*/
|
||||
@injectable()
|
||||
export class MiniBrowserProps {
|
||||
|
||||
/**
|
||||
* `show` if the toolbar should be visible. If `read-only`, the toolbar is visible but the address cannot be changed and it acts as a link instead.\
|
||||
* `hide` if the toolbar should be hidden. `show` by default. If the `startPage` is not defined, this property is always `show`.
|
||||
*/
|
||||
readonly toolbar?: 'show' | 'hide' | 'read-only';
|
||||
|
||||
/**
|
||||
* If defined, the browser will load this page on startup. Otherwise, it show a blank page.
|
||||
*/
|
||||
readonly startPage?: string;
|
||||
|
||||
/**
|
||||
* Sandbox options for the underlying `iframe`. Defaults to `SandboxOptions#DEFAULT` if not provided.
|
||||
*/
|
||||
readonly sandbox?: MiniBrowserProps.SandboxOptions[];
|
||||
|
||||
/**
|
||||
* The optional icon class for the widget.
|
||||
*/
|
||||
readonly iconClass?: string;
|
||||
|
||||
/**
|
||||
* The desired name of the widget.
|
||||
*/
|
||||
readonly name?: string;
|
||||
|
||||
/**
|
||||
* `true` if the `iFrame`'s background has to be reset to the default white color. Otherwise, `false`. `false` is the default.
|
||||
*/
|
||||
readonly resetBackground?: boolean;
|
||||
|
||||
}
|
||||
|
||||
export namespace MiniBrowserProps {
|
||||
|
||||
/**
|
||||
* Enumeration of the supported `sandbox` options for the `iframe`.
|
||||
*/
|
||||
export enum SandboxOptions {
|
||||
|
||||
/**
|
||||
* Allows form submissions.
|
||||
*/
|
||||
'allow-forms',
|
||||
|
||||
/**
|
||||
* Allows popups, such as `window.open()`, `showModalDialog()`, `target=”_blank”`, etc.
|
||||
*/
|
||||
'allow-popups',
|
||||
|
||||
/**
|
||||
* Allows pointer lock.
|
||||
*/
|
||||
'allow-pointer-lock',
|
||||
|
||||
/**
|
||||
* Allows the document to maintain its origin. Pages loaded from https://example.com/ will retain access to that origin’s data.
|
||||
*/
|
||||
'allow-same-origin',
|
||||
|
||||
/**
|
||||
* Allows JavaScript execution. Also allows features to trigger automatically (as they’d be trivial to implement via JavaScript).
|
||||
*/
|
||||
'allow-scripts',
|
||||
|
||||
/**
|
||||
* Allows the document to break out of the frame by navigating the top-level `window`.
|
||||
*/
|
||||
'allow-top-navigation',
|
||||
|
||||
/**
|
||||
* Allows the embedded browsing context to open modal windows.
|
||||
*/
|
||||
'allow-modals',
|
||||
|
||||
/**
|
||||
* Allows the embedded browsing context to disable the ability to lock the screen orientation.
|
||||
*/
|
||||
'allow-orientation-lock',
|
||||
|
||||
/**
|
||||
* Allows a sandboxed document to open new windows without forcing the sandboxing flags upon them.
|
||||
* This will allow, for example, a third-party advertisement to be safely sandboxed without forcing the same restrictions upon a landing page.
|
||||
*/
|
||||
'allow-popups-to-escape-sandbox',
|
||||
|
||||
/**
|
||||
* Allows embedders to have control over whether an iframe can start a presentation session.
|
||||
*/
|
||||
'allow-presentation',
|
||||
|
||||
/**
|
||||
* Allows the embedded browsing context to navigate (load) content to the top-level browsing context only when initiated by a user gesture.
|
||||
* If this keyword is not used, this operation is not allowed.
|
||||
*/
|
||||
'allow-top-navigation-by-user-activation'
|
||||
}
|
||||
|
||||
export namespace SandboxOptions {
|
||||
|
||||
/**
|
||||
* The default `sandbox` options, if other is not provided.
|
||||
*
|
||||
* See: https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
|
||||
*/
|
||||
export const DEFAULT: SandboxOptions[] = [
|
||||
SandboxOptions['allow-same-origin'],
|
||||
SandboxOptions['allow-scripts'],
|
||||
SandboxOptions['allow-popups'],
|
||||
SandboxOptions['allow-forms'],
|
||||
SandboxOptions['allow-modals']
|
||||
];
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const MiniBrowserContentFactory = Symbol('MiniBrowserContentFactory');
|
||||
export type MiniBrowserContentFactory = (props: MiniBrowserProps) => MiniBrowserContent;
|
||||
|
||||
@injectable()
|
||||
export class MiniBrowserContent extends BaseWidget {
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(WindowService)
|
||||
protected readonly windowService: WindowService;
|
||||
|
||||
@inject(LocationMapperService)
|
||||
protected readonly locationMapper: LocationMapperService;
|
||||
|
||||
@inject(KeybindingRegistry)
|
||||
protected readonly keybindings: KeybindingRegistry;
|
||||
|
||||
@inject(ApplicationShellMouseTracker)
|
||||
protected readonly mouseTracker: ApplicationShellMouseTracker;
|
||||
|
||||
@inject(FileService)
|
||||
protected readonly fileService: FileService;
|
||||
|
||||
protected readonly submitInputEmitter = new Emitter<string>();
|
||||
protected readonly navigateBackEmitter = new Emitter<void>();
|
||||
protected readonly navigateForwardEmitter = new Emitter<void>();
|
||||
protected readonly refreshEmitter = new Emitter<void>();
|
||||
protected readonly openEmitter = new Emitter<void>();
|
||||
|
||||
protected readonly input: HTMLInputElement;
|
||||
protected readonly loadIndicator: HTMLElement;
|
||||
protected readonly errorBar: HTMLElement & Readonly<{ message: HTMLElement }>;
|
||||
protected readonly frame: HTMLIFrameElement;
|
||||
// eslint-disable-next-line max-len
|
||||
// XXX This is a hack to be able to tack the mouse events when drag and dropping the widgets. On `mousedown` we put a transparent div over the `iframe` to avoid losing the mouse tacking.
|
||||
protected readonly transparentOverlay: HTMLElement;
|
||||
// XXX It is a hack. Instead of loading the PDF in an iframe we use `PDFObject` to render it in a div.
|
||||
protected readonly pdfContainer: HTMLElement;
|
||||
|
||||
protected frameLoadTimeout: number;
|
||||
protected readonly initialHistoryLength: number;
|
||||
protected readonly toDisposeOnGo = new DisposableCollection();
|
||||
|
||||
constructor(@inject(MiniBrowserProps) protected readonly props: MiniBrowserProps) {
|
||||
super();
|
||||
this.node.tabIndex = 0;
|
||||
this.addClass(MiniBrowserContentStyle.MINI_BROWSER);
|
||||
this.input = this.createToolbar(this.node).input;
|
||||
const contentArea = this.createContentArea(this.node);
|
||||
this.frame = contentArea.frame;
|
||||
this.transparentOverlay = contentArea.transparentOverlay;
|
||||
this.loadIndicator = contentArea.loadIndicator;
|
||||
this.errorBar = contentArea.errorBar;
|
||||
this.pdfContainer = contentArea.pdfContainer;
|
||||
this.initialHistoryLength = history.length;
|
||||
this.toDispose.pushAll([
|
||||
this.submitInputEmitter,
|
||||
this.navigateBackEmitter,
|
||||
this.navigateForwardEmitter,
|
||||
this.refreshEmitter,
|
||||
this.openEmitter
|
||||
]);
|
||||
}
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
this.toDispose.push(this.mouseTracker.onMousedown(e => {
|
||||
if (this.frame.style.display !== 'none') {
|
||||
this.transparentOverlay.style.display = 'block';
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(this.mouseTracker.onMouseup(e => {
|
||||
if (this.frame.style.display !== 'none') {
|
||||
this.transparentOverlay.style.display = 'none';
|
||||
}
|
||||
}));
|
||||
const { startPage } = this.props;
|
||||
if (startPage) {
|
||||
setTimeout(() => this.go(startPage), 500);
|
||||
this.listenOnContentChange(startPage);
|
||||
}
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
if (this.getToolbarProps() !== 'hide') {
|
||||
this.input.focus();
|
||||
} else {
|
||||
this.node.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected async listenOnContentChange(location: string): Promise<void> {
|
||||
if (await this.fileService.exists(new URI(location))) {
|
||||
const fileUri = new URI(location);
|
||||
const watcher = this.fileService.watch(fileUri);
|
||||
this.toDispose.push(watcher);
|
||||
const onFileChange = (event: FileChangesEvent) => {
|
||||
if (event.contains(fileUri, FileChangeType.ADDED) || event.contains(fileUri, FileChangeType.UPDATED)) {
|
||||
this.go(location, {
|
||||
showLoadIndicator: false
|
||||
});
|
||||
}
|
||||
};
|
||||
this.toDispose.push(this.fileService.onDidFilesChange(debounce(onFileChange, 500)));
|
||||
}
|
||||
}
|
||||
|
||||
protected createToolbar(parent: HTMLElement): HTMLDivElement & Readonly<{ input: HTMLInputElement }> {
|
||||
const toolbar = document.createElement('div');
|
||||
toolbar.classList.add(this.getToolbarProps() === 'read-only' ? MiniBrowserContentStyle.TOOLBAR_READ_ONLY : MiniBrowserContentStyle.TOOLBAR);
|
||||
parent.appendChild(toolbar);
|
||||
this.createPrevious(toolbar);
|
||||
this.createNext(toolbar);
|
||||
this.createRefresh(toolbar);
|
||||
const input = this.createInput(toolbar);
|
||||
input.readOnly = this.getToolbarProps() === 'read-only';
|
||||
this.createOpen(toolbar);
|
||||
if (this.getToolbarProps() === 'hide') {
|
||||
toolbar.style.display = 'none';
|
||||
}
|
||||
return Object.assign(toolbar, { input });
|
||||
}
|
||||
|
||||
protected getToolbarProps(): 'show' | 'hide' | 'read-only' {
|
||||
return !this.props.startPage ? 'show' : this.props.toolbar || 'show';
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
protected createContentArea(parent: HTMLElement): HTMLElement & Readonly<{ frame: HTMLIFrameElement, loadIndicator: HTMLElement, errorBar: HTMLElement & Readonly<{ message: HTMLElement }>, pdfContainer: HTMLElement, transparentOverlay: HTMLElement }> {
|
||||
const contentArea = document.createElement('div');
|
||||
contentArea.classList.add(MiniBrowserContentStyle.CONTENT_AREA);
|
||||
|
||||
const loadIndicator = document.createElement('div');
|
||||
loadIndicator.classList.add(MiniBrowserContentStyle.PRE_LOAD);
|
||||
loadIndicator.style.display = 'none';
|
||||
|
||||
const errorBar = this.createErrorBar();
|
||||
|
||||
const frame = this.createIFrame();
|
||||
this.submitInputEmitter.event(input => this.go(input, {
|
||||
preserveFocus: false
|
||||
}));
|
||||
this.navigateBackEmitter.event(this.handleBack.bind(this));
|
||||
this.navigateForwardEmitter.event(this.handleForward.bind(this));
|
||||
this.refreshEmitter.event(this.handleRefresh.bind(this));
|
||||
this.openEmitter.event(this.handleOpen.bind(this));
|
||||
|
||||
const transparentOverlay = document.createElement('div');
|
||||
transparentOverlay.classList.add(MiniBrowserContentStyle.TRANSPARENT_OVERLAY);
|
||||
transparentOverlay.style.display = 'none';
|
||||
|
||||
const pdfContainer = document.createElement('div');
|
||||
pdfContainer.classList.add(MiniBrowserContentStyle.PDF_CONTAINER);
|
||||
pdfContainer.id = `${this.id}-pdf-container`;
|
||||
pdfContainer.style.display = 'none';
|
||||
|
||||
contentArea.appendChild(errorBar);
|
||||
contentArea.appendChild(transparentOverlay);
|
||||
contentArea.appendChild(pdfContainer);
|
||||
contentArea.appendChild(loadIndicator);
|
||||
contentArea.appendChild(frame);
|
||||
|
||||
parent.appendChild(contentArea);
|
||||
return Object.assign(contentArea, { frame, loadIndicator, errorBar, pdfContainer, transparentOverlay });
|
||||
}
|
||||
|
||||
protected createIFrame(): HTMLIFrameElement {
|
||||
const frame = document.createElement('iframe');
|
||||
const sandbox = (this.props.sandbox || MiniBrowserProps.SandboxOptions.DEFAULT).map(name => MiniBrowserProps.SandboxOptions[name]);
|
||||
frame.sandbox.add(...sandbox);
|
||||
this.toDispose.push(addEventListener(frame, 'load', this.onFrameLoad.bind(this)));
|
||||
this.toDispose.push(addEventListener(frame, 'error', this.onFrameError.bind(this)));
|
||||
return frame;
|
||||
}
|
||||
|
||||
protected createErrorBar(): HTMLElement & Readonly<{ message: HTMLElement }> {
|
||||
const errorBar = document.createElement('div');
|
||||
errorBar.classList.add(MiniBrowserContentStyle.ERROR_BAR);
|
||||
errorBar.style.display = 'none';
|
||||
|
||||
const icon = document.createElement('span');
|
||||
icon.classList.add(...codiconArray('info'));
|
||||
errorBar.appendChild(icon);
|
||||
|
||||
const message = document.createElement('span');
|
||||
errorBar.appendChild(message);
|
||||
|
||||
return Object.assign(errorBar, { message });
|
||||
}
|
||||
|
||||
protected onFrameLoad(): void {
|
||||
clearTimeout(this.frameLoadTimeout);
|
||||
this.maybeResetBackground();
|
||||
this.hideLoadIndicator();
|
||||
this.hideErrorBar();
|
||||
}
|
||||
|
||||
protected onFrameError(): void {
|
||||
clearTimeout(this.frameLoadTimeout);
|
||||
this.maybeResetBackground();
|
||||
this.hideLoadIndicator();
|
||||
this.showErrorBar('An error occurred while loading this page');
|
||||
}
|
||||
|
||||
protected onFrameTimeout(): void {
|
||||
clearTimeout(this.frameLoadTimeout);
|
||||
this.maybeResetBackground();
|
||||
this.hideLoadIndicator();
|
||||
this.showErrorBar('Still loading...');
|
||||
}
|
||||
|
||||
protected showLoadIndicator(): void {
|
||||
this.loadIndicator.classList.remove(MiniBrowserContentStyle.FADE_OUT);
|
||||
this.loadIndicator.style.display = 'block';
|
||||
}
|
||||
|
||||
protected hideLoadIndicator(): void {
|
||||
// Start the fade-out transition.
|
||||
this.loadIndicator.classList.add(MiniBrowserContentStyle.FADE_OUT);
|
||||
// Actually hide the load indicator after the transition is finished.
|
||||
const preloadStyle = window.getComputedStyle(this.loadIndicator);
|
||||
const transitionDuration = parseCssTime(preloadStyle.transitionDuration, 0);
|
||||
setTimeout(() => {
|
||||
// But don't hide it if it was shown again since the transition started.
|
||||
if (this.loadIndicator.classList.contains(MiniBrowserContentStyle.FADE_OUT)) {
|
||||
this.loadIndicator.style.display = 'none';
|
||||
this.loadIndicator.classList.remove(MiniBrowserContentStyle.FADE_OUT);
|
||||
}
|
||||
}, transitionDuration);
|
||||
}
|
||||
|
||||
protected showErrorBar(message: string): void {
|
||||
this.errorBar.message.textContent = message;
|
||||
this.errorBar.style.display = 'block';
|
||||
}
|
||||
|
||||
protected hideErrorBar(): void {
|
||||
this.errorBar.message.textContent = '';
|
||||
this.errorBar.style.display = 'none';
|
||||
}
|
||||
|
||||
protected maybeResetBackground(): void {
|
||||
if (this.props.resetBackground === true) {
|
||||
this.frame.style.backgroundColor = 'white';
|
||||
}
|
||||
}
|
||||
|
||||
protected handleBack(): void {
|
||||
if (history.length - this.initialHistoryLength > 0) {
|
||||
history.back();
|
||||
}
|
||||
}
|
||||
|
||||
protected handleForward(): void {
|
||||
if (history.length > this.initialHistoryLength) {
|
||||
history.forward();
|
||||
}
|
||||
}
|
||||
|
||||
protected handleRefresh(): void {
|
||||
// Initial pessimism; use the location of the input.
|
||||
let location: string | undefined = this.props.startPage;
|
||||
// Use the the location from the `input`.
|
||||
if (this.input && this.input.value) {
|
||||
location = this.input.value;
|
||||
}
|
||||
try {
|
||||
const { contentDocument } = this.frame;
|
||||
if (contentDocument && contentDocument.location) {
|
||||
location = contentDocument.location.href;
|
||||
}
|
||||
} catch {
|
||||
// Security exception due to CORS when trying to access the `location.href` of the content document.
|
||||
}
|
||||
if (location) {
|
||||
this.go(location, {
|
||||
preserveFocus: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected handleOpen(): void {
|
||||
const location = this.frameSrc() || this.input.value;
|
||||
if (location) {
|
||||
this.windowService.openNewWindow(location);
|
||||
}
|
||||
}
|
||||
|
||||
protected createInput(parent: HTMLElement): HTMLInputElement {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.spellcheck = false;
|
||||
input.classList.add('theia-input');
|
||||
this.toDispose.pushAll([
|
||||
addEventListener(input, 'keydown', this.handleInputChange.bind(this)),
|
||||
addEventListener(input, 'click', () => {
|
||||
if (this.getToolbarProps() === 'read-only') {
|
||||
this.handleOpen();
|
||||
} else {
|
||||
if (input.value) {
|
||||
input.select();
|
||||
}
|
||||
}
|
||||
})
|
||||
]);
|
||||
parent.appendChild(input);
|
||||
return input;
|
||||
}
|
||||
|
||||
protected handleInputChange(e: KeyboardEvent): void {
|
||||
const { key } = KeyCode.createKeyCode(e);
|
||||
if (key && Key.ENTER.keyCode === key.keyCode && this.getToolbarProps() === 'show') {
|
||||
const { target } = e;
|
||||
if (target instanceof HTMLInputElement) {
|
||||
this.mapLocation(target.value).then(location => this.submitInputEmitter.fire(location));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected createPrevious(parent: HTMLElement): HTMLElement {
|
||||
return this.onClick(this.createButton(parent, 'Show The Previous Page', MiniBrowserContentStyle.PREVIOUS), this.navigateBackEmitter);
|
||||
}
|
||||
|
||||
protected createNext(parent: HTMLElement): HTMLElement {
|
||||
return this.onClick(this.createButton(parent, 'Show The Next Page', MiniBrowserContentStyle.NEXT), this.navigateForwardEmitter);
|
||||
}
|
||||
|
||||
protected createRefresh(parent: HTMLElement): HTMLElement {
|
||||
return this.onClick(this.createButton(parent, 'Reload This Page', MiniBrowserContentStyle.REFRESH), this.refreshEmitter);
|
||||
}
|
||||
|
||||
protected createOpen(parent: HTMLElement): HTMLElement {
|
||||
const button = this.onClick(this.createButton(parent, 'Open In A New Window', MiniBrowserContentStyle.OPEN), this.openEmitter);
|
||||
return button;
|
||||
}
|
||||
|
||||
protected createButton(parent: HTMLElement, title: string, ...className: string[]): HTMLElement {
|
||||
const button = document.createElement('div');
|
||||
button.title = title;
|
||||
button.classList.add(...className, MiniBrowserContentStyle.BUTTON);
|
||||
parent.appendChild(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected onClick(element: HTMLElement, emitter: Emitter<any>): HTMLElement {
|
||||
this.toDispose.push(addEventListener(element, 'click', () => {
|
||||
if (!element.classList.contains(MiniBrowserContentStyle.DISABLED)) {
|
||||
emitter.fire(undefined);
|
||||
}
|
||||
}));
|
||||
return element;
|
||||
}
|
||||
|
||||
protected mapLocation(location: string): Promise<string> {
|
||||
return this.locationMapper.map(location);
|
||||
}
|
||||
|
||||
protected setInput(value: string): void {
|
||||
if (this.input.value !== value) {
|
||||
this.input.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected frameSrc(): string {
|
||||
let src = this.frame.src;
|
||||
try {
|
||||
const { contentWindow } = this.frame;
|
||||
if (contentWindow) {
|
||||
src = contentWindow.location.href;
|
||||
}
|
||||
} catch {
|
||||
// CORS issue. Ignored.
|
||||
}
|
||||
if (src === 'about:blank') {
|
||||
src = '';
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
protected contentDocument(): Document | null {
|
||||
try {
|
||||
let { contentDocument } = this.frame;
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
if (contentDocument === null) {
|
||||
const { contentWindow } = this.frame;
|
||||
if (contentWindow) {
|
||||
contentDocument = contentWindow.document;
|
||||
}
|
||||
}
|
||||
return contentDocument;
|
||||
} catch {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected async go(location: string, options?: Partial<{
|
||||
/* default: true */
|
||||
showLoadIndicator: boolean,
|
||||
/* default: true */
|
||||
preserveFocus: boolean
|
||||
}>): Promise<void> {
|
||||
const { showLoadIndicator, preserveFocus } = {
|
||||
showLoadIndicator: true,
|
||||
preserveFocus: true,
|
||||
...options
|
||||
};
|
||||
if (location) {
|
||||
try {
|
||||
this.toDisposeOnGo.dispose();
|
||||
const url = await this.mapLocation(location);
|
||||
this.setInput(url);
|
||||
if (this.getToolbarProps() === 'read-only') {
|
||||
this.input.title = `Open ${url} In A New Window`;
|
||||
}
|
||||
clearTimeout(this.frameLoadTimeout);
|
||||
this.frameLoadTimeout = window.setTimeout(this.onFrameTimeout.bind(this), 4000);
|
||||
if (showLoadIndicator) {
|
||||
this.showLoadIndicator();
|
||||
}
|
||||
if (url.endsWith('.pdf')) {
|
||||
this.pdfContainer.style.display = 'block';
|
||||
this.frame.style.display = 'none';
|
||||
PDFObject.embed(url, this.pdfContainer, {
|
||||
// eslint-disable-next-line max-len, @typescript-eslint/quotes
|
||||
fallbackLink: `<p style="padding: 0px 15px 0px 15px">Your browser does not support inline PDFs. Click on this <a href='[url]' target="_blank">link</a> to open the PDF in a new tab.</p>`
|
||||
});
|
||||
clearTimeout(this.frameLoadTimeout);
|
||||
this.hideLoadIndicator();
|
||||
if (!preserveFocus) {
|
||||
this.pdfContainer.focus();
|
||||
}
|
||||
} else {
|
||||
this.pdfContainer.style.display = 'none';
|
||||
this.frame.style.display = 'block';
|
||||
this.frame.src = url;
|
||||
// The load indicator will hide itself if the content of the iframe was loaded.
|
||||
if (!preserveFocus) {
|
||||
this.frame.addEventListener('load', () => {
|
||||
const window = this.frame.contentWindow;
|
||||
if (window) {
|
||||
window.focus();
|
||||
}
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
// Delegate all the `keypress` events from the `iframe` to the application.
|
||||
this.toDisposeOnGo.push(addEventListener(this.frame, 'load', () => {
|
||||
try {
|
||||
const { contentDocument } = this.frame;
|
||||
if (contentDocument) {
|
||||
const keypressHandler = (e: KeyboardEvent) => this.keybindings.run(e);
|
||||
contentDocument.addEventListener('keypress', keypressHandler, true);
|
||||
this.toDisposeOnDetach.push(Disposable.create(() => contentDocument.removeEventListener('keypress', keypressHandler)));
|
||||
}
|
||||
} catch {
|
||||
// There is not much we could do with the security exceptions due to CORS.
|
||||
}
|
||||
}));
|
||||
} catch (e) {
|
||||
clearTimeout(this.frameLoadTimeout);
|
||||
this.hideLoadIndicator();
|
||||
this.showErrorBar(String(e));
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import '../../src/browser/style/index.css';
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
|
||||
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
|
||||
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import { WebSocketConnectionProvider } from '@theia/core/lib/browser/messaging/ws-connection-provider';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
|
||||
import { TabBarToolbarContribution } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { CommandContribution } from '@theia/core/lib/common/command';
|
||||
import { MenuContribution } from '@theia/core/lib/common/menu';
|
||||
import { NavigatableWidgetOptions } from '@theia/core/lib/browser/navigatable';
|
||||
import { MiniBrowserOpenHandler } from './mini-browser-open-handler';
|
||||
import { MiniBrowserService, MiniBrowserServicePath } from '../common/mini-browser-service';
|
||||
import { MiniBrowser, MiniBrowserOptions } from './mini-browser';
|
||||
import { MiniBrowserProps, MiniBrowserContentFactory, MiniBrowserContent } from './mini-browser-content';
|
||||
import {
|
||||
LocationMapperService,
|
||||
FileLocationMapper,
|
||||
HttpLocationMapper,
|
||||
HttpsLocationMapper,
|
||||
LocationMapper,
|
||||
LocationWithoutSchemeMapper,
|
||||
} from './location-mapper-service';
|
||||
import { MiniBrowserFrontendSecurityWarnings } from './mini-browser-frontend-security-warnings';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(MiniBrowserContent).toSelf();
|
||||
bind(MiniBrowserContentFactory).toFactory(context => (props: MiniBrowserProps) => {
|
||||
const { container } = context;
|
||||
const child = container.createChild();
|
||||
child.bind(MiniBrowserProps).toConstantValue(props);
|
||||
return child.get(MiniBrowserContent);
|
||||
});
|
||||
|
||||
bind(MiniBrowser).toSelf();
|
||||
bind(WidgetFactory).toDynamicValue(context => ({
|
||||
id: MiniBrowser.ID,
|
||||
async createWidget(options: NavigatableWidgetOptions): Promise<MiniBrowser> {
|
||||
const { container } = context;
|
||||
const child = container.createChild();
|
||||
const uri = new URI(options.uri);
|
||||
child.bind(MiniBrowserOptions).toConstantValue({ uri });
|
||||
return child.get(MiniBrowser);
|
||||
}
|
||||
})).inSingletonScope();
|
||||
|
||||
bind(MiniBrowserOpenHandler).toSelf().inSingletonScope();
|
||||
bind(OpenHandler).toService(MiniBrowserOpenHandler);
|
||||
bind(FrontendApplicationContribution).toService(MiniBrowserOpenHandler);
|
||||
bind(CommandContribution).toService(MiniBrowserOpenHandler);
|
||||
bind(MenuContribution).toService(MiniBrowserOpenHandler);
|
||||
bind(TabBarToolbarContribution).toService(MiniBrowserOpenHandler);
|
||||
|
||||
bindContributionProvider(bind, LocationMapper);
|
||||
bind(LocationMapper).to(FileLocationMapper).inSingletonScope();
|
||||
bind(LocationMapper).to(HttpLocationMapper).inSingletonScope();
|
||||
bind(LocationMapper).to(HttpsLocationMapper).inSingletonScope();
|
||||
bind(LocationWithoutSchemeMapper).toSelf().inSingletonScope();
|
||||
bind(LocationMapper).toService(LocationWithoutSchemeMapper);
|
||||
bind(LocationMapperService).toSelf().inSingletonScope();
|
||||
|
||||
bind(MiniBrowserService).toDynamicValue(
|
||||
ctx => WebSocketConnectionProvider.createProxy(ctx.container, MiniBrowserServicePath)
|
||||
).inSingletonScope();
|
||||
|
||||
bind(MiniBrowserFrontendSecurityWarnings).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(MiniBrowserFrontendSecurityWarnings);
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { MessageService } from '@theia/core';
|
||||
import { Dialog, FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
import { WindowService } from '@theia/core/lib/browser/window/window-service';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { MiniBrowserEndpoint } from '../common/mini-browser-endpoint';
|
||||
import { MiniBrowserEnvironment } from './environment/mini-browser-environment';
|
||||
|
||||
@injectable()
|
||||
export class MiniBrowserFrontendSecurityWarnings implements FrontendApplicationContribution {
|
||||
|
||||
@inject(WindowService)
|
||||
protected windowService: WindowService;
|
||||
|
||||
@inject(MessageService)
|
||||
protected messageService: MessageService;
|
||||
|
||||
@inject(MiniBrowserEnvironment)
|
||||
protected miniBrowserEnvironment: MiniBrowserEnvironment;
|
||||
|
||||
initialize(): void {
|
||||
this.checkHostPattern();
|
||||
}
|
||||
|
||||
protected async checkHostPattern(): Promise<void> {
|
||||
if (FrontendApplicationConfigProvider.get()['warnOnPotentiallyInsecureHostPattern'] === false) {
|
||||
return;
|
||||
}
|
||||
const hostPattern = await this.miniBrowserEnvironment.hostPatternPromise;
|
||||
if (hostPattern !== MiniBrowserEndpoint.HOST_PATTERN_DEFAULT) {
|
||||
const goToReadme = nls.localize('theia/webview/goToReadme', 'Go To README');
|
||||
const message = nls.localize('theia/webview/messageWarning', '\
|
||||
The {0} endpoint\'s host pattern has been changed to `{1}`; changing the pattern can lead to security vulnerabilities. \
|
||||
See `{2}` for more information.', 'mini-browser', hostPattern, '@theia/mini-browser/README.md');
|
||||
this.messageService.warn(message, Dialog.OK, goToReadme).then(action => {
|
||||
if (action === goToReadme) {
|
||||
this.windowService.openNewWindow('https://www.npmjs.com/package/@theia/mini-browser', { external: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
312
packages/mini-browser/src/browser/mini-browser-open-handler.ts
Normal file
312
packages/mini-browser/src/browser/mini-browser-open-handler.ts
Normal file
@@ -0,0 +1,312 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Widget } from '@theia/core/shared/@lumino/widgets';
|
||||
import { injectable, inject, optional } from '@theia/core/shared/inversify';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { codicon, QuickInputService } from '@theia/core/lib/browser';
|
||||
import { ApplicationShell } from '@theia/core/lib/browser/shell';
|
||||
import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common/command';
|
||||
import { MenuContribution, MenuModelRegistry } from '@theia/core/lib/common/menu';
|
||||
import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar';
|
||||
import { NavigatableWidget, NavigatableWidgetOpenHandler } from '@theia/core/lib/browser/navigatable';
|
||||
import { open, OpenerService } from '@theia/core/lib/browser/opener-service';
|
||||
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application-contribution';
|
||||
import { WidgetOpenerOptions } from '@theia/core/lib/browser/widget-open-handler';
|
||||
import { MiniBrowserService } from '../common/mini-browser-service';
|
||||
import { MiniBrowser, MiniBrowserProps } from './mini-browser';
|
||||
import { LocationMapperService } from './location-mapper-service';
|
||||
import { nls } from '@theia/core/lib/common/nls';
|
||||
|
||||
export namespace MiniBrowserCommands {
|
||||
|
||||
export const PREVIEW_CATEGORY = 'Preview';
|
||||
export const PREVIEW_CATEGORY_KEY = nls.getDefaultKey(PREVIEW_CATEGORY);
|
||||
|
||||
export const PREVIEW = Command.toLocalizedCommand({
|
||||
id: 'mini-browser.preview',
|
||||
label: 'Open Preview',
|
||||
iconClass: codicon('open-preview')
|
||||
}, 'vscode.markdown-language-features/package/markdown.preview.title');
|
||||
export const OPEN_SOURCE: Command = {
|
||||
id: 'mini-browser.open.source',
|
||||
iconClass: codicon('go-to-file')
|
||||
};
|
||||
export const OPEN_URL = Command.toDefaultLocalizedCommand({
|
||||
id: 'mini-browser.openUrl',
|
||||
category: PREVIEW_CATEGORY,
|
||||
label: 'Open URL'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Further options for opening a new `Mini Browser` widget.
|
||||
*/
|
||||
export interface MiniBrowserOpenerOptions extends WidgetOpenerOptions, MiniBrowserProps {
|
||||
/**
|
||||
* Controls how the mini-browser widget should be opened.
|
||||
* - `source`: editable source.
|
||||
* - `preview`: rendered content of the source.
|
||||
*/
|
||||
openFor?: 'source' | 'preview';
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MiniBrowserOpenHandler extends NavigatableWidgetOpenHandler<MiniBrowser>
|
||||
implements FrontendApplicationContribution, CommandContribution, MenuContribution, TabBarToolbarContribution {
|
||||
|
||||
static PREVIEW_URI = new URI().withScheme('__minibrowser__preview__');
|
||||
|
||||
/**
|
||||
* Instead of going to the backend with each file URI to ask whether it can handle the current file or not,
|
||||
* we have this map of extension and priority pairs that we populate at application startup.
|
||||
* The real advantage of this approach is the following: [Lumino cannot run async code when invoking `isEnabled`/`isVisible`
|
||||
* for the command handlers](https://github.com/eclipse-theia/theia/issues/1958#issuecomment-392829371)
|
||||
* so the menu item would be always visible for the user even if the file type cannot be handled eventually.
|
||||
* Hopefully, we could get rid of this hack once we have migrated the existing Lumino code to [React](https://github.com/eclipse-theia/theia/issues/1915).
|
||||
*/
|
||||
protected readonly supportedExtensions: Map<string, number> = new Map();
|
||||
|
||||
readonly id = MiniBrowser.ID;
|
||||
readonly label = nls.localize(MiniBrowserCommands.PREVIEW_CATEGORY_KEY, MiniBrowserCommands.PREVIEW_CATEGORY);
|
||||
|
||||
@inject(OpenerService)
|
||||
protected readonly openerService: OpenerService;
|
||||
|
||||
@inject(LabelProvider)
|
||||
protected readonly labelProvider: LabelProvider;
|
||||
|
||||
@inject(QuickInputService) @optional()
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
|
||||
@inject(MiniBrowserService)
|
||||
protected readonly miniBrowserService: MiniBrowserService;
|
||||
|
||||
@inject(LocationMapperService)
|
||||
protected readonly locationMapperService: LocationMapperService;
|
||||
|
||||
onStart(): void {
|
||||
this.miniBrowserService.supportedFileExtensions().then(entries => {
|
||||
entries.forEach(entry => {
|
||||
const { extension, priority } = entry;
|
||||
this.supportedExtensions.set(extension, priority);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
canHandle(uri: URI, options?: MiniBrowserOpenerOptions): number {
|
||||
// It does not guard against directories. For instance, a folder with this name: `Hahahah.html`.
|
||||
// We could check with the FS, but then, this method would become async again.
|
||||
const extension = uri.toString().split('.').pop();
|
||||
if (!extension) {
|
||||
return 0;
|
||||
}
|
||||
if (options?.openFor === 'source') {
|
||||
return -100;
|
||||
} else if (options?.openFor === 'preview') {
|
||||
return 200; // higher than that of the editor.
|
||||
} else {
|
||||
return this.supportedExtensions.get(extension.toLocaleLowerCase()) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
override async open(uri: URI, options?: MiniBrowserOpenerOptions): Promise<MiniBrowser> {
|
||||
const widget = await super.open(uri, options);
|
||||
const area = this.shell.getAreaFor(widget);
|
||||
if (area === 'right' || area === 'left') {
|
||||
const panelLayout = area === 'right' ? this.shell.getLayoutData().rightPanel : this.shell.getLayoutData().leftPanel;
|
||||
const minSize = this.shell.mainPanel.node.offsetWidth / 2;
|
||||
if (panelLayout && panelLayout.size && panelLayout.size <= minSize) {
|
||||
requestAnimationFrame(() => this.shell.resize(minSize, area));
|
||||
}
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
protected override async getOrCreateWidget(uri: URI, options?: MiniBrowserOpenerOptions): Promise<MiniBrowser> {
|
||||
const props = await this.options(uri, options);
|
||||
const widget = await super.getOrCreateWidget(uri, props);
|
||||
widget.setProps(props);
|
||||
return widget;
|
||||
}
|
||||
|
||||
protected async options(uri?: URI, options?: MiniBrowserOpenerOptions): Promise<MiniBrowserOpenerOptions & { widgetOptions: ApplicationShell.WidgetOptions }> {
|
||||
// Get the default options.
|
||||
let result = await this.defaultOptions();
|
||||
if (uri) {
|
||||
// Decorate it with a few properties inferred from the URI.
|
||||
const startPage = uri.toString(true);
|
||||
const name = this.labelProvider.getName(uri);
|
||||
const iconClass = `${this.labelProvider.getIcon(uri)} file-icon`;
|
||||
// The background has to be reset to white only for "real" web-pages but not for images, for instance.
|
||||
const resetBackground = await this.resetBackground(uri);
|
||||
result = {
|
||||
...result,
|
||||
startPage,
|
||||
name,
|
||||
iconClass,
|
||||
// Make sure the toolbar is not visible. We have the `iframe.src` anyway.
|
||||
toolbar: 'hide',
|
||||
resetBackground
|
||||
};
|
||||
}
|
||||
if (options) {
|
||||
// Explicit options overrule everything.
|
||||
result = {
|
||||
...result,
|
||||
...options
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected resetBackground(uri: URI): MaybePromise<boolean> {
|
||||
const { scheme } = uri;
|
||||
const uriStr = uri.toString();
|
||||
return scheme === 'http'
|
||||
|| scheme === 'https'
|
||||
|| (scheme === 'file'
|
||||
&& (uriStr.endsWith('html') || uriStr.endsWith('.htm'))
|
||||
);
|
||||
}
|
||||
|
||||
protected async defaultOptions(): Promise<MiniBrowserOpenerOptions & { widgetOptions: ApplicationShell.WidgetOptions }> {
|
||||
return {
|
||||
mode: 'activate',
|
||||
widgetOptions: { area: 'main' },
|
||||
sandbox: MiniBrowserProps.SandboxOptions.DEFAULT,
|
||||
toolbar: 'show'
|
||||
};
|
||||
}
|
||||
|
||||
registerCommands(commands: CommandRegistry): void {
|
||||
commands.registerCommand(MiniBrowserCommands.PREVIEW, {
|
||||
execute: widget => this.preview(widget),
|
||||
isEnabled: widget => this.canPreviewWidget(widget),
|
||||
isVisible: widget => this.canPreviewWidget(widget)
|
||||
});
|
||||
commands.registerCommand(MiniBrowserCommands.OPEN_SOURCE, {
|
||||
execute: widget => this.openSource(widget),
|
||||
isEnabled: widget => !!this.getSourceUri(widget),
|
||||
isVisible: widget => !!this.getSourceUri(widget)
|
||||
});
|
||||
commands.registerCommand(MiniBrowserCommands.OPEN_URL, {
|
||||
execute: (arg?: string) => this.openUrl(arg)
|
||||
});
|
||||
}
|
||||
|
||||
registerMenus(menus: MenuModelRegistry): void {
|
||||
menus.registerMenuAction(['editor_context_menu', 'navigation'], {
|
||||
commandId: MiniBrowserCommands.PREVIEW.id
|
||||
});
|
||||
}
|
||||
|
||||
registerToolbarItems(toolbar: TabBarToolbarRegistry): void {
|
||||
toolbar.registerItem({
|
||||
id: MiniBrowserCommands.PREVIEW.id,
|
||||
command: MiniBrowserCommands.PREVIEW.id,
|
||||
tooltip: nls.localize('vscode.markdown-language-features/package/markdown.previewSide.title', 'Open Preview to the Side')
|
||||
});
|
||||
toolbar.registerItem({
|
||||
id: MiniBrowserCommands.OPEN_SOURCE.id,
|
||||
command: MiniBrowserCommands.OPEN_SOURCE.id,
|
||||
tooltip: nls.localize('vscode.markdown-language-features/package/markdown.showSource.title', 'Open Source')
|
||||
});
|
||||
}
|
||||
|
||||
protected canPreviewWidget(widget?: Widget): boolean {
|
||||
const uri = this.getUriToPreview(widget);
|
||||
return !!uri && !!this.canHandle(uri);
|
||||
}
|
||||
|
||||
protected getUriToPreview(widget?: Widget): URI | undefined {
|
||||
const current = this.getWidgetToPreview(widget);
|
||||
return current && current.getResourceUri();
|
||||
}
|
||||
|
||||
protected getWidgetToPreview(widget?: Widget): NavigatableWidget | undefined {
|
||||
const current = widget ? widget : this.shell.currentWidget;
|
||||
// MiniBrowser is NavigatableWidget and should be excluded from widgets to preview
|
||||
return !(current instanceof MiniBrowser) && NavigatableWidget.is(current) && current || undefined;
|
||||
}
|
||||
|
||||
protected async preview(widget?: Widget): Promise<void> {
|
||||
const ref = this.getWidgetToPreview(widget);
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
const uri = ref.getResourceUri();
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
await this.open(uri, {
|
||||
mode: 'reveal',
|
||||
widgetOptions: { ref, mode: 'open-to-right' },
|
||||
openFor: 'preview'
|
||||
});
|
||||
}
|
||||
|
||||
protected async openSource(ref?: Widget): Promise<void> {
|
||||
const uri = this.getSourceUri(ref);
|
||||
if (uri) {
|
||||
await open(this.openerService, uri, {
|
||||
widgetOptions: { ref, mode: 'tab-after' },
|
||||
openFor: 'source'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected getSourceUri(ref?: Widget): URI | undefined {
|
||||
const uri = ref instanceof MiniBrowser && ref.getResourceUri() || undefined;
|
||||
if (!uri || uri.scheme === 'http' || uri.scheme === 'https' || uri.isEqual(MiniBrowserOpenHandler.PREVIEW_URI)) {
|
||||
return undefined;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
protected async openUrl(arg?: string): Promise<void> {
|
||||
const url = arg ? arg : await this.quickInputService?.input({
|
||||
prompt: nls.localizeByDefault('URL to open'),
|
||||
placeHolder: nls.localize('theia/mini-browser/typeUrl', 'Type a URL')
|
||||
});
|
||||
if (url) {
|
||||
await this.openPreview(url);
|
||||
}
|
||||
}
|
||||
|
||||
async openPreview(startPage: string): Promise<MiniBrowser> {
|
||||
const props = await this.getOpenPreviewProps(await this.locationMapperService.map(startPage));
|
||||
return this.open(MiniBrowserOpenHandler.PREVIEW_URI, props);
|
||||
}
|
||||
|
||||
protected async getOpenPreviewProps(startPage: string): Promise<MiniBrowserOpenerOptions> {
|
||||
const resetBackground = await this.resetBackground(new URI(startPage));
|
||||
return {
|
||||
name: nls.localize(MiniBrowserCommands.PREVIEW_CATEGORY_KEY, MiniBrowserCommands.PREVIEW_CATEGORY),
|
||||
startPage,
|
||||
toolbar: 'read-only',
|
||||
widgetOptions: {
|
||||
area: 'right'
|
||||
},
|
||||
resetBackground,
|
||||
iconClass: codicon('preview'),
|
||||
openFor: 'preview'
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
110
packages/mini-browser/src/browser/mini-browser.ts
Normal file
110
packages/mini-browser/src/browser/mini-browser.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import { Message } from '@theia/core/shared/@lumino/messaging';
|
||||
import URI from '@theia/core/lib/common/uri';
|
||||
import { NavigatableWidget, StatefulWidget } from '@theia/core/lib/browser';
|
||||
import { DisposableCollection } from '@theia/core/lib/common/disposable';
|
||||
import { BaseWidget, codicon, PanelLayout } from '@theia/core/lib/browser/widgets/widget';
|
||||
import { MiniBrowserProps, MiniBrowserContentFactory } from './mini-browser-content';
|
||||
|
||||
export { MiniBrowserProps };
|
||||
|
||||
@injectable()
|
||||
export class MiniBrowserOptions {
|
||||
uri: URI;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MiniBrowser extends BaseWidget implements NavigatableWidget, StatefulWidget {
|
||||
|
||||
static ID = 'mini-browser';
|
||||
static ICON = codicon('globe');
|
||||
|
||||
@inject(MiniBrowserOptions)
|
||||
protected readonly options: MiniBrowserOptions;
|
||||
|
||||
@inject(MiniBrowserContentFactory)
|
||||
protected readonly createContent: MiniBrowserContentFactory;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
const { uri } = this.options;
|
||||
this.id = `${MiniBrowser.ID}:${uri.toString()}`;
|
||||
this.title.closable = true;
|
||||
this.layout = new PanelLayout({ fitPolicy: 'set-no-constraint' });
|
||||
}
|
||||
|
||||
getResourceUri(): URI | undefined {
|
||||
return this.options.uri;
|
||||
}
|
||||
|
||||
createMoveToUri(resourceUri: URI): URI | undefined {
|
||||
return this.options.uri && this.options.uri.withPath(resourceUri.path);
|
||||
}
|
||||
|
||||
protected props: MiniBrowserProps | undefined;
|
||||
protected readonly toDisposeOnProps = new DisposableCollection();
|
||||
|
||||
setProps(raw: MiniBrowserProps): void {
|
||||
const props: MiniBrowserProps = {
|
||||
toolbar: raw.toolbar,
|
||||
startPage: raw.startPage,
|
||||
sandbox: raw.sandbox,
|
||||
iconClass: raw.iconClass,
|
||||
name: raw.name,
|
||||
resetBackground: raw.resetBackground
|
||||
};
|
||||
if (JSON.stringify(props) === JSON.stringify(this.props)) {
|
||||
return;
|
||||
}
|
||||
this.toDisposeOnProps.dispose();
|
||||
this.toDispose.push(this.toDisposeOnProps);
|
||||
this.props = props;
|
||||
|
||||
this.title.caption = this.title.label = props.name || 'Browser';
|
||||
this.title.iconClass = props.iconClass || MiniBrowser.ICON;
|
||||
|
||||
const content = this.createContent(props);
|
||||
(this.layout as PanelLayout).addWidget(content);
|
||||
this.toDisposeOnProps.push(content);
|
||||
}
|
||||
|
||||
protected override onActivateRequest(msg: Message): void {
|
||||
super.onActivateRequest(msg);
|
||||
const widget = (this.layout as PanelLayout).widgets[0];
|
||||
if (widget) {
|
||||
widget.activate();
|
||||
}
|
||||
}
|
||||
|
||||
storeState(): object {
|
||||
const { props } = this;
|
||||
return { props };
|
||||
}
|
||||
|
||||
restoreState(oldState: object): void {
|
||||
if (!this.toDisposeOnProps.disposed) {
|
||||
return;
|
||||
}
|
||||
if ('props' in oldState) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.setProps((<any>oldState)['props']);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
99
packages/mini-browser/src/browser/pdfobject.d.ts
vendored
Normal file
99
packages/mini-browser/src/browser/pdfobject.d.ts
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/**
|
||||
* The documentation was copied from https://pdfobject.com/#api.
|
||||
* License: MIT (https://pipwerks.mit-license.org)
|
||||
*/
|
||||
declare module 'pdfobject' {
|
||||
|
||||
interface Options {
|
||||
|
||||
/**
|
||||
* Alias for PDF Open Parameters "page" option.
|
||||
* Any number entered here will cause the PDF be opened to the specified page number (if the browser supports it). If left unspecified, the PDF will open on page 1.
|
||||
*/
|
||||
readonly page?: string;
|
||||
|
||||
/**
|
||||
* Any string entered here will be appended to the generated <embed> element as the ID.
|
||||
* If left unspecified, no ID will be appended.
|
||||
*/
|
||||
readonly id?: string;
|
||||
|
||||
/**
|
||||
* Will insert the width as an inline style via the style attribute on the <embed> element.
|
||||
* If left unspecified, `PDFObject` will default to `100%`. Is standard CSS, supports all units, including `px`, `%`, `em`, and `rem`.
|
||||
*/
|
||||
readonly width?: string;
|
||||
|
||||
/**
|
||||
* Will insert the height as an inline style via the style attribute on the target element.
|
||||
* If left unspecified, `PDFObject` will default to `100%`. Is standard CSS, supports all units, including `px`, `%`, `em`, and `rem`.
|
||||
*/
|
||||
readonly height?: string;
|
||||
|
||||
/**
|
||||
* Any string entered here will be inserted into the target element when the browser doesn't support inline PDFs.
|
||||
*
|
||||
* **Default**: `"<p>This browser does not support inline PDFs. Please download the PDF to view it: <a href='[url]'>Download PDF</a></p>"`.
|
||||
* Supports HTML. Use the shortcode `[url]` to insert the URL of the PDF (as specified via the URL parameter in the `embed()` method).
|
||||
* Entering `false` will disable the fallback text option and prevent `PDFObject` from inserting fallback text.
|
||||
*/
|
||||
readonly fallbackLink?: string | boolean;
|
||||
|
||||
/**
|
||||
* Allows you to specify Adobe's PDF Open Parameters.
|
||||
*
|
||||
* **Warning**: These are proprietary and not well supported outside of Adobe products.
|
||||
* Most PDF readers support the page parameter, but not much else. `PDF.js` supports `page`, `zoom`, `nameddest`, and `pagemode`.
|
||||
*/
|
||||
readonly pdfOpenParams?: {
|
||||
readonly page?: string;
|
||||
readonly zoom?: string;
|
||||
readonly nameddest?: string;
|
||||
readonly pagemode?: string;
|
||||
}
|
||||
}
|
||||
|
||||
interface PDFObject {
|
||||
|
||||
/**
|
||||
* Returns the embedded element (`<embed>` for most situations, and `<iframe>` when integrated with PDF.js), or `false` if unable to embed.
|
||||
*
|
||||
* The heart of `PDFObject`, the embed method provides a ton of functionality and flexibility.
|
||||
*/
|
||||
embed(url: string, target?: string | HTMLElement /* | jQuery object (HTML node) for target */, options?: Options): HTMLElement;
|
||||
|
||||
/**
|
||||
* Returns the version of PDFObject.
|
||||
*/
|
||||
readonly pdfobjectversion: string;
|
||||
|
||||
/**
|
||||
* Returns `true` or `false` based on detection of `navigator.mimeTypes['application/pdf']` and/or ActiveX `AcroPDF.PDF` or `PDF.PdfCtrl`.
|
||||
*
|
||||
* `PDFObject` does not perform detection for specific vendors (Adobe Reader, FoxIt, PDF.js, etc.).
|
||||
* Note: For those who wish to target PDF.js, there is an option in `PDFObject.embed()` to force use of PDF.js.
|
||||
*/
|
||||
readonly supportsPDFs: boolean;
|
||||
}
|
||||
|
||||
const pdfObject: PDFObject;
|
||||
|
||||
export = pdfObject;
|
||||
|
||||
}
|
||||
157
packages/mini-browser/src/browser/style/index.css
Normal file
157
packages/mini-browser/src/browser/style/index.css
Normal file
@@ -0,0 +1,157 @@
|
||||
/********************************************************************************
|
||||
* Copyright (C) 2018 TypeFox and others.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License v. 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0.
|
||||
*
|
||||
* This Source Code may also be made available under the following Secondary
|
||||
* Licenses when the conditions for such availability set forth in the Eclipse
|
||||
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
* with the GNU Classpath Exception which is available at
|
||||
* https://www.gnu.org/software/classpath/license.html.
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
********************************************************************************/
|
||||
|
||||
:root {
|
||||
--theia-private-mini-browser-height: var(--theia-content-line-height);
|
||||
}
|
||||
|
||||
.theia-mini-browser {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.theia-mini-browser-toolbar {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.theia-mini-browser-toolbar-read-only {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.theia-mini-browser-toolbar .theia-input {
|
||||
width: 100%;
|
||||
line-height: var(--theia-private-mini-browser-height);
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.theia-mini-browser-toolbar-read-only .theia-input {
|
||||
width: 100%;
|
||||
line-height: var(--theia-private-mini-browser-height);
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
cursor: pointer;
|
||||
background: var(--theia-input-background);
|
||||
border: none;
|
||||
text-decoration: underline;
|
||||
outline: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--theia-input-foreground);
|
||||
}
|
||||
|
||||
.theia-mini-browser-toolbar-read-only .theia-input:hover {
|
||||
color: var(--theia-button-hoverBackground);
|
||||
}
|
||||
|
||||
.theia-mini-browser-button {
|
||||
min-width: 1rem;
|
||||
text-align: center;
|
||||
flex-grow: 0;
|
||||
font-family: FontAwesome;
|
||||
font-size: calc(var(--theia-content-font-size) * 0.8);
|
||||
color: var(--theia-icon-foreground);
|
||||
margin: 0px 4px 0px 4px;
|
||||
}
|
||||
|
||||
.theia-mini-browser-button:not(.theia-mini-browser-button-disabled):hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.theia-mini-browser-button-disabled {
|
||||
opacity: var(--theia-mod-disabled-opacity);
|
||||
}
|
||||
|
||||
.theia-mini-browser-previous::before {
|
||||
content: "\f053";
|
||||
}
|
||||
|
||||
.theia-mini-browser-next::before {
|
||||
content: "\f054";
|
||||
}
|
||||
|
||||
.theia-mini-browser-refresh::before {
|
||||
content: "\f021";
|
||||
}
|
||||
|
||||
.theia-mini-browser-open::before {
|
||||
content: "\f08e";
|
||||
}
|
||||
|
||||
.theia-mini-browser-content-area {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.theia-mini-browser-pdf-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.theia-mini-browser-load-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
background: var(--theia-editor-background);
|
||||
background-image: var(--theia-preloader);
|
||||
background-size: 60px 60px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition: opacity 0.8s;
|
||||
}
|
||||
|
||||
.theia-mini-browser-load-indicator.theia-fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.theia-mini-browser-error-bar {
|
||||
height: 19px;
|
||||
padding-left: var(--theia-ui-padding);
|
||||
background-color: var(--theia-inputValidation-errorBorder);
|
||||
color: var(--theia-editor-foreground);
|
||||
font-size: var(--theia-statusBar-font-size);
|
||||
z-index: 1000; /* Above the transparent overlay (`z-index: 999;`). */
|
||||
}
|
||||
|
||||
.theia-mini-browser-error-bar span {
|
||||
margin-top: 3px;
|
||||
margin-left: var(--theia-ui-padding);
|
||||
}
|
||||
|
||||
.theia-mini-browser-content-area iframe {
|
||||
flex-grow: 1;
|
||||
border: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
17
packages/mini-browser/src/browser/style/mini-browser.svg
Normal file
17
packages/mini-browser/src/browser/style/mini-browser.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<!--Copyright (c) 2019 Kenneth Auchenberg. -->
|
||||
<!--Copyright (C) 2019 TypeFox and others.-->
|
||||
<!--Licensed under the MIT License.-->
|
||||
<svg width="25px" height="25px" viewBox="0 0 25 25" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>icon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="icon" fill="#2879FF">
|
||||
<polygon id="Path" points="13.8671875 15.625 13.8671875 14.0625 18.5546875 14.0625 18.5546875 15.625"></polygon>
|
||||
<polygon id="Path" points="13.8671875 12.5 13.8671875 10.9375 21.6796875 10.9375 21.6796875 12.5"></polygon>
|
||||
<polygon id="Path" points="13.8671875 9.375 13.8671875 7.8125 21.6796875 7.8125 21.6796875 9.375"></polygon>
|
||||
<rect id="Rectangle" x="2.9296875" y="7.8125" width="7.8125" height="7.8125"></rect>
|
||||
<path d="M0,0 L0,25 L24.9992188,25 L25,0 L0,0 Z M17.1875,1.5625 L17.1875,3.125 L7.8125,3.125 L7.8125,1.5625 L17.1875,1.5625 Z M6.25,1.5625 L6.25,3.125 L4.6875,3.125 L4.6875,1.5625 L6.25,1.5625 Z M1.5625,1.5625 L3.125,1.5625 L3.125,3.125 L1.5625,3.125 L1.5625,1.5625 Z M23.4367188,23.046875 L1.5625,23.046875 L1.5625,4.6875 L23.4367188,4.6875 L23.4367188,23.046875 Z M23.4375,3.125 L20.3125,3.125 L20.3125,1.5625 L23.4375,1.5625 L23.4375,3.125 Z" id="Shape" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
28
packages/mini-browser/src/common/mini-browser-endpoint.ts
Normal file
28
packages/mini-browser/src/common/mini-browser-endpoint.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/**
|
||||
* The mini-browser can now serve content on its own host/origin.
|
||||
*
|
||||
* The virtual host can be configured with this `THEIA_MINI_BROWSER_HOST_PATTERN`
|
||||
* environment variable. `{{hostname}}` represents the current host, and `{{uuid}}`
|
||||
* will be replace by a random uuid value.
|
||||
*/
|
||||
export namespace MiniBrowserEndpoint {
|
||||
export const PATH = '/mini-browser';
|
||||
export const HOST_PATTERN_ENV = 'THEIA_MINI_BROWSER_HOST_PATTERN';
|
||||
export const HOST_PATTERN_DEFAULT = '{{uuid}}.mini-browser.{{hostname}}';
|
||||
}
|
||||
29
packages/mini-browser/src/common/mini-browser-service.ts
Normal file
29
packages/mini-browser/src/common/mini-browser-service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export const MiniBrowserServicePath = '/services/mini-browser-service';
|
||||
export const MiniBrowserService = Symbol('MiniBrowserService');
|
||||
export interface MiniBrowserService {
|
||||
|
||||
/**
|
||||
* Resolves to an array of file extensions - priority pairs supported by the `Mini Browser`.
|
||||
*
|
||||
* The file extensions start without the leading dot (`.`) and should be treated in a case-insensitive way. This means,
|
||||
* if the `Mini Browser` supports `['jpg']`, then it can open the `MyPicture.JPG` file.
|
||||
*/
|
||||
supportedFileExtensions(): Promise<Readonly<{ extension: string, priority: number }>[]>;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { MiniBrowserEnvironment } from '../../browser/environment/mini-browser-environment';
|
||||
import { ElectronMiniBrowserEnvironment } from './electron-mini-browser-environment';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(MiniBrowserEnvironment).to(ElectronMiniBrowserEnvironment).inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(MiniBrowserEnvironment);
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { Endpoint } from '@theia/core/lib/browser';
|
||||
|
||||
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { MiniBrowserEnvironment } from '../../browser/environment/mini-browser-environment';
|
||||
|
||||
import '@theia/core/lib/electron-common/electron-api';
|
||||
|
||||
@injectable()
|
||||
export class ElectronMiniBrowserEnvironment extends MiniBrowserEnvironment {
|
||||
|
||||
@inject(ElectronSecurityToken)
|
||||
protected readonly electronSecurityToken: ElectronSecurityToken;
|
||||
|
||||
override getEndpoint(uuid: string, hostname?: string): Endpoint {
|
||||
const endpoint = super.getEndpoint(uuid, hostname);
|
||||
|
||||
window.electronTheiaCore.attachSecurityToken(endpoint.getRestUrl().toString(true));
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
protected override getDefaultHostname(): string {
|
||||
const query = self.location.search
|
||||
.substring(1) // remove leading `?`
|
||||
.split('&')
|
||||
.map(entry => entry
|
||||
.split('=', 2)
|
||||
.map(element => decodeURIComponent(element))
|
||||
);
|
||||
for (const [key, value] of query) {
|
||||
if (key === 'port') {
|
||||
return `localhost:${value}`;
|
||||
}
|
||||
}
|
||||
throw new Error('could not resolve Electron\'s backend port');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ElectronMainApplication, ElectronMainApplicationContribution } from '@theia/core/lib/electron-main/electron-main-application';
|
||||
import { ElectronSecurityTokenService } from '@theia/core/lib/electron-main/electron-security-token-service';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { MiniBrowserEndpoint } from '../common/mini-browser-endpoint';
|
||||
|
||||
/**
|
||||
* Since the mini-browser might serve content from a new origin,
|
||||
* we need to attach the ElectronSecurityToken for the Electron
|
||||
* backend to accept HTTP requests.
|
||||
*/
|
||||
@injectable()
|
||||
export class MiniBrowserElectronMainContribution implements ElectronMainApplicationContribution {
|
||||
|
||||
@inject(ElectronSecurityTokenService)
|
||||
protected readonly electronSecurityTokenService: ElectronSecurityTokenService;
|
||||
|
||||
async onStart(app: ElectronMainApplication): Promise<void> {
|
||||
const url = this.getMiniBrowserEndpoint(await app.backendPort);
|
||||
await this.electronSecurityTokenService.setElectronSecurityTokenCookie(url);
|
||||
}
|
||||
|
||||
protected getMiniBrowserEndpoint(port: number): string {
|
||||
const pattern = process.env[MiniBrowserEndpoint.HOST_PATTERN_ENV] ?? MiniBrowserEndpoint.HOST_PATTERN_DEFAULT;
|
||||
return 'http://' + pattern.replace('{{hostname}}', `localhost:${port}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
||||
import { ConnectionHandler, RpcConnectionHandler } from '@theia/core/lib/common';
|
||||
import { MiniBrowserService, MiniBrowserServicePath } from '../common/mini-browser-service';
|
||||
import { MiniBrowserEndpoint, MiniBrowserEndpointHandler, HtmlHandler, ImageHandler, PdfHandler, SvgHandler } from './mini-browser-endpoint';
|
||||
import { WsRequestValidatorContribution } from '@theia/core/lib/node/ws-request-validators';
|
||||
import { MiniBrowserWsRequestValidator } from './mini-browser-ws-validator';
|
||||
import { MiniBrowserBackendSecurityWarnings } from './mini-browser-backend-security-warnings';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(MiniBrowserEndpoint).toSelf().inSingletonScope();
|
||||
bind(BackendApplicationContribution).toService(MiniBrowserEndpoint);
|
||||
bind(MiniBrowserWsRequestValidator).toSelf().inSingletonScope();
|
||||
bind(WsRequestValidatorContribution).toService(MiniBrowserWsRequestValidator);
|
||||
bind(MiniBrowserService).toService(MiniBrowserEndpoint);
|
||||
bind(ConnectionHandler).toDynamicValue(context => new RpcConnectionHandler(MiniBrowserServicePath, () => context.container.get(MiniBrowserService))).inSingletonScope();
|
||||
bindContributionProvider(bind, MiniBrowserEndpointHandler);
|
||||
bind(MiniBrowserEndpointHandler).to(HtmlHandler).inSingletonScope();
|
||||
bind(MiniBrowserEndpointHandler).to(ImageHandler).inSingletonScope();
|
||||
bind(MiniBrowserEndpointHandler).to(PdfHandler).inSingletonScope();
|
||||
bind(MiniBrowserEndpointHandler).to(SvgHandler).inSingletonScope();
|
||||
bind(MiniBrowserBackendSecurityWarnings).toSelf().inSingletonScope();
|
||||
bind(BackendApplicationContribution).toService(MiniBrowserBackendSecurityWarnings);
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2021 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node';
|
||||
import { BackendApplicationConfigProvider } from '@theia/core/lib/node/backend-application-config-provider';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { MiniBrowserEndpoint } from '../common/mini-browser-endpoint';
|
||||
|
||||
@injectable()
|
||||
export class MiniBrowserBackendSecurityWarnings implements BackendApplicationContribution {
|
||||
|
||||
initialize(): void {
|
||||
this.checkHostPattern();
|
||||
}
|
||||
|
||||
protected async checkHostPattern(): Promise<void> {
|
||||
if (BackendApplicationConfigProvider.get()['warnOnPotentiallyInsecureHostPattern'] === false) {
|
||||
return;
|
||||
}
|
||||
const envHostPattern = process.env[MiniBrowserEndpoint.HOST_PATTERN_ENV];
|
||||
if (envHostPattern && envHostPattern !== MiniBrowserEndpoint.HOST_PATTERN_DEFAULT) {
|
||||
console.warn(`\
|
||||
MINI BROWSER SECURITY WARNING
|
||||
|
||||
Changing the @theia/mini-browser host pattern can lead to security vulnerabilities.
|
||||
Current pattern: "${envHostPattern}"
|
||||
Please read @theia/mini-browser/README.md for more information.
|
||||
`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
315
packages/mini-browser/src/node/mini-browser-endpoint.ts
Normal file
315
packages/mini-browser/src/node/mini-browser-endpoint.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
const vhost = require('vhost');
|
||||
import express = require('@theia/core/shared/express');
|
||||
import * as fs from '@theia/core/shared/fs-extra';
|
||||
import { lookup } from 'mime-types';
|
||||
import { injectable, inject, named } from '@theia/core/shared/inversify';
|
||||
import { Application, Request, Response } from '@theia/core/shared/express';
|
||||
import { FileUri } from '@theia/core/lib/common/file-uri';
|
||||
import { ILogger } from '@theia/core/lib/common/logger';
|
||||
import { MaybePromise } from '@theia/core/lib/common/types';
|
||||
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
||||
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
||||
import { MiniBrowserService } from '../common/mini-browser-service';
|
||||
import { MiniBrowserEndpoint as MiniBrowserEndpointNS } from '../common/mini-browser-endpoint';
|
||||
|
||||
/**
|
||||
* The return type of the `FileSystem#resolveContent` method.
|
||||
*/
|
||||
export interface FileStatWithContent {
|
||||
|
||||
/**
|
||||
* The file stat.
|
||||
*/
|
||||
readonly stat: fs.Stats & { uri: string };
|
||||
|
||||
/**
|
||||
* The content of the file as a UTF-8 encoded string.
|
||||
*/
|
||||
readonly content: string;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint handler contribution for the `MiniBrowserEndpoint`.
|
||||
*/
|
||||
export const MiniBrowserEndpointHandler = Symbol('MiniBrowserEndpointHandler');
|
||||
export interface MiniBrowserEndpointHandler {
|
||||
|
||||
/**
|
||||
* Returns with or resolves to the file extensions supported by the current `mini-browser` endpoint handler.
|
||||
* The file extension must not start with the leading `.` (dot). For instance; `'html'` or `['jpg', 'jpeg']`.
|
||||
* The file extensions are case insensitive.
|
||||
*/
|
||||
supportedExtensions(): MaybePromise<string | string[]>;
|
||||
|
||||
/**
|
||||
* Returns a number representing the priority between all the available handlers for the same file extension.
|
||||
*/
|
||||
priority(): number;
|
||||
|
||||
/**
|
||||
* Responds back to the sender.
|
||||
*/
|
||||
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response>;
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MiniBrowserEndpoint implements BackendApplicationContribution, MiniBrowserService {
|
||||
|
||||
private attachRequestHandlerPromise: Promise<void>;
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
@inject(ContributionProvider)
|
||||
@named(MiniBrowserEndpointHandler)
|
||||
protected readonly contributions: ContributionProvider<MiniBrowserEndpointHandler>;
|
||||
|
||||
protected readonly handlers: Map<string, MiniBrowserEndpointHandler> = new Map();
|
||||
|
||||
configure(app: Application): void {
|
||||
this.attachRequestHandlerPromise = this.attachRequestHandler(app);
|
||||
}
|
||||
|
||||
async onStart(): Promise<void> {
|
||||
await Promise.all(Array.from(this.getContributions(), async handler => {
|
||||
const extensions = await handler.supportedExtensions();
|
||||
for (const extension of (Array.isArray(extensions) ? extensions : [extensions]).map(e => e.toLocaleLowerCase())) {
|
||||
const existingHandler = this.handlers.get(extension);
|
||||
if (!existingHandler || handler.priority > existingHandler.priority) {
|
||||
this.handlers.set(extension, handler);
|
||||
}
|
||||
}
|
||||
}));
|
||||
await this.attachRequestHandlerPromise;
|
||||
}
|
||||
|
||||
async supportedFileExtensions(): Promise<Readonly<{ extension: string, priority: number }>[]> {
|
||||
return Array.from(this.handlers.entries(), ([extension, handler]) => ({ extension, priority: handler.priority() }));
|
||||
}
|
||||
|
||||
protected async attachRequestHandler(app: Application): Promise<void> {
|
||||
const miniBrowserApp = express();
|
||||
miniBrowserApp.get('*', async (request, response) => this.response(await this.getUri(request), response));
|
||||
app.use(MiniBrowserEndpointNS.PATH, vhost(await this.getVirtualHostRegExp(), miniBrowserApp));
|
||||
}
|
||||
|
||||
protected async response(uri: string, response: Response): Promise<Response> {
|
||||
const exists = await fs.pathExists(FileUri.fsPath(uri));
|
||||
if (!exists) {
|
||||
return this.missingResourceHandler()(uri, response);
|
||||
}
|
||||
const statWithContent = await this.readContent(uri);
|
||||
try {
|
||||
if (!statWithContent.stat.isDirectory()) {
|
||||
const extension = uri.split('.').pop();
|
||||
if (!extension) {
|
||||
return this.defaultHandler()(statWithContent, response);
|
||||
}
|
||||
const handler = this.handlers.get(extension.toString().toLocaleLowerCase());
|
||||
if (!handler) {
|
||||
return this.defaultHandler()(statWithContent, response);
|
||||
}
|
||||
return handler.respond(statWithContent, response);
|
||||
}
|
||||
} catch (e) {
|
||||
return this.errorHandler()(e, uri, response);
|
||||
}
|
||||
return this.defaultHandler()(statWithContent, response);
|
||||
}
|
||||
|
||||
protected getContributions(): MiniBrowserEndpointHandler[] {
|
||||
return this.contributions.getContributions();
|
||||
}
|
||||
|
||||
protected getUri(request: Request): MaybePromise<string> {
|
||||
return FileUri.create(request.path).toString(true);
|
||||
}
|
||||
|
||||
protected async readContent(uri: string): Promise<FileStatWithContent> {
|
||||
const fsPath = FileUri.fsPath(uri);
|
||||
const [stat, content] = await Promise.all([fs.stat(fsPath), fs.readFile(fsPath, 'utf8')]);
|
||||
return { stat: Object.assign(stat, { uri }), content };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
protected errorHandler(): (error: any, uri: string, response: Response) => MaybePromise<Response> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return async (error: any, uri: string, response: Response) => {
|
||||
const details = error.toString ? error.toString() : error;
|
||||
this.logger.error(`Error occurred while handling request for ${uri}. Details: ${details}`);
|
||||
if (error instanceof Error) {
|
||||
let message = error.message;
|
||||
if (error.stack) {
|
||||
message += `\n${error.stack}`;
|
||||
}
|
||||
this.logger.error(message);
|
||||
} else if (typeof error === 'string') {
|
||||
this.logger.error(error);
|
||||
} else {
|
||||
this.logger.error(`${error}`);
|
||||
}
|
||||
return response.send(500);
|
||||
};
|
||||
}
|
||||
|
||||
protected missingResourceHandler(): (uri: string, response: Response) => MaybePromise<Response> {
|
||||
return async (uri: string, response: Response) => {
|
||||
this.logger.error(`Cannot handle missing resource. URI: ${uri}.`);
|
||||
return response.sendStatus(404);
|
||||
};
|
||||
}
|
||||
|
||||
protected defaultHandler(): (statWithContent: FileStatWithContent, response: Response) => MaybePromise<Response> {
|
||||
return async (statWithContent: FileStatWithContent, response: Response) => {
|
||||
const { content } = statWithContent;
|
||||
const mimeType = lookup(FileUri.fsPath(statWithContent.stat.uri));
|
||||
if (!mimeType) {
|
||||
this.logger.warn(`Cannot handle unexpected resource. URI: ${statWithContent.stat.uri}.`);
|
||||
response.contentType('application/octet-stream');
|
||||
} else {
|
||||
response.contentType(mimeType);
|
||||
}
|
||||
return response.send(content);
|
||||
};
|
||||
}
|
||||
|
||||
protected async getVirtualHostRegExp(): Promise<RegExp> {
|
||||
const pattern = process.env[MiniBrowserEndpointNS.HOST_PATTERN_ENV] || MiniBrowserEndpointNS.HOST_PATTERN_DEFAULT;
|
||||
const vhostRe = pattern
|
||||
.replace(/\./g, '\\.')
|
||||
.replace('{{uuid}}', '.+')
|
||||
.replace('{{hostname}}', '.+');
|
||||
return new RegExp(vhostRe, 'i');
|
||||
}
|
||||
}
|
||||
|
||||
// See `EditorManager#canHandle`.
|
||||
const CODE_EDITOR_PRIORITY = 100;
|
||||
|
||||
/**
|
||||
* Endpoint handler contribution for HTML files.
|
||||
*/
|
||||
@injectable()
|
||||
export class HtmlHandler implements MiniBrowserEndpointHandler {
|
||||
|
||||
supportedExtensions(): string[] {
|
||||
return ['html', 'xhtml', 'htm'];
|
||||
}
|
||||
|
||||
priority(): number {
|
||||
// Prefer Code Editor over Mini Browser
|
||||
// https://github.com/eclipse-theia/theia/issues/2051
|
||||
return 1;
|
||||
}
|
||||
|
||||
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
||||
response.contentType('text/html');
|
||||
return response.send(statWithContent.content);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for JPG resources.
|
||||
*/
|
||||
@injectable()
|
||||
export class ImageHandler implements MiniBrowserEndpointHandler {
|
||||
|
||||
supportedExtensions(): string[] {
|
||||
return ['jpg', 'jpeg', 'png', 'bmp', 'gif'];
|
||||
}
|
||||
|
||||
priority(): number {
|
||||
return CODE_EDITOR_PRIORITY + 1;
|
||||
}
|
||||
|
||||
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
||||
fs.readFile(FileUri.fsPath(statWithContent.stat.uri), (error, data) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
response.contentType('image/jpeg');
|
||||
response.send(data);
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* PDF endpoint handler.
|
||||
*/
|
||||
@injectable()
|
||||
export class PdfHandler implements MiniBrowserEndpointHandler {
|
||||
|
||||
supportedExtensions(): string {
|
||||
return 'pdf';
|
||||
}
|
||||
|
||||
priority(): number {
|
||||
return CODE_EDITOR_PRIORITY + 1;
|
||||
}
|
||||
|
||||
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
||||
// https://stackoverflow.com/questions/11598274/display-pdf-in-browser-using-express-js
|
||||
const encodeRFC5987ValueChars = (input: string) =>
|
||||
encodeURIComponent(input).
|
||||
// Note that although RFC3986 reserves "!", RFC5987 does not, so we do not need to escape it.
|
||||
replace(/['()]/g, escape). // i.e., %27 %28 %29
|
||||
replace(/\*/g, '%2A').
|
||||
// The following are not required for percent-encoding per RFC5987, so we can allow for a little better readability over the wire: |`^.
|
||||
replace(/%(?:7C|60|5E)/g, unescape);
|
||||
|
||||
const fileName = FileUri.create(statWithContent.stat.uri).path.base;
|
||||
fs.readFile(FileUri.fsPath(statWithContent.stat.uri), (error, data) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
// Change `inline` to `attachment` if you would like to force downloading the PDF instead of previewing in the browser.
|
||||
response.setHeader('Content-disposition', `inline; filename*=UTF-8''${encodeRFC5987ValueChars(fileName)}`);
|
||||
response.contentType('application/pdf');
|
||||
response.send(data);
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint contribution for SVG resources.
|
||||
*/
|
||||
@injectable()
|
||||
export class SvgHandler implements MiniBrowserEndpointHandler {
|
||||
|
||||
supportedExtensions(): string {
|
||||
return 'svg';
|
||||
}
|
||||
|
||||
priority(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
respond(statWithContent: FileStatWithContent, response: Response): MaybePromise<Response> {
|
||||
response.contentType('image/svg+xml');
|
||||
return response.send(statWithContent.content);
|
||||
}
|
||||
|
||||
}
|
||||
56
packages/mini-browser/src/node/mini-browser-ws-validator.ts
Normal file
56
packages/mini-browser/src/node/mini-browser-ws-validator.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2020 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { WsRequestValidatorContribution } from '@theia/core/lib/node/ws-request-validators';
|
||||
import * as http from 'http';
|
||||
import { injectable, postConstruct } from '@theia/core/shared/inversify';
|
||||
import * as url from 'url';
|
||||
import { MiniBrowserEndpoint } from '../common/mini-browser-endpoint';
|
||||
|
||||
/**
|
||||
* Prevents explicit WebSocket connections from the mini-browser virtual host.
|
||||
*/
|
||||
@injectable()
|
||||
export class MiniBrowserWsRequestValidator implements WsRequestValidatorContribution {
|
||||
|
||||
protected miniBrowserHostRe: RegExp;
|
||||
|
||||
protected serveSameOrigin: boolean = false;
|
||||
|
||||
@postConstruct()
|
||||
protected init(): void {
|
||||
const pattern = process.env[MiniBrowserEndpoint.HOST_PATTERN_ENV] || MiniBrowserEndpoint.HOST_PATTERN_DEFAULT;
|
||||
if (pattern === '{{hostname}}') {
|
||||
this.serveSameOrigin = true;
|
||||
}
|
||||
const vhostRe = pattern
|
||||
.replace(/\./g, '\\.')
|
||||
.replace('{{uuid}}', '.+')
|
||||
.replace('{{hostname}}', '.+');
|
||||
this.miniBrowserHostRe = new RegExp(vhostRe, 'i');
|
||||
}
|
||||
|
||||
async allowWsUpgrade(request: http.IncomingMessage): Promise<boolean> {
|
||||
if (request.headers.origin && !this.serveSameOrigin) {
|
||||
const origin = url.parse(request.headers.origin);
|
||||
if (origin.host && this.miniBrowserHostRe.test(origin.host)) {
|
||||
// If the origin comes from the WebViews, refuse:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
21
packages/mini-browser/src/package.spec.ts
Normal file
21
packages/mini-browser/src/package.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2018 TypeFox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
describe('mini-browser package', () => {
|
||||
|
||||
it('support code coverage statistics', () => true);
|
||||
|
||||
});
|
||||
19
packages/mini-browser/tsconfig.json
Normal file
19
packages/mini-browser/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../filesystem"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user