trix_embed 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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')
|