feat: PDF viewer block

scarlett
NGPixel 4 weeks ago
parent 09e9166bc6
commit 29d816401c
No known key found for this signature in database

@ -37,10 +37,6 @@ export async function registerSchemas(app: FastifyInstance): Promise<void> {
minimum: 0,
description: 'Seconds. Must be greater than zero when HSTS is enforced.'
},
disallowFloc: {
type: 'boolean',
description: 'Sends `Permissions-Policy: interest-cohort=()`.'
},
disallowIframe: {
type: 'boolean',
description: '`X-Frame-Options: DENY` when on, `SAMEORIGIN` when off.'

@ -74,7 +74,6 @@ defaults:
enforceCsp: false
trustProxy: false
enforceHsts: false
disallowFloc: true
hstsDuration: 0
cspDirectives: ''
uploadScanSVG: true

@ -335,15 +335,6 @@ async function initHTTPServer() {
methods: ['GET', 'HEAD', 'POST', 'OPTIONS']
})
if (security.disallowFloc) {
// -> Helmet dropped its FLoC helper once the proposal was withdrawn, but opting out still costs
// one header and the setting exists
app.addHook('onSend', (req, reply, payload, done) => {
reply.header('Permissions-Policy', 'interest-cohort=()')
done(null, payload)
})
}
// ----------------------------------------
// Public Assets
// ----------------------------------------

@ -459,8 +459,6 @@
"admin.icons.diskCacheValue": "{count} icons ({size})",
"admin.icons.enableSuccess": "The {set} icon set has been enabled.",
"admin.icons.filterSets": "Filter icon sets...",
"admin.icons.howItWorks": "How icons are stored",
"admin.icons.howItWorksHint": "Content references an icon by name, e.g. mdi:account-edit. The first time an icon is used it is fetched from Iconify and saved to the database, which is its permanent home. Memory and disk are caches in front of it and can be discarded at any time.",
"admin.icons.isEnabled": "Enabled",
"admin.icons.loadFailed": "Failed to load the icon sets.",
"admin.icons.memoryCache": "Memory cache",
@ -485,8 +483,6 @@
"admin.icons.storedIconsValue": "{count} icons in the database",
"admin.icons.subtitle": "Choose which icon sets can be used across the wiki",
"admin.icons.title": "Icons",
"admin.icons.upstream": "Icon source",
"admin.icons.upstreamHint": "Icons come from the Iconify API. Once stored, they are served by this wiki and never fetched again — readers never talk to Iconify.",
"admin.instances.activeConnections": "Active Connections",
"admin.instances.activeListeners": "Active Listeners",
"admin.instances.firstSeen": "First Seen",
@ -724,8 +720,6 @@
"admin.security.corsModeHint": "How the API server should handle preflight requests?",
"admin.security.corsRegex": "Regex Pattern",
"admin.security.corsRegexHint": "Pattern against which the request hostname is matched.",
"admin.security.disallowFloc": "Disallow Google FLoC",
"admin.security.disallowFlocHint": "Broadcast that this website should be opted-out of Google Federed Learning of Cohorts (FLoC). Recommended for privacy.",
"admin.security.disallowIframe": "Disallow iFrame Embedding",
"admin.security.disallowIframeHint": "Prevents other websites from embedding your wiki in an iframe. This provides clickjacking protection.",
"admin.security.disallowOpenRedirect": "Block Open Redirect",

@ -9,7 +9,6 @@ export const SECURITY_FIELDS = [
'corsConfig',
'corsMode',
'cspDirectives',
'disallowFloc',
'disallowIframe',
'disallowOpenRedirect',
'enforceCsp',

@ -129,7 +129,6 @@ class Settings {
corsConfig: '',
corsMode: 'OFF',
cspDirectives: '',
disallowFloc: true,
disallowIframe: true,
disallowOpenRedirect: true,
enforceCsp: false,

@ -0,0 +1,6 @@
{
"pdfjs-dist/cmaps": "cmaps",
"pdfjs-dist/standard_fonts": "standard_fonts",
"pdfjs-dist/wasm": "wasm",
"pdfjs-dist/iccs": "iccs"
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,14 @@
/*
The pdf.js parser, as its own bundle.
Everything a PDF costs -- reading the file structure, decoding its images, laying out its glyph
runs -- happens here rather than on the page's thread, which is the whole reason a viewer stays
responsive while a hundred-page document loads. It is a separate entry point because a worker is
loaded by URL, not imported: `component.js` points `GlobalWorkerOptions.workerSrc` at whatever
this file compiles to, next to it in /_blocks.
The rollup config picks this up from the file name alone -- any `worker.js` beside a block's
component becomes `<block>.worker.js` -- so there is nothing to add there for the next block that
needs one.
*/
export * from 'pdfjs-dist/build/pdf.worker.mjs'

@ -19,6 +19,7 @@
"lit": "3.3.3",
"mermaid": "11.16.1",
"pako": "3.0.1",
"pdfjs-dist": "6.2.108",
"uqr": "0.1.3"
},
"devDependencies": {
@ -295,6 +296,271 @@
"@chevrotain/types": "~11.1.2"
}
},
"node_modules/@napi-rs/canvas": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.6.tgz",
"integrity": "sha512-Ud4d0jLr9vSaakvfOTIjgP3hoY8kMIYgHC4mtE4L0bP3St9lf6TyCVsInvFIOEofzcUeMfDydHDy8Fs2WuWz+A==",
"license": "MIT",
"optional": true,
"workspaces": [
"e2e/*"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
},
"optionalDependencies": {
"@napi-rs/canvas-android-arm64": "1.0.6",
"@napi-rs/canvas-darwin-arm64": "1.0.6",
"@napi-rs/canvas-darwin-x64": "1.0.6",
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.6",
"@napi-rs/canvas-linux-arm64-gnu": "1.0.6",
"@napi-rs/canvas-linux-arm64-musl": "1.0.6",
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.6",
"@napi-rs/canvas-linux-x64-gnu": "1.0.6",
"@napi-rs/canvas-linux-x64-musl": "1.0.6",
"@napi-rs/canvas-win32-arm64-msvc": "1.0.6",
"@napi-rs/canvas-win32-x64-msvc": "1.0.6"
}
},
"node_modules/@napi-rs/canvas-android-arm64": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.6.tgz",
"integrity": "sha512-CZUmZEyN/cmSN1OhRJ44vBE6xyb5ekBbsIF8dqNgizbnAiHSFNi8eAA4oyyIAe5KzJ72K16OKC2lAynZLhv32Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.6.tgz",
"integrity": "sha512-FGJsIngRfS9LDaX4t/lFthqn2Km6b61ONzjJFC6+yuabL1NgaK7x0fwrj8276OQFkjCvSpJZ7+CeZWgcy03F2w==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-darwin-x64": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.6.tgz",
"integrity": "sha512-wLDrtfX9V4bu6O55/WwGvPVCjfunL2bEm59lzIFk9ZQOt7oJrPzdx/ytZiQr6bMZ9XOFwvXujszYtpoCK7yUbg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.6.tgz",
"integrity": "sha512-8TllEP/9UPDVxrBN/tnbGBsxxwG8Eo2tgyW8wDy+Gi9rm9si2I6MsDv9MlfI3a7JOuzkBcurHYDHZxwLgzAIdA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.6.tgz",
"integrity": "sha512-YF15AKoGrgU7aoA03CR+cATCRPQcVt6L14Lb/a9hqK7pT7/DhIkqh4qyl+a2GLfqx++yIIJfBuZ3sHM3zlXM1Q==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.6.tgz",
"integrity": "sha512-zXeVWNFpj720HZln0bfAs+1/nP3Wy6Z5hUw60UJu+cvgips5jZtuX2+EjDHFk98q3BY35GaE1TQLwE9S244vPA==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.6.tgz",
"integrity": "sha512-kvy1Ypq4ODEI2WRNGvs588qTMcWCjlEIuZRyW8JXpiBnMUirgxQukIDFVDQEcrLlHP5cqZ9cvvfOkBJ7koOktA==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.6.tgz",
"integrity": "sha512-czHnfLgSCl48iluS6bNrytGYhsbSTUuIvMucAwO0mPEj1uhkHk6SUiRI0xoqv0PSFzpGFfSnQvFpm7vV+tDO+A==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.6.tgz",
"integrity": "sha512-afsg6GyfUx8qZzjsywjimXUEWcSWETY5F80hmMb2FWVSXkRbor8IpftY2gXnasq+Y42Xug/76qTHwqvti7HRTw==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.6.tgz",
"integrity": "sha512-byzha357piy0wgK6IZOQtU4plpt94f6CxHFS7LhJpWYrDWWT/AitQNxRMfCNNaQHZXUB3BwNhQSfH2RpiZ0WaQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.6.tgz",
"integrity": "sha512-gWLOgKGZz3hZhzg49q2PJIURwy2pCWh1zvNFXmLO1yineOPhEpoL3EV6/VIjOiKa1l2P6ieP12Eo385u1MLLVQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/Brooooooklyn"
}
},
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
@ -2486,6 +2752,18 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/pdfjs-dist": {
"version": "6.2.108",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.2.108.tgz",
"integrity": "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=22.13.0 || >=24"
},
"optionalDependencies": {
"@napi-rs/canvas": "^1.0.0"
}
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",

@ -23,6 +23,7 @@
"lit": "3.3.3",
"mermaid": "11.16.1",
"pako": "3.0.1",
"pdfjs-dist": "6.2.108",
"uqr": "0.1.3"
},
"devDependencies": {

@ -152,27 +152,97 @@ function blocksManifest () {
}
}
const IGNORED_DIRS = [
'dist/**',
'node_modules/**'
]
/**
* Copies the runtime data files a block's library fetches for itself into `compiled/<block>/`.
*
* Some libraries deliberately keep part of themselves out of the bundle. pdf.js ships its character
* maps, its fallback fonts, its colour profile and the wasm that decodes JPEG 2000 and JBIG2 images
* as files it asks for only once a document turns out to need one several megabytes that would
* otherwise be carried into every page showing a PDF, to be read by hardly any of them. They still
* have to be somewhere the browser can ask for them, and for a block that means beside it in
* /_blocks, since a block knows no other path it can reach.
*
* `assets.json` beside a component lists them: a directory to copy a package subpath, or one
* starting with `./` for a directory of the block's own mapped to the name it should have under
* `compiled/<block>/`. Everything below it is copied, so a block declares four directories rather
* than two hundred files.
*/
function blockAssets () {
return {
name: 'block-assets',
buildStart () {
for (const listPath of glob.sync('@(block-*)/assets.json', { ignore: IGNORED_DIRS })) {
const blockDir = listPath.split('/')[0]
this.addWatchFile(listPath)
const list = JSON.parse(fs.readFileSync(listPath, 'utf8'))
for (const [source, destination] of Object.entries(list)) {
const from = source.startsWith('.')
? path.resolve(blockDir, source)
: path.resolve('node_modules', source)
if (!fs.existsSync(from)) {
// -> A package that moved its data files between versions, most likely. Silence here
// would be a block that loads and then quietly cannot read half the documents it is
// given, so the build stops instead.
this.error(`${listPath}: "${source}" does not exist — nothing to copy from.`)
}
for (const entry of fs.readdirSync(from, { recursive: true, withFileTypes: true })) {
if (!entry.isFile()) {
continue
}
const filePath = path.join(entry.parentPath, entry.name)
this.emitFile({
type: 'asset',
fileName: path.posix.join(
blockDir,
destination,
path.relative(from, filePath).split(path.sep).join('/')
),
source: fs.readFileSync(filePath)
})
}
}
}
}
}
}
export default {
input: Object.fromEntries(
glob.sync('@(block-*)/component.js', {
ignore: [
'dist/**',
'node_modules/**'
]
}).map(file => {
input: Object.fromEntries([
...glob.sync('@(block-*)/component.js', { ignore: IGNORED_DIRS }).map(file => {
const fileParts = file.split('/')
return [
fileParts[0],
file
]
}),
/*
A `worker.js` beside a component is a second entry point, compiled to `<block>.worker.js`.
A web worker is loaded by URL rather than imported, so its code cannot be part of the bundle
that starts it -- it has to be a file of its own, sitting in /_blocks where the block can point
at it with `new URL('<block>.worker.js', import.meta.url)`. See `block-pdf`, which runs pdf.js's
parser off the page's thread.
*/
...glob.sync('@(block-*)/worker.js', { ignore: IGNORED_DIRS }).map(file => {
const fileParts = file.split('/')
return [
`${fileParts[0]}.worker`,
file
]
})
),
]),
output: {
dir: 'compiled',
format: 'es'
},
plugins: [
blocksManifest(),
blockAssets(),
cssAsString(),
// -> `production` is stated rather than left to be inferred: since v16 the plugin picks the
// `development` or `production` export condition off `process.env.NODE_ENV`, and this build

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width="80px" height="80px"><path fill="#fff" d="M6.5 37.5L6.5 2.5 24.793 2.5 33.5 11.207 33.5 37.5z"/><path fill="#4788c7" d="M24.586,3L33,11.414V37H7V3H24.586 M25,2H6v36h28V11L25,2L25,2z"/><path fill="#dff0fe" d="M24.5 11.5L24.5 2.5 24.793 2.5 33.5 11.207 33.5 11.5z"/><path fill="#4788c7" d="M25 3.414L32.586 11H25V3.414M25 2h-1v10h10v-1L25 2 25 2zM12.044 17.535c.481-.104 1.131-.169 1.781-.169 1.014 0 1.82.144 2.379.677.52.455.754 1.183.754 1.924 0 .949-.286 1.625-.741 2.106-.546.585-1.417.845-2.132.845-.117 0-.221 0-.338-.013v3.263h-1.703V17.535zM13.747 21.526c.091.014.182.014.286.014.858 0 1.235-.611 1.235-1.431 0-.767-.312-1.364-1.105-1.364-.156 0-.312.025-.416.064V21.526zM17.985 17.535c.429-.104 1.053-.169 1.729-.169 1.105 0 1.872.261 2.431.78.754.676 1.144 1.833 1.144 3.536 0 1.769-.455 3.029-1.222 3.705-.598.56-1.456.846-2.665.846-.546 0-1.079-.053-1.417-.104V17.535zM19.688 24.816c.091.025.208.025.299.025.819 0 1.534-.806 1.534-3.224 0-1.794-.468-2.887-1.495-2.887-.117 0-.234 0-.338.039V24.816zM24.368 17.405h3.809v1.482h-2.106v2.249h1.976v1.417h-1.976v3.614h-1.703V17.405z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -12,7 +12,17 @@
{{ t('admin.icons.subtitle') }}
</div>
</div>
<div class="flex-none">
<div class="flex-none flex">
<w-btn
class="acrylic-btn mr-2"
flat
icon="la:broom"
color="purple"
:label="t(`admin.icons.purgeCache`)"
@click="purgeCache">
<w-tooltip>{{ t('admin.icons.purgeCacheHint') }}</w-tooltip>
</w-btn>
<w-separator class="mr-2" vertical />
<w-btn
class="acrylic-btn mr-2"
icon="la:question-circle"
@ -60,21 +70,6 @@
>
<w-list separator>
<w-item v-for="set of state.sets" :key="set.prefix">
<w-item-section side>
<div class="admin-icons-samples">
<w-icon
class="admin-icons-sample"
v-for="sample of sampleRefs(set)"
:key="sample"
:name="sample"
size="24px" />
<w-icon
v-if="sampleRefs(set).length < 1"
name="la:icons"
size="24px"
color="grey" />
</div>
</w-item-section>
<w-item-section>
<w-item-label>
<strong>{{ set.name }}</strong>
@ -165,35 +160,6 @@
</w-item-section>
</w-item>
</w-list>
<w-separator />
<w-card-actions class="px-4">
<w-btn
class="acrylic-btn"
flat
no-caps
icon="la:broom"
color="negative"
:label="t(`admin.icons.purgeCache`)"
@click="purgeCache">
<w-tooltip>{{ t('admin.icons.purgeCacheHint') }}</w-tooltip>
</w-btn>
</w-card-actions>
</w-card>
<!-- ----------------------- -->
<!-- How it works -->
<!-- ----------------------- -->
<w-card class="rounded mt-4" style="width: 350px">
<w-card-header>
{{ t('admin.icons.howItWorks') }}
<template #hint>{{ t('admin.icons.howItWorksHint') }}</template>
</w-card-header>
<w-separator class="mb-2" inset />
<w-item>
<w-item-section>
<w-item-label class="text-grey">{{ t('admin.icons.upstream') }}</w-item-label>
<w-item-label class="text-caption">{{ t('admin.icons.upstreamHint') }}</w-item-label>
</w-item-section>
</w-item>
</w-card>
</div>
</div>
@ -345,13 +311,6 @@ function prettyBytes(bytes) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}
/**
* A few icons of the set to show next to it, from the samples upstream publishes
*/
function sampleRefs(set) {
return (set.info?.samples ?? []).slice(0, 3).map((sample) => `${set.prefix}:${sample}`)
}
/**
* The Iconify page for the set, which lists every icon it holds with its name.
*

@ -64,19 +64,6 @@
</w-card>
</w-item-section>
</w-item>
<w-item tag="label">
<blueprint-icon icon="rfid-signal" />
<w-item-section>
<w-item-label>{{ t(`admin.security.disallowFloc`) }}</w-item-label>
<w-item-label caption>{{ t(`admin.security.disallowFlocHint`) }}</w-item-label>
</w-item-section>
<w-item-section avatar>
<w-toggle
v-model="state.config.disallowFloc"
:aria-label="t(`admin.security.disallowFloc`)" />
</w-item-section>
</w-item>
<w-separator class="my-2" inset />
<w-item tag="label">
<blueprint-icon icon="maximize-window" />
<w-item-section>
@ -447,7 +434,6 @@ const state = reactive({
corsConfig: '',
corsMode: 'OFF',
cspDirectives: '',
disallowFloc: false,
disallowIframe: false,
disallowOpenRedirect: false,
enforceCsp: false,

Loading…
Cancel
Save