aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-09-26 03:29:51 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-09-26 16:11:32 +0200
commit3ba6f8fba9dbd6e479f4297a5a05b51273e461a3 (patch)
tree66c0d598fdcf93db51cf988547c4417bf80906a7
parentfix(query-interpreter): functional dependencies (diff)
downloadrefinery-3ba6f8fba9dbd6e479f4297a5a05b51273e461a3.tar.gz
refinery-3ba6f8fba9dbd6e479f4297a5a05b51273e461a3.tar.zst
refinery-3ba6f8fba9dbd6e479f4297a5a05b51273e461a3.zip
feat(frontend): save in URL fragment
-rw-r--r--subprojects/frontend/src/RootStore.ts34
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts8
-rw-r--r--subprojects/frontend/src/index.tsx100
-rw-r--r--subprojects/frontend/src/persistence/Compressor.ts108
-rw-r--r--subprojects/frontend/src/persistence/compressionMessages.tsx56
-rw-r--r--subprojects/frontend/src/persistence/compressionWorker.ts98
-rw-r--r--subprojects/frontend/src/persistence/initialValue.ts103
-rw-r--r--subprojects/frontend/src/xtext/UpdateService.ts2
-rw-r--r--subprojects/frontend/src/xtext/XtextClient.ts7
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java3
10 files changed, 413 insertions, 106 deletions
diff --git a/subprojects/frontend/src/RootStore.ts b/subprojects/frontend/src/RootStore.ts
index b84c0ce0..e277c808 100644
--- a/subprojects/frontend/src/RootStore.ts
+++ b/subprojects/frontend/src/RootStore.ts
@@ -9,11 +9,20 @@ import { makeAutoObservable, runInAction } from 'mobx';
9 9
10import PWAStore from './PWAStore'; 10import PWAStore from './PWAStore';
11import type EditorStore from './editor/EditorStore'; 11import type EditorStore from './editor/EditorStore';
12import Compressor from './persistence/Compressor';
12import ThemeStore from './theme/ThemeStore'; 13import ThemeStore from './theme/ThemeStore';
13 14
14const log = getLogger('RootStore'); 15const log = getLogger('RootStore');
15 16
16export default class RootStore { 17export default class RootStore {
18 private readonly compressor = new Compressor((text) =>
19 this.setInitialValue(text),
20 );
21
22 private initialValue: string | undefined;
23
24 private editorStoreClass: typeof EditorStore | undefined;
25
17 editorStore: EditorStore | undefined; 26 editorStore: EditorStore | undefined;
18 27
19 readonly pwaStore: PWAStore; 28 readonly pwaStore: PWAStore;
@@ -22,10 +31,12 @@ export default class RootStore {
22 31
23 disposed = false; 32 disposed = false;
24 33
25 constructor(initialValue: string) { 34 constructor() {
26 this.pwaStore = new PWAStore(); 35 this.pwaStore = new PWAStore();
27 this.themeStore = new ThemeStore(); 36 this.themeStore = new ThemeStore();
28 makeAutoObservable(this, { 37 makeAutoObservable<RootStore, 'compressor' | 'editorStoreClass'>(this, {
38 compressor: false,
39 editorStoreClass: false,
29 pwaStore: false, 40 pwaStore: false,
30 themeStore: false, 41 themeStore: false,
31 }); 42 });
@@ -35,11 +46,27 @@ export default class RootStore {
35 if (this.disposed) { 46 if (this.disposed) {
36 return; 47 return;
37 } 48 }
38 this.editorStore = new EditorStore(initialValue, this.pwaStore); 49 this.editorStoreClass = EditorStore;
50 if (this.initialValue !== undefined) {
51 this.setInitialValue(this.initialValue);
52 }
39 }); 53 });
40 })().catch((error) => { 54 })().catch((error) => {
41 log.error('Failed to load EditorStore', error); 55 log.error('Failed to load EditorStore', error);
42 }); 56 });
57 this.compressor.decompressInitial();
58 }
59
60 private setInitialValue(initialValue: string): void {
61 this.initialValue = initialValue;
62 if (this.editorStoreClass !== undefined) {
63 const EditorStore = this.editorStoreClass;
64 this.editorStore = new EditorStore(
65 this.initialValue,
66 this.pwaStore,
67 (text) => this.compressor.compress(text),
68 );
69 }
43 } 70 }
44 71
45 dispose(): void { 72 dispose(): void {
@@ -47,6 +74,7 @@ export default class RootStore {
47 return; 74 return;
48 } 75 }
49 this.editorStore?.dispose(); 76 this.editorStore?.dispose();
77 this.compressor.dispose();
50 this.disposed = true; 78 this.disposed = true;
51 } 79 }
52} 80}
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index 9508858d..87c4040e 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -74,7 +74,11 @@ export default class EditorStore {
74 74
75 selectedGeneratedModel: string | undefined; 75 selectedGeneratedModel: string | undefined;
76 76
77 constructor(initialValue: string, pwaStore: PWAStore) { 77 constructor(
78 initialValue: string,
79 pwaStore: PWAStore,
80 onUpdate: (text: string) => void,
81 ) {
78 this.id = nanoid(); 82 this.id = nanoid();
79 this.state = createEditorState(initialValue, this); 83 this.state = createEditorState(initialValue, this);
80 this.delayedErrors = new EditorErrors(this); 84 this.delayedErrors = new EditorErrors(this);
@@ -86,7 +90,7 @@ export default class EditorStore {
86 if (this.disposed) { 90 if (this.disposed) {
87 return; 91 return;
88 } 92 }
89 this.client = new LazyXtextClient(this, pwaStore); 93 this.client = new LazyXtextClient(this, pwaStore, onUpdate);
90 this.client.start(); 94 this.client.start();
91 }); 95 });
92 })().catch((error) => { 96 })().catch((error) => {
diff --git a/subprojects/frontend/src/index.tsx b/subprojects/frontend/src/index.tsx
index 60debd6b..3fd25e5c 100644
--- a/subprojects/frontend/src/index.tsx
+++ b/subprojects/frontend/src/index.tsx
@@ -15,104 +15,6 @@ import RootStore from './RootStore';
15// https://github.com/mui/material-ui/issues/32727#issuecomment-1659945548 15// https://github.com/mui/material-ui/issues/32727#issuecomment-1659945548
16(window as unknown as { fixViteIssue: unknown }).fixViteIssue = styled; 16(window as unknown as { fixViteIssue: unknown }).fixViteIssue = styled;
17 17
18const initialValue = `% Metamodel
19
20abstract class CompositeElement {
21 contains Region[] regions
22}
23
24class Region {
25 contains Vertex[] vertices opposite region
26}
27
28abstract class Vertex {
29 container Region region opposite vertices
30 contains Transition[] outgoingTransition opposite source
31 Transition[] incomingTransition opposite target
32}
33
34class Transition {
35 container Vertex source opposite outgoingTransition
36 Vertex[1] target opposite incomingTransition
37}
38
39abstract class Pseudostate extends Vertex.
40
41abstract class RegularState extends Vertex.
42
43class Entry extends Pseudostate.
44
45class Exit extends Pseudostate.
46
47class Choice extends Pseudostate.
48
49class FinalState extends RegularState.
50
51class State extends RegularState, CompositeElement.
52
53class Statechart extends CompositeElement.
54
55% Constraints
56
57%% Entry
58
59pred entryInRegion(Region r, Entry e) <->
60 vertices(r, e).
61
62error noEntryInRegion(Region r) <->
63 !entryInRegion(r, _).
64
65error multipleEntryInRegion(Region r) <->
66 entryInRegion(r, e1),
67 entryInRegion(r, e2),
68 e1 != e2.
69
70error incomingToEntry(Transition t, Entry e) <->
71 target(t, e).
72
73error noOutgoingTransitionFromEntry(Entry e) <->
74 !source(_, e).
75
76error multipleTransitionFromEntry(Entry e, Transition t1, Transition t2) <->
77 outgoingTransition(e, t1),
78 outgoingTransition(e, t2),
79 t1 != t2.
80
81%% Exit
82
83error outgoingFromExit(Transition t, Exit e) <->
84 source(t, e).
85
86%% Final
87
88error outgoingFromFinal(Transition t, FinalState e) <->
89 source(t, e).
90
91%% State vs Region
92
93pred stateInRegion(Region r, State s) <->
94 vertices(r, s).
95
96error noStateInRegion(Region r) <->
97 !stateInRegion(r, _).
98
99%% Choice
100
101error choiceHasNoOutgoing(Choice c) <->
102 !source(_, c).
103
104error choiceHasNoIncoming(Choice c) <->
105 !target(_, c).
106
107% Instance model
108
109Statechart(sct).
110
111% Scope
112
113scope node = 20..30, Region = 2..*, Choice = 1..*, Statechart += 0.
114`;
115
116configure({ 18configure({
117 enforceActions: 'always', 19 enforceActions: 'always',
118}); 20});
@@ -121,7 +23,7 @@ let HotRootStore = RootStore;
121let HotApp = App; 23let HotApp = App;
122 24
123function createStore(): RootStore { 25function createStore(): RootStore {
124 return new HotRootStore(initialValue); 26 return new HotRootStore();
125} 27}
126 28
127let rootStore = createStore(); 29let rootStore = createStore();
diff --git a/subprojects/frontend/src/persistence/Compressor.ts b/subprojects/frontend/src/persistence/Compressor.ts
new file mode 100644
index 00000000..94878370
--- /dev/null
+++ b/subprojects/frontend/src/persistence/Compressor.ts
@@ -0,0 +1,108 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import getLogger from '../utils/getLogger';
8
9import {
10 type CompressRequest,
11 CompressorResponse,
12 type DecompressRequest,
13} from './compressionMessages';
14import CompressionWorker from './compressionWorker?worker';
15import initialValue from './initialValue';
16
17const LOG = getLogger('persistence.Compressor');
18
19const FRAGMENT_PREFIX = '#/1/';
20
21export default class Compressor {
22 private readonly worker = new CompressionWorker();
23
24 private readonly hashChangeHandler = () => this.updateHash();
25
26 private fragment: string | undefined;
27
28 private compressing = false;
29
30 private toCompress: string | undefined;
31
32 constructor(private readonly onDecompressed: (text: string) => void) {
33 this.worker.onerror = (error) => LOG.error('Worker error', error);
34 this.worker.onmessageerror = (error) =>
35 LOG.error('Worker message error', error);
36 this.worker.onmessage = (event) => {
37 try {
38 const message = CompressorResponse.parse(event.data);
39 switch (message.response) {
40 case 'compressed':
41 this.fragment = `${FRAGMENT_PREFIX}${message.compressedText}`;
42 this.compressionEnded();
43 window.history.replaceState(null, '', this.fragment);
44 break;
45 case 'decompressed':
46 this.onDecompressed(message.text);
47 break;
48 case 'error':
49 this.compressionEnded();
50 LOG.error('Error processing compressor request', message.message);
51 break;
52 default:
53 LOG.error('Unknown response from compressor worker', event.data);
54 break;
55 }
56 } catch (error) {
57 LOG.error('Error processing worker message', event, error);
58 }
59 };
60 window.addEventListener('hashchange', this.hashChangeHandler);
61 }
62
63 decompressInitial(): void {
64 this.updateHash();
65 if (this.fragment === undefined) {
66 LOG.debug('Loading default source');
67 this.onDecompressed(initialValue);
68 }
69 }
70
71 compress(text: string): void {
72 this.toCompress = text;
73 if (this.compressing) {
74 return;
75 }
76 this.compressing = true;
77 this.worker.postMessage({
78 request: 'compress',
79 text,
80 } satisfies CompressRequest);
81 }
82
83 dispose(): void {
84 window.removeEventListener('hashchange', this.hashChangeHandler);
85 this.worker.terminate();
86 }
87
88 private compressionEnded(): void {
89 this.compressing = false;
90 if (this.toCompress !== undefined) {
91 this.compress(this.toCompress);
92 this.toCompress = undefined;
93 }
94 }
95
96 private updateHash(): void {
97 if (
98 window.location.hash !== this.fragment &&
99 window.location.hash.startsWith(FRAGMENT_PREFIX)
100 ) {
101 this.fragment = window.location.hash;
102 this.worker.postMessage({
103 request: 'decompress',
104 compressedText: this.fragment.substring(FRAGMENT_PREFIX.length),
105 } satisfies DecompressRequest);
106 }
107 }
108}
diff --git a/subprojects/frontend/src/persistence/compressionMessages.tsx b/subprojects/frontend/src/persistence/compressionMessages.tsx
new file mode 100644
index 00000000..37c6e8cd
--- /dev/null
+++ b/subprojects/frontend/src/persistence/compressionMessages.tsx
@@ -0,0 +1,56 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { z } from 'zod';
8
9/* eslint-disable @typescript-eslint/no-redeclare -- Declare types with their companion objects */
10
11export const CompressRequest = z.object({
12 request: z.literal('compress'),
13 text: z.string(),
14});
15
16export type CompressRequest = z.infer<typeof CompressRequest>;
17
18export const DecompressRequest = z.object({
19 request: z.literal('decompress'),
20 compressedText: z.string(),
21});
22
23export type DecompressRequest = z.infer<typeof DecompressRequest>;
24
25export const CompressorRequest = z.union([CompressRequest, DecompressRequest]);
26
27export type CompressorRequest = z.infer<typeof CompressorRequest>;
28
29export const CompressResponse = z.object({
30 response: z.literal('compressed'),
31 compressedText: z.string(),
32});
33
34export type CompressResponse = z.infer<typeof CompressResponse>;
35
36export const DecompressResponse = z.object({
37 response: z.literal('decompressed'),
38 text: z.string(),
39});
40
41export type DecompressResponse = z.infer<typeof DecompressResponse>;
42
43export const ErrorResponse = z.object({
44 response: z.literal('error'),
45 message: z.string(),
46});
47
48export type ErrorResponse = z.infer<typeof ErrorResponse>;
49
50export const CompressorResponse = z.union([
51 CompressResponse,
52 DecompressResponse,
53 ErrorResponse,
54]);
55
56export type CompressorResponse = z.infer<typeof CompressorResponse>;
diff --git a/subprojects/frontend/src/persistence/compressionWorker.ts b/subprojects/frontend/src/persistence/compressionWorker.ts
new file mode 100644
index 00000000..7b93b20b
--- /dev/null
+++ b/subprojects/frontend/src/persistence/compressionWorker.ts
@@ -0,0 +1,98 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import type { Zstd } from '@hpcc-js/wasm';
8// We need to use a deep import for proper code splitting with `vite-plugin-pwa`.
9// @ts-expect-error Typescript doesn't find the declarations for the deep import.
10import { Zstd as zstdLoader } from '@hpcc-js/wasm/zstd';
11
12import type {
13 CompressResponse,
14 CompressorRequest,
15 DecompressResponse,
16 ErrorResponse,
17} from './compressionMessages';
18
19const CONTENT_TYPE = 'application/octet-stream';
20
21const URI_PREFIX = `data:${CONTENT_TYPE};base64,`;
22
23async function base64Encode(buffer: Uint8Array): Promise<string> {
24 const uri = await new Promise((resolve, reject) => {
25 const reader = new FileReader();
26 reader.onload = () => resolve(reader.result);
27 reader.onerror = () => reject(reader.error);
28 reader.readAsDataURL(new File([buffer], '', { type: CONTENT_TYPE }));
29 });
30 if (typeof uri !== 'string') {
31 throw new Error(`Unexpected FileReader result type: ${typeof uri}`);
32 }
33 const base64 = uri.substring(URI_PREFIX.length);
34 return base64.replace(/[+/]/g, (c) => {
35 if (c === '+') {
36 return '-';
37 }
38 if (c === '/') {
39 return '_';
40 }
41 return c;
42 });
43}
44
45async function base64Decode(compressedText: string): Promise<Uint8Array> {
46 const base64 = compressedText.replace(/[-_]/g, (c) => {
47 if (c === '-') {
48 return '+';
49 }
50 if (c === '_') {
51 return '/';
52 }
53 return c;
54 });
55 const result = await fetch(`${URI_PREFIX}${base64}`);
56 return new Uint8Array(await result.arrayBuffer());
57}
58
59let zstd: Awaited<ReturnType<typeof Zstd.load>> | undefined;
60
61globalThis.onmessage = (event) => {
62 (async () => {
63 if (zstd === undefined) {
64 // Since we don't have types for the deep import, we have to cast here.
65 zstd = await (zstdLoader as { load: typeof Zstd.load }).load();
66 }
67 // Since the render thread will only send us valid messages,
68 // we can save a bit of bundle size by using a cast instead of `parse`
69 // to avoid having to include `zod` in the worker.
70 const message = event.data as CompressorRequest;
71 if (message.request === 'compress') {
72 const encoder = new TextEncoder();
73 const encodedBuffer = encoder.encode(message.text);
74 const compressedBuffer = zstd.compress(encodedBuffer, 3);
75 const compressedText = await base64Encode(compressedBuffer);
76 globalThis.postMessage({
77 response: 'compressed',
78 compressedText,
79 } satisfies CompressResponse);
80 } else if (message.request === 'decompress') {
81 const decodedBuffer = await base64Decode(message.compressedText);
82 const uncompressedBuffer = zstd.decompress(decodedBuffer);
83 const decoder = new TextDecoder();
84 const text = decoder.decode(uncompressedBuffer);
85 globalThis.postMessage({
86 response: 'decompressed',
87 text,
88 } satisfies DecompressResponse);
89 } else {
90 throw new Error(`Unknown request: ${JSON.stringify(event.data)}`);
91 }
92 })().catch((error) => {
93 globalThis.postMessage({
94 response: 'error',
95 message: String(error),
96 } satisfies ErrorResponse);
97 });
98};
diff --git a/subprojects/frontend/src/persistence/initialValue.ts b/subprojects/frontend/src/persistence/initialValue.ts
new file mode 100644
index 00000000..25b24813
--- /dev/null
+++ b/subprojects/frontend/src/persistence/initialValue.ts
@@ -0,0 +1,103 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7export default `% Metamodel
8
9abstract class CompositeElement {
10 contains Region[] regions
11}
12
13class Region {
14 contains Vertex[] vertices opposite region
15}
16
17abstract class Vertex {
18 container Region region opposite vertices
19 contains Transition[] outgoingTransition opposite source
20 Transition[] incomingTransition opposite target
21}
22
23class Transition {
24 container Vertex source opposite outgoingTransition
25 Vertex[1] target opposite incomingTransition
26}
27
28abstract class Pseudostate extends Vertex.
29
30abstract class RegularState extends Vertex.
31
32class Entry extends Pseudostate.
33
34class Exit extends Pseudostate.
35
36class Choice extends Pseudostate.
37
38class FinalState extends RegularState.
39
40class State extends RegularState, CompositeElement.
41
42class Statechart extends CompositeElement.
43
44% Constraints
45
46%% Entry
47
48pred entryInRegion(Region r, Entry e) <->
49 vertices(r, e).
50
51error noEntryInRegion(Region r) <->
52 !entryInRegion(r, _).
53
54error multipleEntryInRegion(Region r) <->
55 entryInRegion(r, e1),
56 entryInRegion(r, e2),
57 e1 != e2.
58
59error incomingToEntry(Transition t, Entry e) <->
60 target(t, e).
61
62error noOutgoingTransitionFromEntry(Entry e) <->
63 !source(_, e).
64
65error multipleTransitionFromEntry(Entry e, Transition t1, Transition t2) <->
66 outgoingTransition(e, t1),
67 outgoingTransition(e, t2),
68 t1 != t2.
69
70%% Exit
71
72error outgoingFromExit(Transition t, Exit e) <->
73 source(t, e).
74
75%% Final
76
77error outgoingFromFinal(Transition t, FinalState e) <->
78 source(t, e).
79
80%% State vs Region
81
82pred stateInRegion(Region r, State s) <->
83 vertices(r, s).
84
85error noStateInRegion(Region r) <->
86 !stateInRegion(r, _).
87
88%% Choice
89
90error choiceHasNoOutgoing(Choice c) <->
91 !source(_, c).
92
93error choiceHasNoIncoming(Choice c) <->
94 !target(_, c).
95
96% Instance model
97
98Statechart(sct).
99
100% Scope
101
102scope node = 20..30, Region = 2..*, Choice = 1..*, Statechart += 0.
103`;
diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts
index 70e79764..0532f8df 100644
--- a/subprojects/frontend/src/xtext/UpdateService.ts
+++ b/subprojects/frontend/src/xtext/UpdateService.ts
@@ -56,6 +56,7 @@ export default class UpdateService {
56 constructor( 56 constructor(
57 private readonly store: EditorStore, 57 private readonly store: EditorStore,
58 private readonly webSocketClient: XtextWebSocketClient, 58 private readonly webSocketClient: XtextWebSocketClient,
59 private readonly onUpdate: (text: string) => void,
59 ) { 60 ) {
60 this.resourceName = `${nanoid(7)}.problem`; 61 this.resourceName = `${nanoid(7)}.problem`;
61 this.tracker = new UpdateStateTracker(store); 62 this.tracker = new UpdateStateTracker(store);
@@ -122,6 +123,7 @@ export default class UpdateService {
122 if (!this.tracker.needsUpdate) { 123 if (!this.tracker.needsUpdate) {
123 return; 124 return;
124 } 125 }
126 this.onUpdate(this.store.state.sliceDoc());
125 await this.tracker.runExclusive(() => this.updateExclusive()); 127 await this.tracker.runExclusive(() => this.updateExclusive());
126 } 128 }
127 129
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts
index 7486d737..21ac5750 100644
--- a/subprojects/frontend/src/xtext/XtextClient.ts
+++ b/subprojects/frontend/src/xtext/XtextClient.ts
@@ -49,13 +49,18 @@ export default class XtextClient {
49 constructor( 49 constructor(
50 private readonly store: EditorStore, 50 private readonly store: EditorStore,
51 private readonly pwaStore: PWAStore, 51 private readonly pwaStore: PWAStore,
52 onUpdate: (text: string) => void,
52 ) { 53 ) {
53 this.webSocketClient = new XtextWebSocketClient( 54 this.webSocketClient = new XtextWebSocketClient(
54 () => this.onReconnect(), 55 () => this.onReconnect(),
55 () => this.onDisconnect(), 56 () => this.onDisconnect(),
56 this.onPush.bind(this), 57 this.onPush.bind(this),
57 ); 58 );
58 this.updateService = new UpdateService(store, this.webSocketClient); 59 this.updateService = new UpdateService(
60 store,
61 this.webSocketClient,
62 onUpdate,
63 );
59 this.contentAssistService = new ContentAssistService(this.updateService); 64 this.contentAssistService = new ContentAssistService(this.updateService);
60 this.highlightingService = new HighlightingService( 65 this.highlightingService = new HighlightingService(
61 store, 66 store,
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
index fab94689..cc87917f 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
@@ -23,7 +23,8 @@ public class SecurityHeadersFilter implements Filter {
23 // Use 'data:' for displaying inline SVG backgrounds. 23 // Use 'data:' for displaying inline SVG backgrounds.
24 "img-src 'self' data:; " + 24 "img-src 'self' data:; " +
25 "font-src 'self'; " + 25 "font-src 'self'; " +
26 "connect-src 'self'; " + 26 // Fetch data:application/octet-stream;base64 URIs to unpack compressed URL fragments.
27 "connect-src 'self' data:; " +
27 "manifest-src 'self'; " + 28 "manifest-src 'self'; " +
28 "worker-src 'self' blob:;"); 29 "worker-src 'self' blob:;");
29 httpResponse.setHeader("X-Content-Type-Options", "nosniff"); 30 httpResponse.setHeader("X-Content-Type-Options", "nosniff");