/********************************************************************************
 * 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 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/node/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 , response ) : MaybePromise < Response > ; } @ injectable ( ) export class MiniBrowserEndpoint implements BackendApplicationContribution , MiniBrowserService { deprecated static HANDLE_PATH = '/mini-browser/' ; 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 ) { this . attachRequestHandlerPromise = this . attachRequestHandler ( app ) ; } async onStart ( ) { 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 ( ) { return Array . from ( this . handlers . entries ( ) , ( [ extension , handler ] ) => ( { extension , priority : handler . priority ( ) } ) ) ; } protected async attachRequestHandler ( app ) { 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 , 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 ( ) { return this . contributions . getContributions ( ) ; } protected getUri ( request ) { return FileUri . create ( request . path ) . toString ( true ) ; } protected async readContent ( uri ) { 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 } ; } protected errorHandler ( ) { return async ( error , uri , 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 ( ) { return async ( uri , response ) => { this . logger . error ( `Cannot handle missing resource. URI: ${ uri }.` ) ; return response . sendStatus ( 404 ) ; } ; } protected defaultHandler ( ) { return async ( statWithContent , 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 ( ) { 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' ) ; } } const CODE_EDITOR_PRIORITY = 100 ; /**
 * Endpoint handler contribution for HTML files.
 */ @ injectable ( ) export class HtmlHandler implements MiniBrowserEndpointHandler { supportedExtensions ( ) { return [ 'html' , 'xhtml' , 'htm' ] ; } priority ( ) { return 1 ; } respond ( statWithContent , response ) { response . contentType ( 'text/html' ) ; return response . send ( statWithContent . content ) ; } } /**
 * Handler for JPG resources.
 */ @ injectable ( ) export class ImageHandler implements MiniBrowserEndpointHandler { supportedExtensions ( ) { return [ 'jpg' , 'jpeg' , 'png' , 'bmp' , 'gif' ] ; } priority ( ) { return CODE_EDITOR_PRIORITY + 1 ; } respond ( statWithContent , 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 ( ) { return 'pdf' ; } priority ( ) { return CODE_EDITOR_PRIORITY + 1 ; } respond ( statWithContent , response ) { const encodeRFC5987ValueChars = ( input ) => encodeURIComponent ( input ) . replace ( /['()]/g , escape ) . replace ( /\*/g , '%2A' ) . 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 ; } 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 ( ) { return 'svg' ; } priority ( ) { return 1 ; } respond ( statWithContent , response ) { response . contentType ( 'image/svg+xml' ) ; return response . send ( statWithContent . content ) ; } }