Last active
August 9, 2021 17:24
-
-
Save ironblock/16bb5220afedf3003690e91e02a71fa2 to your computer and use it in GitHub Desktop.
Restore cut/copy/paste functionality on webpages that try to block it
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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