@schukai/monster 4.137.1 → 4.137.2

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/README.md CHANGED
@@ -91,7 +91,7 @@ be closed immediately.
91
91
 
92
92
  ## License
93
93
 
94
- Copyright © 2024 Volker Schukai
94
+ Copyright © 2026 Volker Schukai
95
95
 
96
96
  Licensed under [AGPL](https://www.gnu.org/licenses/agpl-3.0.de.html). Commercial licenses are also available.
97
97
 
package/package.json CHANGED
@@ -1 +1 @@
1
- {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.137.1"}
1
+ {"author":"Volker Schukai","dependencies":{"@floating-ui/dom":"^1.7.6"},"description":"Monster is a simple library for creating fast, robust and lightweight websites.","homepage":"https://monsterjs.org/","keywords":["framework","web","dom","css","sass","mobile-first","app","front-end","templates","schukai","core","shopcloud","alvine","monster","buildmap","stack","observer","observable","uuid","node","nodelist","css-in-js","logger","log","theme"],"license":"AGPL 3.0","main":"source/monster.mjs","module":"source/monster.mjs","name":"@schukai/monster","repository":{"type":"git","url":"https://gitlab.schukai.com/oss/libraries/javascript/monster.git"},"type":"module","version":"4.137.2"}
@@ -15,10 +15,8 @@
15
15
  import { internalStateSymbol } from "../../constants.mjs";
16
16
  import { extend } from "../../data/extend.mjs";
17
17
  import { getGlobalFunction } from "../../types/global.mjs";
18
- import { addAttributeToken } from "../attributes.mjs";
19
18
  import {
20
19
  ATTRIBUTE_CLASS,
21
- ATTRIBUTE_ERRORMESSAGE,
22
20
  ATTRIBUTE_ID,
23
21
  ATTRIBUTE_SRC,
24
22
  ATTRIBUTE_TITLE,
@@ -35,6 +33,9 @@ import { instanceSymbol } from "../../constants.mjs";
35
33
 
36
34
  export { Data };
37
35
 
36
+ const KEY_ALLOWED_ORIGINS = "allowedOrigins";
37
+ const JSON_SCRIPT_TYPE_PATTERN = /^(application\/json|text\/json|application\/[a-z0-9.+-]+\+json)$/i;
38
+
38
39
  /**
39
40
  * This class is used by the resource manager to embed data.
40
41
  *
@@ -54,6 +55,7 @@ class Data extends Resource {
54
55
  mode: "cors",
55
56
  credentials: "same-origin",
56
57
  type: "application/json",
58
+ [KEY_ALLOWED_ORIGINS]: ["self"],
57
59
  });
58
60
  }
59
61
 
@@ -73,7 +75,6 @@ class Data extends Resource {
73
75
  * @return {Monster.DOM.Resource}
74
76
  */
75
77
  connect() {
76
- const self = this;
77
78
  if (!(this[referenceSymbol] instanceof HTMLElement)) {
78
79
  this.create();
79
80
  }
@@ -105,6 +106,12 @@ class Data extends Resource {
105
106
  */
106
107
  function createElement() {
107
108
  const document = this.getOption(KEY_DOCUMENT);
109
+ const type = this.getOption(ATTRIBUTE_TYPE);
110
+
111
+ if (!isInertJSONType(type)) {
112
+ throw new Error("unsupported data resource type");
113
+ }
114
+
108
115
  this[referenceSymbol] = document.createElement(TAG_SCRIPT);
109
116
 
110
117
  for (const key of [
@@ -127,6 +134,8 @@ function createElement() {
127
134
  * throws {Error} target not found
128
135
  */
129
136
  function appendToDocument() {
137
+ const document = this.getOption(KEY_DOCUMENT);
138
+ const url = getValidatedURL.call(this, document);
130
139
  const targetNode = document.querySelector(this.getOption(KEY_QUERY, "head"));
131
140
  if (!(targetNode instanceof HTMLElement)) {
132
141
  throw new Error("target not found");
@@ -134,7 +143,7 @@ function appendToDocument() {
134
143
 
135
144
  targetNode.appendChild(this[referenceSymbol]);
136
145
 
137
- getGlobalFunction("fetch")(this.getOption(ATTRIBUTE_SRC), {
146
+ getGlobalFunction("fetch")(url.toString(), {
138
147
  method: "GET", // *GET, POST, PUT, DELETE, etc.
139
148
  mode: this.getOption("mode", "cors"), // no-cors, *cors, same-origin
140
149
  cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
@@ -159,9 +168,48 @@ function appendToDocument() {
159
168
  loaded: true,
160
169
  error: e.toString(),
161
170
  });
162
-
163
- targetNode.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString());
164
171
  });
165
172
 
166
173
  return this;
167
174
  }
175
+
176
+ /**
177
+ * @private
178
+ * @param {string} type
179
+ * @return {boolean}
180
+ */
181
+ function isInertJSONType(type) {
182
+ if (type === undefined) {
183
+ return false;
184
+ }
185
+
186
+ return JSON_SCRIPT_TYPE_PATTERN.test(String(type).split(";")[0].trim());
187
+ }
188
+
189
+ /**
190
+ * @private
191
+ * @param {Document} document
192
+ * @return {URL}
193
+ */
194
+ function getValidatedURL(document) {
195
+ const url = new URL(this.getOption(ATTRIBUTE_SRC), document.baseURI);
196
+
197
+ if (!["http:", "https:"].includes(url.protocol)) {
198
+ throw new Error("unsupported data resource protocol");
199
+ }
200
+
201
+ const allowedOriginsOption = this.getOption(KEY_ALLOWED_ORIGINS, ["self"]);
202
+ const allowedOrigins = Array.isArray(allowedOriginsOption)
203
+ ? allowedOriginsOption
204
+ : [allowedOriginsOption];
205
+ const documentOrigin = new URL(document.baseURI).origin;
206
+ const allowed = allowedOrigins.map((origin) =>
207
+ origin === "self" ? documentOrigin : new URL(origin, document.baseURI).origin,
208
+ );
209
+
210
+ if (!allowed.includes(url.origin)) {
211
+ throw new Error("data resource origin not allowed");
212
+ }
213
+
214
+ return url;
215
+ }
@@ -63,7 +63,7 @@ describe('Data', function () {
63
63
  it('setEventTypes()', function (done) {
64
64
 
65
65
  const data = new Data({
66
- src: new DataUrl('', 'text/javascript').toString()
66
+ src: '/data.json'
67
67
  });
68
68
 
69
69
  data.connect().available().then(() => {
@@ -71,12 +71,77 @@ describe('Data', function () {
71
71
  }).catch(e => done(e));
72
72
 
73
73
  })
74
+
75
+ it('rejects executable script types', function () {
76
+
77
+ const data = new Data({
78
+ src: new DataUrl('console.log(1);', 'text/javascript').toString(),
79
+ type: 'text/javascript'
80
+ });
81
+
82
+ expect(() => data.connect()).to.throw('unsupported data resource type');
83
+ expect(document.querySelector('script')).not.to.exist;
84
+
85
+ })
86
+
87
+ it('rejects remote origins by default', function () {
88
+
89
+ const data = new Data({
90
+ src: 'https://cdn.example/data.json'
91
+ });
92
+
93
+ expect(() => data.connect()).to.throw('data resource origin not allowed');
94
+ expect(document.querySelector('script')).not.to.exist;
95
+
96
+ })
97
+
98
+ it('allows configured remote origins', function (done) {
99
+
100
+ const data = new Data({
101
+ src: 'https://cdn.example/data.json',
102
+ allowedOrigins: ['https://cdn.example']
103
+ });
104
+
105
+ data.connect().available().then(() => {
106
+ expect(data.isConnected()).to.be.true;
107
+
108
+ done();
109
+ }).catch(e => done(e));
110
+
111
+ })
112
+
113
+ it('does not expose fetch errors in DOM attributes', function (done) {
114
+
115
+ globalThis['fetch'] = function () {
116
+ return Promise.reject(new Error('token=secret'));
117
+ };
118
+
119
+ const data = new Data({
120
+ src: '/data.json',
121
+ id: 'data-error'
122
+ });
123
+
124
+ data.connect();
125
+
126
+ setTimeout(() => {
127
+ try {
128
+ const script = document.getElementById('data-error');
129
+
130
+ expect(script.hasAttribute('data-monster-error')).to.be.false;
131
+
132
+ done();
133
+ } catch (e) {
134
+ done(e);
135
+ }
136
+ }, 0);
137
+
138
+ })
74
139
  });
75
140
 
76
141
  describe('External Data', () => {
77
142
 
78
143
  let id = new ID('data').toString();
79
- let server, data, url = 'https://cdnjs.cloudflare.com/ajax/libs/layzr.js/2.2.2/layzr.min.js';
144
+ let server, data, url = '/layzr.json';
80
145
 
81
146
  beforeEach(() => {
82
147
 
@@ -126,4 +191,4 @@ describe('Data', function () {
126
191
  });
127
192
 
128
193
 
129
- });
194
+ });
@@ -104,7 +104,7 @@ describe('ResourceManager', function () {
104
104
 
105
105
  describe('check availability example.json', function () {
106
106
  it('add data and check content', function (done) {
107
- manager.addData('https://example.com/example.json').connect().available().then(r => {
107
+ manager.addData('/example.json').connect().available().then(r => {
108
108
  expect(document.querySelector('html').outerHTML).contains('>{"a":"test"}</script></head>');
109
109
  done();
110
110
  }).catch(e => done(e));
@@ -115,4 +115,4 @@ describe('ResourceManager', function () {
115
115
  });
116
116
 
117
117
 
118
- });
118
+ });