Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix site copy/cut/paste/text selection restrictions for password/input fields #974

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ const otherFeatures = /** @type {const} */([
'webCompat',
'windowsPermissionUsage',
'brokerProtection',
'performanceMetrics'
'performanceMetrics',
'copyPasteMenuSelectionOverride'
])

/** @typedef {baseFeatures[number]|otherFeatures[number]} FeatureName */
Expand All @@ -36,7 +37,8 @@ export const platformSupport = {
'duckPlayer',
'brokerProtection',
'performanceMetrics',
'clickToLoad'
'clickToLoad',
'copyPasteMenuSelectionOverride'
],
android: [
...baseFeatures,
Expand Down
215 changes: 215 additions & 0 deletions src/features/copy-paste-menu-selection-override.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import ContentFeature from "../content-feature.js";

Check failure on line 1 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Strings must use singlequote

Check failure on line 1 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Extra semicolon

export default class CopyPasteMenuSelectionOverride extends ContentFeature {
init () {
try {
(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need this.

var lastContextMenuEventX = -1;

Check warning on line 7 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Unexpected var, use let or const instead

Check failure on line 7 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Extra semicolon
var lastContextMenuEventY = -1;

Check warning on line 8 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Unexpected var, use let or const instead

Check failure on line 8 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Extra semicolon
var lastContextMenuEventWasPrevented = false;

Check warning on line 9 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Unexpected var, use let or const instead

Check failure on line 9 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Extra semicolon
var isSelectionColorOverrideStyleAdded = false;

Check warning on line 10 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Unexpected var, use let or const instead

Check failure on line 10 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Extra semicolon

// - Suppress alerts displayed on right click
document.addEventListener('DOMContentLoaded', function () {
// add alert suppression script to page content world.
// use invisible `_ddg-suppress-alert-flag` element to pass `shouldSuppressAlert` value
// from client content world script handling `contextmenu` events.
var alertSuppressionScript = document.createElement('script');

Check warning on line 17 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Unexpected var, use let or const instead

Check failure on line 17 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Extra semicolon
alertSuppressionScript.textContent = `
(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this needs to be an injected script and instead just a method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason behind this is the ContentScopeScript being injected as a defaultClient scope script and the alert method is called by the document from the page scope, so they don‘t intersect, this hack is here to pass shouldSuppressAlert flag between the page and defaultClient scope and handle the alert call in the page scope. I‘m open for suggestions on how to handle this in "the right way".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah because in "src/features.js" you put it in apple-isolated, put it in the normal.

const flagElement = document.createElement('_ddg-suppress-alert-flag');
flagElement.style.display = 'none';
flagElement.setAttribute('value', 'false');
document.head.appendChild(flagElement);

function shouldSuppressAlert() {
return flagElement.getAttribute('value') === 'true';
}

const originalWindowAlert = window.alert;
// prevent websites displaying alerts on right click
window.alert = function(msg) {
if (shouldSuppressAlert()) {
console.log("suppressed alert on contextmenu: " + msg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the console log.

} else {
originalWindowAlert(msg);
}
};
})();
`;

Check failure on line 39 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Extra semicolon
document.head.appendChild(alertSuppressionScript);

Check failure on line 40 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Extra semicolon
});

Check failure on line 41 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Extra semicolon

// - Disable context menu prevention if user right-clicks at the same point for a second time
document.addEventListener("contextmenu", function(e) {
if (lastContextMenuEventWasPrevented && e.clientX == lastContextMenuEventX && e.clientY == lastContextMenuEventY) {
// second same point click: disable next `contextmenu` event handlers
e.stopImmediatePropagation();
lastContextMenuEventWasPrevented = false;
return true;
}
// remember click position
lastContextMenuEventX = e.clientX;
lastContextMenuEventY = e.clientY;
// prevent websites displaying alerts on right click
const suppressAlertFlagElement = document.getElementsByTagName("_ddg-suppress-alert-flag")[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear why you need a DOM node to store this variable at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to pass value between page and defaultClient scopes, see the comment above.

suppressAlertFlagElement.setAttribute('value', 'true');
// check if the context menu event handling was prevented
setTimeout(function() {
lastContextMenuEventWasPrevented = e.defaultPrevented;
// stop alerts suppression
suppressAlertFlagElement.setAttribute('value', 'false');
}, 0);

return true;
}, true);

// - Always cut/copy selected text
function cutCopyHandler(e) {
const selectedText = window.getSelection().toString();
if (selectedText.trim().length > 0) {
// disable all custom `cut`/`copy` events handlers if there‘s text selected
e.stopImmediatePropagation();
}
return true;
}
document.addEventListener('copy', cutCopyHandler, true);
document.addEventListener('cut', cutCopyHandler, true);

// get key press event key number
function keyCode(e) {
if (window.event) {
return window.event.keyCode;
} else {
return e.which;
}
}

// is there a selected text field?
function isInputActive() {
let activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
return true;
}
return false;
}

// - Fix custom Ctrl/Cmd + X/C/V handlers preventing cut/copy/paste
document.addEventListener('keydown', function(e) {
if (e.ctrlKey || e.metaKey) {
const key = keyCode(e);
// disable custom ctrl/cmd+x/c event handlers if there‘s text selected
if ((key == 67 /* C */ || key == 88 /* X */) && window.getSelection().toString().trim().length > 0) {
e.stopImmediatePropagation();

// disable custom ctrl/cmd+v event handlers if there‘s a text field selected
} else if (key == 86 /* V */ && isInputActive()) {
e.stopImmediatePropagation();
}
}
return true;
}, true);

// - Disable custom `paste` handlers when there‘s a text field selected
document.addEventListener('paste', function(e) {
if (isInputActive()) {
e.stopImmediatePropagation();
}
return true;
}, true);

// - Disable selection start handlers - always allow text selection
document.addEventListener('selectstart', function(e) {
e.stopImmediatePropagation();
return true;
}, true);

// add default selection color override CSS rule
function addSelectionColorOverrideStyleIfNeeded() {
if (isSelectionColorOverrideStyleAdded) {
return;
}

isSelectionColorOverrideStyleAdded = true;
const styleElement = document.createElement('style');

// use default (highlight) selection color
const cssRules = `
*.__ddg-override-selection-background-color:not(input):not(textarea)::selection {
background-color: highlight !important;
}
`;
styleElement.textContent = cssRules;

document.head.appendChild(styleElement);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to add these styles inline rather than injecting DOM nodes.

}

// helper function to enumerate all text nodes in selection range
function forEachTextNodeInRange(range, callback) {
const startContainer = range.startContainer;
const endContainer = range.endContainer;

// helper function to enumerate all text nodes in a node
function enumerateTextNodes(node, callback) {
if (node.nodeType === Node.TEXT_NODE) {
return callback(node);
} else {
for (let child = node.firstChild; child; child = child.nextSibling) {
if (enumerateTextNodes(child, callback) === false) {
return false;
}
}
}
}

// get the common ancestor container of the range
const commonAncestorContainer = range.commonAncestorContainer;
// track whether the enumeration has got into the selection range
let inRange = false;

enumerateTextNodes(commonAncestorContainer, function(node) {
if (node === startContainer) {
inRange = true;
}
// stop enumeration when the selection range end node is reached
var shouldContinue = !(node === endContainer);

Check warning on line 175 in src/features/copy-paste-menu-selection-override.js

View workflow job for this annotation

GitHub Actions / unit (ubuntu-20.04)

Unexpected var, use let or const instead
if (inRange) {
shouldContinue = callback(node) && shouldContinue;
}

return shouldContinue;
});
}

// - Override transparent text selection color resetting it to default selection color
document.addEventListener('selectionchange', function(e) {
const selection = document.getSelection();
if (selection.rangeCount === 0) { return }
const range = selection.getRangeAt(0);

// enumerate all text nodes in selection
forEachTextNodeInRange(range, ((text) => {
const node = text.parentNode;
if (node.classList.contains('__ddg-override-selection-background-color')) {
return true; // selection color is already overriden – proceed next
}

const selectionColor = window.getComputedStyle(node, '::selection').backgroundColor;
// if text node selection color is set to `transparent` (0,0,0,0) or is not set explicitly - set it to default (`highlight`)
if (selectionColor === 'rgba(0, 0, 0, 0)' || selectionColor === 'transparent') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a bunch of other values here that are could cause it not to show. Like visibility:hidden, also does #00000000 appear in computed styles or is it translated to rgba?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like the default in Chrome is rgba(0, 0, 0, 0), I'm not sure how the style applies but we should probably not apply in default case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, the default is detected rgba(0, 0, 0, 0) (i.e. not explicitly defined), but may also be used from the client side to hide selection.

but we should probably not apply in default case

I agree, but there seems no way to distinguish the "not defined" case and an explicit rgba(0, 0, 0, 0). The overridden style sets selection background color to highlight which is a default browser selection color, so in case it‘s not explicitly defined – the selection colors remains the same.

addSelectionColorOverrideStyleIfNeeded();
// add default text selection color override CSS class to common ancestor node
node.classList.add('__ddg-override-selection-background-color');
}
return true; // next
}));

return true;
}, true);

})();
} catch {
// Throw away this exception, it's likely a confict with another extension
}
}
}
Loading