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.
- checksums.yaml +4 -4
- data/README.md +120 -47
- data/app/assets/builds/trix-embed.js +38 -19
- data/app/assets/builds/trix-embed.metafile.json +1 -1
- data/app/javascript/controller.js +342 -145
- data/app/javascript/enumerable.js +19 -0
- data/app/javascript/forms.js +134 -0
- data/app/javascript/guard.js +29 -47
- data/app/javascript/index.js +9 -3
- data/app/javascript/media.js +12 -2
- data/app/javascript/metadata.js +4 -0
- data/app/javascript/renderer.js +155 -75
- data/app/javascript/store.js +13 -1
- data/app/javascript/templates.js +45 -28
- data/app/javascript/urls.js +57 -42
- data/app/models/trix_embed/attachment.rb +20 -6
- data/app/views/action_text/contents/_content.html.erb +1 -1
- data/lib/trix_embed/engine.rb +1 -1
- data/lib/trix_embed/version.rb +1 -1
- metadata +37 -4
@@ -1,180 +1,289 @@
|
|
1
|
+
import { sample, shuffle } from './enumerable'
|
1
2
|
import { generateKey, encryptValues, decryptValues } from './encryption'
|
2
|
-
import {
|
3
|
-
|
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
|
-
|
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
|
-
|
19
|
-
errorTemplate: String, // dom id of template to use for
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
33
|
-
if (this.
|
34
|
-
|
35
|
-
|
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
|
-
|
40
|
-
this.
|
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
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
157
|
+
extractLabelFromElement(el, options = { default: null }) {
|
158
|
+
let value = el.title
|
159
|
+
if (value && value.length) return value
|
79
160
|
|
80
|
-
|
81
|
-
|
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
|
-
|
88
|
-
|
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
|
-
|
95
|
-
|
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
|
-
|
99
|
-
if (validMediaURLs[0] === sanitizedPastedContent || validStandardURLs[0] === sanitizedPastedContent)
|
100
|
-
return this.editor.insertLineBreak()
|
170
|
+
element = element.cloneNode(true)
|
101
171
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
120
|
-
element
|
121
|
-
|
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
|
-
|
124
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
159
|
-
|
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
|
-
|
166
|
-
|
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
|
-
|
171
|
-
|
172
|
-
.
|
173
|
-
.
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
198
|
-
|
199
|
-
}
|
302
|
+
const id = this.element.getAttribute('toolbar')
|
303
|
+
let toolbar = id ? document.getElementById(id) : null
|
200
304
|
|
201
|
-
|
202
|
-
|
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 [
|
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
|
-
|
232
|
-
|
233
|
-
|
234
|
-
const hosts = await encryptValues(key, this.hostsValue)
|
370
|
+
rememberConfig() {
|
371
|
+
return new Promise(async resolve => {
|
372
|
+
let fakes
|
235
373
|
|
236
|
-
|
237
|
-
|
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
|
-
|
240
|
-
|
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
|
-
|
243
|
-
|
244
|
-
this.
|
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
|
-
|
250
|
-
|
251
|
-
|
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')
|