@devvit/ui-renderer 0.10.10-next-2023-11-14-d6fe14bf4.0 → 0.10.10-next-2023-11-14-fabd613d1.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"renderImageBlock.d.ts","sourceRoot":"","sources":["../../../library/src/blocks/templates/renderImageBlock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAA6B,YAAY,EAAE,MAAM,WAAW,CAAC;AAKpE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CA8C/E"}
1
+ {"version":3,"file":"renderImageBlock.d.ts","sourceRoot":"","sources":["../../../library/src/blocks/templates/renderImageBlock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAyD,YAAY,EAAE,MAAM,WAAW,CAAC;AAKhG,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CAiD/E"}
@@ -1,7 +1,7 @@
1
1
  import { nothing } from 'lit';
2
2
  import { getTemplateRenderingStrategy } from '@reddit/faceplate-ui/faceplateUIConfig.js';
3
3
  import { defaultClasses, defaultStyles, onClickAction, resizeModeClass } from '../attributes.js';
4
- import { classMap, isValidImageURL } from './util.js';
4
+ import { classMap, isDataUrl, isValidImageURL, sanitizeDataUrl } from './util.js';
5
5
  import { VerifiedPublicImageHosts } from './constants.js';
6
6
  const FALLBACK_IMG_URL = 'https://i.redd.it/p1vmc5ulmpib1.png';
7
7
  export function renderImageBlock(block, ctx) {
@@ -27,7 +27,7 @@ export function renderImageBlock(block, ctx) {
27
27
  const onClick = onClickAction(block, ctx);
28
28
  return html `
29
29
  <img
30
- src="${imageUrl}"
30
+ src="${isDataUrl(imageUrl) ? sanitizeDataUrl(imageUrl) : imageUrl}"
31
31
  width="${width}"
32
32
  height="${height}"
33
33
  alt="${description}"
@@ -36,7 +36,10 @@ export function renderImageBlock(block, ctx) {
36
36
  @click="${ifDefined(onClick)}"
37
37
  data-debug-block-type="image"
38
38
  @error="${{
39
- handleEvent: (event) => (event.target.src = FALLBACK_IMG_URL),
39
+ handleEvent: (event) => {
40
+ console.error(`An error rendering the image with url:`, imageUrl);
41
+ event.target.src = FALLBACK_IMG_URL;
42
+ },
40
43
  once: true,
41
44
  }}"
42
45
  />
@@ -1,8 +1,8 @@
1
+ import { TemplateResult, nothing } from 'lit';
1
2
  import type { DirectiveClass, DirectiveResult } from 'lit/directive.js';
2
3
  import { ClassInfo, classMap as clientClassMap } from 'lit/directives/class-map.js';
3
- import { ref as clientRef, RefOrCallback } from 'lit/directives/ref.js';
4
+ import { RefOrCallback, ref as clientRef } from 'lit/directives/ref.js';
4
5
  import { StyleInfo } from 'lit/directives/style-map.js';
5
- import { nothing, TemplateResult } from 'lit';
6
6
  import type { UnsafeString } from '@reddit/baseplate/html.js';
7
7
  import { Block, BlockColor, BlockSizes, BlockSizes_Dimension_Value, BlockSizeUnit } from '@devvit/protos';
8
8
  export type TemplateLike = TemplateResult | typeof nothing;
@@ -16,6 +16,23 @@ export declare function resolveIcon(name: string): TemplateResult;
16
16
  * @param styleInfo
17
17
  */
18
18
  export declare function sanitizeStyleInfo(styleInfo: StyleInfo): StyleInfo;
19
+ export declare function isDataUrl(input: string): boolean;
20
+ export declare function parseDataUrl(dataUrl: string): {
21
+ mimeType: string | undefined;
22
+ charset: string | undefined;
23
+ isBase64: boolean;
24
+ data: string;
25
+ } | null;
26
+ export declare function isAcceptableDataUrl(maybeDataUrl: string): boolean;
27
+ /**
28
+ * Attempts to sanitize data URLs to prevent two things:
29
+ *
30
+ * 1. XSS attacks
31
+ * 2. Allowing images to be shown to users that bypass Reddit's safety checks
32
+ *
33
+ * NOTE: We only allow a mime type of image/svg+xml at this time.
34
+ */
35
+ export declare function sanitizeDataUrl(dataUrl: string): string;
19
36
  export declare function isValidImageURL(imageUrl: string): boolean;
20
37
  /**
21
38
  * Returns a BlockSizes object either directly from the block or constructs
@@ -1 +1 @@
1
- {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../library/src/blocks/templates/util.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,QAAQ,IAAI,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAEpF,OAAO,EAAE,GAAG,IAAI,SAAS,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,KAAK,CAAC;AAE9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAK9D,OAAO,EACL,KAAK,EACL,UAAU,EACV,UAAU,EACV,0BAA0B,EAC1B,aAAa,EACd,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG,OAAO,OAAO,CAAC;AAE3D,wBAAgB,QAAQ,CACtB,SAAS,EAAE,SAAS,EACpB,WAAW,CAAC,EAAE,OAAO,GACpB,MAAM,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAW5C;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,CAAC,cAAc,CAAC,GAAG,YAAY,CAMxF;AAED,wBAAgB,GAAG,CAAC,GAAG,EAAE,aAAa,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,GAAG,MAAM,CAM7E;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,EAAE,IAAI,WAAW,CAEtF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAiBxD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,CASjE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAWzD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,CA2B7D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,0BAA0B,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKhG;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,CAUpE;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,GAAG,SAAS,EAC9B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,GAC/B,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,CAIzD"}
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../../../library/src/blocks/templates/util.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,QAAQ,IAAI,cAAc,EAAE,MAAM,6BAA6B,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,GAAG,IAAI,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAGxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAK9D,OAAO,EACL,KAAK,EACL,UAAU,EACV,UAAU,EACV,0BAA0B,EAC1B,aAAa,EACd,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG,OAAO,OAAO,CAAC;AAE3D,wBAAgB,QAAQ,CACtB,SAAS,EAAE,SAAS,EACpB,WAAW,CAAC,EAAE,OAAO,GACpB,MAAM,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAW5C;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,eAAe,CAAC,cAAc,CAAC,GAAG,YAAY,CAMxF;AAED,wBAAgB,GAAG,CAAC,GAAG,EAAE,aAAa,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,GAAG,MAAM,CAM7E;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,GAAG,SAAS,GAAG,EAAE,IAAI,WAAW,CAEtF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAiBxD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,GAAG,SAAS,CASjE;AAcD,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG;IAC7C,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,IAAI,CAWP;AASD,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAMjE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAyIvD;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAazD;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,UAAU,GAAG,SAAS,CA2B7D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,0BAA0B,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKhG;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,CAUpE;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,GAAG,SAAS,EAC9B,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,GAC/B,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC,CAIzD"}
@@ -1,6 +1,7 @@
1
+ import DOMPurify from 'isomorphic-dompurify';
1
2
  import { classMap as clientClassMap } from 'lit/directives/class-map.js';
2
- import { unsafeHTML as clientUnsafeHTML } from 'lit/directives/unsafe-html.js';
3
3
  import { ref as clientRef } from 'lit/directives/ref.js';
4
+ import { unsafeHTML as clientUnsafeHTML } from 'lit/directives/unsafe-html.js';
4
5
  import { unsafeHTML as serverUnsafeHTML } from '@reddit/baseplate/html.js';
5
6
  import { getTemplateRenderingStrategy } from '@reddit/faceplate-ui/faceplateUIConfig.js';
6
7
  import faceplateIcons from '@reddit/faceplate-ui/svgs/svg-manifest.js';
@@ -67,8 +68,183 @@ export function sanitizeStyleInfo(styleInfo) {
67
68
  }
68
69
  return styleInfo;
69
70
  }
71
+ const DATA_URL_REGEX =
72
+ // eslint-disable-next-line security/detect-unsafe-regex
73
+ /^data:([a-zA-Z]+\/[a-zA-Z0-9.+_-]+)?(;charset=[a-zA-Z0-9._+-]+)?(;base64)?,(.*)/;
74
+ function preprocessDataUrl(input) {
75
+ /**
76
+ * This can be submitted to us as multi-line and that will break the parser
77
+ * so we remove all newlines here
78
+ */
79
+ return input.trim().replace(/[\r\n]+/g, '');
80
+ }
81
+ export function isDataUrl(input) {
82
+ return !!preprocessDataUrl(input).match(DATA_URL_REGEX);
83
+ }
84
+ export function parseDataUrl(dataUrl) {
85
+ const matches = DATA_URL_REGEX.exec(preprocessDataUrl(dataUrl));
86
+ if (!matches)
87
+ return null;
88
+ return {
89
+ mimeType: matches[1],
90
+ charset: matches[2]?.split('=')[1],
91
+ isBase64: !!matches[3],
92
+ data: matches[4].trim(),
93
+ };
94
+ }
95
+ function isAcceptableDataUrlMimeType(mimeType) {
96
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
97
+ const ACCEPTABLE_MIME_TYPES = ['image/svg+xml'];
98
+ return ACCEPTABLE_MIME_TYPES.includes(mimeType);
99
+ }
100
+ export function isAcceptableDataUrl(maybeDataUrl) {
101
+ const parsedDataUrl = parseDataUrl(maybeDataUrl);
102
+ if (!parsedDataUrl?.mimeType)
103
+ return false;
104
+ return isAcceptableDataUrlMimeType(parsedDataUrl.mimeType);
105
+ }
106
+ /**
107
+ * Attempts to sanitize data URLs to prevent two things:
108
+ *
109
+ * 1. XSS attacks
110
+ * 2. Allowing images to be shown to users that bypass Reddit's safety checks
111
+ *
112
+ * NOTE: We only allow a mime type of image/svg+xml at this time.
113
+ */
114
+ export function sanitizeDataUrl(dataUrl) {
115
+ const parsedDataUrl = parseDataUrl(dataUrl);
116
+ if (!parsedDataUrl || !parsedDataUrl.data || !parsedDataUrl.mimeType)
117
+ return '';
118
+ if (!isAcceptableDataUrlMimeType(parsedDataUrl.mimeType))
119
+ return '';
120
+ try {
121
+ let dataToSanitize;
122
+ if (parsedDataUrl.isBase64) {
123
+ dataToSanitize = decodeURIComponent(atob(parsedDataUrl.data));
124
+ }
125
+ else {
126
+ dataToSanitize = decodeURIComponent(parsedDataUrl.data);
127
+ }
128
+ const sanitizeCss = (cssText) => {
129
+ // Define a regex pattern that matches CSS properties that can include URLs
130
+ const urlPattern =
131
+ // eslint-disable-next-line security/detect-unsafe-regex
132
+ /(?:background(-image)?|border-image(-source)?)\s*:\s*(url\(['"]?(data:image\/(?:png|jpeg|gif);base64,[^)]+|https?:\/\/[^)]+)['"]?\))(?:;\s*)?/gi;
133
+ // Remove any matching URL patterns from the CSS text
134
+ return cssText.replace(urlPattern, '');
135
+ };
136
+ // This is for <style> tags
137
+ DOMPurify.addHook('uponSanitizeElement', (node) => {
138
+ if (node.tagName === 'style') {
139
+ if (node.textContent) {
140
+ node.textContent = sanitizeCss(node.textContent);
141
+ }
142
+ }
143
+ /**
144
+ * xmlns is optional when using a SVG inside of the DOM. However,
145
+ * since we are using it within a data url, it is required.
146
+ *
147
+ * https://stackoverflow.com/questions/18467982/are-svg-parameters-such-as-xmlns-and-version-needed
148
+ */
149
+ if (node.tagName === 'svg') {
150
+ if (!node.getAttribute('xmlns')) {
151
+ node.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
152
+ }
153
+ }
154
+ });
155
+ // This is for style attribute tags
156
+ DOMPurify.addHook('uponSanitizeAttribute', (_node, data) => {
157
+ if (data.attrName === 'style') {
158
+ data.attrValue = sanitizeCss(data.attrValue);
159
+ }
160
+ });
161
+ const sanitized = DOMPurify.sanitize(dataToSanitize, {
162
+ // Only allow svg and associated tags
163
+ // Note that these are some of the big offenders that are left out:
164
+ // 'script', 'image', 'foreignObject', 'object', 'use'
165
+ //
166
+ // To support more mimeTypes, remove the ALLOWED_TAGS array fully.
167
+ //
168
+ // src: https://github.com/cure53/DOMPurify/blob/2c66eb1f6ae39cc7001a97889a9bb78b688a6f99/src/tags.js#L124
169
+ ALLOWED_TAGS: [
170
+ 'svg',
171
+ 'altglyph',
172
+ 'altglyphdef',
173
+ 'altglyphitem',
174
+ 'animatecolor',
175
+ 'animatemotion',
176
+ 'animatetransform',
177
+ 'circle',
178
+ 'clippath',
179
+ 'defs',
180
+ 'desc',
181
+ 'ellipse',
182
+ 'filter',
183
+ 'font',
184
+ 'g',
185
+ 'glyph',
186
+ 'glyphref',
187
+ 'hkern',
188
+ 'line',
189
+ 'lineargradient',
190
+ 'marker',
191
+ 'mask',
192
+ 'metadata',
193
+ 'mpath',
194
+ 'path',
195
+ 'pattern',
196
+ 'polygon',
197
+ 'polyline',
198
+ 'radialgradient',
199
+ 'rect',
200
+ 'stop',
201
+ 'style',
202
+ 'switch',
203
+ 'symbol',
204
+ 'text',
205
+ 'textpath',
206
+ 'title',
207
+ 'tref',
208
+ 'tspan',
209
+ 'view',
210
+ 'vkern',
211
+ ],
212
+ // These are required for svg animate tags
213
+ ADD_ATTR: ['from', 'to', 'animate'],
214
+ FORBID_ATTR: ['image'],
215
+ });
216
+ // eslint-disable-next-line @reddit/i18n-shreddit/no-unwrapped-strings
217
+ let sanitizedDataUrl = `data:${parsedDataUrl.mimeType}`;
218
+ if (parsedDataUrl.charset) {
219
+ sanitizedDataUrl += `;charset=${parsedDataUrl.charset}`;
220
+ }
221
+ if (parsedDataUrl.isBase64) {
222
+ // eslint-disable-next-line @reddit/i18n-shreddit/no-unwrapped-strings
223
+ sanitizedDataUrl += `;base64`;
224
+ }
225
+ /**
226
+ * In data url land there are characters that need to be encoded that
227
+ * a user may have passed in like hex-code colors that will break
228
+ * the image without giving any relevant messages. To be safe, we
229
+ * encode the entire sanitized string.
230
+ *
231
+ * More info:
232
+ * https://stackoverflow.com/questions/69216560/why-hexadecimal-color-dont-work-with-utf8-data-url-s-for-svg
233
+ *
234
+ * You may be able to get away with just replacing # with %23 in case
235
+ * people complain about the data url being hard to read.
236
+ */
237
+ sanitizedDataUrl += `,${parsedDataUrl.isBase64 ? btoa(sanitized) : encodeURIComponent(sanitized)}`;
238
+ return sanitizedDataUrl;
239
+ }
240
+ catch (error) {
241
+ return '';
242
+ }
243
+ }
70
244
  export function isValidImageURL(imageUrl) {
71
245
  try {
246
+ if (isDataUrl(imageUrl))
247
+ return isAcceptableDataUrl(imageUrl);
72
248
  // The second "base" param helps to handle relative paths to local files
73
249
  // https://developer.mozilla.org/en-US/docs/Web/API/URL/URL#parameters
74
250
  const hostName = new URL(imageUrl, `https://i.${REDD_IT}`)?.hostname;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devvit/ui-renderer",
3
- "version": "0.10.10-next-2023-11-14-d6fe14bf4.0",
3
+ "version": "0.10.10-next-2023-11-14-fabd613d1.0",
4
4
  "license": "BSD-3-Clause",
5
5
  "repository": {
6
6
  "type": "git",
@@ -54,10 +54,11 @@
54
54
  },
55
55
  "types": "./index.d.ts",
56
56
  "dependencies": {
57
- "@devvit/protos": "0.10.10-next-2023-11-14-d6fe14bf4.0",
58
- "@devvit/runtime-lite": "0.10.10-next-2023-11-14-d6fe14bf4.0",
59
- "@devvit/runtimes": "0.10.10-next-2023-11-14-d6fe14bf4.0",
57
+ "@devvit/protos": "0.10.10-next-2023-11-14-fabd613d1.0",
58
+ "@devvit/runtime-lite": "0.10.10-next-2023-11-14-fabd613d1.0",
59
+ "@devvit/runtimes": "0.10.10-next-2023-11-14-fabd613d1.0",
60
60
  "@lottiefiles/lottie-player": "1.7.1",
61
+ "isomorphic-dompurify": "1.9.0",
61
62
  "p-queue": "7.3.4",
62
63
  "rxjs": "7.5.7"
63
64
  },
@@ -83,11 +84,12 @@
83
84
  "devDependencies": {
84
85
  "@devvit/eslint-config": "0.10.9",
85
86
  "@devvit/repo-tools": "0.10.9",
86
- "@devvit/tsconfig": "0.10.10-next-2023-11-14-d6fe14bf4.0",
87
+ "@devvit/tsconfig": "0.10.10-next-2023-11-14-fabd613d1.0",
87
88
  "@lit/localize": "0.11.4",
88
89
  "@open-wc/testing-helpers": "2.3.0",
89
90
  "@reddit/baseplate": "0.14.0",
90
91
  "@reddit/eslint-plugin-i18n-shreddit": "0.1.0",
92
+ "@types/dompurify": "3.0.5",
91
93
  "autoprefixer": "10.4.14",
92
94
  "backstopjs": "6.2.1",
93
95
  "chokidar": "3.5.3",
@@ -112,5 +114,5 @@
112
114
  "directory": "dist"
113
115
  },
114
116
  "source": "./src/index.ts",
115
- "gitHead": "bc36a2bf53e5626e925cf6733b7c83df67fbfa26"
117
+ "gitHead": "6f6cadcdc83bf711b273e7359ae4976bbaea29b5"
116
118
  }