From f57e21576d65940cb2ef37542a034d879aa45a71 Mon Sep 17 00:00:00 2001 From: Marcos Spessatto Defendi Date: Wed, 10 Jan 2024 11:53:07 -0300 Subject: [PATCH] chore: internalize meteor inject initial (from meteor 3) (#31367) --- .../meteor-inject-initial/lib/inject-core.js | 50 ++++++ .../lib/inject-server.js | 156 ++++++++++++++++++ .../packages/meteor-inject-initial/package.js | 16 ++ 3 files changed, 222 insertions(+) create mode 100644 apps/meteor/packages/meteor-inject-initial/lib/inject-core.js create mode 100644 apps/meteor/packages/meteor-inject-initial/lib/inject-server.js create mode 100644 apps/meteor/packages/meteor-inject-initial/package.js diff --git a/apps/meteor/packages/meteor-inject-initial/lib/inject-core.js b/apps/meteor/packages/meteor-inject-initial/lib/inject-core.js new file mode 100644 index 0000000000000..6f3a23a66c962 --- /dev/null +++ b/apps/meteor/packages/meteor-inject-initial/lib/inject-core.js @@ -0,0 +1,50 @@ +// Hijack core node API and attach data to the response dynamically +// We are simply using this hack because, there is no way to alter +// Meteor's html content on the server side + +Inject._hijackWrite = function (res) { + const originalWrite = res.write; + res.write = function (chunk, encoding) { + // prevent hijacking other http requests + if (!res.iInjected && encoding === undefined && /^/.test(chunk)) { + chunk = chunk.toString(); + + for (id in Inject.rawModHtmlFuncs) { + chunk = Inject.rawModHtmlFuncs[id](chunk, res); + if (!_.isString(chunk)) { + throw new Error(`Inject func id "${id}" must return HTML, not ${typeof chunk}\n${JSON.stringify(chunk, null, 2)}`); + } + } + + res.iInjected = true; + } + + originalWrite.call(res, chunk, encoding); + }; +}; + +WebApp.connectHandlers.use(function (req, res, next) { + // We only separate this to make testing easier + Inject._hijackWrite(res); + + next(); +}); + +// meteor algorithm to check if this is a meteor serving http request or not +Inject.appUrl = function (url) { + if (url === '/favicon.ico' || url === '/robots.txt') return false; + + // NOTE: app.manifest is not a web standard like favicon.ico and + // robots.txt. It is a file id we have chosen to use for HTML5 + // appcache URLs. It is included here to prevent using an appcache + // then removing it from poisoning an app permanently. Eventually, + // once we have server side routing, this won't be needed as + // unknown URLs with return a 404 automatically. + if (url === '/app.manifest') return false; + + // Avoid serving app HTML for declared routes such as /sockjs/. + if (typeof RoutePolicy !== 'undefined' && RoutePolicy.classify(url)) return false; + + // we currently return app HTML on all URLs by default + return true; +}; diff --git a/apps/meteor/packages/meteor-inject-initial/lib/inject-server.js b/apps/meteor/packages/meteor-inject-initial/lib/inject-server.js new file mode 100644 index 0000000000000..bfc85c7fdaef4 --- /dev/null +++ b/apps/meteor/packages/meteor-inject-initial/lib/inject-server.js @@ -0,0 +1,156 @@ +function escapeReplaceString(str) { + /* + * When using string.replace(str, newSubStr), the dollar sign ("$") is + * considered a special character in newSubStr, and needs to be escaped + * as "$$". We have to do this twice, for escaping the newSubStr in + * this function, and for the resulting string which is passed back. + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace + */ + return str.replace(/\$/g, '$$$$'); +} + +Inject = { + // stores in a script type=application/ejson tag, accessed with Injected.obj('id') + obj(id, data, res) { + this._checkForObjOrFunction(data, 'Inject.obj(id, data [,res]) expects `data` to be an Object or Function'); + + if (res) { + this._resAssign(res, 'objList', id, data); + } else { + this.objList[id] = data; + } + }, + objList: {}, + + // Inserts a META called `id`, whose `content` can be accessed with Injected.meta() + meta(id, data, res) { + this._checkForTextOrFunction(data, 'Inject.meta(id, data [,res]) expects `data` to be an String or Function'); + + if (res) { + this._resAssign(res, 'metaList', id, data); + } else { + this.metaList[id] = data; + } + }, + metaList: {}, + + rawHead(id, textOrFunc, res) { + this._checkForTextOrFunction(textOrFunc, 'Inject.rawHead(id, content [,res]) expects `content` to be an String or Function'); + + if (res) { + this._resAssign(res, 'rawHeads', id, textOrFunc); + } else { + this.rawHeads[id] = textOrFunc; + } + }, + rawHeads: {}, + + rawBody(id, textOrFunc, res) { + this._checkForTextOrFunction(textOrFunc, 'Inject.rawBody(id, content [,res]) expects `content` to be an String or Function'); + + if (res) { + this._resAssign(res, 'rawBodies', id, textOrFunc); + } else { + this.rawBodies[id] = textOrFunc; + } + }, + rawBodies: {}, + + // The callback receives the entire HTML page and must return a modified version + rawModHtml(id, func) { + if (!_.isFunction(func)) { + const message = `Inject func id "${id}" should be a function, not ${typeof func}`; + throw new Error(message); + } + + this.rawModHtmlFuncs[id] = func; + }, + rawModHtmlFuncs: {}, + + _injectObjects(html, res) { + const objs = _.extend({}, Inject.objList, res.Inject && res.Inject.objList); + if (_.isEmpty(objs)) { + return html; + } + + let obj; + let injectHtml = ''; + for (id in objs) { + obj = _.isFunction(objs[id]) ? objs[id](res) : objs[id]; + injectHtml += ` \n`; + } + + return html.replace('', `\n${escapeReplaceString(injectHtml)}`); + }, + + _injectMeta(html, res) { + const metas = _.extend({}, Inject.metaList, res.Inject && res.Inject.metaList); + if (_.isEmpty(metas)) return html; + + let injectHtml = ''; + for (id in metas) { + const meta = this._evalToText(metas[id], res, html); + (injectHtml += ` \n`), res; + } + + return html.replace('', `\n${escapeReplaceString(injectHtml)}`); + }, + + _injectHeads(html, res) { + const heads = _.extend({}, Inject.rawHeads, res.Inject && res.Inject.rawHeads); + if (_.isEmpty(heads)) return html; + + let injectHtml = ''; + for (id in heads) { + const head = this._evalToText(heads[id], res, html); + injectHtml += `${head}\n`; + } + + return html.replace('', `\n${escapeReplaceString(injectHtml)}`); + }, + + _injectBodies(html, res) { + const bodies = _.extend({}, Inject.rawBodies, res.Inject && res.Inject.rawBodies); + if (_.isEmpty(bodies)) return html; + + let injectHtml = ''; + for (id in bodies) { + const body = this._evalToText(bodies[id], res, html); + injectHtml += `${body}\n`; + } + + return html.replace('', `\n${escapeReplaceString(injectHtml)}`); + }, + + // ensure object exists and store there + _resAssign(res, key, id, value) { + if (!res.Inject) res.Inject = {}; + if (!res.Inject[key]) res.Inject[key] = {}; + res.Inject[key][id] = value; + }, + + _checkForTextOrFunction(arg, message) { + if (!(_.isString(arg) || _.isFunction(arg))) { + throw new Error(message); + } + }, + + _checkForObjOrFunction(arg, message) { + if (!(_.isObject(arg) || _.isFunction(arg))) { + throw new Error(message); + } + }, + + // we don't handle errors here. Let them to handle in a higher level + _evalToText(textOrFunc, res, html) { + if (_.isFunction(textOrFunc)) { + return textOrFunc(res, html); + } + return textOrFunc; + }, +}; + +Inject.rawModHtml('injectHeads', Inject._injectHeads.bind(Inject)); +Inject.rawModHtml('injectMeta', Inject._injectMeta.bind(Inject)); +Inject.rawModHtml('injectBodies', Inject._injectBodies.bind(Inject)); +Inject.rawModHtml('injectObjects', Inject._injectObjects.bind(Inject)); diff --git a/apps/meteor/packages/meteor-inject-initial/package.js b/apps/meteor/packages/meteor-inject-initial/package.js new file mode 100644 index 0000000000000..5ae6f530aa606 --- /dev/null +++ b/apps/meteor/packages/meteor-inject-initial/package.js @@ -0,0 +1,16 @@ +Package.describe({ + summary: 'Allow injection of arbitrary data to initial Meteor HTML page', + version: '1.0.5', + git: 'https://github.com/meteorhacks/meteor-inject-initial.git', + name: 'meteorhacks:inject-initial', +}); + +Package.onUse(function (api) { + api.use(['routepolicy', 'webapp'], 'server'); + api.use(['ejson', 'underscore'], ['server']); + + api.addFiles('lib/inject-server.js', 'server'); + api.addFiles('lib/inject-core.js', 'server'); + + api.export('Inject', 'server'); +});