Skip to content

Instantly share code, notes, and snippets.

@ironblock
Last active August 9, 2021 17:24
Show Gist options
  • Save ironblock/16bb5220afedf3003690e91e02a71fa2 to your computer and use it in GitHub Desktop.
Save ironblock/16bb5220afedf3003690e91e02a71fa2 to your computer and use it in GitHub Desktop.
Restore cut/copy/paste functionality on webpages that try to block it
/**
* RESTORE COPY/CUT/PASTE FUNCTIONALITY TO WEBPAGES
*
* Sometimes cut, copy, and paste are blocked by genius web developers or their
* super genius boss, who think that blocking these features actually works, as
* though a webpage was somehow an application running in "the cloud" that they
* have control over, rather than a jumped up text document running entirely on
* the user's machine.
*
* Worse, some think that it's somehow "more secure" if you can't paste a
* password or credit card number, ensuring that users will pick shorter
* passwords or reuse one they already know.
*
* Anyways. Here's a bookmarklet that will remove their hard work and restore
* order to the world.
*
* NOTE: Despite best efforts, the feature for "watch for new restrictions"
* can't detect all cases where an element is changed within muliple nested
* shadow DOMs. In those cases, just run the bookmarklet again. No new event
* listeners will be added to elements that already have one attached.
*
* To use it, save a new bookmark in your browser, then change the URL to the
* following code (everything on that line, including the "javascript:" bit):
**/
// COPY THIS CODE TO YOUR BOOKMARKLET:
javascript:(function(){function%20a(a){function%20b(){d&&(0<g.length&&/^[~+>]$/.test(g[g.length-1])&&g.push("%20"),g.push(d))}var%20c,d,e,f,g=[],h=[0],i=0,j=/^\s+$/,k=[/\s+|\/\*|["'>~+[(]/g,/\s+|\/\*|["'[\]()]/g,/\s+|\/\*|["'[\]()]/g,null,/\*\//g];for(a=a.trim();;)if(d="",e=k[h[h.length-1]],e.lastIndex=i,c=e.exec(a),!c){d=a.substr(i),b();break}else%20if(f=i,i=e.lastIndex,f<i-c[0].length&&(d=a.substring(f,i-c[0].length)),3>h[h.length-1]){if(b(),"["===c[0])h.push(1);else%20if("("===c[0])h.push(2);else%20if(/^["']$/.test(c[0]))h.push(3),k[3]=new%20RegExp(c[0],"g");else%20if("/*"===c[0])h.push(4);else%20if(/^[\])]$/.test(c[0])&&0<h.length)h.pop();else%20if(/^(?:\s+|[~+>])$/.test(c[0])&&(0<g.length&&!j.test(g[g.length-1])&&0===h[h.length-1]&&g.push("%20"),1===h[h.length-1]&&5===g.length&&"="===g[2].charAt(g[2].length-1)&&(g[4]="%20"+g[4]),j.test(c[0])))continue;g.push(c[0])}else%20g[g.length-1]+=d,/(?:[^\\]|(?:^|[^\\])(?:\\\\)+)$/.test(g[g.length-1])&&(4===h[h.length-1]&&(2>g.length||j.test(g[g.length-2])?g.pop():g[g.length-1]="%20",c[0]=""),h.pop()),g[g.length-1]+=c[0];return%20g.join("").trim()}function%20b(a,b=document,d=null){return%20c(a,!0,b,d)}function%20c(b,c,f,g=null){b=a(b);let%20i=f.querySelector(b);if(document.head.createShadowRoot||document.head.attachShadow){if(!c&&i)return%20i;const%20a=e(b,",");return%20a.reduce((a,b)=>{if(!c&&a)return%20a;const%20i=e(b.replace(/^\s+/g,"").replace(/\s*([>+~]+)\s*/g,"$1"),"%20").filter(a=>!!a).map(a=>e(a,">")),j=i.length-1,k=i[j][i[j].length-1],l=h(k,f,g),m=d(i,j,f);return%20c?(a=a.concat(l.filter(m)),a):(a=l.find(m),a||null)},c?[]:null)}return%20c?f.querySelectorAll(b):i}function%20d(a,b,c){return%20d=>{let%20e=b,h=d,i=!1;for(;h&&!f(h);){let%20b=!0;if(1===a[e].length)b=h.matches(a[e]);else{const%20d=[].concat(a[e]).reverse();let%20f=h;for(const%20a%20of%20d){if(!f||!f.matches(a)){b=!1;break}f=g(f,c)}}if(b&&0===e){i=!0;break}b&&e--,h=g(h,c)}return%20i}}function%20e(a,b){return%20a.match(/\\?.|^$/g).reduce((a,d)=>("\""!==d||a.sQuote?"'"!==d||a.quote?a.quote||a.sQuote||d!==b?a.a[a.a.length-1]+=d:a.a.push(""):(a.sQuote^=1,a.a[a.a.length-1]+=d):(a.quote^=1,a.a[a.a.length-1]+=d),a),{a:[""]}).a}function%20f(a){return%20a.nodeType===Node.DOCUMENT_FRAGMENT_NODE||a.nodeType===Node.DOCUMENT_NODE}function%20g(a,b){const%20c=a.parentNode;return%20c&&c.host&&11===c.nodeType?c.host:c===b?null:c}function%20h(a=null,b,c=null){let%20d=[];if(c)d=c;else{const%20a=function(b){for(let%20c=0;c<b.length;c++){const%20e=b[c];d.push(e),e.shadowRoot&&a(e.shadowRoot.querySelectorAll("*"))}};b.shadowRoot&&a(b.shadowRoot.querySelectorAll("*")),a(b.querySelectorAll("*"))}return%20a?d.filter(b=>b.matches(a)):d}const%20i={attributes:!0,childList:!0,subtree:!0,characterData:!0},j=new%20Map,k=a=>a.filter(a=>!!a),l=a=>0<a?`Restored%20copy/cut/paste%20functionality%20on%20${a}%20elements.`:"No%20elements%20were%20found%20to%20be%20restricting%20copy/cut/paste%20functionality.",m=(a=document.body)=>Array.from(a.querySelectorAll("iframe")),n=(a=document.body)=>Array.from(a.querySelectorAll("*")).filter(a=>null!==a.shadowRoot),o=(a=document.body)=>{const%20b=[...m(a),...n(a)];return%20b.forEach(a=>b.push(o(a))),k(b).flat(1/0)},p=(a=document.body)=>Array.from(b("[oncut],[oncopy],[onpaste]",a)),q=a=>a.map(a=>p(a)).filter(a=>!!a).flat(1/0),r=a=>{let%20b=0;return%20a.forEach(a=>{a.hasAttributes("oncut,oncopy,onpaste")&&(b++,a.removeAttribute("oncut"),a.removeAttribute("oncopy"),a.removeAttribute("onpaste"))}),console.info(l(b)),a.length},s=a=>{const%20b=[];a.forEach(a=>{switch(a.type){case"childList":a.addedNodes.length&&(console.info("Mutated%20elements:%20One%20or%20more%20elements%20was%20was%20added"),b.push(...Array.from(a.addedNodes)));break;case"attributes":console.info("Mutated%20elements:%20An%20element's%20attributes%20changed"),b.push(a.target);}b.length&&(console.info(`Mutated%20elements:%20${b.length}%20to%20check`),r(q(k(b.map(o)))))})},t=a=>{a.forEach(a=>{j.set(a,new%20MutationObserver(a=>{s(a),t(o())})),j.get(a).observe(a,i)})},u=r(q(o())),v=`${l(u)}%20New%20restrictions%20may%20be%20added%20as%20you%20use%20this%20site.%20Would%20you%20like%20to%20try%20to%20automatically%20remove%20new%20restrictions%20as%20they%20are%20added?`;confirm(v)&&t([document.body,...o()])})();
// SOURCE CODE SO YOU CAN SEE I'M NOT DOING ANYTHING SHADY
javascript: (function restoreCopyCutPaste() {
// Local copy of v1.0.0 of https://github.com/Georgegriff/query-selector-shadow-dom
function normalizeSelector(sel) {
// save unmatched text, if any
function saveUnmatched() {
if (unmatched) {
// whitespace needed after combinator?
if (tokens.length > 0 && /^[~+>]$/.test(tokens[tokens.length - 1])) {
tokens.push(" ");
}
// save unmatched text
tokens.push(unmatched);
}
}
var tokens = [],
match,
unmatched,
regex,
state = [0],
next_match_idx = 0,
prev_match_idx,
not_escaped_pattern = /(?:[^\\]|(?:^|[^\\])(?:\\\\)+)$/,
whitespace_pattern = /^\s+$/,
state_patterns = [
/\s+|\/\*|["'>~+[(]/g, // general
/\s+|\/\*|["'[\]()]/g, // [..] set
/\s+|\/\*|["'[\]()]/g, // (..) set
null, // string literal (placeholder)
/\*\//g, // comment
];
sel = sel.trim();
// eslint-disable-next-line no-constant-condition
while (true) {
unmatched = "";
regex = state_patterns[state[state.length - 1]];
regex.lastIndex = next_match_idx;
match = regex.exec(sel);
// matched text to process?
if (match) {
prev_match_idx = next_match_idx;
next_match_idx = regex.lastIndex;
// collect the previous string chunk not matched before this token
if (prev_match_idx < next_match_idx - match[0].length) {
unmatched = sel.substring(
prev_match_idx,
next_match_idx - match[0].length
);
}
// general, [ ] pair, ( ) pair?
if (state[state.length - 1] < 3) {
saveUnmatched();
// starting a [ ] pair?
if (match[0] === "[") {
state.push(1);
}
// starting a ( ) pair?
else if (match[0] === "(") {
state.push(2);
}
// starting a string literal?
else if (/^["']$/.test(match[0])) {
state.push(3);
state_patterns[3] = new RegExp(match[0], "g");
}
// starting a comment?
else if (match[0] === "/*") {
state.push(4);
}
// ending a [ ] or ( ) pair?
else if (/^[\])]$/.test(match[0]) && state.length > 0) {
state.pop();
}
// handling whitespace or a combinator?
else if (/^(?:\s+|[~+>])$/.test(match[0])) {
// need to insert whitespace before?
if (
tokens.length > 0 &&
!whitespace_pattern.test(tokens[tokens.length - 1]) &&
state[state.length - 1] === 0
) {
// add normalized whitespace
tokens.push(" ");
}
// case-insensitive attribute selector CSS L4
if (
state[state.length - 1] === 1 &&
tokens.length === 5 &&
tokens[2].charAt(tokens[2].length - 1) === "="
) {
tokens[4] = " " + tokens[4];
}
// whitespace token we can skip?
if (whitespace_pattern.test(match[0])) {
continue;
}
}
// save matched text
tokens.push(match[0]);
}
// otherwise, string literal or comment
else {
// save unmatched text
tokens[tokens.length - 1] += unmatched;
// unescaped terminator to string literal or comment?
if (not_escaped_pattern.test(tokens[tokens.length - 1])) {
// comment terminator?
if (state[state.length - 1] === 4) {
// ok to drop comment?
if (
tokens.length < 2 ||
whitespace_pattern.test(tokens[tokens.length - 2])
) {
tokens.pop();
}
// otherwise, turn comment into whitespace
else {
tokens[tokens.length - 1] = " ";
}
// handled already
match[0] = "";
}
state.pop();
}
// append matched text to existing token
tokens[tokens.length - 1] += match[0];
}
}
// otherwise, end of processing (no more matches)
else {
unmatched = sel.substr(next_match_idx);
saveUnmatched();
break;
}
}
return tokens.join("").trim();
}
function querySelectorAllDeep(selector, root = document, allElements = null) {
return _querySelectorDeep(selector, true, root, allElements);
}
function querySelectorDeep(selector, root = document, allElements = null) {
return _querySelectorDeep(selector, false, root, allElements);
}
function _querySelectorDeep(selector, findMany, root, allElements = null) {
selector = normalizeSelector(selector);
let lightElement = root.querySelector(selector);
if (document.head.createShadowRoot || document.head.attachShadow) {
// no need to do any special if selector matches something specific in light-dom
if (!findMany && lightElement) {
return lightElement;
}
// split on commas because those are a logical divide in the operation
const selectionsToMake = splitByCharacterUnlessQuoted(selector, ",");
return selectionsToMake.reduce(
(acc, minimalSelector) => {
// if not finding many just reduce the first match
if (!findMany && acc) {
return acc;
}
// do best to support complex selectors and split the query
const splitSelector = splitByCharacterUnlessQuoted(
minimalSelector
//remove white space at start of selector
.replace(/^\s+/g, "")
.replace(/\s*([>+~]+)\s*/g, "$1"),
" "
)
// filter out entry white selectors
.filter((entry) => !!entry)
// convert "a > b" to ["a", "b"]
.map((entry) => splitByCharacterUnlessQuoted(entry, ">"));
const possibleElementsIndex = splitSelector.length - 1;
const lastSplitPart =
splitSelector[possibleElementsIndex][
splitSelector[possibleElementsIndex].length - 1
];
const possibleElements = collectAllElementsDeep(
lastSplitPart,
root,
allElements
);
const findElements = findMatchingElement(
splitSelector,
possibleElementsIndex,
root
);
if (findMany) {
acc = acc.concat(possibleElements.filter(findElements));
return acc;
} else {
acc = possibleElements.find(findElements);
return acc || null;
}
},
findMany ? [] : null
);
} else {
if (!findMany) {
return lightElement;
} else {
return root.querySelectorAll(selector);
}
}
}
function findMatchingElement(splitSelector, possibleElementsIndex, root) {
return (element) => {
let position = possibleElementsIndex;
let parent = element;
let foundElement = false;
while (parent && !isDocumentNode(parent)) {
let foundMatch = true;
if (splitSelector[position].length === 1) {
foundMatch = parent.matches(splitSelector[position]);
} else {
// selector is in the format "a > b"
// make sure a few parents match in order
const reversedParts = [].concat(splitSelector[position]).reverse();
let newParent = parent;
for (const part of reversedParts) {
if (!newParent || !newParent.matches(part)) {
foundMatch = false;
break;
}
newParent = findParentOrHost(newParent, root);
}
}
if (foundMatch && position === 0) {
foundElement = true;
break;
}
if (foundMatch) {
position--;
}
parent = findParentOrHost(parent, root);
}
return foundElement;
};
}
function splitByCharacterUnlessQuoted(selector, character) {
return selector.match(/\\?.|^$/g).reduce(
(p, c) => {
if (c === '"' && !p.sQuote) {
p.quote ^= 1;
p.a[p.a.length - 1] += c;
} else if (c === "'" && !p.quote) {
p.sQuote ^= 1;
p.a[p.a.length - 1] += c;
} else if (!p.quote && !p.sQuote && c === character) {
p.a.push("");
} else {
p.a[p.a.length - 1] += c;
}
return p;
},
{ a: [""] }
).a;
}
/**
* Checks if the node is a document node or not.
* @param {Node} node
* @returns {node is Document | DocumentFragment}
*/
function isDocumentNode(node) {
return (
node.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
node.nodeType === Node.DOCUMENT_NODE
);
}
function findParentOrHost(element, root) {
const parentNode = element.parentNode;
return parentNode && parentNode.host && parentNode.nodeType === 11
? parentNode.host
: parentNode === root
? null
: parentNode;
}
/**
* Finds all elements on the page, inclusive of those within shadow roots.
* @param {string=} selector Simple selector to filter the elements by. e.g. 'a', 'div.main'
* @return {!Array<string>} List of anchor hrefs.
* @author ebidel@ (Eric Bidelman)
* License Apache-2.0
*/
function collectAllElementsDeep(
selector = null,
root,
cachedElements = null
) {
let allElements = [];
if (cachedElements) {
allElements = cachedElements;
} else {
const findAllElements = function (nodes) {
for (let i = 0; i < nodes.length; i++) {
const el = nodes[i];
allElements.push(el);
// If the element has a shadow root, dig deeper.
if (el.shadowRoot) {
findAllElements(el.shadowRoot.querySelectorAll("*"));
}
}
};
if (root.shadowRoot) {
findAllElements(root.shadowRoot.querySelectorAll("*"));
}
findAllElements(root.querySelectorAll("*"));
}
return selector
? allElements.filter((el) => el.matches(selector))
: allElements;
}
const criteria = "[oncut],[oncopy],[onpaste]";
const observerConfig = {
attributes: true,
childList: true,
subtree: true,
characterData: true,
};
const observers = new Map();
const removeFalsey = (array) => array.filter((element) => !!element);
const formatFeedback = (num) =>
num > 0
? `Restored copy/cut/paste functionality on ${num} elements.`
: "No elements were found to be restricting copy/cut/paste functionality.";
const findIframes = (root = document.body) =>
Array.from(root.querySelectorAll("iframe"));
const findShadowRoots = (root = document.body) =>
Array.from(root.querySelectorAll("*")).filter(
(element) => element.shadowRoot !== null
);
const findRootsRecursive = (root = document.body) => {
const roots = [...findIframes(root), ...findShadowRoots(root)];
roots.forEach((subRoot) => roots.push(findRootsRecursive(subRoot)));
return removeFalsey(roots).flat(Infinity);
};
const collectElements = (root = document.body) =>
Array.from(querySelectorAllDeep(criteria, root));
const collectFromRoots = (roots) =>
roots
.map((root) => collectElements(root))
.filter((root) => !!root)
.flat(Infinity);
const cleanElements = (elements) => {
let count = 0;
elements.forEach((element) => {
if (element.hasAttributes("oncut,oncopy,onpaste")) {
count++;
element.removeAttribute("oncut");
element.removeAttribute("oncopy");
element.removeAttribute("onpaste");
}
});
console.info(formatFeedback(count));
return elements.length;
};
const handleMutation = (mutationList) => {
const elements = [];
mutationList.forEach((mutation) => {
switch (mutation.type) {
case "childList":
if (mutation.addedNodes.length) {
console.info(
"Mutated elements: One or more elements was was added"
);
elements.push(...Array.from(mutation.addedNodes));
}
break;
case "attributes":
console.info("Mutated elements: An element's attributes changed");
elements.push(mutation.target);
break;
}
if (elements.length) {
console.info(`Mutated elements: ${elements.length} to check`);
cleanElements(
collectFromRoots(removeFalsey(elements.map(findRootsRecursive)))
);
}
});
};
const debounce = (func, duration) => {
let timeout;
return function (...args) {
const effect = () => {
timeout = null;
return func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(effect, duration);
};
};
const configureObservers = (roots) => {
roots.forEach((root) => {
observers.set(
root,
new MutationObserver((mutationList) => {
handleMutation(mutationList);
configureObservers(findRootsRecursive());
})
);
observers.get(root).observe(root, observerConfig);
});
};
const numberRestored = cleanElements(collectFromRoots(findRootsRecursive()));
const message = `${formatFeedback(
numberRestored
)} New restrictions may be added as you use this site. Would you like to try to automatically remove new restrictions as they are added?`;
if (confirm(message)) {
configureObservers([document.body, ...findRootsRecursive()]);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment