trix_embed 0.0.2 → 0.0.3

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,180 +1,289 @@
1
+ import { sample, shuffle } from './enumerable'
1
2
  import { generateKey, encryptValues, decryptValues } from './encryption'
2
- import { extractURLsFromElement, validateURL } from './urls'
3
- import { getMediaType, mediaTags } from './media'
3
+ import {
4
+ createURLObject,
5
+ createURLTextNodeTreeWalker,
6
+ extractURLs,
7
+ extractURLFromElement,
8
+ validateURL
9
+ } from './urls'
10
+ import { getMediaType, mediaTags, trixAttachmentTagName, trixEmbedMediaTypes } from './media'
4
11
  import Guard from './guard'
5
12
  import Store from './store'
6
13
  import Renderer from './renderer'
7
14
 
8
- const defaultOptions = {
9
- Controller: null,
10
- Trix: null
11
- }
12
-
13
- export function getTrixEmbedControllerClass(options = defaultOptions) {
15
+ export function getTrixEmbedControllerClass(options = { Controller: null, Trix: null }) {
14
16
  const { Controller, Trix } = options
15
17
  return class extends Controller {
16
18
  static values = {
17
19
  // templates
18
- validTemplate: String, // dom id of template to use for valid embeds
19
- errorTemplate: String, // dom id of template to use for invalid embeds
20
- headerTemplate: String, // dom id of template to use for embed headers
21
- iframeTemplate: String, // dom id of template to use for iframe embeds
22
- imageTemplate: String, // dom id of template to use for image embeds
20
+ embeddedTemplate: String, // dom id of template to use for EMBEDDED MEDIA info
21
+ errorTemplate: String, // dom id of template to use for UNEXPECTED ERRORS
22
+ iframeTemplate: String, // dom id of template to use for IFRAME EMBEDS
23
+ imageTemplate: String, // dom id of template to use for IMAGE EMBEDS
24
+ linkTemplate: String, // dom id of template to use for ALLOWED LINKS
25
+ prohibitedTemplate: String, // dom id of template to use for PROHIBITED URLS
26
+ warningTemplate: String, // dom id of template to use when invalid embeds are detected
23
27
 
24
28
  // security related values
25
- hosts: Array, // list of hosts/domains that embeds are allowed from
29
+ allowedLinkHosts: Array, // list of hosts/domains that links are allowed from
30
+ blockedLinkHosts: Array, // list of hosts/domains that links are NOT allowed from
31
+ allowedMediaHosts: Array, // list of hosts/domains that media is allowed from
32
+ blockedMediaHosts: Array, // list of hosts/domains that media is NOT allowed from
26
33
  paranoid: { type: Boolean, default: true } // guard against attacks
27
34
  }
28
35
 
29
- async connect() {
36
+ connect() {
37
+ this.onPaste = this.paste.bind(this)
38
+ this.element.addEventListener('trix-paste', this.onPaste, true)
39
+
40
+ // forget config when navigating away
41
+ this.onBeforeFetchResponse = this.beforeFetchResponse.bind(this)
42
+ addEventListener('turbo:before-fetch-response', this.onBeforeFetchResponse, true)
43
+
44
+ // forget config when navigating away
45
+ this.onBeforeUnload = this.forgetConfig.bind(this)
46
+ addEventListener('beforeunload', this.onBeforeUnload, true)
47
+
30
48
  this.store = new Store(this)
31
49
  this.guard = new Guard(this)
32
- await this.rememberConfig()
33
- if (this.paranoid) this.guard.protect()
34
- this.toolbarElement.querySelector('[data-trix-button-group="file-tools"]')?.remove()
35
- window.addEventListener('beforeunload', () => this.disconnect()) // TODO: this may not be necessary
50
+
51
+ if (this.key) return // already configured
52
+
53
+ this.rememberConfig().then(() => {
54
+ if (this.paranoid) this.guard.protect()
55
+ })
56
+ }
57
+
58
+ reconnect() {
59
+ const value = this.element.getAttribute('data-controller') || ''
60
+ const values = new Set(value.split(' '))
61
+ values.add('trix-embed')
62
+ this.element.setAttribute('data-controller', [...values].join(' ').trim())
36
63
  }
37
64
 
38
65
  disconnect() {
39
- if (this.paranoid) this.guard.cleanup()
40
- this.forgetConfig()
66
+ this.element.removeEventListener('trix-paste', this.onPaste, true)
67
+ removeEventListener('turbo:before-fetch-response', this.onBeforeFetchResponse, true)
68
+ removeEventListener('beforeunload', this.onBeforeUnload, true)
69
+ this.reconnect() // can't get rid of this controller after it's been connected
70
+ }
71
+
72
+ beforeFetchResponse(event) {
73
+ try {
74
+ const editors = event.target.querySelectorAll('trix-editor')
75
+ if (editors.includes(this.element)) this.forgetConfig()
76
+ } catch {}
41
77
  }
42
78
 
43
79
  async paste(event) {
44
- const { html, string, range } = event.paste
45
- let content = html || string || ''
46
- const pastedTemplate = this.buildPastedTemplate(content)
47
- const pastedElement = pastedTemplate.content.firstElementChild
48
- const sanitizedPastedElement = this.sanitizePastedElement(pastedElement)
49
- const sanitizedPastedContent = sanitizedPastedElement.innerHTML.trim()
50
- const pastedURLs = extractURLsFromElement(pastedElement)
51
-
52
- // no URLs were pasted, let Trix handle it ...............................................................
53
- if (!pastedURLs.length) return
54
-
55
- event.preventDefault()
56
- this.editor.setSelectedRange(range)
57
- const hosts = await this.hosts
58
- const renderer = new Renderer(this)
80
+ if (this.formElement) this.formElement.trixEmbedPasting = true
59
81
 
60
82
  try {
61
- // Media URLs (images, videos, audio etc.)
62
- const mediaURLs = pastedURLs.filter(url => getMediaType(url))
63
- Array.from(pastedTemplate.content.firstElementChild.querySelectorAll('iframe')).forEach(frame => {
64
- if (!mediaURLs.includes(frame.src)) mediaURLs.push(frame.src)
65
- })
66
- const validMediaURLs = mediaURLs.filter(url => validateURL(url, hosts))
67
- const invalidMediaURLs = mediaURLs.filter(url => !validMediaURLs.includes(url))
68
-
69
- // Standard URLs (non-media resources i.e. web pages etc.)
70
- const standardURLs = pastedURLs.filter(url => !mediaURLs.includes(url))
71
- const validStandardURLs = standardURLs.filter(url => validateURL(url, hosts))
72
- const invalidStandardURLs = standardURLs.filter(url => !validStandardURLs.includes(url))
83
+ const { html, string, range } = event.paste
84
+ let content = html || string || ''
85
+ const pastedElement = this.createTemplateElement(content)
86
+ const pastedURLs = extractURLs(pastedElement)
87
+
88
+ // no URLs were pasted, let Trix handle it .............................................................
89
+ if (!pastedURLs.length) return
90
+
91
+ event.preventDefault()
92
+ this.editor.setSelectedRange(range)
93
+
94
+ try {
95
+ const renderer = new Renderer(this)
96
+
97
+ // Media URLs (images, videos, audio etc.)
98
+ const allowedMediaHosts = await this.allowedMediaHosts
99
+ const blockedMediaHosts = await this.blockedMediaHosts
100
+ let mediaURLs = new Set(pastedURLs.filter(url => getMediaType(url)))
101
+ const iframes = [...pastedElement.querySelectorAll('iframe')]
102
+ iframes.forEach(frame => mediaURLs.add(frame.src))
103
+ mediaURLs = [...mediaURLs]
104
+ const validMediaURLs = mediaURLs.filter(url =>
105
+ validateURL(url, allowedMediaHosts, blockedMediaHosts)
106
+ )
107
+ const invalidMediaURLs = mediaURLs.filter(url => !validMediaURLs.includes(url))
108
+
109
+ // Link URLs (non-media resources i.e. web pages etc.)
110
+ const allowedLinkHosts = await this.allowedLinkHosts
111
+ const blockedLinkHosts = await this.blockedLinkHosts
112
+ const linkURLs = pastedURLs.filter(url => !mediaURLs.includes(url))
113
+ const validLinkURLs = linkURLs.filter(url => validateURL(url, allowedLinkHosts, blockedLinkHosts))
114
+ const invalidLinkURLs = linkURLs.filter(url => !validLinkURLs.includes(url))
115
+
116
+ // 1. render warnings ................................................................................
117
+ if (invalidMediaURLs.length || invalidLinkURLs.length) {
118
+ const invalidURLs = [...new Set([...invalidMediaURLs, ...invalidLinkURLs])]
119
+ const allowedHosts = [...new Set([...allowedMediaHosts, ...allowedLinkHosts])].filter(
120
+ host => !this.reservedDomains.includes(host)
121
+ )
122
+ const blockedHosts = [...new Set([...blockedMediaHosts, ...blockedLinkHosts])].filter(
123
+ host => !this.reservedDomains.includes(host)
124
+ )
125
+ await this.insert(renderer.renderWarnings(invalidURLs, allowedHosts, blockedHosts))
126
+ }
127
+
128
+ // 2. render valid media urls (i.e. embeds) ..........................................................
129
+ if (validMediaURLs.length) await this.insert(renderer.renderEmbeds(validMediaURLs))
130
+
131
+ // 3. exit early if there is only 1 URL and it's a valid media URL (i.e. a single embed) .............
132
+ if (pastedURLs.length === 1 && validMediaURLs.length === 1) return
133
+
134
+ // 4. render the pasted content as HTML .............................................................
135
+ const sanitizedPastedElement = this.sanitizePastedElement(pastedElement, {
136
+ renderer,
137
+ validMediaURLs,
138
+ validLinkURLs
139
+ })
140
+ const sanitizedPastedContent = sanitizedPastedElement.innerHTML.trim()
141
+ if (sanitizedPastedContent.length)
142
+ await this.insert(sanitizedPastedContent, { disposition: 'inline' })
143
+ } catch (e) {
144
+ this.insert(renderer.renderError(e))
145
+ }
146
+ } finally {
147
+ if (this.formElement) delete this.formElement.trixEmbedPasting
148
+ }
149
+ }
73
150
 
74
- let urls
151
+ createTemplateElement(content) {
152
+ const template = document.createElement('template')
153
+ template.innerHTML = `<div>${content.trim()}</div>`
154
+ return template.content.firstElementChild
155
+ }
75
156
 
76
- // 1. render invalid media urls ..........................................................................
77
- urls = invalidMediaURLs
78
- if (urls.length) await this.insert(renderer.renderErrors(urls, hosts.sort()))
157
+ extractLabelFromElement(el, options = { default: null }) {
158
+ let value = el.title
159
+ if (value && value.length) return value
79
160
 
80
- // 2. render invalid standard urls .......................................................................
81
- urls = invalidStandardURLs
82
- if (urls.length) {
83
- await this.insert(renderer.renderHeader('Pasted URLs'))
84
- await this.insert(renderer.renderLinks(urls), { disposition: 'inline' })
85
- }
161
+ value = el.textContent.trim()
162
+ if (value && value.length) return value
86
163
 
87
- // 3. render valid media urls ............................................................................
88
- urls = validMediaURLs
89
- if (urls.length) {
90
- if (urls.length > 1) await this.insert(renderer.renderHeader('Embedded Media'))
91
- await this.insert(renderer.renderEmbeds(urls))
92
- }
164
+ return options.default
165
+ }
93
166
 
94
- // 4. render valid standard urls .........................................................................
95
- urls = validStandardURLs
96
- if (urls.length) await this.insert(renderer.renderEmbeds(validStandardURLs))
167
+ sanitizePastedElement(element, options = { renderer: null, validMediaURLs: [], validLinkURLs: [] }) {
168
+ const { renderer, validMediaURLs, validLinkURLs } = options
97
169
 
98
- // exit early if there is only one valid URL and it is the same as the pasted content
99
- if (validMediaURLs[0] === sanitizedPastedContent || validStandardURLs[0] === sanitizedPastedContent)
100
- return this.editor.insertLineBreak()
170
+ element = element.cloneNode(true)
101
171
 
102
- // 5. render the pasted content as sanitized HTML ........................................................
103
- if (sanitizedPastedContent.length) {
104
- await this.insert(renderer.renderHeader('Pasted Content', sanitizedPastedContent))
105
- this.editor.insertLineBreak()
106
- this.insert(sanitizedPastedContent, { disposition: 'inline' })
107
- }
108
- } catch (ex) {
109
- this.insert(renderer.renderException(ex))
172
+ // sanitize text nodes
173
+ const walker = createURLTextNodeTreeWalker(element)
174
+ const textNodes = []
175
+ let textNode
176
+ while ((textNode = walker.nextNode())) {
177
+ textNode.replacements = textNode.replacements || new Set()
178
+ textNodes.push(textNode)
179
+
180
+ const words = textNode.nodeValue.split(/\s+/)
181
+ const matches = words.filter(word => word.startsWith('http'))
182
+
183
+ matches.forEach(match => {
184
+ const url = createURLObject(match)?.href
185
+ const replacement =
186
+ validLinkURLs.includes(url) || validLinkURLs.includes(url)
187
+ ? renderer.render('link', { url, label: url })
188
+ : renderer.render('prohibited', { url, label: 'Prohibited URL:', description: '' })
189
+ textNode.replacements.add({ match, replacement })
190
+ })
110
191
  }
111
- }
192
+ textNodes.forEach(node => {
193
+ if (!node.replacements.size) return
194
+ let content = node.nodeValue
195
+ // sort by length to replace the most specific matches first
196
+ const replacements = [...node.replacements].sort((a, b) => b.match.length - a.match.length)
197
+ replacements.forEach(entry => (content = content.replaceAll(entry.match, entry.replacement)))
198
+ node.replaceWith(this.createTemplateElement(content))
199
+ })
112
200
 
113
- buildPastedTemplate(content) {
114
- const template = document.createElement('template')
115
- template.innerHTML = `<div>${content.trim()}</div>`
116
- return template
117
- }
201
+ // sanitize anchor tags
202
+ element.querySelectorAll('a').forEach(el => {
203
+ const url = extractURLFromElement(el)
204
+ const label = this.extractLabelFromElement(el, { default: url })
205
+ const replacement = validLinkURLs.includes(url)
206
+ ? renderer.render('link', { url, label })
207
+ : renderer.render('prohibited', { url, label: 'Prohibited Link:', description: `(${label})` })
208
+ el.replaceWith(this.createTemplateElement(replacement))
209
+ })
118
210
 
119
- sanitizePastedElement(element) {
120
- element = element.cloneNode(true)
121
- element.querySelectorAll(mediaTags.join(', ')).forEach(tag => tag.remove())
211
+ // sanitize media tags
212
+ element.querySelectorAll(mediaTags.join(', ')).forEach(el => {
213
+ const url = extractURLFromElement(el)
214
+ const label = this.extractLabelFromElement(el, { default: url })
122
215
 
123
- const tags = element.querySelectorAll('*')
124
- const newlines = element.innerHTML.match(/\r\n|\n|\r/g) || []
216
+ const replacement = validMediaURLs.includes(url)
217
+ ? renderer.render('embedded', { url, label: 'Allowed Media:', description: '(Embedded Above)' })
218
+ : renderer.render('prohibited', { url, label: 'Prohibited Media:', description: '' })
125
219
 
126
- // replace newlines with <br> if there are <= 10% tags to newlines
127
- if ((newlines.length ? tags.length / newlines.length : 0) <= 0.1)
128
- element.innerHTML = element.innerHTML.replaceAll(/\r\n|\n|\r/g, '<br>')
220
+ el.replaceWith(this.createTemplateElement(replacement))
221
+ })
222
+
223
+ // sanitize newlines (best effort)
224
+ element.innerHTML.replaceAll(/(\n|\r|\f|\v)+/g, '<br>')
129
225
 
130
226
  return element
131
227
  }
132
228
 
133
- insertAttachment(content, options = { delay: 0 }) {
229
+ createAttachment(content) {
230
+ return new Trix.Attachment({ content, contentType: trixEmbedMediaTypes.attachment })
231
+ }
232
+
233
+ insertNewlines(count = 1, options = { delay: 1 }) {
134
234
  const { delay } = options
135
235
  return new Promise(resolve => {
136
236
  setTimeout(() => {
137
- const attachment = new Trix.Attachment({ content, contentType: 'application/vnd.trix-embed' })
138
- this.editor.insertAttachment(attachment)
237
+ for (let i = 0; i < count; i++) this.editor.insertLineBreak()
139
238
  resolve()
140
239
  }, delay)
141
240
  })
142
241
  }
143
242
 
144
- insertHTML(content, options = { delay: 0 }) {
243
+ insertAttachment(content, options = { delay: 1 }) {
244
+ const { delay } = options
245
+ return new Promise(resolve => {
246
+ setTimeout(() => {
247
+ this.editor.insertAttachment(this.createAttachment(content))
248
+ this.insertNewlines(1, { delay: delay }).finally(resolve)
249
+ }, delay)
250
+ })
251
+ }
252
+
253
+ insertHTML(content, options = { delay: 1 }) {
145
254
  const { delay } = options
146
255
  return new Promise(resolve => {
147
256
  setTimeout(() => {
148
257
  this.editor.insertHTML(content)
149
- // shenanigans to ensure that Trix considers this block of content closed
150
- this.editor.moveCursorInDirection('forward')
151
- this.editor.insertLineBreak()
152
- this.editor.moveCursorInDirection('backward')
153
- resolve()
258
+ this.insertNewlines(1, { delay }).finally(resolve)
154
259
  }, delay)
155
260
  })
156
261
  }
157
262
 
158
- insert(content, options = { delay: 0, disposition: 'attachment' }) {
159
- const { delay, disposition } = options
263
+ insert(content, options = { delay: 1, disposition: 'attachment' }) {
264
+ let { delay, disposition } = options
160
265
 
161
266
  if (content?.length) {
162
267
  return new Promise(resolve => {
163
268
  setTimeout(() => {
164
269
  if (typeof content === 'string') {
165
- if (disposition === 'inline') return this.insertHTML(content, { delay }).then(resolve)
166
- else return this.insertAttachment(content, { delay }).then(resolve)
270
+ return disposition === 'inline'
271
+ ? this.insertHTML(content, { delay })
272
+ .catch(e => this.renderError(e))
273
+ .finally(resolve)
274
+ : this.insertAttachment(content, { delay })
275
+ .catch(e => this.renderError(e))
276
+ .finally(resolve)
167
277
  }
168
278
 
169
279
  if (Array.isArray(content)) {
170
- if (disposition === 'inline')
171
- return content
172
- .reduce((p, c, i) => p.then(this.insertHTML(c, { delay })), Promise.resolve())
173
- .then(resolve)
174
- else
175
- return content
176
- .reduce((p, c, i) => p.then(this.insertAttachment(c, { delay })), Promise.resolve())
177
- .then(resolve)
280
+ const promises =
281
+ disposition === 'inline'
282
+ ? content.map(c => this.insertHTML(c, { delay: delay + 1 }))
283
+ : content.map(c => this.insertAttachment(c, { delay: delay + 1 }))
284
+ return Promise.all(promises)
285
+ .catch(e => this.renderError(e))
286
+ .finally(resolve)
178
287
  }
179
288
 
180
289
  resolve()
@@ -185,27 +294,31 @@ export function getTrixEmbedControllerClass(options = defaultOptions) {
185
294
  return Promise.resolve()
186
295
  }
187
296
 
188
- // Returns the Trix editor
189
- //
190
- // @returns {TrixEditor}
191
- //
192
297
  get editor() {
193
298
  return this.element.editor
194
299
  }
195
300
 
196
301
  get toolbarElement() {
197
- const sibling = this.element.previousElementSibling
198
- return sibling?.tagName.match(/trix-toolbar/i) ? sibling : null
199
- }
302
+ const id = this.element.getAttribute('toolbar')
303
+ let toolbar = id ? document.getElementById(id) : null
200
304
 
201
- get inputElement() {
202
- return document.getElementById(this.element.getAttribute('input'))
305
+ if (!toolbar) {
306
+ const sibling = this.element.previousElementSibling
307
+ toolbar = sibling?.tagName.match(/trix-toolbar/i) ? sibling : null
308
+ }
309
+
310
+ return toolbar
203
311
  }
204
312
 
205
313
  get formElement() {
206
314
  return this.element.closest('form')
207
315
  }
208
316
 
317
+ get inputElement() {
318
+ const id = this.element.getAttribute('input')
319
+ return id ? this.formElement?.querySelector(`#${id}`) : null
320
+ }
321
+
209
322
  get paranoid() {
210
323
  return !!this.store.read('paranoid')
211
324
  }
@@ -213,42 +326,126 @@ export function getTrixEmbedControllerClass(options = defaultOptions) {
213
326
  get key() {
214
327
  try {
215
328
  return JSON.parse(this.store.read('key'))[2]
216
- } catch {}
217
- }
218
-
219
- get hosts() {
220
- try {
221
- return decryptValues(this.key, JSON.parse(this.store.read('hosts')))
222
329
  } catch {
223
- return []
330
+ return null
224
331
  }
225
332
  }
226
333
 
334
+ get hostsValueDescriptors() {
335
+ return Object.values(this.valueDescriptorMap).filter(descriptor =>
336
+ descriptor.name.endsWith('HostsValue')
337
+ )
338
+ }
339
+
227
340
  get reservedDomains() {
228
- return ['example.com', 'test.com', 'invalid.com', 'example.cat', 'nic.example', 'example.co.uk']
341
+ return [
342
+ 'embed.example',
343
+ 'embed.invalid',
344
+ 'embed.local',
345
+ 'embed.localhost',
346
+ 'embed.test',
347
+ 'trix.embed.example',
348
+ 'trix.embed.invalid',
349
+ 'trix.embed.local',
350
+ 'trix.embed.localhost',
351
+ 'trix.embed.test',
352
+ 'trix.example',
353
+ 'trix.invalid',
354
+ 'trix.local',
355
+ 'trix.localhost',
356
+ 'trix.test',
357
+ 'www.embed.example',
358
+ 'www.embed.invalid',
359
+ 'www.embed.local',
360
+ 'www.embed.localhost',
361
+ 'www.embed.test',
362
+ 'www.trix.example',
363
+ 'www.trix.invalid',
364
+ 'www.trix.local',
365
+ 'www.trix.localhost',
366
+ 'www.trix.test'
367
+ ]
229
368
  }
230
369
 
231
- async rememberConfig() {
232
- const key = await generateKey()
233
- const fakes = await encryptValues(key, this.reservedDomains)
234
- const hosts = await encryptValues(key, this.hostsValue)
370
+ rememberConfig() {
371
+ return new Promise(async resolve => {
372
+ let fakes
235
373
 
236
- this.store.write('key', JSON.stringify([fakes[0], fakes[1], key, fakes[2]]))
237
- this.element.removeAttribute('data-trix-embed-key-value')
374
+ // encryption key
375
+ const key = await generateKey()
376
+ fakes = await encryptValues(key, sample(this.reservedDomains, 3))
377
+ this.store.write('key', JSON.stringify([fakes[0], fakes[1], key, fakes[2]]))
238
378
 
239
- this.store.write('hosts', JSON.stringify(hosts))
240
- this.element.removeAttribute('data-trix-embed-hosts-value')
379
+ // paranoid
380
+ if (this.paranoidValue !== false) {
381
+ fakes = await encryptValues(key, sample(this.reservedDomains, 4))
382
+ this.store.write('paranoid', JSON.stringify(fakes))
383
+ }
384
+ this.element.removeAttribute('data-trix-embed-paranoid-value')
385
+
386
+ // host lists
387
+ //
388
+ // - allowedLinkHosts
389
+ // - blockedLinkHosts
390
+ // - allowedMediaHosts
391
+ // - blockedMediaHosts
392
+ // - etc.
393
+ //
394
+ this.hostsValueDescriptors.forEach(async descriptor => {
395
+ const { name } = descriptor
396
+ const property = name.slice(0, name.lastIndexOf('Value'))
397
+
398
+ let value = this[name]
399
+
400
+ // ensure minimum length to help with security-through-obscurity
401
+ if (value.length < 4) value = value.concat(sample(this.reservedDomains, 4 - value.length))
402
+
403
+ // store the property value
404
+ this.store.write(property, JSON.stringify(await encryptValues(key, value)))
405
+
406
+ // create the property getter (returns a promise)
407
+ if (!this.hasOwnProperty(property)) {
408
+ Object.defineProperty(this, property, {
409
+ get: async () => {
410
+ try {
411
+ const hosts = await decryptValues(this.key, JSON.parse(this.store.read(property)))
412
+ return hosts.filter(host => !this.reservedDomains.includes(host))
413
+ } catch (error) {
414
+ console.error(`Failed to get '${property}'!`, error)
415
+ return []
416
+ }
417
+ }
418
+ })
419
+ }
420
+
421
+ // cleanup the dom
422
+ this.element.removeAttribute(`data-trix-embed-${descriptor.key}`)
423
+ })
241
424
 
242
- if (this.paranoidValue !== false) {
243
- this.store.write('paranoid', JSON.stringify(fakes.slice(3)))
244
- this.element.removeAttribute('data-trix-embed-paranoid')
245
- }
425
+ // more security-through-obscurity
426
+ fakes = await encryptValues(key, sample(this.reservedDomains, 4))
427
+ this.store.write('securityHosts', fakes)
428
+ fakes = await encryptValues(key, sample(this.reservedDomains, 4))
429
+ this.store.write('obscurityHosts', fakes)
430
+
431
+ resolve()
432
+ })
246
433
  }
247
434
 
248
435
  forgetConfig() {
249
- this.store.remove('key')
250
- this.store.remove('hosts')
251
- this.store.remove('paranoid')
436
+ try {
437
+ this.store?.remove('key')
438
+ this.store?.remove('paranoid')
439
+
440
+ this.hostsValueDescriptors.forEach(async descriptor => {
441
+ const { name } = descriptor
442
+ const property = name.slice(0, name.lastIndexOf('Value'))
443
+ this.store?.remove(property)
444
+ })
445
+
446
+ this.store?.remove('securityHosts')
447
+ this.store?.remove('obscurityHosts')
448
+ } catch {}
252
449
  }
253
450
  }
254
451
  }
@@ -0,0 +1,19 @@
1
+ const random = cap => Math.floor(Math.random() * cap)
2
+
3
+ export const sample = (object, count = null) => {
4
+ const array = [...object]
5
+ if (count === 'all') count = array.length
6
+ const cap = array.length
7
+ const result = []
8
+ const indexes = new Set()
9
+ while (result.length < count) {
10
+ let i = random(cap)
11
+ while (indexes.has(i)) i = random(cap)
12
+ indexes.add(i)
13
+ result.push(array[i])
14
+ }
15
+
16
+ return typeof count === 'number' ? result : result[0]
17
+ }
18
+
19
+ export const shuffle = object => sample(object, 'all')