From 535c97e8cfbdd0fdcf92760de3c8ff930d1765d3 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Mon, 22 Mar 2021 10:51:07 +0100 Subject: [PATCH] Merge pull request #3774 from overleaf/jpa-meta [frontend] import meta tag processing from das7pads fork GitOrigin-RevId: ca74ff9fbbcb51091a626a45468ff3d24d6136ca --- services/web/Makefile | 4 ++ services/web/app/views/layout.pug | 65 ++++++++++----------- services/web/app/views/project/list.pug | 25 ++++---- services/web/bin/lint_pug_templates | 31 ++++++++++ services/web/frontend/js/base.js | 4 +- services/web/frontend/js/libraries.js | 3 + services/web/frontend/js/utils/meta.js | 48 +++++++++++++++ services/web/test/smoke/src/support/Csrf.js | 2 +- 8 files changed, 135 insertions(+), 47 deletions(-) create mode 100755 services/web/bin/lint_pug_templates create mode 100644 services/web/frontend/js/utils/meta.js diff --git a/services/web/Makefile b/services/web/Makefile index bef5d01c9e..5fb1649c8e 100644 --- a/services/web/Makefile +++ b/services/web/Makefile @@ -220,6 +220,10 @@ lint_misc: --ignore-pattern 'modules/*/test/**/*.js' \ --max-warnings=0 +lint: lint_pug +lint_pug: + bin/lint_pug_templates + lint_in_docker: $(RUN_LINT_FORMAT) make lint -j --output-sync diff --git a/services/web/app/views/layout.pug b/services/web/app/views/layout.pug index eb6836c11b..6213377395 100644 --- a/services/web/app/views/layout.pug +++ b/services/web/app/views/layout.pug @@ -69,45 +69,42 @@ html( script(type='text/javascript'). window.ga = function() { console.log("would send to GA", arguments) }; - script(type="text/javascript"). - window.csrfToken = "#{csrfToken}"; + block meta + meta(name="ol-csrfToken" content=csrfToken) //- Configure dynamically loaded assets (via webpack) to be downloaded from CDN //- See: https://webpack.js.org/guides/public-path/#on-the-fly - window.baseAssetPath = "#{buildBaseAssetPath()}" + meta(name="ol-baseAssetPath" content=buildBaseAssetPath()) + + meta(name="ol-usersEmail" content=getUserEmail()) + meta(name="ol-sharelatex" data-type="json" content={ + siteUrl: settings.siteUrl, + wsUrl, + }) + meta(name="ol-ab" data-type="json" content={}) + meta(name="ol-user_id" content=getLoggedInUserId()) + //- Internationalisation settings + meta(name="ol-i18n" data-type="json" content={ + currentLangCode: currentLngCode + }) + //- Expose some settings globally to the frontend + meta(name="ol-ExposedSettings" data-type="json" content=ExposedSettings) + + if (typeof(settings.algolia) != "undefined") + meta(name="ol-sharelatex.algolia" data-type="json" content={ + app_id: settings.algolia.app_id, + api_key: settings.algolia.read_only_api_key, + indexes: settings.algolia.indexes + }) + + if (typeof(settings.templates) != "undefined") + meta(name="ol-sharelatex.templates" data-type="json" content={ + user_id : settings.templates.user_id, + cdnDomain : settings.templates.cdnDomain, + indexName : settings.templates.indexName + }) block head-scripts - meta(id="ol-usersEmail" content=getUserEmail()) - script. - window.sharelatex = { - siteUrl: '#{settings.siteUrl}', - wsUrl: '#{wsUrl}', - }; - window.ab = {}; - window.user_id = '#{getLoggedInUserId()}'; - //- Internationalisation settings - window.i18n = { - currentLangCode: '#{currentLngCode}' - } - //- Expose some settings globally to the frontend - window.ExposedSettings = JSON.parse('!{StringHelper.stringifyJsonForScript(ExposedSettings)}'); - - - if (typeof(settings.algolia) != "undefined") - script. - window.sharelatex.algolia = { - app_id:'#{settings.algolia.app_id}', - api_key:'#{settings.algolia.read_only_api_key}', - indexes:!{StringHelper.stringifyJsonForScript(settings.algolia.indexes)} - } - - - if (typeof(settings.templates) != "undefined") - script. - window.sharelatex.templates = { - user_id : '!{settings.templates.user_id}', - cdnDomain : '!{settings.templates.cdnDomain}', - indexName : '!{settings.templates.indexName}' - } - body if(settings.recaptcha && settings.recaptcha.siteKeyV3) diff --git a/services/web/app/views/project/list.pug b/services/web/app/views/project/list.pug index 600a0986d0..ae71073a32 100644 --- a/services/web/app/views/project/list.pug +++ b/services/web/app/views/project/list.pug @@ -3,18 +3,23 @@ extends ../layout block vars - var suppressNavContentLinks = true -block content - script#data(type="application/json"). - !{StringHelper.stringifyJsonForScript({ projects, tags, notifications, notificationsInstitution, userAffiliations, userEmails, allInReconfirmNotificationPeriods, reconfirmedViaSAML })} - - script(type="text/javascript"). - window.data = JSON.parse(document.querySelector("#data").text); - window.algolia = { +block append meta + meta(name="ol-projects" data-type="json" content=projects) + meta(name="ol-tags" data-type="json" content=tags) + meta(name="ol-notifications" data-type="json" content=notifications) + meta(name="ol-notificationsInstitution" data-type="json" content=notificationsInstitution) + meta(name="ol-userAffiliations" data-type="json" content=userAffiliations) + meta(name="ol-userEmails" data-type="json" content=userEmails) + meta(name="ol-allInReconfirmNotificationPeriods" data-type="json" content=allInReconfirmNotificationPeriods) + meta(name="ol-reconfirmedViaSAML" content=reconfirmedViaSAML) + meta(name="ol-algolia" data-type="json" content={ institutions: { - app_id: '#{algolia_app_id}', - api_key: '#{algolia_api_key}' + app_id: algolia_app_id, + api_key: algolia_api_key } - }; + }) + +block content main.content.content-alt.project-list-page( ng-controller="ProjectPageController" diff --git a/services/web/bin/lint_pug_templates b/services/web/bin/lint_pug_templates new file mode 100755 index 0000000000..6f255d9745 --- /dev/null +++ b/services/web/bin/lint_pug_templates @@ -0,0 +1,31 @@ +#!/bin/sh + +set -e + +TEMPLATES_EXTENDING_META_BLOCK=$(\ + grep \ + --files-with-matches \ + --recursive app/views modules/*/app/views \ + --regex 'block append meta' \ + --regex 'block prepend meta' \ + --regex 'append meta' \ + --regex 'prepend meta' \ +) + +for file in ${TEMPLATES_EXTENDING_META_BLOCK}; do + if ! grep "$file" --quiet --extended-regexp -e 'extends .+layout'; then + cat <&2 + +ERROR: $file is a partial template and extends 'block meta'. + +Using block append/prepend in a partial will duplicate the block contents into + the due to a bug in pug. +Putting meta tags in the can lead to Angular XSS. + +You will need to refactor the partial and move the block into the top level + page template that extends the global layout.pug. + +MSG + exit 1 + fi +done diff --git a/services/web/frontend/js/base.js b/services/web/frontend/js/base.js index bf34417eb3..746e1317f8 100644 --- a/services/web/frontend/js/base.js +++ b/services/web/frontend/js/base.js @@ -22,6 +22,7 @@ import './modules/recursionHelper' import './modules/errorCatcher' import './modules/localStorage' import './modules/sessionStorage' +import getMeta from './utils/meta' const App = angular .module('SharelatexApp', [ @@ -80,8 +81,7 @@ const App = angular }) App.run(($rootScope, $templateCache) => { - const usersEmailElement = document.getElementById('ol-usersEmail') - $rootScope.usersEmail = usersEmailElement && usersEmailElement.content + $rootScope.usersEmail = getMeta('ol-usersEmail') // UI Select templates are hard-coded and use Glyphicon icons (which we don't import). // The line below simply overrides the hard-coded template with our own, which is diff --git a/services/web/frontend/js/libraries.js b/services/web/frontend/js/libraries.js index b3c4aea741..95a25ee90e 100644 --- a/services/web/frontend/js/libraries.js +++ b/services/web/frontend/js/libraries.js @@ -14,6 +14,9 @@ import 'libs/select/select' // Polyfill fetch for IE11 import 'isomorphic-unfetch' +// Rewrite meta elements +import './utils/meta' + // Configure dynamically loaded assets (via webpack) to be downloaded from CDN // See: https://webpack.js.org/guides/public-path/#on-the-fly // eslint-disable-next-line no-undef, camelcase diff --git a/services/web/frontend/js/utils/meta.js b/services/web/frontend/js/utils/meta.js new file mode 100644 index 0000000000..bf707b6af7 --- /dev/null +++ b/services/web/frontend/js/utils/meta.js @@ -0,0 +1,48 @@ +import _ from 'lodash' + +// cache for parsed values +const cache = new Map() + +export default function getMeta(name, fallback) { + if (cache.has(name)) return cache.get(name) + const element = document.head.querySelector(`meta[name="${name}"]`) + if (!element) { + return fallback + } + const plainTextValue = element.content + let value + switch (element.dataset.type) { + case 'boolean': + // in pug: content=false -> no content field + // in pug: content=true -> empty content field + value = element.hasAttribute('content') + break + case 'json': + if (!plainTextValue) { + // JSON.parse('') throws + value = undefined + } else { + value = JSON.parse(plainTextValue) + } + break + default: + value = plainTextValue + } + cache.set(name, value) + return value +} + +function convertMetaToWindowAttributes() { + window.data = window.data || {} + Array.from(document.querySelectorAll('meta[name^="ol-"]')) + .map(element => element.name) + // process short labels before long ones: + // e.g. assign 'sharelatex' before 'sharelatex.templates' + .sort() + .forEach(nameWithNamespace => { + const label = nameWithNamespace.slice('ol-'.length) + _.set(window, label, getMeta(nameWithNamespace)) + _.set(window.data, label, getMeta(nameWithNamespace)) + }) +} +convertMetaToWindowAttributes() diff --git a/services/web/test/smoke/src/support/Csrf.js b/services/web/test/smoke/src/support/Csrf.js index 69b8c1dfa7..eab171657d 100644 --- a/services/web/test/smoke/src/support/Csrf.js +++ b/services/web/test/smoke/src/support/Csrf.js @@ -1,6 +1,6 @@ const OError = require('@overleaf/o-error') const { assertHasStatusCode } = require('./requestHelper') -const CSRF_REGEX = /window.csrfToken = "(.+?)"/ +const CSRF_REGEX = // function _parseCsrf(body) { const match = CSRF_REGEX.exec(body)