trix_embed 0.0.2 → 0.0.4
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.
- checksums.yaml +4 -4
- data/README.md +120 -47
- data/app/assets/builds/trix-embed.js +5 -22
- data/app/assets/builds/trix-embed.metafile.json +1 -1
- data/app/javascript/controller.js +343 -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 +153 -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,290 @@
|
|
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
|
-
const
|
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
|
+
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
|
-
|
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
|
-
|
77
|
-
|
78
|
-
|
158
|
+
extractLabelFromElement(el, options = { default: null }) {
|
159
|
+
let value = el.title
|
160
|
+
if (value && value.length) return value
|
79
161
|
|
80
|
-
|
81
|
-
|
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
|
-
|
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
|
-
}
|
165
|
+
return options.default
|
166
|
+
}
|
93
167
|
|
94
|
-
|
95
|
-
|
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
|
-
|
99
|
-
if (validMediaURLs[0] === sanitizedPastedContent || validStandardURLs[0] === sanitizedPastedContent)
|
100
|
-
return this.editor.insertLineBreak()
|
171
|
+
element = element.cloneNode(true)
|
101
172
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
120
|
-
element
|
121
|
-
|
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
|
-
|
124
|
-
|
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
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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:
|
159
|
-
|
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
|
-
|
166
|
-
|
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
|
-
|
171
|
-
|
172
|
-
.
|
173
|
-
.
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
198
|
-
|
199
|
-
}
|
303
|
+
const id = this.element.getAttribute('toolbar')
|
304
|
+
let toolbar = id ? document.getElementById(id) : null
|
200
305
|
|
201
|
-
|
202
|
-
|
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 [
|
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
|
-
|
232
|
-
|
233
|
-
|
234
|
-
const hosts = await encryptValues(key, this.hostsValue)
|
371
|
+
rememberConfig() {
|
372
|
+
return new Promise(async resolve => {
|
373
|
+
let fakes
|
235
374
|
|
236
|
-
|
237
|
-
|
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
|
-
|
240
|
-
|
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
|
-
|
243
|
-
|
244
|
-
this.
|
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
|
-
|
250
|
-
|
251
|
-
|
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')
|