// Copyright (c) 2018, 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 https from 'https'; import path from 'path'; import fs from 'fs-extra'; import semverParser from 'semver'; import _ from 'underscore'; import {AppDefaultArguments} from '../app.js'; import {CompilerInfo} from '../types/compiler.interfaces.js'; import type {LanguageKey} from '../types/languages.interfaces.js'; import type {ToolTypeKey} from '../types/tool.interfaces.js'; import {logger} from './logger.js'; import type {PropertyGetter, PropertyValue} from './properties.interfaces.js'; import {CompilerProps} from './properties.js'; import {Source} from './sources/index.js'; import {BaseTool, getToolTypeByKey} from './tooling/index.js'; import {asSafeVer, getHash, splitArguments, splitIntoArray} from './utils.js'; // TODO: Figure out if same as libraries.interfaces.ts? export type VersionInfo = { version: string; staticliblink: string[]; alias: string[]; dependencies: string[]; path: string[]; libpath: string[]; liblink: string[]; lookupversion?: PropertyValue; options: string[]; hidden: boolean; packagedheaders?: boolean; }; export type OptionsHandlerLibrary = { name: string; url: string; description: string; staticliblink: string[]; liblink: string[]; dependencies: string[]; versions: Record; examples: string[]; options: string[]; packagedheaders?: boolean; }; // TODO: Is this the same as Options in static/options.interfaces.ts? export type ClientOptionsType = { googleAnalyticsAccount: string; googleAnalyticsEnabled: boolean; sharingEnabled: boolean; githubEnabled: boolean; showSponsors: boolean; gapiKey: string; googleShortLinkRewrite: string[]; urlShortenService: string; defaultSource: string; compilers: CompilerInfo[]; libs: Record>; remoteLibs: Record; tools: Record; defaultLibs: Record; defaultCompiler: Record; compileOptions: Record; supportsBinary: Record; supportsBinaryObject: Record; supportsExecute: boolean; supportsLibraryCodeFilter: boolean; languages: Record; sources: { name: string; urlpart: string; }[]; sentryDsn: string; sentryEnvironment: string | number | true; release: string; gitReleaseCommit: string; cookieDomainRe: string; localStoragePrefix: PropertyValue; cvCompilerCountMax: number; defaultFontScale: number; doCache: boolean; thirdPartyIntegrationEnabled: boolean; statusTrackingEnabled: boolean; compilerVersionsUrl?: string; policies: { cookies: { enabled: boolean; key: string; }; privacy: { enabled: boolean; key: string; }; }; motdUrl: string; pageloadUrl: string; }; /*** * Handles the setup of the options object passed on each page request */ export class ClientOptionsHandler { compilerProps: CompilerProps['get']; ceProps: PropertyGetter; supportsBinary: Record; supportsBinaryObject: Record; supportsExecutePerLanguage: Record; supportsExecute: boolean; supportsLibraryCodeFilterPerLanguage: Record; supportsLibraryCodeFilter: boolean; remoteLibs: Record; options: ClientOptionsType; optionsJSON: string; optionsHash: string; /*** * * @param {Object[]} fileSources - Files to show in the Load/Save pane * @param {string} fileSources.name - UI display name of the file * @param {string} fileSources.urlpart - Relative url path to fetch the file from * @param {CompilerProps} compilerProps * @param {Object} defArgs - Compiler Explorer arguments */ constructor(fileSources: Source[], compilerProps: CompilerProps, defArgs: AppDefaultArguments) { this.compilerProps = compilerProps.get.bind(compilerProps); this.ceProps = compilerProps.ceProps; const ceProps = compilerProps.ceProps; const sources = _.sortBy( fileSources.map(source => { return {name: source.name, urlpart: source.urlpart}; }), 'name', ); /*** * @type {CELanguages} */ const languages = compilerProps.languages; this.supportsBinary = this.compilerProps(languages, 'supportsBinary', true, res => !!res); this.supportsBinaryObject = this.compilerProps(languages, 'supportsBinaryObject', true, res => !!res); this.supportsExecutePerLanguage = this.compilerProps(languages, 'supportsExecute', true, res => !!res); this.supportsExecute = Object.values(this.supportsExecutePerLanguage).some(Boolean); this.supportsLibraryCodeFilterPerLanguage = this.compilerProps(languages, 'supportsLibraryCodeFilter', false); this.supportsLibraryCodeFilter = Object.values(this.supportsLibraryCodeFilterPerLanguage).some(Boolean); const libs = this.parseLibraries(this.compilerProps(languages, 'libs')); const tools = this.parseTools(this.compilerProps(languages, 'tools')); this.remoteLibs = {}; const cookiePolicyEnabled = !!ceProps('cookiePolicyEnabled'); const privacyPolicyEnabled = !!ceProps('privacyPolicyEnabled'); const cookieDomainRe = ceProps('cookieDomainRe', ''); this.options = { googleAnalyticsAccount: ceProps('clientGoogleAnalyticsAccount', 'UA-55180-6'), googleAnalyticsEnabled: ceProps('clientGoogleAnalyticsEnabled', false), sharingEnabled: ceProps('clientSharingEnabled', true), githubEnabled: ceProps('clientGitHubRibbonEnabled', true), showSponsors: ceProps('showSponsors', false), gapiKey: ceProps('googleApiKey', ''), googleShortLinkRewrite: ceProps('googleShortLinkRewrite', '').split('|'), urlShortenService: ceProps('urlShortenService', 'default'), defaultSource: ceProps('defaultSource', ''), compilers: [], libs: libs, remoteLibs: {}, tools: tools, defaultLibs: this.compilerProps(languages, 'defaultLibs', ''), defaultCompiler: this.compilerProps(languages, 'defaultCompiler', ''), compileOptions: this.compilerProps(languages, 'defaultOptions', ''), supportsBinary: this.supportsBinary, supportsBinaryObject: this.supportsBinaryObject, supportsExecute: this.supportsExecute, supportsLibraryCodeFilter: this.supportsLibraryCodeFilter, languages: languages, sources: sources, sentryDsn: ceProps('sentryDsn', ''), sentryEnvironment: ceProps('sentryEnvironment') || defArgs.env[0], release: defArgs.releaseBuildNumber || defArgs.gitReleaseName, gitReleaseCommit: defArgs.gitReleaseName || '', cookieDomainRe, localStoragePrefix: ceProps('localStoragePrefix'), cvCompilerCountMax: ceProps('cvCompilerCountMax', 6), defaultFontScale: ceProps('defaultFontScale', 14), doCache: defArgs.doCache, thirdPartyIntegrationEnabled: ceProps('thirdPartyIntegrationEnabled', true), compilerVersionsUrl: ceProps('compilerVersionsUrl', undefined), statusTrackingEnabled: ceProps('statusTrackingEnabled', true), policies: { cookies: { enabled: cookiePolicyEnabled, key: 'cookie_status', }, privacy: { enabled: privacyPolicyEnabled, key: 'privacy_status', }, }, motdUrl: ceProps('motdUrl', ''), pageloadUrl: ceProps('pageloadUrl', ''), }; // Will be immediately replaced with actual values this.optionsJSON = ''; this.optionsHash = ''; this._updateOptionsHash(); } parseTools(baseTools: Record) { const tools: Record> = {}; for (const [lang, forLang] of Object.entries(baseTools)) { if (lang && forLang) { tools[lang] = {}; for (const tool of forLang.split(':')) { const toolBaseName = `tools.${tool}`; const className = this.compilerProps(lang, toolBaseName + '.class'); const Tool = getToolTypeByKey(className); const toolPath = this.compilerProps(lang, toolBaseName + '.exe'); if (fs.existsSync(toolPath)) { tools[lang][tool] = new Tool( { id: tool, name: this.compilerProps(lang, toolBaseName + '.name'), type: this.compilerProps(lang, toolBaseName + '.type') as ToolTypeKey, exe: toolPath, exclude: splitIntoArray(this.compilerProps(lang, toolBaseName + '.exclude')), includeKey: this.compilerProps(lang, toolBaseName + '.includeKey'), options: splitArguments(this.compilerProps(lang, toolBaseName + '.options')), args: this.compilerProps(lang, toolBaseName + '.args'), languageId: this.compilerProps( lang, toolBaseName + '.languageId', ) as LanguageKey, stdinHint: this.compilerProps(lang, toolBaseName + '.stdinHint'), monacoStdin: this.compilerProps(lang, toolBaseName + '.monacoStdin'), icon: this.compilerProps(lang, toolBaseName + '.icon'), darkIcon: this.compilerProps(lang, toolBaseName + '.darkIcon'), compilerLanguage: lang as LanguageKey, }, { ceProps: this.ceProps, compilerProps: propname => this.compilerProps(lang, propname), }, ); } else { logger.warn(`Unable to stat ${toolBaseName} tool binary`); } } } } return tools; } parseLibraries(baseLibs: Record) { // Record language -> {Record lib name -> lib} const libraries: Record> = {}; for (const [lang, forLang] of Object.entries(baseLibs)) { if (lang && forLang) { libraries[lang] = {}; for (const lib of forLang.split(':')) { const libBaseName = `libs.${lib}`; libraries[lang][lib] = { name: this.compilerProps(lang, libBaseName + '.name'), url: this.compilerProps(lang, libBaseName + '.url'), description: this.compilerProps(lang, libBaseName + '.description'), staticliblink: splitIntoArray(this.compilerProps(lang, libBaseName + '.staticliblink')), liblink: splitIntoArray(this.compilerProps(lang, libBaseName + '.liblink')), dependencies: splitIntoArray(this.compilerProps(lang, libBaseName + '.dependencies')), versions: {}, examples: splitIntoArray(this.compilerProps(lang, libBaseName + '.examples')), options: splitArguments(this.compilerProps(lang, libBaseName + '.options', '')), packagedheaders: this.compilerProps(lang, libBaseName + '.packagedheaders', false), }; const listedVersions = `${this.compilerProps(lang, libBaseName + '.versions')}`; if (listedVersions) { for (const version of listedVersions.split(':')) { const libVersionName = libBaseName + `.versions.${version}`; const versionObject: VersionInfo = { version: this.compilerProps(lang, libVersionName + '.version'), staticliblink: splitIntoArray( this.compilerProps(lang, libVersionName + '.staticliblink'), libraries[lang][lib].staticliblink, ), alias: splitIntoArray(this.compilerProps(lang, libVersionName + '.alias')), dependencies: splitIntoArray( this.compilerProps(lang, libVersionName + '.dependencies'), libraries[lang][lib].dependencies, ), path: [], libpath: [], liblink: splitIntoArray( this.compilerProps(lang, libVersionName + '.liblink'), libraries[lang][lib].liblink, ), // Library options might get overridden later options: libraries[lang][lib].options, hidden: this.compilerProps(lang, libVersionName + '.hidden', false), packagedheaders: libraries[lang][lib].packagedheaders, }; const lookupversion = this.compilerProps(lang, libVersionName + '.lookupversion'); if (lookupversion) { versionObject.lookupversion = lookupversion; } const includes = this.compilerProps(lang, libVersionName + '.path'); if (includes) { versionObject.path = includes.split(path.delimiter); } else if (version !== 'autodetect') { logger.warn(`Library ${lib} ${version} (${lang}) has no include paths`); } const libpath = this.compilerProps(lang, libVersionName + '.libpath'); if (libpath) { versionObject.libpath = libpath.split(path.delimiter); } const options = this.compilerProps(lang, libVersionName + '.options'); if (options !== undefined) { versionObject.options = splitArguments(options); } versionObject.packagedheaders = this.compilerProps( lang, libVersionName + '.packagedheaders', libraries[lang][lib].packagedheaders, ); libraries[lang][lib].versions[version] = versionObject; } } else { logger.warn(`No versions found for ${lib} library`); } } } } for (const langGroup of Object.values(libraries)) { for (const libGroup of Object.values(langGroup)) { const versions = Object.values(libGroup.versions); versions.sort((a, b) => semverParser.compare(asSafeVer(a.version), asSafeVer(b.version), true)); let order = 0; // Set $order to index on array. As group is an array, iteration order is guaranteed. for (const lib of versions) { lib['$order'] = order++; } } } return libraries; } getRemoteId(remoteUrl, language) { const url = new URL(remoteUrl); return url.host.replaceAll('.', '_') + '_' + language; } libArrayToObject(libsArr) { const libs = {}; for (const lib of libsArr) { libs[lib.id] = lib; const versions = lib.versions; lib.versions = {}; for (const version of versions) { lib.versions[version.id] = version; } } return libs; } async getRemoteLibraries(language, remoteUrl) { const remoteId = this.getRemoteId(remoteUrl, language); if (!this.remoteLibs[remoteId]) { return new Promise(resolve => { const url = remoteUrl + '/api/libraries/' + language; logger.info(`Fetching remote libraries from ${url}`); let fullData = ''; https.get(url, res => { res.on('data', data => { fullData += data; }); res.on('end', () => { try { const libsArr = JSON.parse(fullData); this.remoteLibs[remoteId] = this.libArrayToObject(libsArr); } catch (e) { logger.error('Error while fetching remote libraries, but continuing.', e); this.remoteLibs[remoteId] = {}; } resolve(this.remoteLibs[remoteId]); }); }); }); } return this.remoteLibs[remoteId]; } async fetchRemoteLibrariesIfNeeded(language, remote) { await this.getRemoteLibraries(language, remote.target); } async setCompilers(compilers: CompilerInfo[]) { const forbiddenKeys = new Set([ 'exe', 'versionFlag', 'versionRe', 'compilerType', 'demangler', 'objdumper', 'postProcess', 'demanglerType', 'isSemVer', ]); const copiedCompilers = JSON.parse(JSON.stringify(compilers)) as CompilerInfo[]; const semverGroups: Record = {}; // Reset the supportsExecute flag in case critical compilers change for (const key of Object.keys(this.options.languages)) { this.options.languages[key].supportsExecute = false; } for (const [compilersKey, compiler] of copiedCompilers.entries()) { if (compiler.supportsExecute) { this.options.languages[compiler.lang].supportsExecute = true; } if (compiler.isSemVer) { if (!semverGroups[compiler.group]) semverGroups[compiler.group] = []; // Desired index which will keep the array in order semverGroups[compiler.group].push(compiler); } if (compiler.remote) { await this.fetchRemoteLibrariesIfNeeded(compiler.lang, compiler.remote); } for (const propKey of Object.keys(compiler)) { if (forbiddenKeys.has(propKey)) { delete copiedCompilers[compilersKey][propKey]; } } } for (const group of Object.values(semverGroups)) { group.sort((a, b) => semverParser.compare(asSafeVer(a.semver), asSafeVer(b.semver), true)); let order = 0; // Set $order to -index on array. As group is an array, iteration order is guaranteed. for (const compiler of group) { compiler['$order'] = -order++; } } this.options.compilers = copiedCompilers; this.options.remoteLibs = this.remoteLibs; this._updateOptionsHash(); } _updateOptionsHash() { this.optionsJSON = JSON.stringify(this.options); this.optionsHash = getHash(this.options, 'Options Hash V1'); logger.info(`OPTIONS HASH: ${this.optionsHash}`); } get() { return this.options; } getJSON() { return this.optionsJSON; } getHash() { return this.optionsHash; } }