@tindalabs/shield 0.1.0
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.
- package/LICENSE +9 -0
- package/README.md +357 -0
- package/dist/assess.d.ts +16 -0
- package/dist/assess.js +220 -0
- package/dist/config/default-extensions-config.json +103 -0
- package/dist/core/ContentProtector.d.ts +63 -0
- package/dist/core/ContentProtector.js +281 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +2 -0
- package/dist/core/mediator/ContentProtectionMediator.d.ts +86 -0
- package/dist/core/mediator/ContentProtectionMediator.js +238 -0
- package/dist/core/mediator/eventDataTypes.d.ts +112 -0
- package/dist/core/mediator/eventDataTypes.js +23 -0
- package/dist/core/mediator/handlers/abstractEventHandler.d.ts +41 -0
- package/dist/core/mediator/handlers/abstractEventHandler.js +59 -0
- package/dist/core/mediator/handlers/devToolsEventHandler.d.ts +9 -0
- package/dist/core/mediator/handlers/devToolsEventHandler.js +95 -0
- package/dist/core/mediator/handlers/eventHandlerRegistry.d.ts +9 -0
- package/dist/core/mediator/handlers/eventHandlerRegistry.js +34 -0
- package/dist/core/mediator/handlers/extensionEventHandlers.d.ts +40 -0
- package/dist/core/mediator/handlers/extensionEventHandlers.js +140 -0
- package/dist/core/mediator/handlers/iFrameEventHandlers.d.ts +27 -0
- package/dist/core/mediator/handlers/iFrameEventHandlers.js +93 -0
- package/dist/core/mediator/handlers/screenShotEventHandlers.d.ts +34 -0
- package/dist/core/mediator/handlers/screenShotEventHandlers.js +111 -0
- package/dist/core/mediator/protection-event.d.ts +77 -0
- package/dist/core/mediator/protection-event.js +32 -0
- package/dist/core/mediator/types.d.ts +105 -0
- package/dist/core/mediator/types.js +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/otel.d.ts +24 -0
- package/dist/otel.js +83 -0
- package/dist/policy.d.ts +98 -0
- package/dist/policy.js +97 -0
- package/dist/strategies/AbstractStrategy.d.ts +124 -0
- package/dist/strategies/AbstractStrategy.js +256 -0
- package/dist/strategies/ClipboardStrategy.d.ts +67 -0
- package/dist/strategies/ClipboardStrategy.js +291 -0
- package/dist/strategies/ContextMenuStrategy.d.ts +60 -0
- package/dist/strategies/ContextMenuStrategy.js +454 -0
- package/dist/strategies/DevToolsStrategy.d.ts +55 -0
- package/dist/strategies/DevToolsStrategy.js +314 -0
- package/dist/strategies/ExtensionStrategy.d.ts +66 -0
- package/dist/strategies/ExtensionStrategy.js +486 -0
- package/dist/strategies/IFrameStrategy.d.ts +49 -0
- package/dist/strategies/IFrameStrategy.js +255 -0
- package/dist/strategies/KeyboardStrategy.d.ts +35 -0
- package/dist/strategies/KeyboardStrategy.js +130 -0
- package/dist/strategies/PrintStrategy.d.ts +47 -0
- package/dist/strategies/PrintStrategy.js +201 -0
- package/dist/strategies/ScreenshotStrategy.d.ts +90 -0
- package/dist/strategies/ScreenshotStrategy.js +502 -0
- package/dist/strategies/SelectionStrategy.d.ts +49 -0
- package/dist/strategies/SelectionStrategy.js +216 -0
- package/dist/strategies/WatermarkStrategy.d.ts +56 -0
- package/dist/strategies/WatermarkStrategy.js +287 -0
- package/dist/strategies/index.d.ts +10 -0
- package/dist/strategies/index.js +11 -0
- package/dist/types/assessment.d.ts +62 -0
- package/dist/types/assessment.js +1 -0
- package/dist/types/index.d.ts +278 -0
- package/dist/types/index.js +17 -0
- package/dist/utils/DOMObserver.d.ts +68 -0
- package/dist/utils/DOMObserver.js +134 -0
- package/dist/utils/base/LoggableComponent.d.ts +44 -0
- package/dist/utils/base/LoggableComponent.js +56 -0
- package/dist/utils/detectors/AbstractDevToolsDetector.d.ts +98 -0
- package/dist/utils/detectors/AbstractDevToolsDetector.js +127 -0
- package/dist/utils/detectors/dateToStringDetector.d.ts +43 -0
- package/dist/utils/detectors/dateToStringDetector.js +96 -0
- package/dist/utils/detectors/debugLibDetector.d.ts +64 -0
- package/dist/utils/detectors/debugLibDetector.js +195 -0
- package/dist/utils/detectors/debuggerDetector.d.ts +51 -0
- package/dist/utils/detectors/debuggerDetector.js +211 -0
- package/dist/utils/detectors/defineGetterDetector.d.ts +48 -0
- package/dist/utils/detectors/defineGetterDetector.js +150 -0
- package/dist/utils/detectors/detectorInterface.d.ts +36 -0
- package/dist/utils/detectors/detectorInterface.js +1 -0
- package/dist/utils/detectors/devToolsDetectorManager.d.ts +88 -0
- package/dist/utils/detectors/devToolsDetectorManager.js +243 -0
- package/dist/utils/detectors/funcToStringDetector.d.ts +43 -0
- package/dist/utils/detectors/funcToStringDetector.js +90 -0
- package/dist/utils/detectors/regToStringDetector.d.ts +43 -0
- package/dist/utils/detectors/regToStringDetector.js +129 -0
- package/dist/utils/detectors/sizeDetector.d.ts +54 -0
- package/dist/utils/detectors/sizeDetector.js +134 -0
- package/dist/utils/detectors/timingDetector.d.ts +55 -0
- package/dist/utils/detectors/timingDetector.js +143 -0
- package/dist/utils/dom.d.ts +20 -0
- package/dist/utils/dom.js +83 -0
- package/dist/utils/environment.d.ts +29 -0
- package/dist/utils/environment.js +267 -0
- package/dist/utils/eventManager.d.ts +162 -0
- package/dist/utils/eventManager.js +548 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/intervalManager.d.ts +91 -0
- package/dist/utils/intervalManager.js +221 -0
- package/dist/utils/keyboardShortcutManager/keyboardShortcutManager.d.ts +41 -0
- package/dist/utils/keyboardShortcutManager/keyboardShortcutManager.js +135 -0
- package/dist/utils/keyboardShortcutManager/keyboardShortcuts.d.ts +18 -0
- package/dist/utils/keyboardShortcutManager/keyboardShortcuts.js +195 -0
- package/dist/utils/logging/simple/Loggable.d.ts +33 -0
- package/dist/utils/logging/simple/Loggable.js +1 -0
- package/dist/utils/logging/simple/LoggingDelegate.d.ts +42 -0
- package/dist/utils/logging/simple/LoggingDelegate.js +53 -0
- package/dist/utils/logging/simple/SimpleLoggingService.d.ts +39 -0
- package/dist/utils/logging/simple/SimpleLoggingService.js +58 -0
- package/dist/utils/orientation.d.ts +15 -0
- package/dist/utils/orientation.js +32 -0
- package/dist/utils/protectedContentManager.d.ts +155 -0
- package/dist/utils/protectedContentManager.js +424 -0
- package/dist/utils/securityOverlayManager.d.ts +253 -0
- package/dist/utils/securityOverlayManager.js +786 -0
- package/dist/utils/timeoutManager.d.ts +50 -0
- package/dist/utils/timeoutManager.js +113 -0
- package/package.json +61 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { isBrowser } from "../utils/environment";
|
|
2
|
+
import { intervalManager } from "../utils/intervalManager";
|
|
3
|
+
import { AbstractStrategy, StrategyErrorType } from "./AbstractStrategy";
|
|
4
|
+
import { ProtectionEventType } from "../core/mediator/protection-event";
|
|
5
|
+
/**
|
|
6
|
+
* Strategy for preventing content from being embedded in external iframes
|
|
7
|
+
*/
|
|
8
|
+
export class FrameEmbeddingProtectionStrategy extends AbstractStrategy {
|
|
9
|
+
/**
|
|
10
|
+
* Create a new FrameEmbeddingProtectionStrategy
|
|
11
|
+
* @param options Configuration options
|
|
12
|
+
* @param targetElement Element containing sensitive content to protect
|
|
13
|
+
* @param customHandler Optional custom handler for frame embedding detection
|
|
14
|
+
* @param debugMode Enable debug mode for troubleshooting
|
|
15
|
+
*/
|
|
16
|
+
constructor(options, targetElement = null, customHandler, debugMode = false) {
|
|
17
|
+
super("FrameEmbeddingProtectionStrategy", debugMode);
|
|
18
|
+
this.targetElement = null;
|
|
19
|
+
this.isEmbedded = false;
|
|
20
|
+
this.isExternalFrame = false;
|
|
21
|
+
this.intervalId = null;
|
|
22
|
+
this.taskId = null;
|
|
23
|
+
this.parentDomain = null;
|
|
24
|
+
this.options = {
|
|
25
|
+
showOverlay: true,
|
|
26
|
+
overlayOptions: {
|
|
27
|
+
title: "Embedding Not Allowed",
|
|
28
|
+
message: "This content cannot be displayed in an embedded frame.",
|
|
29
|
+
secondaryMessage: "Please visit the original website to view this content.",
|
|
30
|
+
textColor: "white",
|
|
31
|
+
backgroundColor: "rgba(220, 38, 38, 0.9)", // Red with opacity
|
|
32
|
+
},
|
|
33
|
+
hideContent: true,
|
|
34
|
+
allowedDomains: [],
|
|
35
|
+
blockAllFrames: false,
|
|
36
|
+
...options,
|
|
37
|
+
};
|
|
38
|
+
this.targetElement = targetElement;
|
|
39
|
+
this.customHandler = customHandler;
|
|
40
|
+
this.log("Initialized with options:", {
|
|
41
|
+
allowedDomains: this.options.allowedDomains,
|
|
42
|
+
blockAllFrames: this.options.blockAllFrames,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if the page is embedded in an iframe
|
|
47
|
+
*/
|
|
48
|
+
checkIfEmbedded() {
|
|
49
|
+
return (this.safeExecute("checkIfEmbedded", StrategyErrorType.APPLICATION, () => {
|
|
50
|
+
if (!isBrowser())
|
|
51
|
+
return false;
|
|
52
|
+
// Check if the page is in an iframe
|
|
53
|
+
this.isEmbedded = window.self !== window.top;
|
|
54
|
+
if (this.isEmbedded) {
|
|
55
|
+
// Check if it's an external iframe (cross-origin)
|
|
56
|
+
try {
|
|
57
|
+
// If we can access parent.location.hostname, it's same-origin
|
|
58
|
+
const parentHostname = window.parent.location.hostname;
|
|
59
|
+
const currentHostname = window.location.hostname;
|
|
60
|
+
this.parentDomain = parentHostname;
|
|
61
|
+
this.isExternalFrame = parentHostname !== currentHostname;
|
|
62
|
+
// Check if the parent domain is in the allowed domains list
|
|
63
|
+
if (this.isExternalFrame && this.options.allowedDomains && this.options.allowedDomains.length > 0) {
|
|
64
|
+
this.isExternalFrame = !this.options.allowedDomains.includes(parentHostname);
|
|
65
|
+
}
|
|
66
|
+
this.log(`Embedded in iframe. Parent: ${parentHostname}, Current: ${currentHostname}`);
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
// If we can't access parent.location, it's definitely cross-origin
|
|
71
|
+
this.isExternalFrame = true;
|
|
72
|
+
this.parentDomain = null;
|
|
73
|
+
this.log("Embedded in cross-origin iframe (security exception)");
|
|
74
|
+
}
|
|
75
|
+
// If blockAllFrames is true, treat all frames as external
|
|
76
|
+
if (this.options.blockAllFrames) {
|
|
77
|
+
this.isExternalFrame = true;
|
|
78
|
+
this.log("Blocking all frames regardless of origin");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return this.isEmbedded && this.isExternalFrame;
|
|
82
|
+
}) || false);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Publish frame embedding detection event
|
|
86
|
+
*/
|
|
87
|
+
publishFrameEmbeddingEvent() {
|
|
88
|
+
return this.safeExecute("publishFrameEmbeddingEvent", StrategyErrorType.APPLICATION, () => {
|
|
89
|
+
if (!isBrowser() || !this.mediator)
|
|
90
|
+
return;
|
|
91
|
+
// Publish frame embedding detected event
|
|
92
|
+
this.mediator.publish({
|
|
93
|
+
type: ProtectionEventType.FRAME_EMBEDDING_DETECTED,
|
|
94
|
+
source: this.STRATEGY_NAME,
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
data: {
|
|
97
|
+
isEmbedded: this.isEmbedded,
|
|
98
|
+
isExternalFrame: this.isExternalFrame,
|
|
99
|
+
parentDomain: this.parentDomain || undefined,
|
|
100
|
+
targetElement: this.targetElement,
|
|
101
|
+
showOverlay: this.options.showOverlay,
|
|
102
|
+
hideContent: this.options.hideContent,
|
|
103
|
+
overlayOptions: this.options.overlayOptions,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
this.log("Published FRAME_EMBEDDING_DETECTED event");
|
|
107
|
+
// Call custom handler if provided
|
|
108
|
+
if (this.customHandler) {
|
|
109
|
+
this.customHandler(this.isEmbedded, this.isExternalFrame);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Apply the protection strategy
|
|
115
|
+
*/
|
|
116
|
+
apply() {
|
|
117
|
+
return this.safeExecute("apply", StrategyErrorType.APPLICATION, () => {
|
|
118
|
+
if (this.isAppliedFlag) {
|
|
119
|
+
this.log("Protection already applied");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// Check if embedded in an external iframe
|
|
123
|
+
const isExternallyEmbedded = this.checkIfEmbedded();
|
|
124
|
+
if (isExternallyEmbedded) {
|
|
125
|
+
this.publishFrameEmbeddingEvent();
|
|
126
|
+
}
|
|
127
|
+
// Register with IntervalManager for periodic checks
|
|
128
|
+
try {
|
|
129
|
+
this.taskId = intervalManager.registerTask("iframe-protection", () => this.safeExecute("intervalTask", StrategyErrorType.APPLICATION, () => {
|
|
130
|
+
const currentlyEmbedded = this.checkIfEmbedded();
|
|
131
|
+
if (currentlyEmbedded) {
|
|
132
|
+
// Publish event to check if we need to apply protection
|
|
133
|
+
this.publishFrameEmbeddingEvent();
|
|
134
|
+
}
|
|
135
|
+
}), 2000);
|
|
136
|
+
if (!this.taskId) {
|
|
137
|
+
this.handleError(StrategyErrorType.APPLICATION, "Failed to register interval task", new Error("Task registration returned empty ID"));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (intervalError) {
|
|
141
|
+
this.handleError(StrategyErrorType.APPLICATION, "Error registering interval task", intervalError);
|
|
142
|
+
// Try to use a fallback interval method if IntervalManager fails
|
|
143
|
+
this.intervalId = window.setInterval(() => {
|
|
144
|
+
this.safeExecute("fallbackInterval", StrategyErrorType.APPLICATION, () => {
|
|
145
|
+
const currentlyEmbedded = this.checkIfEmbedded();
|
|
146
|
+
if (currentlyEmbedded) {
|
|
147
|
+
this.publishFrameEmbeddingEvent();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}, 2000);
|
|
151
|
+
this.log("Using fallback interval method after IntervalManager failure");
|
|
152
|
+
}
|
|
153
|
+
this.isAppliedFlag = true;
|
|
154
|
+
this.log("Protection applied");
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Remove the protection strategy
|
|
159
|
+
*/
|
|
160
|
+
remove() {
|
|
161
|
+
return this.safeExecute("remove", StrategyErrorType.REMOVAL, () => {
|
|
162
|
+
if (!this.isAppliedFlag) {
|
|
163
|
+
this.log("Protection not applied");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Clear interval via IntervalManager
|
|
167
|
+
if (this.taskId !== null) {
|
|
168
|
+
intervalManager.unregisterTask(this.taskId);
|
|
169
|
+
this.taskId = null;
|
|
170
|
+
this.log("Interval task unregistered");
|
|
171
|
+
}
|
|
172
|
+
// Also clear the old interval for backwards compatibility or if both are used
|
|
173
|
+
if (this.intervalId !== null) {
|
|
174
|
+
window.clearInterval(this.intervalId);
|
|
175
|
+
this.intervalId = null;
|
|
176
|
+
this.log("Legacy interval cleared");
|
|
177
|
+
}
|
|
178
|
+
// Call parent class remove method to handle event cleanup
|
|
179
|
+
super.remove();
|
|
180
|
+
// Publish STRATEGY_REMOVED event
|
|
181
|
+
if (this.mediator) {
|
|
182
|
+
this.mediator.publish({
|
|
183
|
+
type: ProtectionEventType.STRATEGY_REMOVED,
|
|
184
|
+
source: this.STRATEGY_NAME,
|
|
185
|
+
timestamp: Date.now(),
|
|
186
|
+
data: {
|
|
187
|
+
strategyName: this.STRATEGY_NAME,
|
|
188
|
+
reason: "strategy_removed",
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
// Remove meta tags if we added them
|
|
193
|
+
if (isBrowser()) {
|
|
194
|
+
const xfoMeta = document.querySelector('meta[http-equiv="X-Frame-Options"]');
|
|
195
|
+
if (xfoMeta && xfoMeta.parentNode) {
|
|
196
|
+
xfoMeta.parentNode.removeChild(xfoMeta);
|
|
197
|
+
}
|
|
198
|
+
const cspMeta = document.querySelector('meta[http-equiv="Content-Security-Policy"]');
|
|
199
|
+
if (cspMeta && cspMeta.parentNode) {
|
|
200
|
+
cspMeta.parentNode.removeChild(cspMeta);
|
|
201
|
+
}
|
|
202
|
+
this.log("Removed meta tags");
|
|
203
|
+
}
|
|
204
|
+
// Always reset state
|
|
205
|
+
this.isAppliedFlag = false;
|
|
206
|
+
this.isEmbedded = false;
|
|
207
|
+
this.isExternalFrame = false;
|
|
208
|
+
this.parentDomain = null;
|
|
209
|
+
this.log("Protection removed");
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Update strategy options
|
|
214
|
+
* @param options Options to update
|
|
215
|
+
*/
|
|
216
|
+
updateOptions(options) {
|
|
217
|
+
return this.safeExecute("updateOptions", StrategyErrorType.OPTION_UPDATE, () => {
|
|
218
|
+
if (!options) {
|
|
219
|
+
this.handleError(StrategyErrorType.OPTION_UPDATE, "Invalid options in updateOptions", new Error("Options is null or undefined"));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const typedOptions = options;
|
|
223
|
+
this.log("Updating options", typedOptions);
|
|
224
|
+
// Store previous options for comparison
|
|
225
|
+
const previousOptions = { ...this.options };
|
|
226
|
+
this.options = {
|
|
227
|
+
...this.options,
|
|
228
|
+
...typedOptions,
|
|
229
|
+
};
|
|
230
|
+
// Check if any critical options changed that would require reapplying protection
|
|
231
|
+
const criticalOptionsChanged = previousOptions.blockAllFrames !== this.options.blockAllFrames ||
|
|
232
|
+
JSON.stringify(previousOptions.allowedDomains) !== JSON.stringify(this.options.allowedDomains);
|
|
233
|
+
if (this.isAppliedFlag &&
|
|
234
|
+
(criticalOptionsChanged ||
|
|
235
|
+
typedOptions.overlayOptions?.title ||
|
|
236
|
+
typedOptions.overlayOptions?.message ||
|
|
237
|
+
typedOptions.overlayOptions?.backgroundColor)) {
|
|
238
|
+
// Re-check if we're embedded and update protection if needed
|
|
239
|
+
const isExternallyEmbedded = this.checkIfEmbedded();
|
|
240
|
+
if (isExternallyEmbedded) {
|
|
241
|
+
// Publish updated event
|
|
242
|
+
this.publishFrameEmbeddingEvent();
|
|
243
|
+
this.log("Reapplied frame protection with updated options");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Set debug mode
|
|
250
|
+
* @param enabled Whether debug mode should be enabled
|
|
251
|
+
*/
|
|
252
|
+
setDebugMode(enabled) {
|
|
253
|
+
super.setDebugMode(enabled);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CustomEventHandlers } from "../types";
|
|
2
|
+
import { AbstractStrategy } from "./AbstractStrategy";
|
|
3
|
+
/**
|
|
4
|
+
* Strategy for preventing keyboard shortcuts
|
|
5
|
+
*/
|
|
6
|
+
export declare class KeyboardStrategy extends AbstractStrategy {
|
|
7
|
+
private handler;
|
|
8
|
+
private customHandler?;
|
|
9
|
+
private eventId;
|
|
10
|
+
private shortcutManager;
|
|
11
|
+
private categories;
|
|
12
|
+
/**
|
|
13
|
+
* Create a new KeyboardStrategy
|
|
14
|
+
* @param customHandler Optional custom handler for blocked keyboard shortcuts
|
|
15
|
+
* @param debugMode Enable debug mode for troubleshooting
|
|
16
|
+
*/
|
|
17
|
+
constructor(customHandler?: CustomEventHandlers["onKeyboardShortcutBlocked"], debugMode?: boolean);
|
|
18
|
+
/**
|
|
19
|
+
* Handle keydown events
|
|
20
|
+
*/
|
|
21
|
+
private handleKeyDown;
|
|
22
|
+
/**
|
|
23
|
+
* Apply keyboard protection
|
|
24
|
+
*/
|
|
25
|
+
apply(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Remove keyboard protection
|
|
28
|
+
*/
|
|
29
|
+
remove(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Update strategy options
|
|
32
|
+
* @param options Options to update
|
|
33
|
+
*/
|
|
34
|
+
updateOptions(options: Record<string, unknown>): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { isBrowser } from "../utils/environment";
|
|
2
|
+
import { AbstractStrategy, StrategyErrorType } from "./AbstractStrategy";
|
|
3
|
+
import { eventManager } from "../utils/eventManager";
|
|
4
|
+
import { KeyboardShortcutManager } from "../utils/keyboardShortcutManager/keyboardShortcutManager";
|
|
5
|
+
import { ShortcutCategory } from "../utils/keyboardShortcutManager/keyboardShortcuts";
|
|
6
|
+
/**
|
|
7
|
+
* Strategy for preventing keyboard shortcuts
|
|
8
|
+
*/
|
|
9
|
+
export class KeyboardStrategy extends AbstractStrategy {
|
|
10
|
+
/**
|
|
11
|
+
* Create a new KeyboardStrategy
|
|
12
|
+
* @param customHandler Optional custom handler for blocked keyboard shortcuts
|
|
13
|
+
* @param debugMode Enable debug mode for troubleshooting
|
|
14
|
+
*/
|
|
15
|
+
constructor(customHandler, debugMode = false) {
|
|
16
|
+
super("KeyboardStrategy", debugMode);
|
|
17
|
+
this.eventId = "";
|
|
18
|
+
this.categories = [
|
|
19
|
+
ShortcutCategory.COPY,
|
|
20
|
+
ShortcutCategory.SAVE,
|
|
21
|
+
ShortcutCategory.PRINT,
|
|
22
|
+
ShortcutCategory.VIEW_SOURCE,
|
|
23
|
+
ShortcutCategory.DEVTOOLS,
|
|
24
|
+
];
|
|
25
|
+
this.customHandler = customHandler;
|
|
26
|
+
this.handler = this.handleKeyDown.bind(this);
|
|
27
|
+
this.shortcutManager = KeyboardShortcutManager.getInstance();
|
|
28
|
+
this.log("Initialized with shortcut categories:", this.categories);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Handle keydown events
|
|
32
|
+
*/
|
|
33
|
+
handleKeyDown(e) {
|
|
34
|
+
return this.safeExecute("handleKeyDown", StrategyErrorType.EVENT_HANDLING, () => {
|
|
35
|
+
// Cast to KeyboardEvent
|
|
36
|
+
const keyEvent = e;
|
|
37
|
+
// Use the shortcut manager to detect shortcuts
|
|
38
|
+
const shortcut = this.shortcutManager.matchesShortcut(keyEvent, this.categories);
|
|
39
|
+
if (shortcut) {
|
|
40
|
+
// Prevent the default action
|
|
41
|
+
keyEvent.preventDefault();
|
|
42
|
+
keyEvent.stopPropagation();
|
|
43
|
+
this.log(`Blocked keyboard shortcut: ${shortcut.id} (${this.shortcutManager.getShortcutDescription(shortcut)})`);
|
|
44
|
+
// Call custom handler if provided
|
|
45
|
+
if (this.customHandler) {
|
|
46
|
+
this.customHandler(keyEvent);
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Apply keyboard protection
|
|
55
|
+
*/
|
|
56
|
+
apply() {
|
|
57
|
+
return this.safeExecute("apply", StrategyErrorType.APPLICATION, () => {
|
|
58
|
+
if (this.isAppliedFlag) {
|
|
59
|
+
this.log("Protection already applied");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (isBrowser()) {
|
|
63
|
+
// Register the keydown event using our registerEvent method from AbstractStrategy
|
|
64
|
+
this.eventId = this.registerEvent(document, "keydown", this.handler, {
|
|
65
|
+
capture: true,
|
|
66
|
+
priority: 10, // High priority to ensure it runs before other handlers
|
|
67
|
+
});
|
|
68
|
+
this.isAppliedFlag = true;
|
|
69
|
+
this.log("Keyboard protection applied", {
|
|
70
|
+
hasCustomHandler: !!this.customHandler,
|
|
71
|
+
eventId: this.eventId,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
this.log("Browser environment not available, protection not applied");
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Remove keyboard protection
|
|
81
|
+
*/
|
|
82
|
+
remove() {
|
|
83
|
+
return this.safeExecute("remove", StrategyErrorType.REMOVAL, () => {
|
|
84
|
+
if (!this.isAppliedFlag) {
|
|
85
|
+
this.log("Protection not applied");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (isBrowser()) {
|
|
89
|
+
// Try multiple removal strategies for robustness
|
|
90
|
+
// 1. Try removing by specific event ID if we have it
|
|
91
|
+
if (this.eventId) {
|
|
92
|
+
const removed = eventManager.removeEventListener(document, this.eventId);
|
|
93
|
+
this.log(`Removed event by ID ${this.eventId}: ${removed}`);
|
|
94
|
+
}
|
|
95
|
+
// 2. Remove all events for this owner using the parent class method
|
|
96
|
+
this.removeEventsByOwner();
|
|
97
|
+
// 3. Try direct DOM removal as a last resort
|
|
98
|
+
try {
|
|
99
|
+
document.removeEventListener("keydown", this.handler, { capture: true });
|
|
100
|
+
this.log("Removed event via direct DOM API");
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
102
|
+
}
|
|
103
|
+
catch (domError) {
|
|
104
|
+
// Ignore errors in direct DOM removal
|
|
105
|
+
}
|
|
106
|
+
this.eventId = "";
|
|
107
|
+
this.isAppliedFlag = false;
|
|
108
|
+
this.log("Keyboard protection removed");
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Update strategy options
|
|
114
|
+
* @param options Options to update
|
|
115
|
+
*/
|
|
116
|
+
updateOptions(options) {
|
|
117
|
+
return this.safeExecute("updateOptions", StrategyErrorType.OPTION_UPDATE, () => {
|
|
118
|
+
this.log("Updating options", options);
|
|
119
|
+
// Handle debug mode if present
|
|
120
|
+
if (options.debugMode !== undefined) {
|
|
121
|
+
this.setDebugMode(!!options.debugMode);
|
|
122
|
+
}
|
|
123
|
+
// Handle categories if present
|
|
124
|
+
if (options.categories !== undefined && Array.isArray(options.categories)) {
|
|
125
|
+
this.categories = options.categories;
|
|
126
|
+
this.log("Updated shortcut categories:", this.categories);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CustomEventHandlers } from "../types";
|
|
2
|
+
import { AbstractStrategy } from "./AbstractStrategy";
|
|
3
|
+
/**
|
|
4
|
+
* Strategy for preventing printing
|
|
5
|
+
*/
|
|
6
|
+
export declare class PrintStrategy extends AbstractStrategy {
|
|
7
|
+
private beforePrintHandler;
|
|
8
|
+
private afterPrintHandler;
|
|
9
|
+
private styleElement;
|
|
10
|
+
private customHandler?;
|
|
11
|
+
/**
|
|
12
|
+
* Create a new PrintStrategy
|
|
13
|
+
* @param customHandler Optional custom handler for print attempts
|
|
14
|
+
* @param debugMode Enable debug mode for troubleshooting
|
|
15
|
+
*/
|
|
16
|
+
constructor(customHandler?: CustomEventHandlers["onPrintAttempt"], debugMode?: boolean);
|
|
17
|
+
/**
|
|
18
|
+
* Handle beforeprint event
|
|
19
|
+
*/
|
|
20
|
+
private handleBeforePrint;
|
|
21
|
+
/**
|
|
22
|
+
* Handle afterprint event
|
|
23
|
+
*/
|
|
24
|
+
private handleAfterPrint;
|
|
25
|
+
/**
|
|
26
|
+
* Create and inject print-blocking CSS
|
|
27
|
+
*/
|
|
28
|
+
private injectPrintStyles;
|
|
29
|
+
/**
|
|
30
|
+
* Remove print-blocking CSS
|
|
31
|
+
*/
|
|
32
|
+
private removePrintStyles;
|
|
33
|
+
/**
|
|
34
|
+
* Apply print protection
|
|
35
|
+
*/
|
|
36
|
+
apply(): void;
|
|
37
|
+
/**
|
|
38
|
+
* Remove print protection
|
|
39
|
+
* Override the base implementation to handle additional cleanup
|
|
40
|
+
*/
|
|
41
|
+
remove(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Update strategy options
|
|
44
|
+
* @param options Options to update
|
|
45
|
+
*/
|
|
46
|
+
updateOptions(options: Record<string, unknown>): void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { isBrowser, isPrintSupported, isBeforePrintSupported } from "../utils/environment";
|
|
2
|
+
import { AbstractStrategy, StrategyErrorType } from "./AbstractStrategy";
|
|
3
|
+
/**
|
|
4
|
+
* Strategy for preventing printing
|
|
5
|
+
*/
|
|
6
|
+
export class PrintStrategy extends AbstractStrategy {
|
|
7
|
+
/**
|
|
8
|
+
* Create a new PrintStrategy
|
|
9
|
+
* @param customHandler Optional custom handler for print attempts
|
|
10
|
+
* @param debugMode Enable debug mode for troubleshooting
|
|
11
|
+
*/
|
|
12
|
+
constructor(customHandler, debugMode = false) {
|
|
13
|
+
super("PrintStrategy", debugMode);
|
|
14
|
+
this.styleElement = null;
|
|
15
|
+
this.customHandler = customHandler;
|
|
16
|
+
this.beforePrintHandler = this.handleBeforePrint.bind(this);
|
|
17
|
+
this.afterPrintHandler = this.handleAfterPrint.bind(this);
|
|
18
|
+
this.log("Initialized with print support:", {
|
|
19
|
+
printSupported: isPrintSupported(),
|
|
20
|
+
beforePrintSupported: isBeforePrintSupported(),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Handle beforeprint event
|
|
25
|
+
*/
|
|
26
|
+
handleBeforePrint(e) {
|
|
27
|
+
return this.safeExecute("handleBeforePrint", StrategyErrorType.EVENT_HANDLING, () => {
|
|
28
|
+
this.log("Print attempt detected");
|
|
29
|
+
if (this.customHandler) {
|
|
30
|
+
this.customHandler(e);
|
|
31
|
+
}
|
|
32
|
+
// Stop printing
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
if (isBrowser()) {
|
|
35
|
+
window.stop();
|
|
36
|
+
this.log("Print operation stopped");
|
|
37
|
+
}
|
|
38
|
+
}, 0);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Handle afterprint event
|
|
43
|
+
*/
|
|
44
|
+
handleAfterPrint(_e) {
|
|
45
|
+
return this.safeExecute("handleAfterPrint", StrategyErrorType.EVENT_HANDLING, () => {
|
|
46
|
+
this.log("Print operation completed or was cancelled");
|
|
47
|
+
// Cleanup if needed
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create and inject print-blocking CSS
|
|
52
|
+
*/
|
|
53
|
+
injectPrintStyles() {
|
|
54
|
+
return this.safeExecute("injectPrintStyles", StrategyErrorType.APPLICATION, () => {
|
|
55
|
+
if (!isBrowser())
|
|
56
|
+
return;
|
|
57
|
+
this.styleElement = document.createElement("style");
|
|
58
|
+
this.styleElement.setAttribute("type", "text/css");
|
|
59
|
+
this.styleElement.setAttribute("data-content-security", "print-blocker");
|
|
60
|
+
const css = `
|
|
61
|
+
@media print {
|
|
62
|
+
body * {
|
|
63
|
+
display: none !important;
|
|
64
|
+
}
|
|
65
|
+
body:after {
|
|
66
|
+
content: "Printing is disabled for this content";
|
|
67
|
+
display: block !important;
|
|
68
|
+
font-size: 24px;
|
|
69
|
+
text-align: center;
|
|
70
|
+
margin: 100px auto;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
`;
|
|
74
|
+
this.styleElement.textContent = css;
|
|
75
|
+
document.head.appendChild(this.styleElement);
|
|
76
|
+
this.log("Print-blocking CSS injected");
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Remove print-blocking CSS
|
|
81
|
+
*/
|
|
82
|
+
removePrintStyles() {
|
|
83
|
+
return this.safeExecute("removePrintStyles", StrategyErrorType.REMOVAL, () => {
|
|
84
|
+
if (!this.styleElement || !isBrowser())
|
|
85
|
+
return;
|
|
86
|
+
// First try to remove by reference
|
|
87
|
+
if (this.styleElement.parentNode) {
|
|
88
|
+
this.styleElement.parentNode.removeChild(this.styleElement);
|
|
89
|
+
this.styleElement = null;
|
|
90
|
+
this.log("Print-blocking CSS removed");
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// If reference is stale, try to find by attribute
|
|
94
|
+
const styles = document.querySelectorAll('style[data-content-security="print-blocker"]');
|
|
95
|
+
styles.forEach((style) => {
|
|
96
|
+
if (style.parentNode) {
|
|
97
|
+
style.parentNode.removeChild(style);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.styleElement = null;
|
|
101
|
+
if (styles.length > 0) {
|
|
102
|
+
this.log(`Removed ${styles.length} print-blocking styles by selector`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Apply print protection
|
|
109
|
+
*/
|
|
110
|
+
apply() {
|
|
111
|
+
return this.safeExecute("apply", StrategyErrorType.APPLICATION, () => {
|
|
112
|
+
if (this.isAppliedFlag) {
|
|
113
|
+
this.log("Protection already applied");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.log("Applying print protection", {
|
|
117
|
+
hasCustomHandler: !!this.customHandler,
|
|
118
|
+
printSupported: isPrintSupported(),
|
|
119
|
+
beforePrintSupported: isBeforePrintSupported(),
|
|
120
|
+
});
|
|
121
|
+
if (isBrowser()) {
|
|
122
|
+
// Only add event listeners if the events are supported
|
|
123
|
+
if (isPrintSupported()) {
|
|
124
|
+
// Use the registerEvent method from AbstractStrategy
|
|
125
|
+
const afterPrintId = this.registerEvent(window, "afterprint", this.afterPrintHandler, { priority: 10 });
|
|
126
|
+
if (afterPrintId) {
|
|
127
|
+
this.log(`Registered afterprint event with ID ${afterPrintId}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (isBeforePrintSupported()) {
|
|
131
|
+
// Use the registerEvent method from AbstractStrategy
|
|
132
|
+
const beforePrintId = this.registerEvent(window, "beforeprint", this.beforePrintHandler, { priority: 10 });
|
|
133
|
+
if (beforePrintId) {
|
|
134
|
+
this.log(`Registered beforeprint event with ID ${beforePrintId}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
this.injectPrintStyles();
|
|
138
|
+
this.isAppliedFlag = true;
|
|
139
|
+
this.log(`Protection applied with ${this.eventIds.length} event handlers`);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Remove print protection
|
|
145
|
+
* Override the base implementation to handle additional cleanup
|
|
146
|
+
*/
|
|
147
|
+
remove() {
|
|
148
|
+
return this.safeExecute("remove", StrategyErrorType.REMOVAL, () => {
|
|
149
|
+
if (!this.isAppliedFlag) {
|
|
150
|
+
this.log("Protection not applied");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (isBrowser()) {
|
|
154
|
+
// Remove all events for this owner using the parent class method
|
|
155
|
+
this.removeEventsByOwner();
|
|
156
|
+
// Try direct DOM removal as a fallback
|
|
157
|
+
try {
|
|
158
|
+
if (isPrintSupported()) {
|
|
159
|
+
window.removeEventListener("afterprint", this.afterPrintHandler);
|
|
160
|
+
}
|
|
161
|
+
if (isBeforePrintSupported()) {
|
|
162
|
+
window.removeEventListener("beforeprint", this.beforePrintHandler);
|
|
163
|
+
}
|
|
164
|
+
this.log("Removed events via direct DOM API");
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
// Ignore errors in direct DOM removal
|
|
169
|
+
}
|
|
170
|
+
// Remove print styles
|
|
171
|
+
this.removePrintStyles();
|
|
172
|
+
// Clear the event IDs array (parent class method will do this too, but being explicit)
|
|
173
|
+
this.eventIds = [];
|
|
174
|
+
this.isAppliedFlag = false;
|
|
175
|
+
this.log("Print protection removed");
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Update strategy options
|
|
181
|
+
* @param options Options to update
|
|
182
|
+
*/
|
|
183
|
+
updateOptions(options) {
|
|
184
|
+
return this.safeExecute("updateOptions", StrategyErrorType.OPTION_UPDATE, () => {
|
|
185
|
+
this.log("Updating options", options);
|
|
186
|
+
// Handle debug mode if present
|
|
187
|
+
if (options.debugMode !== undefined) {
|
|
188
|
+
this.setDebugMode(!!options.debugMode);
|
|
189
|
+
}
|
|
190
|
+
// Handle blockMessage if present
|
|
191
|
+
if (options.blockMessage && typeof options.blockMessage === "string") {
|
|
192
|
+
// If we're already applied, we need to update the CSS
|
|
193
|
+
if (this.isAppliedFlag) {
|
|
194
|
+
this.removePrintStyles();
|
|
195
|
+
this.injectPrintStyles();
|
|
196
|
+
this.log("Updated print styles with new message");
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|