trix_embed 0.0.2 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,180 +1,290 @@
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
+ const renderer = new Renderer(this)
94
+
95
+ try {
96
+ // Media URLs (images, videos, audio etc.)
97
+ const allowedMediaHosts = await this.allowedMediaHosts
98
+ const blockedMediaHosts = await this.blockedMediaHosts
99
+ let mediaURLs = new Set(pastedURLs.filter(url => getMediaType(url)))
100
+ const iframes = [...pastedElement.querySelectorAll('iframe')]
101
+ iframes.forEach(frame => mediaURLs.add(frame.src))
102
+ mediaURLs = [...mediaURLs]
103
+ const validMediaURLs = mediaURLs.filter(url =>
104
+ validateURL(url, allowedMediaHosts, blockedMediaHosts)
105
+ )
106
+ const invalidMediaURLs = mediaURLs.filter(url => !validMediaURLs.includes(url))
107
+
108
+ // Link URLs (non-media resources i.e. web pages etc.)
109
+ const allowedLinkHosts = await this.allowedLinkHosts
110
+ const blockedLinkHosts = await this.blockedLinkHosts
111
+ const linkURLs = pastedURLs.filter(url => !mediaURLs.includes(url))
112
+ const validLinkURLs = linkURLs.filter(url => validateURL(url, allowedLinkHosts, blockedLinkHosts))
113
+ const invalidLinkURLs = linkURLs.filter(url => !validLinkURLs.includes(url))
114
+
115
+ // 1. render warnings ................................................................................
116
+ if (invalidMediaURLs.length || invalidLinkURLs.length) {
117
+ const invalidURLs = [...new Set([...invalidMediaURLs, ...invalidLinkURLs])]
118
+ const allowedHosts = [...new Set([...allowedMediaHosts, ...allowedLinkHosts])].filter(
119
+ host => !this.reservedDomains.includes(host)
120
+ )
121
+ const blockedHosts = [...new Set([...blockedMediaHosts, ...blockedLinkHosts])].filter(
122
+ host => !this.reservedDomains.includes(host)
123
+ )
124
+ console.log('allowedHosts', allowedHosts)
125
+ console.log('blockedHosts', blockedHosts)
126
+ await this.insert(renderer.renderWarnings(invalidURLs, allowedHosts, blockedHosts))
127
+ }
128
+
129
+ // 2. render valid media urls (i.e. embeds) ..........................................................
130
+ if (validMediaURLs.length) await this.insert(renderer.renderEmbeds(validMediaURLs))
131
+
132
+ // 3. exit early if there is only 1 URL and it's a valid media URL (i.e. a single embed) .............
133
+ if (pastedURLs.length === 1 && validMediaURLs.length === 1) return
134
+
135
+ // 4. render the pasted content as HTML .............................................................
136
+ const sanitizedPastedElement = this.sanitizePastedElement(pastedElement, {
137
+ renderer,
138
+ validMediaURLs,
139
+ validLinkURLs
140
+ })
141
+ const sanitizedPastedContent = sanitizedPastedElement.innerHTML.trim()
142
+ if (sanitizedPastedContent.length)
143
+ await this.insert(sanitizedPastedContent, { disposition: 'inline' })
144
+ } catch (e) {
145
+ this.insert(renderer.renderError(e))
146
+ }
147
+ } finally {
148
+ if (this.formElement) delete this.formElement.trixEmbedPasting
149
+ }
150
+ }
73
151
 
74
- let urls
152
+ createTemplateElement(content) {
153
+ const template = document.createElement('template')
154
+ template.innerHTML = `<div>${content.trim()}</div>`
155
+ return template.content.firstElementChild
156
+ }
75
157
 
76
- // 1. render invalid media urls ..........................................................................
77
- urls = invalidMediaURLs
78
- if (urls.length) await this.insert(renderer.renderErrors(urls, hosts.sort()))
158
+ extractLabelFromElement(el, options = { default: null }) {
159
+ let value = el.title
160
+ if (value && value.length) return value
79
161
 
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
- }
162
+ value = el.textContent.trim()
163
+ if (value && value.length) return value
86
164
 
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
- }
165
+ return options.default
166
+ }
93
167
 
94
- // 4. render valid standard urls .........................................................................
95
- urls = validStandardURLs
96
- if (urls.length) await this.insert(renderer.renderEmbeds(validStandardURLs))
168
+ sanitizePastedElement(element, options = { renderer: null, validMediaURLs: [], validLinkURLs: [] }) {
169
+ const { renderer, validMediaURLs, validLinkURLs } = options
97
170
 
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()
171
+ element = element.cloneNode(true)
101
172
 
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))
173
+ // sanitize text nodes
174
+ const walker = createURLTextNodeTreeWalker(element)
175
+ const textNodes = []
176
+ let textNode
177
+ while ((textNode = walker.nextNode())) {
178
+ textNode.replacements = textNode.replacements || new Set()
179
+ textNodes.push(textNode)
180
+
181
+ const words = textNode.nodeValue.split(/\s+/)
182
+ const matches = words.filter(word => word.startsWith('http'))
183
+
184
+ matches.forEach(match => {
185
+ const url = createURLObject(match)?.href
186
+ const replacement =
187
+ validLinkURLs.includes(url) || validLinkURLs.includes(url)
188
+ ? renderer.render('link', { url, label: url })
189
+ : renderer.render('prohibited', { url, label: 'Prohibited URL:', description: '' })
190
+ textNode.replacements.add({ match, replacement })
191
+ })
110
192
  }
111
- }
193
+ textNodes.forEach(node => {
194
+ if (!node.replacements.size) return
195
+ let content = node.nodeValue
196
+ // sort by length to replace the most specific matches first
197
+ const replacements = [...node.replacements].sort((a, b) => b.match.length - a.match.length)
198
+ replacements.forEach(entry => (content = content.replaceAll(entry.match, entry.replacement)))
199
+ node.replaceWith(this.createTemplateElement(content))
200
+ })
112
201
 
113
- buildPastedTemplate(content) {
114
- const template = document.createElement('template')
115
- template.innerHTML = `<div>${content.trim()}</div>`
116
- return template
117
- }
202
+ // sanitize anchor tags
203
+ element.querySelectorAll('a').forEach(el => {
204
+ const url = extractURLFromElement(el)
205
+ const label = this.extractLabelFromElement(el, { default: url })
206
+ const replacement = validLinkURLs.includes(url)
207
+ ? renderer.render('link', { url, label })
208
+ : renderer.render('prohibited', { url, label: 'Prohibited Link:', description: `(${label})` })
209
+ el.replaceWith(this.createTemplateElement(replacement))
210
+ })
118
211
 
119
- sanitizePastedElement(element) {
120
- element = element.cloneNode(true)
121
- element.querySelectorAll(mediaTags.join(', ')).forEach(tag => tag.remove())
212
+ // sanitize media tags
213
+ element.querySelectorAll(mediaTags.join(', ')).forEach(el => {
214
+ const url = extractURLFromElement(el)
215
+ const label = this.extractLabelFromElement(el, { default: url })
122
216
 
123
- const tags = element.querySelectorAll('*')
124
- const newlines = element.innerHTML.match(/\r\n|\n|\r/g) || []
217
+ const replacement = validMediaURLs.includes(url)
218
+ ? renderer.render('embedded', { url, label: 'Allowed Media:', description: '(Embedded Above)' })
219
+ : renderer.render('prohibited', { url, label: 'Prohibited Media:', description: '' })
125
220
 
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>')
221
+ el.replaceWith(this.createTemplateElement(replacement))
222
+ })
223
+
224
+ // sanitize newlines (best effort)
225
+ element.innerHTML.replaceAll(/(\n|\r|\f|\v)+/g, '<br>')
129
226
 
130
227
  return element
131
228
  }
132
229
 
133
- insertAttachment(content, options = { delay: 0 }) {
230
+ createAttachment(content) {
231
+ return new Trix.Attachment({ content, contentType: trixEmbedMediaTypes.attachment })
232
+ }
233
+
234
+ insertNewlines(count = 1, options = { delay: 1 }) {
134
235
  const { delay } = options
135
236
  return new Promise(resolve => {
136
237
  setTimeout(() => {
137
- const attachment = new Trix.Attachment({ content, contentType: 'application/vnd.trix-embed' })
138
- this.editor.insertAttachment(attachment)
238
+ for (let i = 0; i < count; i++) this.editor.insertLineBreak()
139
239
  resolve()
140
240
  }, delay)
141
241
  })
142
242
  }
143
243
 
144
- insertHTML(content, options = { delay: 0 }) {
244
+ insertAttachment(content, options = { delay: 1 }) {
245
+ const { delay } = options
246
+ return new Promise(resolve => {
247
+ setTimeout(() => {
248
+ this.editor.insertAttachment(this.createAttachment(content))
249
+ this.insertNewlines(1, { delay: delay }).finally(resolve)
250
+ }, delay)
251
+ })
252
+ }
253
+
254
+ insertHTML(content, options = { delay: 1 }) {
145
255
  const { delay } = options
146
256
  return new Promise(resolve => {
147
257
  setTimeout(() => {
148
258
  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()
259
+ this.insertNewlines(1, { delay }).finally(resolve)
154
260
  }, delay)
155
261
  })
156
262
  }
157
263
 
158
- insert(content, options = { delay: 0, disposition: 'attachment' }) {
159
- const { delay, disposition } = options
264
+ insert(content, options = { delay: 1, disposition: 'attachment' }) {
265
+ let { delay, disposition } = options
160
266
 
161
267
  if (content?.length) {
162
268
  return new Promise(resolve => {
163
269
  setTimeout(() => {
164
270
  if (typeof content === 'string') {
165
- if (disposition === 'inline') return this.insertHTML(content, { delay }).then(resolve)
166
- else return this.insertAttachment(content, { delay }).then(resolve)
271
+ return disposition === 'inline'
272
+ ? this.insertHTML(content, { delay })
273
+ .catch(e => this.renderError(e))
274
+ .finally(resolve)
275
+ : this.insertAttachment(content, { delay })
276
+ .catch(e => this.renderError(e))
277
+ .finally(resolve)
167
278
  }
168
279
 
169
280
  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)
281
+ const promises =
282
+ disposition === 'inline'
283
+ ? content.map(c => this.insertHTML(c, { delay: delay + 1 }))
284
+ : content.map(c => this.insertAttachment(c, { delay: delay + 1 }))
285
+ return Promise.all(promises)
286
+ .catch(e => this.renderError(e))
287
+ .finally(resolve)
178
288
  }
179
289
 
180
290
  resolve()
@@ -185,27 +295,31 @@ export function getTrixEmbedControllerClass(options = defaultOptions) {
185
295
  return Promise.resolve()
186
296
  }
187
297
 
188
- // Returns the Trix editor
189
- //
190
- // @returns {TrixEditor}
191
- //
192
298
  get editor() {
193
299
  return this.element.editor
194
300
  }
195
301
 
196
302
  get toolbarElement() {
197
- const sibling = this.element.previousElementSibling
198
- return sibling?.tagName.match(/trix-toolbar/i) ? sibling : null
199
- }
303
+ const id = this.element.getAttribute('toolbar')
304
+ let toolbar = id ? document.getElementById(id) : null
200
305
 
201
- get inputElement() {
202
- return document.getElementById(this.element.getAttribute('input'))
306
+ if (!toolbar) {
307
+ const sibling = this.element.previousElementSibling
308
+ toolbar = sibling?.tagName.match(/trix-toolbar/i) ? sibling : null
309
+ }
310
+
311
+ return toolbar
203
312
  }
204
313
 
205
314
  get formElement() {
206
315
  return this.element.closest('form')
207
316
  }
208
317
 
318
+ get inputElement() {
319
+ const id = this.element.getAttribute('input')
320
+ return id ? this.formElement?.querySelector(`#${id}`) : null
321
+ }
322
+
209
323
  get paranoid() {
210
324
  return !!this.store.read('paranoid')
211
325
  }
@@ -213,42 +327,126 @@ export function getTrixEmbedControllerClass(options = defaultOptions) {
213
327
  get key() {
214
328
  try {
215
329
  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
330
  } catch {
223
- return []
331
+ return null
224
332
  }
225
333
  }
226
334
 
335
+ get hostsValueDescriptors() {
336
+ return Object.values(this.valueDescriptorMap).filter(descriptor =>
337
+ descriptor.name.endsWith('HostsValue')
338
+ )
339
+ }
340
+
227
341
  get reservedDomains() {
228
- return ['example.com', 'test.com', 'invalid.com', 'example.cat', 'nic.example', 'example.co.uk']
342
+ return [
343
+ 'embed.example',
344
+ 'embed.invalid',
345
+ 'embed.local',
346
+ 'embed.localhost',
347
+ 'embed.test',
348
+ 'trix.embed.example',
349
+ 'trix.embed.invalid',
350
+ 'trix.embed.local',
351
+ 'trix.embed.localhost',
352
+ 'trix.embed.test',
353
+ 'trix.example',
354
+ 'trix.invalid',
355
+ 'trix.local',
356
+ 'trix.localhost',
357
+ 'trix.test',
358
+ 'www.embed.example',
359
+ 'www.embed.invalid',
360
+ 'www.embed.local',
361
+ 'www.embed.localhost',
362
+ 'www.embed.test',
363
+ 'www.trix.example',
364
+ 'www.trix.invalid',
365
+ 'www.trix.local',
366
+ 'www.trix.localhost',
367
+ 'www.trix.test'
368
+ ]
229
369
  }
230
370
 
231
- async rememberConfig() {
232
- const key = await generateKey()
233
- const fakes = await encryptValues(key, this.reservedDomains)
234
- const hosts = await encryptValues(key, this.hostsValue)
371
+ rememberConfig() {
372
+ return new Promise(async resolve => {
373
+ let fakes
235
374
 
236
- this.store.write('key', JSON.stringify([fakes[0], fakes[1], key, fakes[2]]))
237
- this.element.removeAttribute('data-trix-embed-key-value')
375
+ // encryption key
376
+ const key = await generateKey()
377
+ fakes = await encryptValues(key, sample(this.reservedDomains, 3))
378
+ this.store.write('key', JSON.stringify([fakes[0], fakes[1], key, fakes[2]]))
238
379
 
239
- this.store.write('hosts', JSON.stringify(hosts))
240
- this.element.removeAttribute('data-trix-embed-hosts-value')
380
+ // paranoid
381
+ if (this.paranoidValue !== false) {
382
+ fakes = await encryptValues(key, sample(this.reservedDomains, 4))
383
+ this.store.write('paranoid', JSON.stringify(fakes))
384
+ }
385
+ this.element.removeAttribute('data-trix-embed-paranoid-value')
386
+
387
+ // host lists
388
+ //
389
+ // - allowedLinkHosts
390
+ // - blockedLinkHosts
391
+ // - allowedMediaHosts
392
+ // - blockedMediaHosts
393
+ // - etc.
394
+ //
395
+ this.hostsValueDescriptors.forEach(async descriptor => {
396
+ const { name } = descriptor
397
+ const property = name.slice(0, name.lastIndexOf('Value'))
398
+
399
+ let value = this[name]
400
+
401
+ // ensure minimum length to help with security-through-obscurity
402
+ if (value.length < 4) value = value.concat(sample(this.reservedDomains, 4 - value.length))
403
+
404
+ // store the property value
405
+ this.store.write(property, JSON.stringify(await encryptValues(key, value)))
406
+
407
+ // create the property getter (returns a promise)
408
+ if (!this.hasOwnProperty(property)) {
409
+ Object.defineProperty(this, property, {
410
+ get: async () => {
411
+ try {
412
+ const hosts = await decryptValues(this.key, JSON.parse(this.store.read(property)))
413
+ return hosts.filter(host => !this.reservedDomains.includes(host))
414
+ } catch (error) {
415
+ console.error(`Failed to get '${property}'!`, error)
416
+ return []
417
+ }
418
+ }
419
+ })
420
+ }
421
+
422
+ // cleanup the dom
423
+ this.element.removeAttribute(`data-trix-embed-${descriptor.key}`)
424
+ })
241
425
 
242
- if (this.paranoidValue !== false) {
243
- this.store.write('paranoid', JSON.stringify(fakes.slice(3)))
244
- this.element.removeAttribute('data-trix-embed-paranoid')
245
- }
426
+ // more security-through-obscurity
427
+ fakes = await encryptValues(key, sample(this.reservedDomains, 4))
428
+ this.store.write('securityHosts', fakes)
429
+ fakes = await encryptValues(key, sample(this.reservedDomains, 4))
430
+ this.store.write('obscurityHosts', fakes)
431
+
432
+ resolve()
433
+ })
246
434
  }
247
435
 
248
436
  forgetConfig() {
249
- this.store.remove('key')
250
- this.store.remove('hosts')
251
- this.store.remove('paranoid')
437
+ try {
438
+ this.store?.remove('key')
439
+ this.store?.remove('paranoid')
440
+
441
+ this.hostsValueDescriptors.forEach(async descriptor => {
442
+ const { name } = descriptor
443
+ const property = name.slice(0, name.lastIndexOf('Value'))
444
+ this.store?.remove(property)
445
+ })
446
+
447
+ this.store?.remove('securityHosts')
448
+ this.store?.remove('obscurityHosts')
449
+ } catch {}
252
450
  }
253
451
  }
254
452
  }
@@ -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')