deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View 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>

View 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"
}

View File

@@ -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);
});

View File

@@ -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;
}
}

View 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();
}
}

View File

@@ -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';
}

View 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 origins data.
*/
'allow-same-origin',
/**
* Allows JavaScript execution. Also allows features to trigger automatically (as theyd 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);
}
}
}
}

View File

@@ -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);
});

View File

@@ -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 });
}
});
}
}
}

View 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'
};
}
}

View 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']);
}
}
}

View 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;
}

View 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;
}

View 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

View 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}}';
}

View 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 }>[]>;
}

View File

@@ -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);
});

View File

@@ -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');
}
}

View File

@@ -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}`);
}
}

View File

@@ -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);
});

View File

@@ -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.
`
);
}
}
}

View 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);
}
}

View 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;
}
}

View 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);
});

View File

@@ -0,0 +1,19 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../core"
},
{
"path": "../filesystem"
}
]
}