const logger = require('@overleaf/logger') const Settings = require('@overleaf/settings') const querystring = require('querystring') const _ = require('lodash') const { URL } = require('url') const Path = require('path') const moment = require('moment') const pug = require('pug-runtime') const request = require('request') const Features = require('./Features') const SessionManager = require('../Features/Authentication/SessionManager') const PackageVersions = require('./PackageVersions') const Modules = require('./Modules') const SafeHTMLSubstitute = require('../Features/Helpers/SafeHTMLSubstitution') let webpackManifest switch (process.env.NODE_ENV) { case 'production': // Only load webpack manifest file in production. webpackManifest = require(`../../../public/manifest.json`) break case 'development': // In dev, fetch the manifest from the webpack container. loadManifestFromWebpackDevServer() setInterval(loadManifestFromWebpackDevServer, 10 * 1000) break default: // In ci, all entries are undefined. webpackManifest = {} } function loadManifestFromWebpackDevServer(done = function () {}) { request( { uri: `${Settings.apis.webpack.url}/manifest.json`, headers: { Host: 'localhost' }, json: true, }, (err, res, body) => { if (!err && res.statusCode !== 200) { err = new Error(`webpack responded with statusCode: ${res.statusCode}`) } if (err) { logger.err({ err }, 'cannot fetch webpack manifest') return done(err) } webpackManifest = body done() } ) } const IN_CI = process.env.NODE_ENV === 'test' function getWebpackAssets(entrypoint, section) { if (IN_CI) { // Emit an empty list of entries in CI. return [] } return webpackManifest.entrypoints[entrypoint].assets[section] || [] } const I18N_HTML_INJECTIONS = new Set() module.exports = function (webRouter, privateApiRouter, publicApiRouter) { if (process.env.NODE_ENV === 'development') { // In the dev-env, delay requests until we fetched the manifest once. webRouter.use(function (req, res, next) { if (!webpackManifest) { loadManifestFromWebpackDevServer(next) } else { next() } }) } webRouter.use(function (req, res, next) { res.locals.session = req.session next() }) function addSetContentDisposition(req, res, next) { res.setContentDisposition = function (type, opts) { const directives = _.map( opts, (v, k) => `${k}="${encodeURIComponent(v)}"` ) const contentDispositionValue = `${type}; ${directives.join('; ')}` res.setHeader('Content-Disposition', contentDispositionValue) } next() } webRouter.use(addSetContentDisposition) privateApiRouter.use(addSetContentDisposition) publicApiRouter.use(addSetContentDisposition) webRouter.use(function (req, res, next) { req.externalAuthenticationSystemUsed = Features.externalAuthenticationSystemUsed res.locals.externalAuthenticationSystemUsed = Features.externalAuthenticationSystemUsed req.hasFeature = res.locals.hasFeature = Features.hasFeature next() }) webRouter.use(function (req, res, next) { let staticFilesBase const cdnAvailable = Settings.cdn && Settings.cdn.web && !!Settings.cdn.web.host const cdnBlocked = req.query.nocdn === 'true' || req.session.cdnBlocked const userId = SessionManager.getLoggedInUserId(req.session) if (cdnBlocked && req.session.cdnBlocked == null) { logger.log( { user_id: userId, ip: req != null ? req.ip : undefined }, 'cdnBlocked for user, not using it and turning it off for future requets' ) req.session.cdnBlocked = true } const host = req.headers && req.headers.host const isSmoke = host.slice(0, 5).toLowerCase() === 'smoke' if (cdnAvailable && !isSmoke && !cdnBlocked) { staticFilesBase = Settings.cdn.web.host } else { staticFilesBase = '' } res.locals.buildBaseAssetPath = function () { // Return the base asset path (including the CDN url) so that webpack can // use this to dynamically fetch scripts (e.g. PDFjs worker) return staticFilesBase + '/' } res.locals.buildJsPath = function (jsFile) { return staticFilesBase + webpackManifest[jsFile] } res.locals.buildCopiedJsAssetPath = function (jsFile) { return staticFilesBase + (webpackManifest[jsFile] || '/' + jsFile) } res.locals.entrypointScripts = function (entrypoint) { const chunks = getWebpackAssets(entrypoint, 'js') return chunks.map(chunk => staticFilesBase + chunk) } res.locals.entrypointStyles = function (entrypoint) { const chunks = getWebpackAssets(entrypoint, 'css') return chunks.map(chunk => staticFilesBase + chunk) } res.locals.mathJaxPath = `/js/libs/mathjax/MathJax.js?${querystring.stringify( { config: 'TeX-AMS_HTML,Safe', v: require('mathjax/package.json').version, } )}` res.locals.lib = PackageVersions.lib res.locals.moment = moment const IEEE_BRAND_ID = 15 res.locals.isIEEE = brandVariation => (brandVariation != null ? brandVariation.brand_id : undefined) === IEEE_BRAND_ID res.locals.getCssThemeModifier = function (userSettings, brandVariation) { // Themes only exist in OL v2 if (Settings.overleaf != null) { // The IEEE theme takes precedence over the user personal setting, i.e. a user with // a theme setting of "light" will still get the IEE theme in IEEE branded projects. if (res.locals.isIEEE(brandVariation)) { return 'ieee-' } else if (userSettings && userSettings.overallTheme != null) { return userSettings.overallTheme } } } res.locals.buildStylesheetPath = function (cssFileName) { return staticFilesBase + webpackManifest[cssFileName] } res.locals.buildCssPath = function (themeModifier = '') { return res.locals.buildStylesheetPath(`${themeModifier}style.css`) } res.locals.buildImgPath = function (imgFile) { const path = Path.join('/img/', imgFile) return staticFilesBase + path } next() }) webRouter.use(function (req, res, next) { res.locals.translate = function (key, vars, components) { vars = vars || {} if (Settings.i18n.checkForHTMLInVars) { Object.entries(vars).forEach(([field, value]) => { if (pug.escape(value) !== value) { const violationsKey = key + field // do not flood the logs, log one sample per pod + key + field if (!I18N_HTML_INJECTIONS.has(violationsKey)) { logger.warn( { key, field, value }, 'html content in translations context vars' ) I18N_HTML_INJECTIONS.add(violationsKey) } } }) } vars.appName = Settings.appName const locale = req.i18n.translate(key, vars) if (components) { return SafeHTMLSubstitute.render(locale, components) } else { return locale } } // Don't include the query string parameters, otherwise Google // treats ?nocdn=true as the canonical version const parsedOriginalUrl = new URL(req.originalUrl, Settings.siteUrl) res.locals.currentUrl = parsedOriginalUrl.pathname res.locals.currentUrlWithQueryParams = parsedOriginalUrl.pathname + parsedOriginalUrl.search res.locals.capitalize = function (string) { if (string.length === 0) { return '' } return string.charAt(0).toUpperCase() + string.slice(1) } next() }) webRouter.use(function (req, res, next) { res.locals.getUserEmail = function () { const user = SessionManager.getSessionUser(req.session) const email = (user != null ? user.email : undefined) || '' return email } next() }) webRouter.use(function (req, res, next) { res.locals.StringHelper = require('../Features/Helpers/StringHelper') next() }) webRouter.use(function (req, res, next) { res.locals.buildReferalUrl = function (referalMedium) { let url = Settings.siteUrl const currentUser = SessionManager.getSessionUser(req.session) if ( currentUser != null && (currentUser != null ? currentUser.referal_id : undefined) != null ) { url += `?r=${currentUser.referal_id}&rm=${referalMedium}&rs=b` // Referal source = bonus } return url } res.locals.getReferalId = function () { const currentUser = SessionManager.getSessionUser(req.session) if ( currentUser != null && (currentUser != null ? currentUser.referal_id : undefined) != null ) { return currentUser.referal_id } } next() }) webRouter.use(function (req, res, next) { res.locals.csrfToken = req != null ? req.csrfToken() : undefined next() }) webRouter.use(function (req, res, next) { res.locals.gaToken = Settings.analytics && Settings.analytics.ga && Settings.analytics.ga.token res.locals.gaTokenV4 = Settings.analytics && Settings.analytics.ga && Settings.analytics.ga.tokenV4 res.locals.gaOptimizeId = _.get(Settings, ['analytics', 'gaOptimize', 'id']) next() }) webRouter.use(function (req, res, next) { res.locals.getReqQueryParam = field => req.query != null ? req.query[field] : undefined next() }) webRouter.use(function (req, res, next) { const currentUser = SessionManager.getSessionUser(req.session) if (currentUser != null) { res.locals.user = { email: currentUser.email, first_name: currentUser.first_name, last_name: currentUser.last_name, } } next() }) webRouter.use(function (req, res, next) { res.locals.getLoggedInUserId = () => SessionManager.getLoggedInUserId(req.session) res.locals.getSessionUser = () => SessionManager.getSessionUser(req.session) next() }) webRouter.use(function (req, res, next) { // Clone the nav settings so they can be modified for each request res.locals.nav = {} for (const key in Settings.nav) { res.locals.nav[key] = _.clone(Settings.nav[key]) } res.locals.templates = Settings.templateLinks next() }) webRouter.use(function (req, res, next) { if (Settings.reloadModuleViewsOnEachRequest) { Modules.loadViewIncludes() } res.locals.moduleIncludes = Modules.moduleIncludes res.locals.moduleIncludesAvailable = Modules.moduleIncludesAvailable next() }) webRouter.use(function (req, res, next) { // TODO if (Settings.overleaf != null) { res.locals.overallThemes = [ { name: 'Default', val: '', path: res.locals.buildCssPath(), }, { name: 'Light', val: 'light-', path: res.locals.buildCssPath('light-'), }, ] } next() }) webRouter.use(function (req, res, next) { res.locals.settings = Settings next() }) webRouter.use(function (req, res, next) { res.locals.ExposedSettings = { isOverleaf: Settings.overleaf != null, appName: Settings.appName, hasSamlBeta: req.session.samlBeta, hasSamlFeature: Features.hasFeature('saml'), samlInitPath: _.get(Settings, ['saml', 'ukamf', 'initPath']), hasLinkUrlFeature: Features.hasFeature('link-url'), hasLinkedProjectFileFeature: Features.hasFeature('linked-project-file'), hasLinkedProjectOutputFileFeature: Features.hasFeature( 'linked-project-output-file' ), siteUrl: Settings.siteUrl, emailConfirmationDisabled: Settings.emailConfirmationDisabled, maxEntitiesPerProject: Settings.maxEntitiesPerProject, maxUploadSize: Settings.maxUploadSize, recaptchaSiteKeyV3: Settings.recaptcha != null ? Settings.recaptcha.siteKeyV3 : undefined, recaptchaDisabled: Settings.recaptcha != null ? Settings.recaptcha.disabled : undefined, textExtensions: Settings.textExtensions, validRootDocExtensions: Settings.validRootDocExtensions, sentryAllowedOriginRegex: Settings.sentry.allowedOriginRegex, sentryDsn: Settings.sentry.publicDSN, sentryEnvironment: Settings.sentry.environment, sentryRelease: Settings.sentry.release, enableSubscriptions: Settings.enableSubscriptions, } next() }) }