@adobe/helix-html-pipeline 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## [1.3.1](https://github.com/adobe/helix-html-pipeline/compare/v1.3.0...v1.3.1) (2022-03-18)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **deps:** update dependency @adobe/helix-shared-utils to v2.0.6 ([#26](https://github.com/adobe/helix-html-pipeline/issues/26)) ([186c376](https://github.com/adobe/helix-html-pipeline/commit/186c376d0252b0c96ee461670cf45a711aa93f4f))
7
+ * preserve formatting of script tags ([#25](https://github.com/adobe/helix-html-pipeline/issues/25)) ([7009f20](https://github.com/adobe/helix-html-pipeline/commit/7009f20d37190f5704b7f9363c59912b4272c0bf)), closes [#23](https://github.com/adobe/helix-html-pipeline/issues/23)
8
+
9
+ # [1.3.0](https://github.com/adobe/helix-html-pipeline/compare/v1.2.1...v1.3.0) (2022-03-17)
10
+
11
+
12
+ ### Features
13
+
14
+ * provide pipes for OPTIONS and POSTs ([#24](https://github.com/adobe/helix-html-pipeline/issues/24)) ([1dfc47e](https://github.com/adobe/helix-html-pipeline/commit/1dfc47e764a0b1d8acee80b51b845be2e54a16f5))
15
+
16
+ ## [1.2.1](https://github.com/adobe/helix-html-pipeline/compare/v1.2.0...v1.2.1) (2022-03-16)
17
+
18
+
19
+ ### Bug Fixes
20
+
21
+ * reject double-slashes ([#22](https://github.com/adobe/helix-html-pipeline/issues/22)) ([5aee75d](https://github.com/adobe/helix-html-pipeline/commit/5aee75d4109550525d971c64d87e4f2420863c30)), closes [#20](https://github.com/adobe/helix-html-pipeline/issues/20)
22
+
1
23
  # [1.2.0](https://github.com/adobe/helix-html-pipeline/compare/v1.1.3...v1.2.0) (2022-03-16)
2
24
 
3
25
 
package/README.md CHANGED
@@ -3,8 +3,8 @@
3
3
  This package contains the common code for `helix-pipeline-service` and `helix-cloudflare-pipeline` for rendering the html response for helix3. it has the following design goals:
4
4
 
5
5
  - be platform neutral, i.e. not using node or browser specific modules or dependencies.
6
- - +/-0 runtime dependencies
7
- - offer extension interfaces where platform abstraction is required (e.g. reading from s3)
6
+ - +/-0 runtime dependencies (eg. node [crypto](https://nodejs.org/api/crypto.html))
7
+ - offer extension interfaces where platform abstraction is required (e.g. reading from S3, sending to SQS)
8
8
 
9
9
  ## Status
10
10
  [![codecov](https://img.shields.io/codecov/c/github/adobe/helix-html-pipeline.svg)](https://codecov.io/gh/adobe/helix-html-pipeline)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-html-pipeline",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Helix HTML Pipeline",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@adobe/helix-markdown-support": "3.1.2",
36
- "@adobe/helix-shared-utils": "2.0.5",
36
+ "@adobe/helix-shared-utils": "2.0.6",
37
37
  "github-slugger": "1.4.0",
38
38
  "hast-util-raw": "7.2.1",
39
39
  "hast-util-select": "5.0.1",
@@ -9,12 +9,13 @@
9
9
  * OF ANY KIND; either express or implied. See the License for the specific language
10
10
  * governing permissions and limitations under the License.
11
11
  */
12
- import {PathInfo, S3Loader, PipelineTimer} from "./index";
12
+ import {PathInfo, S3Loader, FormsMessageDispatcher, PipelineTimer} from "./index";
13
13
  import {PipelineContent} from "./PipelineContent";
14
14
 
15
15
  declare interface PipelineOptions {
16
16
  log: Console;
17
17
  s3Loader: S3Loader;
18
+ messageDispatcher: FormsMessageDispatcher;
18
19
  owner: string;
19
20
  repo: string;
20
21
  ref: string;
@@ -31,6 +32,7 @@ declare class PipelineState {
31
32
  content: PipelineContent;
32
33
  contentBusId: string;
33
34
  s3Loader: S3Loader;
35
+ messageDispatcher: FormsMessageDispatcher;
34
36
 
35
37
  /**
36
38
  * Content bus partition
@@ -36,6 +36,7 @@ export class PipelineState {
36
36
  helixConfig: undefined,
37
37
  metadata: undefined,
38
38
  s3Loader: opts.s3Loader,
39
+ messageDispatcher: opts.messageDispatcher,
39
40
  timer: opts.timer,
40
41
  });
41
42
  }
@@ -0,0 +1,160 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import { cleanupHeaderValue } from '@adobe/helix-shared-utils';
13
+ import { PipelineResponse } from './PipelineResponse.js';
14
+ import fetchMetadata from './steps/fetch-metadata.js';
15
+ import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
16
+ import { getOriginalHost } from './steps/utils.js';
17
+
18
+ function error(log, msg, status, response) {
19
+ log.error(msg);
20
+ response.status = status;
21
+ response.headers.set('x-error', cleanupHeaderValue(msg));
22
+ return response;
23
+ }
24
+
25
+ /**
26
+ * Converts URLSearchParams to an object
27
+ * @param {URLSearchParams} searchParams the search params object
28
+ * @returns {Object} The converted object
29
+ */
30
+ function searchParamsToObject(searchParams) {
31
+ const result = {};
32
+
33
+ for (const key of searchParams.keys()) {
34
+ // get all values association with the key
35
+ const values = searchParams.getAll(key);
36
+
37
+ // if multiple values, convert to array
38
+ result[key] = (values.length === 1) ? values[0] : values;
39
+ }
40
+
41
+ return result;
42
+ }
43
+
44
+ /**
45
+ * Extracts and parses the body data from the request
46
+ * @param {PipelineRequest} request the request object (see fetch api)
47
+ * @returns {Object} The body data
48
+ * @throws {Error} If an error occurs parsing the body
49
+ */
50
+ export async function extractBodyData(request) {
51
+ let { body } = request;
52
+ if (!body) {
53
+ throw Error('missing body');
54
+ }
55
+ const type = request.headers.get('content-type');
56
+
57
+ // if content is form-urlencoded we need place the object in the body
58
+ // in a "data" property in the body as this is what forms-service expects.
59
+ if (/^application\/x-www-form-urlencoded/.test(type)) {
60
+ // did they pass an object in the body when a form-urlencoded body was expected?
61
+ if (body === '[object Object]') {
62
+ throw Error('invalid form-urlencoded body');
63
+ }
64
+
65
+ body = {
66
+ data: searchParamsToObject(new URLSearchParams(body)),
67
+ };
68
+
69
+ // else treat the body as json
70
+ } else if (/^application\/json/.test(type)) {
71
+ body = JSON.parse(body);
72
+ // verify the body data is as expected
73
+ if (!body.data) {
74
+ throw Error('missing body.data');
75
+ }
76
+ } else {
77
+ throw Error(`post body content-type not supported: ${type}`);
78
+ }
79
+ return body;
80
+ }
81
+
82
+ /**
83
+ * Handle a pipeline POST request.
84
+ * At this point POST's only apply to json files that are backed by a workbook.
85
+ * @param {PipelineState} state pipeline options
86
+ * @param {PipelineRequest} request
87
+ * @returns {Promise<PipelineResponse>} a response
88
+ */
89
+ export async function formsPipe(state, request) {
90
+ const { log } = state;
91
+
92
+ // todo: improve
93
+ const response = new PipelineResponse('', {
94
+ headers: {
95
+ 'content-type': 'text/plain; charset=utf-8',
96
+ },
97
+ });
98
+ await fetchMetadata(state, request, response);
99
+ await setCustomResponseHeaders(state, request, response);
100
+
101
+ const {
102
+ owner, repo, ref, contentBusId, partition, s3Loader,
103
+ } = state;
104
+ const { path } = state.info;
105
+ const resourcePath = `${path}.json`;
106
+
107
+ // block all POSTs to resources with extensions
108
+ if (state.info.originalExtension !== '') {
109
+ return error(log, 'POST to URL with extension not allowed', 405, response);
110
+ }
111
+
112
+ // head workbook in content bus
113
+ const resourceFetchResponse = await s3Loader.headObject('helix-content-bus', `${contentBusId}/${partition}${resourcePath}`);
114
+ if (resourceFetchResponse.status !== 200) {
115
+ return resourceFetchResponse;
116
+ }
117
+
118
+ let body;
119
+ try {
120
+ body = await extractBodyData(request);
121
+ } catch (err) {
122
+ return error(log, err.message, 400, response);
123
+ }
124
+
125
+ const sheets = resourceFetchResponse.headers.get('x-amz-meta-x-sheet-names');
126
+ if (!sheets) {
127
+ return error(log, `Target workbook at ${resourcePath} missing x-sheet-names header.`, 403, response);
128
+ }
129
+
130
+ const sourceLocation = resourceFetchResponse.headers.get('x-amz-meta-x-source-location');
131
+ const referer = request.headers.get('referer') || 'unknown';
132
+ const sheetNames = sheets.split(',');
133
+
134
+ if (!sourceLocation || !sheetNames.includes('incoming')) {
135
+ return error(log, `Target workbook at ${resourcePath} is not setup to intake data.`, 403, response);
136
+ }
137
+
138
+ // Send message to SQS if workbook contains and incoming
139
+ // sheet and the source location is not null
140
+ const host = getOriginalHost(request.headers);
141
+
142
+ const message = {
143
+ url: `https://${ref}--${repo}--${owner}.hlx.live${resourcePath}`,
144
+ body,
145
+ host,
146
+ sourceLocation,
147
+ referer,
148
+ };
149
+
150
+ try {
151
+ // Send message to forms queue
152
+ const { requestId, messageId } = await state.messageDispatcher.dispatch(message);
153
+ response.status = 201;
154
+ response.headers.set('x-request-id', requestId);
155
+ response.headers.set('x-message-id', messageId);
156
+ return response;
157
+ } catch (err) {
158
+ return error(log, `Failed to send message to forms queue: ${err}`, 500, response);
159
+ }
160
+ }
package/src/index.d.ts CHANGED
@@ -83,6 +83,19 @@ declare interface S3Loader {
83
83
  headObject(bucketId, key): Promise<PipelineResponse>;
84
84
  }
85
85
 
86
+ declare interface DispatchMessageResponse {
87
+ messageId:string,
88
+ requestId:string,
89
+ }
90
+
91
+ declare interface FormsMessageDispatcher {
92
+ /**
93
+ * Dispatches the message to the forms queue
94
+ * @param {object} message
95
+ */
96
+ dispatch(message:object): Promise<DispatchMessageResponse>;
97
+ }
98
+
86
99
  /**
87
100
  * Timer
88
101
  */
package/src/index.js CHANGED
@@ -11,6 +11,8 @@
11
11
  */
12
12
  export * from './html-pipe.js';
13
13
  export * from './json-pipe.js';
14
+ export * from './options-pipe.js';
15
+ export * from './forms-pipe.js';
14
16
  export * from './PipelineContent.js';
15
17
  export * from './PipelineRequest.js';
16
18
  export * from './PipelineResponse.js';
@@ -0,0 +1,37 @@
1
+ /*
2
+ * Copyright 2022 Adobe. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at http://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+ import { PipelineResponse } from './PipelineResponse.js';
13
+ import fetchMetadata from './steps/fetch-metadata.js';
14
+ import setCustomResponseHeaders from './steps/set-custom-response-headers.js';
15
+
16
+ /**
17
+ * Handles options requests
18
+ * @param {PipelineState} state pipeline options
19
+ * @param {PipelineRequest} request
20
+ * @returns {Response} a response
21
+ */
22
+ export async function optionsPipe(state, request) {
23
+ // todo: improve
24
+ const response = new PipelineResponse('', {
25
+ status: 204,
26
+ headers: {
27
+ // Set preflight cache duration
28
+ 'access-control-max-age': '86400',
29
+ // Allow content type header
30
+ 'access-control-allow-headers': 'content-type',
31
+ },
32
+ });
33
+ await fetchMetadata(state, request, response);
34
+ await setCustomResponseHeaders(state, request, response);
35
+
36
+ return response;
37
+ }
@@ -12,6 +12,8 @@
12
12
  import { toHtml } from 'hast-util-to-html';
13
13
  // import rehypeFormat from 'rehype-format';
14
14
  import rehypeMinifyWhitespace from 'rehype-minify-whitespace';
15
+ import { visit } from 'unist-util-visit';
16
+
15
17
  /**
16
18
  * Serializes the response document to HTML
17
19
  * @param {PipelineState} state
@@ -32,7 +34,25 @@ export default function stringify(state, req, res) {
32
34
  // TODO: for the next breaking release, pretty print the HTML with rehypeFormat.
33
35
  // TODO: but for backward compatibility, output all on 1 line.
34
36
  // rehypeFormat()(doc);
37
+
38
+ // due to a bug in rehype-minify-whitespace, script content is also minified to 1 line, which
39
+ // can result in errors https://github.com/rehypejs/rehype-minify/issues/44
40
+ // so we 'save' all text first and revert it afterwards
41
+ visit(doc, (node) => {
42
+ if (node.tagName === 'script' && node.children[0]?.type === 'text') {
43
+ node.children[0].savedValue = node.children[0].value;
44
+ }
45
+ });
46
+
35
47
  rehypeMinifyWhitespace()(doc);
48
+
49
+ visit(doc, (node) => {
50
+ if (node.tagName === 'script' && node.children[0]?.type === 'text') {
51
+ node.children[0].value = node.children[0].savedValue;
52
+ delete node.children[0].savedValue;
53
+ }
54
+ });
55
+
36
56
  res.body = toHtml(doc, {
37
57
  upperDoctype: true,
38
58
  });
package/src/utils/path.js CHANGED
@@ -20,7 +20,10 @@ export function getPathInfo(path) {
20
20
  // eslint-disable-next-line no-param-reassign
21
21
  path = '/';
22
22
  }
23
- const segs = path.split(/\/+/);
23
+ if (path.match(/\/\/+/)) {
24
+ return null;
25
+ }
26
+ const segs = path.split('/');
24
27
  segs.shift(); // remove _emptyness_ before first slash
25
28
  if (segs.length < 1) {
26
29
  return null;