trix_embed 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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')