diff --git a/package-lock.json b/package-lock.json index 34ad1c210..bc2e696e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1645,6 +1645,11 @@ "safe-buffer": "5.1.2" } }, + "bcp-47-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-1.0.0.tgz", + "integrity": "sha512-6rzWe2U70JHrYfBqBnhoeV7HykhmCQ6X2RrBcbpkyCwtNpkGadr3m3LF8ZLXpQ7qrsLCOm3eis2lsVemkLsvHg==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -3090,6 +3095,11 @@ } } }, + "direction": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.2.tgz", + "integrity": "sha512-hSKoz5FBn+zhP9vWKkVQaaxnRDg3/MoPdcg2au54HIUDR8MrP8Ah1jXSJwCXel6SV3Afh5DSzc8Uqv2r1UoQwQ==" + }, "dockerfile-ast": { "version": "0.0.12", "resolved": "https://registry.npmjs.org/dockerfile-ast/-/dockerfile-ast-0.0.12.tgz", @@ -5192,6 +5202,11 @@ "xtend": "^4.0.1" } }, + "hast-util-has-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-1.0.2.tgz", + "integrity": "sha512-EBzRiKIIe9wouLSjqun5ti0oYcEe5U1eEpuOPtcihmP3KvFRovOmmXypf1B/QalQr9S4YoVgLOSg6gW98ihRbA==" + }, "hast-util-is-element": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-1.0.2.tgz", @@ -5202,6 +5217,27 @@ "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.1.tgz", "integrity": "sha512-Xyh0v+nHmQvrOqop2Jqd8gOdyQtE8sIP9IQf7mlVDqp924W4w/8Liuguk2L2qei9hARnQSG2m+wAOCxM7npJVw==" }, + "hast-util-select": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-3.0.0.tgz", + "integrity": "sha512-4JVNXENly4s6CTAXbU7OwhVreoLqMV72YeW06wdqoqUCP77uKk5kYIRSN28+PjUYpQuXgtff1FdILwsvWMOL7w==", + "requires": { + "bcp-47-match": "^1.0.0", + "comma-separated-tokens": "^1.0.2", + "css-selector-parser": "^1.3.0", + "direction": "^1.0.2", + "hast-util-has-property": "^1.0.0", + "hast-util-is-element": "^1.0.0", + "hast-util-to-string": "^1.0.1", + "hast-util-whitespace": "^1.0.0", + "not": "^0.1.0", + "nth-check": "^1.0.1", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.1.0", + "unist-util-visit": "^1.3.1", + "zwitch": "^1.0.0" + } + }, "hast-util-to-html": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-5.0.0.tgz", @@ -5229,6 +5265,11 @@ } } }, + "hast-util-to-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-1.0.1.tgz", + "integrity": "sha512-EC6awGe0ZMUNYmS2hMVaKZxvjVtQA4RhXjtgE20AxGG49MM7OUUfaHc6VcVYv2YwzNlrZQGe5teimCxW1Rk+fA==" + }, "hast-util-whitespace": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-1.0.2.tgz", diff --git a/package.json b/package.json index 2be99be7c..4ef07b9f6 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "fs-extra": "^7.0.0", "github-slugger": "^1.2.1", "hast-to-hyperscript": "^7.0.0", + "hast-util-select": "^3.0.0", "hast-util-to-html": "^5.0.0", "hyperscript": "^2.0.2", "js-yaml": "^3.13.1", diff --git a/src/defaults/html.pipe.js b/src/defaults/html.pipe.js index 101cf6063..706073dd2 100644 --- a/src/defaults/html.pipe.js +++ b/src/defaults/html.pipe.js @@ -34,6 +34,7 @@ const parseFrontmatter = require('../html/parse-frontmatter'); const rewriteLinks = require('../html/static-asset-links'); const tohast = require('../html/html-to-hast'); const tohtml = require('../html/stringify-hast'); +const addHeaders = require('../html/add-headers'); /* eslint no-param-reassign: off */ /* eslint newline-per-chained-call: off */ @@ -67,6 +68,7 @@ const htmlpipe = (cont, payload, action) => { .after(debug) .after(tohast) // start HTML post-processing .after(rewriteLinks).when(production) + .after(addHeaders) .after(tohtml) // end HTML post-processing .after(flag).expose('esi').when(esi) // flag ESI when there is ESI in the response .error(selectStatus(production())); diff --git a/src/html/add-headers.js b/src/html/add-headers.js new file mode 100644 index 000000000..6e6b52d75 --- /dev/null +++ b/src/html/add-headers.js @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +const { selectAll } = require('hast-util-select'); + +function addHeaders({ response: { headers, hast } }) { + const linkheaders = selectAll('link[rel][href]', hast).reduce((h, { properties: { href, rel } }) => { + if (!href.match(/; rel="${rel}"`; + } + return h; + }, headers); + + const metaheaders = selectAll('meta[http-equiv][content]', hast).reduce((h, { properties: { httpEquiv, content } }) => { + const name = httpEquiv; + const value = content; + if (!h[name]) { + // eslint-disable-next-line no-param-reassign + h[name] = value; + } + return h; + }, linkheaders); + + return { + response: { + headers: metaheaders, + }, + }; +} + +module.exports = addHeaders; diff --git a/test/testHTML.js b/test/testHTML.js index ea876ac24..bb92d929a 100644 --- a/test/testHTML.js +++ b/test/testHTML.js @@ -117,6 +117,69 @@ const crequest = { url: '/test/test.html', }; +describe('Testing HTML Pipeline in Production', () => { + let production; + before('Fake Production Mode', () => { + // eslint-disable-next-line no-underscore-dangle + production = process.env.__OW_ACTIVATION_ID; + // eslint-disable-next-line no-underscore-dangle + process.env.__OW_ACTIVATION_ID = 'fake'; + }); + + + it('html.pipe adds headers from meta and link tags', async () => { + const result = await pipe( + ({ content }) => ({ + response: { + status: 201, + headers: { + Foo: 'bar', + }, + body: ` + + Hello World + + + + + + + + + + ${content.document.body.innerHTML} + +`, + }, + }), + { + request: crequest, + content: { + body: 'Hello World', + }, + }, + { + request: { params }, + secrets, + logger, + }, + ); + + assert.equal(result.response.status, 201); + assert.equal(result.response.headers['Content-Type'], 'text/html', 'keeps content-type'); + assert.equal(result.response.headers['X-ESI'], 'enabled', 'detects ESI'); + assert.equal(result.response.headers.Expires, '3000', 'allows setting through meta http-equiv'); + assert.equal(result.response.headers.Exceeds, undefined, 'ignores invalid meta tags'); + assert.equal(result.response.headers.Foo, 'bar', 'does not override existing headers'); + assert.equal(result.response.headers.Link, '; rel="next",; rel="first"', 'allows setting through link'); + }); + + after('Reset Production Mode', () => { + // eslint-disable-next-line no-underscore-dangle + process.env.__OW_ACTIVATION_ID = production; + }); +}); + describe('Testing HTML Pipeline', () => { setupPolly({ logging: false,