| // Copyright (c) 2023, Compiler Explorer Authors |
| // All rights reserved. |
| // |
| // Redistribution and use in source and binary forms, with or without |
| // modification, are permitted provided that the following conditions are met: |
| // |
| // * Redistributions of source code must retain the above copyright notice, |
| // this list of conditions and the following disclaimer. |
| // * Redistributions in binary form must reproduce the above copyright |
| // notice, this list of conditions and the following disclaimer in the |
| // documentation and/or other materials provided with the distribution. |
| // |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE |
| // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| // POSSIBILITY OF SUCH DAMAGE. |
| |
| import * as Sentry from '@sentry/node'; |
| import express from 'express'; |
| |
| import {assert, unwrap} from '../assert.js'; |
| import {ClientState} from '../clientstate.js'; |
| import {ClientStateGoldenifier, ClientStateNormalizer} from '../clientstate-normalizer.js'; |
| import {isString} from '../common-utils.js'; |
| import {logger} from '../logger.js'; |
| import {StorageBase} from '../storage/index.js'; |
| import * as utils from '../utils.js'; |
| |
| import {ApiHandler} from './api.js'; |
| |
| export type HandlerConfig = { |
| compileHandler: any; |
| clientOptionsHandler: any; |
| storageHandler: StorageBase; |
| ceProps: any; |
| opts: any; |
| defArgs: any; |
| renderConfig: any; |
| renderGoldenLayout: any; |
| staticHeaders: any; |
| contentPolicyHeader: any; |
| }; |
| |
| export class RouteAPI { |
| renderGoldenLayout: any; |
| storageHandler: StorageBase; |
| apiHandler: ApiHandler; |
| |
| constructor(private readonly router: express.Router, config: HandlerConfig) { |
| this.renderGoldenLayout = config.renderGoldenLayout; |
| |
| this.storageHandler = config.storageHandler; |
| |
| // if for testing purposes |
| if (config.compileHandler) { |
| this.apiHandler = new ApiHandler( |
| config.compileHandler, |
| config.ceProps, |
| config.storageHandler, |
| config.clientOptionsHandler.options.urlShortenService, |
| ); |
| |
| this.apiHandler.setReleaseInfo(config.defArgs.gitReleaseName, config.defArgs.releaseBuildNumber); |
| } else { |
| this.apiHandler = undefined as any; |
| } |
| } |
| |
| InitializeRoutes() { |
| this.router |
| .use('/api', this.apiHandler.handle) |
| .get('/z/:id', this.storedStateHandler.bind(this)) |
| .get('/z/:id/code/:session', this.storedCodeHandler.bind(this)) |
| .get('/resetlayout/:id', this.storedStateHandlerResetLayout.bind(this)) |
| .get('/clientstate/:clientstatebase64([^]*)', this.unstoredStateHandler.bind(this)) |
| .get('/fromsimplelayout', this.simpleLayoutHandler.bind(this)); |
| } |
| |
| storedCodeHandler(req: express.Request, res: express.Response, next: express.NextFunction) { |
| const id = req.params.id; |
| const sessionid = parseInt(req.params.session); |
| this.storageHandler |
| .expandId(id) |
| .then(result => { |
| const config = JSON.parse(result.config); |
| |
| let clientstate: ClientState | null = null; |
| if (config.content) { |
| const normalizer = new ClientStateNormalizer(); |
| normalizer.fromGoldenLayout(config); |
| |
| clientstate = normalizer.normalized; |
| } else { |
| clientstate = new ClientState(config); |
| } |
| |
| const session = clientstate.findSessionById(sessionid); |
| if (!session) throw {msg: `Session ${sessionid} doesn't exist in this shortlink`}; |
| |
| res.set('Content-Type', 'text/plain'); |
| res.send(session.source); |
| }) |
| .catch(err => { |
| logger.debug(`Exception thrown when expanding ${id}: `, err); |
| logger.warn('Exception value:', err); |
| Sentry.captureException(err); |
| next({ |
| statusCode: 404, |
| message: `ID "${id}/${sessionid}" could not be found`, |
| }); |
| }); |
| } |
| |
| storedStateHandler(req: express.Request, res: express.Response, next: express.NextFunction) { |
| const id = req.params.id; |
| this.storageHandler |
| .expandId(id) |
| .then(result => { |
| let config = JSON.parse(result.config); |
| if (config.sessions) { |
| config = this.getGoldenLayoutFromClientState(new ClientState(config)); |
| } |
| const metadata = this.getMetaDataFromLink(req, result, config); |
| this.renderGoldenLayout(config, metadata, req, res); |
| // And finally, increment the view count |
| // If any errors pop up, they are just logged, but the response should still be valid |
| // It's really unlikely that it happens as a result of the id not being there though, |
| // but can be triggered with a missing implementation for a derived storage (s3/local...) |
| this.storageHandler.incrementViewCount(id).catch(err => { |
| logger.error(`Error incrementing view counts for ${id} - ${err}`); |
| }); |
| }) |
| .catch(err => { |
| logger.warn(`Could not expand ${id}: ${err}`); |
| next({ |
| statusCode: 404, |
| message: `ID "${id}" could not be found`, |
| }); |
| }); |
| } |
| |
| getGoldenLayoutFromClientState(state: ClientState) { |
| const goldenifier = new ClientStateGoldenifier(); |
| goldenifier.fromClientState(state); |
| return goldenifier.golden; |
| } |
| |
| unstoredStateHandler(req: express.Request, res: express.Response) { |
| const state = JSON.parse(Buffer.from(req.params.clientstatebase64, 'base64').toString()); |
| const config = this.getGoldenLayoutFromClientState(new ClientState(state)); |
| const metadata = this.getMetaDataFromLink(req, null, config); |
| |
| this.renderGoldenLayout(config, metadata, req, res); |
| } |
| |
| simpleLayoutHandler(req: express.Request, res: express.Response) { |
| const state = new ClientState(); |
| const session = state.findOrCreateSession(1); |
| assert(isString(req.query.lang)); |
| session.language = req.query.lang; |
| assert(isString(req.query.code)); |
| session.source = req.query.code; |
| const compiler = session.findOrCreateCompiler(1); |
| compiler.id = req.query.compiler; |
| compiler.options = req.query.compiler_flags || ''; |
| |
| this.renderClientState(state, undefined, req, res); |
| } |
| |
| renderClientState(clientstate: ClientState, metadata, req: express.Request, res: express.Response) { |
| const config = this.getGoldenLayoutFromClientState(clientstate); |
| |
| this.renderGoldenLayout(config, metadata, req, res); |
| } |
| |
| storedStateHandlerResetLayout(req: express.Request, res: express.Response, next: express.NextFunction) { |
| const id = req.params.id; |
| this.storageHandler |
| .expandId(id) |
| .then(result => { |
| let config = JSON.parse(result.config); |
| |
| if (config.content) { |
| const normalizer = new ClientStateNormalizer(); |
| normalizer.fromGoldenLayout(config); |
| config = normalizer.normalized; |
| } else { |
| config = new ClientState(config); |
| } |
| |
| const metadata = this.getMetaDataFromLink(req, result, config); |
| this.renderClientState(config, metadata, req, res); |
| }) |
| .catch(err => { |
| logger.warn(`Exception thrown when expanding ${id}`); |
| logger.warn('Exception value:', err); |
| Sentry.captureException(err); |
| next({ |
| statusCode: 404, |
| message: `ID "${id}" could not be found`, |
| }); |
| }); |
| } |
| |
| escapeLine(req: express.Request, line: string) { |
| return line.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); |
| } |
| |
| filterCode(req: express.Request, code: string, lang) { |
| let lines = code.split('\n'); |
| if (lang.previewFilter !== null) { |
| lines = lines.filter(line => !lang.previewFilter.test(line)); |
| } |
| return lines.map(line => this.escapeLine(req, line)).join('\n'); |
| } |
| |
| getMetaDataFromLink(req: express.Request, link: {config: string; specialMetadata: any} | null, config) { |
| const metadata = { |
| ogDescription: null as string | null, |
| ogAuthor: null as string | null, |
| ogTitle: 'Compiler Explorer', |
| }; |
| |
| if (link) { |
| metadata.ogDescription = link.specialMetadata ? link.specialMetadata.description.S : null; |
| metadata.ogAuthor = link.specialMetadata ? link.specialMetadata.author.S : null; |
| metadata.ogTitle = link.specialMetadata ? link.specialMetadata.title.S : 'Compiler Explorer'; |
| } |
| |
| if (!metadata.ogDescription) { |
| let lang; |
| let source = ''; |
| |
| const sources = utils.glGetMainContents(config.content); |
| if (sources.editors.length === 1) { |
| const editor = sources.editors[0]; |
| lang = this.apiHandler.languages[editor.language]; |
| source = editor.source; |
| } else { |
| const normalizer = new ClientStateNormalizer(); |
| normalizer.fromGoldenLayout(config); |
| const clientstate = normalizer.normalized; |
| |
| if (clientstate.trees && clientstate.trees.length === 1) { |
| const tree = clientstate.trees[0]; |
| lang = this.apiHandler.languages[tree.compilerLanguageId]; |
| |
| if (tree.isCMakeProject) { |
| const firstSource = tree.files.find(file => { |
| return unwrap(file.filename).startsWith('CMakeLists.txt'); |
| }); |
| |
| if (firstSource) { |
| source = firstSource.content; |
| } |
| } else { |
| const firstSource = tree.files.find(file => { |
| return unwrap(file.filename).startsWith('example.'); |
| }); |
| |
| if (firstSource) { |
| source = firstSource.content; |
| } |
| } |
| } |
| } |
| |
| if (lang) { |
| metadata.ogDescription = this.filterCode(req, source, lang); |
| metadata.ogTitle += ` - ${lang.name}`; |
| if (sources && sources.compilers.length === 1) { |
| const compilerId = sources.compilers[0].compiler; |
| const compiler = this.apiHandler.compilers.find(c => c.id === compilerId); |
| if (compiler) { |
| metadata.ogTitle += ` (${compiler.name})`; |
| } |
| } |
| } else { |
| metadata.ogDescription = source; |
| } |
| } else if (metadata.ogAuthor && metadata.ogAuthor !== '.') { |
| metadata.ogDescription += `\nAuthor(s): ${metadata.ogAuthor}`; |
| } |
| |
| return metadata; |
| } |
| } |