Greasy Fork is available in English.

Proxified Links DEBUG

Proxified hyperlinks to a proxy instance or Farside with no nonsense

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
  1. // ==UserScript==
  2. // @name Proxified Links DEBUG
  3. // @author proxi
  4. // @homepageURL https://greasyfork.org/en/scripts/485274-proxified-links
  5. // @copyright 2023 Schimon Jehudah (http://schimon.i2p)
  6. // @license AGPL-3.0-only; https://www.gnu.org/licenses/agpl-3.0.en.html
  7. // @namespace com.proxi.proxified
  8. // @description Proxified hyperlinks to a proxy instance or Farside with no nonsense
  9. // Add or remove preferred services and instances yourself!
  10. //
  11. // Forked from Proxify Links v23.10.17, by Schimon Jehudah, and modified with prejudice and style
  12. // - Proxy, proxy, proxy; see and configure non-proxying frontends yourself before opening links
  13. // - Use a small hardcoded list of favorite proxy instances, or:
  14. // - Hold X key to use Farside redirection where possible
  15. // - Hold Z key to use the original link whenever needed
  16. // - Easily use first-party frontends with discretion or when instances are down
  17. // - Clearnet by default following Farside, add personal lists of decentralized nodes
  18. // - Set noreferrer on supported links, optionally all links if `ENABLE_REFERER_HIDE_PAGEWIDE`,
  19. // preventing referer header while browsing without breaking common ajax functionality
  20. // - Use GET requests on supported search engines particularly with noscript
  21. // (Recommend disabled if `ENABLE_REFERER_HIDE` is off)
  22. // @run-at document-end
  23. // @version 0.3.0
  24. // @match *://*/*
  25. // @icon data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgOTAwMCA5MDAwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbDpzcGFjZT0icHJlc2VydmUiPjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InllbGxvdyIvPjxwYXRoIGQ9Ik0uMjc5LS4xNDYuMTUgMGgtLjE3MmwuMjQtLjI2Ni0uMTMyLS4yNTNoLjE0OWwuMDc2LjE0OC4xMzEtLjE0OGguMTdsLS4yNC4yNjkuMTMuMjVILjM1NEwuMjc5LS4xNDZaIiBzdHlsZT0iZmlsbDp3aGl0ZTsiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE2NzUgNzcyOCkgc2NhbGUoODAwMCkiLz48cGF0aCBkPSJNLjE4OCAwSC4wNDFMLjE5LS43MTZoLjI5MWEuMzQuMzQgMCAwIDEgLjEyMy4wMThjLjAzLjAxMy4wNTQuMDMzLjA3Mi4wNjFhLjE5Mi4xOTIgMCAwIDEgLjAyNi4xMDEuMjczLjI3MyAwIDAgMS0uMDIxLjEwNS4yNC4yNCAwIDAgMS0uMDUxLjA4Mi4yMzcuMjM3IDAgMCAxLS4xNjEuMDcxIDEuMTY2IDEuMTY2IDAgMCAxLS4xMy4wMDVILjI0NUwuMTg4IDBaTS4yNy0uMzkyaC4wNDVhLjUzLjUzIDAgMCAwIC4xNTYtLjAxNC4xMzEuMTMxIDAgMCAwIC4wNjEtLjA0Ny4xMjMuMTIzIDAgMCAwIC4wMjMtLjA3MS4wNy4wNyAwIDAgMC0uMDEyLS4wNDIuMDYyLjA2MiAwIDAgMC0uMDMyLS4wMjUuMzU4LjM1OCAwIDAgMC0uMDkzLS4wMDdILjMxM0wuMjctLjM5MloiIHRyYW5zZm9ybT0ibWF0cml4KDkzMjAgMCAwIDkzMTMgMTEyOCA3NzI4KSIvPjxwYXRoIGQ9Im0yNTEuNjg0IDY1NS45ODkuMTYyLjIzIDUyMi40ODYgMTczLjI5Mi0xMjEuMjk4LTE3My4zNTItLjUwMy0uMTdIMjUxLjY4NFptMTM0OS40NzYgMzE4LjYxLTI4My45OS05NS4wMTZjLTY0LjE5LS45NjItMTQ4LjMyLTEuNDQzLTI1Mi4zOS0xLjQ0M0g4MDguMzU5bC0xNy43MjMtMjUuMzI4LTIxMi4wNTQgMTIxLjc4N2gzNDYuMzg0bDEwMi4wODQtNTkuNzM3IDE3Ny4xNiA1OS43MzdoMzk2Ljk1WiIgc3R5bGU9ImZpbGw6eWVsbG93Z3JlZW4iIHRyYW5zZm9ybT0ibWF0cml4KDMuNDMgMCAwIC0xMS40NCA2NDIgMTUyMzUuNikiLz48cGF0aCBkPSJNMTUwMCA4MTIwaDU4MDB2MzYwSDE3NTB6Ii8+IDwvc3ZnPg==
  26. // ==/UserScript==
  27.  
  28. /**
  29. * Basic configurations for functionality and performance
  30. */
  31. const KEY_MODIFIED = 'x'; // X key to modify clicked link to Farside, if applicable
  32. const KEY_BONAFIDE = 'z'; // Z key to revert clicked link to the original destination
  33. const TOUCH_MODIFIED = 3; // Simple 3-finger tap gesture to modify tapped link to Farside, if applicable
  34. const TOUCH_BONAFIDE = 5; // Simple 5-finger tap gesture to revert tapped link to the original destination
  35. const TOUCH_PROXIFIED = 2; // Reserved 2-finger tap gesture to reset tapped link to static proxified
  36. const ROTATE_SITE_PROXIFIED = true; // Optional: Rotate proxy instances instead of random to prevent adjacent duplicates
  37. const ENABLE_RERENDER = true; // Optional: Forcibly rerender for browser to update UI, may degrade performance
  38.  
  39. // Optional keys to limit unintentional triggering on non-links
  40. // Set to empty '' string, proxification will happen automatically on selection
  41. // TODO: improve touch support
  42. const KEY_PROXIBITE = 'b'; // Optional B key to allow heavier elements to be proxified
  43. const KEY_FRAMEBITE = 'f'; // Optional B+F key combination to trigger when iframes are updated
  44. const TOUCH_PROXIBITE = 4; // Simple 4-finger tap gesture to allow all heavier elements to be proxified
  45. const TOUCH_FRAMEBITE = 4;
  46. // TODO: support multiple keys to whitelist specific keys that will trigger handlers, consider screenreader controls
  47. const KEY_NAVIGATE = ''; // Optional filter for key nav used to proxify the selected element, e.g Tab
  48.  
  49. // Optional proxified iframes with supported src, proxified immediately when selected
  50. // Best used if iframe content is already blocked by a content blocker (e.g. ublock click2load),
  51. // so the original frame is never loaded automatically even if switching back from proxified (B+F+Z)
  52. const ENABLE_IFRAME_PROXIFIED = true;
  53.  
  54. // Optional noreferer override on all proxified links
  55. // If disabled, only upgrade undefined or noopener to noreferrer for proxified links
  56. // Disable if proxified links work but bona fide (Z) links are breaking
  57. const ENABLE_REFERER_HIDE = true;
  58.  
  59. // Optional noreferer override on all links on the page, if ENABLE_REFERER_HIDE is also on
  60. // Disable if unrelated links or site authentication windows are breaking
  61. const ENABLE_REFERER_HIDE_PAGEWIDE = true;
  62.  
  63. // Optional proxified links using query string if the main url does not match a proxy
  64. // Useful for tracking links or old-fashioned HTML GET search, e.g. DuckDuckGo (uddg)
  65. // Strongly recommend used with `ENABLE_REFERER_HIDE_PAGEWIDE` to avoid exposing search engine and keywords
  66. // Disable if random links that shouldn't be proxified still are
  67. const ENABLE_QUERY_PROXIFIED = true;
  68. const ENABLE_QUERY_PROXIFIED_ON = []; // if empty, all query parameters will be included
  69. const ENABLE_QUERY_PROXIFIED_OFF = ['piurl' /* Startpage image proxy */]; // blacklisted query parameters
  70.  
  71. // Optional stripping links of attributes that are necessary to proxify links (Google),
  72. // and/or optional removal of link trackers on sites or search engines
  73. const ENABLE_ATTRIBUTES_SMITE = true;
  74. /** @type {{ [host: string]: {attributes: string[], allowFuzzy?: boolean} }} */
  75. const ENABLE_ATTRIBUTES_SMITE_ON_SITE = { 'google.com': { attributes: ['data-sb'] } };
  76.  
  77. // Optional search engine GET requests instead of POST for improved search navigation
  78. // Used with ENABLE_REFERER_HIDE and ENABLE_REFERER_HIDE_PAGEWIDE on
  79. // CAUTION: May be unsupported, and subject to negligent server logging query string
  80. const ENABLE_SEARCH_GET = true;
  81. /** @type {{ [host: string]: { formSelector: string } }} */
  82. const ENABLE_SEARCH_GET_ON_SITE = {
  83. 'html.duckduckgo.com': { formSelector: 'form:has(#search_form_input_homepage), form:has(.btn[type="submit"])' },
  84. 'startpage.com': { formSelector: '#search, form:has(.header-nav-item), form:has(button[type="submit"]' },
  85. };
  86.  
  87. /**
  88. * Configure site settings or add an instance of a proxy to the site's `redirect` list
  89. *
  90. * As is, use a shortlist of useful instances and mature proxy services
  91. * Out of box is an opinionated, not comprehensive or up-to-date, list of useful instances,
  92. * not ordered alphabetically but in the order of best proxied or most commonly linked
  93. *
  94. * @typedef {{
  95. * redirect?: {
  96. * replacements?: string[],
  97. * suffix?: string, // optional suffix to a random replacement host from `replacements`
  98. * routes?: ProxyRoute[], // optional route whitelist by matching url regex, defaults to replacing url host only if undefined
  99. * inherit?: string // inherit any undefined properties in `redirect`
  100. * },
  101. * redirectToFarside?: {
  102. * replacements?: string[],
  103. * suffix?: string, // optional suffix to a random replacement host from `replacements`
  104. * routes?: ProxyRoute[], // optional route whitelist by matching url regex, defaults to replacing url host only if undefined
  105. * inherit?: string // inherit any undefined properties in `redirectToFarside`
  106. * },
  107. * allowFuzzy?: boolean, // allows fuzzy host matching, for subdomains
  108. * allowIframe?: boolean, // allows matching on iframe src
  109. * inherit?: string // inherit any undefined redirect rules
  110. * }} Proxy
  111. * @typedef {{
  112. * regex: RegExp, // route regex match
  113. * suffix?: string, // optional route suffix on host, appending to parent `suffix` if defined
  114. * }} ProxyRoute
  115. * @typedef {{ [host: string]: Proxy }} ProxyList
  116. */
  117.  
  118. /**
  119. * @type {ProxyList}
  120. */
  121. const PROXIES = {
  122. // youtube.com, youtu.be, m.youtube.com, youtube-nocookie.com
  123. // /watch, /trending, /@, /channel/
  124. // Allows iframe matching too, which is useful when content blocking embeds
  125. // e.g. ublock ||youtube.com^$3p,frame,redirect=click2load.html
  126. 'youtube.com': {
  127. allowIframe: true,
  128. redirect: {
  129. replacements: [
  130. 'https://piped.video',
  131. 'https://piped.smnz.de',
  132. //'https://piped.projectsegfau.lt', // down
  133. 'https://piped.privacydev.net',
  134. //'https://piped.lunar.icu', // embedded frame blocked by x-frame-options
  135. 'https://piped.adminforge.de',
  136. //'https://pd.vern.cc', // low availability
  137. // The following Invidious instances not only allow video proxy but proxy by default
  138. //'https://iv.datura.network', // proxy off by default
  139. //'https://invidious.projectsegfau.lt', // down
  140. //'https://invidious.fdn.fr', // down
  141. ],
  142. },
  143. redirectToFarside: {
  144. // Only use Farside's Piped redirect since most Invidious instances do not proxy videos by default
  145. // Aside from proxy by default, Invidious is preferred for nojs, configuration, and download functionality
  146. // Specific Invidious instances that proxy by default are included in the instance `redirect` list
  147. replacements: ['https://farside.link/piped'],
  148. },
  149. },
  150. 'youtu.be': {
  151. redirect: {
  152. inherit: 'youtube.com',
  153. routes: [
  154. {
  155. regex: /^https?:\/\/(www\.)?youtu\.be\/([A-Za-z0-9_-]+)\??(.*)$/,
  156. suffix: '/watch?v=$2&$3', // manually add in path and params to support invidious https://github.com/iv-org/invidious/issues/3933
  157. },
  158. ],
  159. },
  160. redirectToFarside: {
  161. inherit: 'youtube.com',
  162. routes: [
  163. {
  164. regex: /^https?:\/\/(www\.)?youtu\.be\/([A-Za-z0-9_-]+)\??(.*)$/,
  165. suffix: '/watch?v=$2&$3', // manually add in path and params to support invidious https://github.com/iv-org/invidious/issues/3933
  166. },
  167. ],
  168. },
  169. },
  170. 'm.youtube.com': { inherit: 'youtube.com' },
  171. 'youtube-nocookie.com': { inherit: 'youtube.com' },
  172.  
  173. // reddit.com
  174. 'reddit.com': {
  175. redirect: {
  176. replacements: [
  177. //'https://libreddit.projectsegfau.lt', // low availability
  178. //'https://libreddit.privacydev.net', // low availability
  179. 'https://l.opnxng.com',
  180. //'https://reddit.invak.id', // down
  181. 'https://libreddit.kavin.rocks',
  182. 'https://red.artemislena.eu',
  183. ],
  184. },
  185. redirectToFarside: {
  186. // teddit no longer actively maintained: https://codeberg.org/teddit/teddit
  187. replacements: ['https://farside.link/libreddit'],
  188. },
  189. },
  190. // redd.it image shortlinks
  191. 'i.redd.it': {
  192. redirect: {
  193. inherit: 'reddit.com',
  194. suffix: '/img',
  195. },
  196. redirectToFarside: {
  197. inherit: 'reddit.com',
  198. suffix: '/img',
  199. },
  200. },
  201. 'preview.redd.it': {
  202. redirect: {
  203. inherit: 'reddit.com',
  204. suffix: '/preview/pre',
  205. },
  206. redirectToFarside: {
  207. inherit: 'reddit.com',
  208. suffix: '/preview/pre',
  209. },
  210. },
  211. 'external-preview.redd.it': {
  212. redirect: {
  213. inherit: 'reddit.com',
  214. suffix: '/preview/external-pre',
  215. },
  216. redirectToFarside: {
  217. inherit: 'reddit.com',
  218. suffix: '/preview/external-pre',
  219. },
  220. },
  221.  
  222. /**
  223. *
  224. * Below are less mature or partially featured services
  225. *
  226. */
  227.  
  228. // stackoverflow.com, {subdomain}.stackexchange.com
  229. // superuser.com, serverfault.com, and other stack sites pending AnonymousOverflow support
  230. 'stackoverflow.com': {
  231. redirect: {
  232. replacements: [
  233. // 'https://ao.vern.cc', // low availability
  234. 'https://overflow.smnz.de',
  235. //'https://overflow.lunar.icu', // TODO: reenable, temp disable old version incompat w/ non-stackexchange sites
  236. 'https://overflow.adminforge.de',
  237. //'https://overflow.hostux.net', // low stability
  238. // 'https://overflow.projectsegfau.lt', // low availability
  239. ],
  240. },
  241. redirectToFarside: {
  242. replacements: ['https://farside.link/anonymousoverflow'],
  243. },
  244. },
  245. '.stackexchange.com': {
  246. allowFuzzy: true, // enable matching loosely with arbitrary subdomain
  247. redirect: {
  248. inherit: 'stackoverflow.com',
  249. routes: [
  250. {
  251. regex: /^https?:\/\/(www\.)?([a-z]+)\..*?\//g,
  252. suffix: '/exchange/$2/', // suffix /exchange/{subdomain} on hostname
  253. },
  254. ],
  255. },
  256. redirectToFarside: {
  257. inherit: 'stackoverflow.com',
  258. routes: [
  259. {
  260. regex: /^https?:\/\/(www\.)?([a-z]+)\..*?\//g,
  261. suffix: '/exchange/$2/',
  262. },
  263. ],
  264. },
  265. },
  266. // non-stackexchange domains are appended to route /exchange/{uri}
  267. 'superuser.com': {
  268. redirect: {
  269. inherit: 'stackoverflow.com',
  270. routes: [
  271. {
  272. regex: /^https?:\/\/(www\.)?(.*)/g, // take entire URI in group 2
  273. suffix: '/exchange/$2',
  274. },
  275. ],
  276. },
  277. redirectToFarSide: {
  278. inherit: 'stackoverflow.com',
  279. routes: [
  280. {
  281. regex: /^https?:\/\/(www\.)?(.*)/g, // take entire URI in group 2
  282. suffix: '/exchange/$2',
  283. },
  284. ],
  285. },
  286. },
  287. 'serverfault.com': { inherit: 'superuser.com' },
  288. 'askubuntu.com': { inherit: 'superuser.com' },
  289. 'stackapps.com': { inherit: 'superuser.com' },
  290.  
  291. // quora.com
  292. 'quora.com': {
  293. redirect: {
  294. replacements: ['https://quetre.iket.me', 'https://quetre.pussthecat.org', 'https://quetre.privacydev.net'],
  295. },
  296. redirectToFarside: {
  297. replacements: ['https://farside.link/quetre'],
  298. },
  299. },
  300.  
  301. // {artist}.bandcamp.com
  302. // Note: bandcamp.com/search route not supported, add above for 'bandcamp.com' if this rare link is needed is the wild
  303. // Note: {cdn}.bcbits.com routes not supported, add below for '.bcbits.com' if this rare link is needed in the wild
  304. '.bandcamp.com': {
  305. allowFuzzy: true,
  306. redirect: {
  307. replacements: ['https://tent.sny.sh', 'https://tn.vern.cc'],
  308. routes: [
  309. {
  310. // {artist}.bandcamp.com with no additional path except optional /music
  311. // exclude daily.bandcamp.com
  312. regex: /^https?:\/\/(www\.)?((?!daily\.)[a-z0-9\-]+)\.bandcamp\.com\/?(music)?$/g,
  313. suffix: '/artist.php?name=$2',
  314. },
  315. {
  316. // {artist}.bandcamp.com/{release}/{name}
  317. // exclude daily.bandcamp.com, e.g. daily.bandcamp.com/features/{article}
  318. regex: /^https?:\/\/(www\.)?((?!daily\.)[a-z0-9\-]+)\.bandcamp\.com\/([a-z]+)\/([a-z0-9\-]+)/g,
  319. suffix: '/release.php?artist=$2&type=$3&name=$4',
  320. },
  321. ],
  322. },
  323. },
  324.  
  325. // instagram.com
  326. // Low feature parity
  327. 'instagram.com': {
  328. redirect: {
  329. replacements: ['https://ig.opnxng.com', 'https://proxigram.lunar.icu'],
  330. },
  331. redirectToFarside: {
  332. replacements: ['https://farside.link/proxigram'],
  333. },
  334. },
  335.  
  336. // tiktok
  337. // Low feature parity
  338. 'tiktok.com': {
  339. redirect: {
  340. replacements: [
  341. 'https://proxitok.pussthecat.org',
  342. 'https://tok.artemislena.eu',
  343. 'https://tok.adminforge.de',
  344. 'https://tik.hostux.net',
  345. 'https://proxitok.lunar.icu',
  346. ],
  347. },
  348. redirectToFarside: {
  349. replacements: ['https://farside.link/proxitok'],
  350. },
  351. },
  352.  
  353. // imgur.com, i.imgur.com, i.stack.imgur.com
  354. 'imgur.com': {
  355. redirect: {
  356. replacements: [
  357. 'https://rimgo.pussthecat.org',
  358. 'https://imgur.artemislena.eu',
  359. // 'https://rimgo.vern.cc', // low availability
  360. // 'https://rimgo.hostux.net', // down
  361. // 'https://rimgo.lunar.icu', // down
  362. 'https://rimgo.eu.projectsegfau.lt',
  363. ],
  364. },
  365. redirectToFarside: {
  366. replacements: ['https://farside.link/rimgo'],
  367. },
  368. },
  369. 'i.imgur.com': { inherit: 'imgur.com' },
  370. 'i.stack.imgur.com': {
  371. inherit: 'imgur.com',
  372. redirect: { inherit: 'imgur.com', suffix: '/stack' },
  373. redirectToFarside: { inherit: 'imgur.com', suffix: '/stack' },
  374. },
  375.  
  376. // github.com, gists.github.com
  377. // /explore, /{group}/{repo}, /{group}/{repo}/archive, gists.github.com -> /gists/
  378. // Low feature parity
  379. // Use only for repo landing page, downloads, and gists
  380. 'github.com': {
  381. redirect: {
  382. replacements: [
  383. 'https://gothub.lunar.icu',
  384. 'https://g.opnxng.com',
  385. //'https://gothub.projectsegfau.lt', // low availability
  386. 'https://gothub.dev.projectsegfau.lt',
  387. ],
  388. },
  389. redirectToFarside: {
  390. replacements: ['https://farside.link/gothub'],
  391. },
  392. },
  393. // gist.github.com
  394. 'gist.github.com': {
  395. redirect: {
  396. inherit: 'github.com',
  397. suffix: '/gist/',
  398. routes: [{ regex: /https?:\/\/(.*?)\//g }], // replace entire domain
  399. },
  400. redirectToFarside: {
  401. inherit: 'github.com',
  402. suffix: '/gist/',
  403. routes: [{ regex: /https?:\/\/(.*?)\//g }],
  404. },
  405. },
  406.  
  407. // imdb.com, m.imdb.com
  408. 'imdb.com': {
  409. redirect: {
  410. replacements: [
  411. 'https://libremdb.pussthecat.org',
  412. 'https://libremdb.iket.me',
  413. //'https://ld.vern.cc', // low availability
  414. 'https://libremdb.lunar.icu',
  415. ],
  416. },
  417. redirectToFarside: {
  418. replacements: ['https://farside.link/libremdb'],
  419. },
  420. },
  421. 'm.imdb.com': { inherit: 'imdb.com' },
  422.  
  423. // genius.com
  424. // Low feature parity
  425. 'genius.com': {
  426. redirect: {
  427. replacements: ['https://dm.vern.cc', 'https://dumb.lunar.icu'],
  428. },
  429. redirectToFarside: {
  430. replacements: ['https://farside.link/dumb'],
  431. },
  432. },
  433.  
  434. // medium.com - Uncomment to use
  435. // Low feature parity by design
  436. // Not a proxy by design, alternative frontend still requests from the official servers
  437. //
  438. // Recommend setting Medium to noscript and/or loading through more standard proxies such as TOR
  439. // Medium with JS disabled works as of now, but other proxy sites such as archive.org can be used if needed
  440. /* --- Remove this line to use --- //
  441. 'medium.com': {
  442. redirect: {
  443. replacements: ['https://scribe.rip', 'https://sc.vern.cc', 'https://m.opnxng.com'],
  444. },
  445. redirectToFarside: {
  446. replacements: ['https://farside.link/scribe'],
  447. },
  448. },
  449. // ------------------------------- */
  450.  
  451. // fandom.com - Uncomment to use
  452. // Not a full proxy, alternative frontend still requests from the official servers
  453. //
  454. // Recommend simply using a content blocker to block ads and other annoyances
  455. /* --- Remove this line to use --- //
  456. '.fandom.com': {
  457. allowFuzzy: true,
  458. redirect: {
  459. replacements: [
  460. 'https://breezewiki.com',
  461. 'https://antifandom.com',
  462. 'https://breezewiki.pussthecat.org',
  463. 'https://bw.projectsegfau.lt',
  464. 'https://breeze.hostux.net',
  465. 'https://bw.artemislena.eu',
  466. 'https://breeze.nohost.network',
  467. 'https://z.opnxng.com',
  468. ],
  469. routes: [
  470. {
  471. regex: /^https?:\/\/(www\.)?([a-z\-]+)\..*?\//g,
  472. suffix: '/$2/', // suffix /{subdomain} on hostname
  473. },
  474. ],
  475. },
  476. redirectToFarside: {
  477. replacements: ['https://farside.link/breezewiki'],
  478. routes: [
  479. {
  480. regex: /^https?:\/\/(www\.)?([a-z\-]+)\..*?\//g,
  481. suffix: '/$2/',
  482. },
  483. ],
  484. },
  485. },
  486. // ------------------------------- */
  487.  
  488. // wikipedia.org - Uncomment to use
  489. //
  490. // Recommend setting Wikipedia to noscript and/or loading through more standard proxies such as TOR
  491. // If absolutely needed, recommend rolling your own Wikiless instance routed through a proxy or VPN
  492. // Wikipedia trustworthiness and scriptless tracking is more or less equivalent to wikiless instances
  493. /* --- Remove this line to use --- //
  494. 'wikipedia.org': {
  495. redirect: {
  496. replacements: [
  497. 'https://wiki.adminforge.de',
  498. 'https://wikiless.lunar.icu',
  499. 'https://wikiless.org',
  500. 'https://wl.vern.cc',
  501. ],
  502. },
  503. redirectToFarside: {
  504. replacements: ['https://farside.link/wikiless'],
  505. },
  506. },
  507. // ------------------------------- */
  508. };
  509.  
  510. /**
  511. * Configure site link exclusions
  512. *
  513. * The most common exclusion will be on the first-party site itself, as many proxies are not complete replacements.
  514. * Common unsuppoorted features and paths are excluded, though this is not intended to exhaustively track the list of
  515. * proxied frontends and availability or configuration of individual instances.
  516. *
  517. * excludeLinks can exclude links that match any provided rule, namely matchingPath
  518. * excludeOn can exclude links when host matches `self` or any provided, e.g. by matchingPath (all paths if empty)
  519. * @typedef {{
  520. * excludeLinks?: {
  521. * matchingPath?: (string|RegExp)[],
  522. * matchingHost?: (string|RegExp)[],
  523. * matchingUrl?: (string|RegExp)[],
  524. * matchingText?: (string|RegExp)[],
  525. * inherit?: string // inherit any undefined properties in `excludeLinks`
  526. * },
  527. * excludeOn?: {
  528. * matchingPath?: (string|RegExp)[],
  529. * matchingHost?: (string|RegExp)[],
  530. * matchingUrl?: (string|RegExp)[],
  531. * matchingBody?: (string|RegExp)[],
  532. * matchingHead?: (string|RegExp)[],
  533. * inherit?: string // inherit any undefined properties in `excludeOn`
  534. * },
  535. * allowFuzzy?: boolean, // allows fuzzy host matching, for subdomains
  536. * inherit?: string // inherits any undefined exclusion rules
  537. * }} Exclusion
  538. * @typedef {{ [host: string]: Exclusion }} ExclusionList
  539. */
  540.  
  541. /**
  542. * Configure per-site exclusions
  543. * @type {ExclusionList}
  544. */
  545. const EXCLUSIONS = {
  546. // youtube.com, m.youtube.com, youtube-nocookie.com
  547. 'youtube.com': {
  548. excludeLinks: {
  549. matchingPath: ['/users/'],
  550. },
  551. excludeOn: {
  552. // exclude linking out from the official site
  553. matchingHost: ['youtube.com', 'youtube-nocookie.com'],
  554. matchingHead: [
  555. '<meta property="og:title" content="Piped">',
  556. /<meta property="og:site_name" content=".*Invidious">/,
  557. ],
  558. },
  559. },
  560. 'm.youtube.com': { inherit: 'youtube.com' },
  561. 'youtube-nocookie.com': { inherit: 'youtube.com' },
  562.  
  563. // reddit.com
  564. 'reddit.com': {
  565. excludeLinks: {
  566. // old.reddit.com can be used almost entirely with noscript
  567. matchingHost: ['old.reddit.com'],
  568. },
  569. excludeOn: {
  570. // exclude on reddit.com but still proxy links while browsing old.reddit.com
  571. matchingUrl: [/^https?:\/\/(www\.)?(?!old\.)reddit.com/],
  572. },
  573. },
  574.  
  575. // stackoverflow.com, {subdomain}.stackexchange.com, superuser.com, etc.
  576. 'stackoverflow.com': {
  577. excludeLinks: {
  578. matchingPath: ['/questions/tagged/', '/users/'],
  579. },
  580. excludeOn: {
  581. matchingHost: [
  582. 'stackoverflow.com',
  583. 'stackexchange.com',
  584. 'superuser.com',
  585. 'serverfault.com',
  586. 'askubuntu.com',
  587. 'stackapps.com',
  588. ],
  589. },
  590. },
  591. '.stackexchange.com': {
  592. allowFuzzy: true,
  593. inherit: 'stackoverflow.com',
  594. },
  595. 'superuser.com': { inherit: 'stackoverflow.com' },
  596. 'serverfault.com': { inherit: 'stackoverflow.com' },
  597. 'askubuntu.com': { inherit: 'stackoverflow.com' },
  598. 'stackapps.com': { inherit: 'stackoverflow.com' },
  599.  
  600. // quora.com
  601. 'quora.com': { excludeOn: { matchingHost: ['quora.com'] } },
  602.  
  603. // {artist}.bandcamp.com
  604. '.bandcamp.com': { allowFuzzy: true, excludeOn: { matchingHost: ['bandcamp.com'] } },
  605.  
  606. // instagram.com
  607. 'instagram.com': {
  608. excludeOn: {
  609. matchingHost: ['instagram.com'],
  610. matchingHead: [
  611. /<meta property="og:title" content="[a-zA-Z0-9 _\-+=.,:;'?\/\\`!@#$%^&*()-_\[\]{}|]+? • Proxigram">/,
  612. ],
  613. },
  614. },
  615.  
  616. // tiktok.com
  617. 'tiktok.com': {
  618. excludeOn: {
  619. matchingHost: ['tiktok.com'],
  620. // exclude on ProxiTok itself, linking out to original
  621. matchingHead: ['<meta property="og:site_name" content="ProxiTok">'],
  622. },
  623. },
  624.  
  625. // imgur.com
  626. 'imgur.com': { excludeOn: { matchingHost: ['imgur.com'] } },
  627. 'i.imgur.com': { inherit: 'imgur.com' },
  628. 'i.stack.imgur.com': { inherit: 'imgur.com' },
  629.  
  630. // github.com
  631. // a lot to blacklist, can also whitelist limited functionality instead
  632. 'github.com': {
  633. excludeLinks: {
  634. matchingText: [],
  635. // prettier-ignore
  636. matchingPath: ['/actions','/blame/','/codespaces/','/collections/','/commit/','/commits/','/compare/','/customer-stories','/delete/','/discussions/','/enterprise/','/events/','/features/','/graphs/','/issues','/marketplace/','/notifications/','/orgs/','/projects/','/pulls', '/pull/','/pulse','/releases','/security','/sessions/','/sponsors/','/tags','/tree/','/wiki/'],
  637. },
  638. excludeOn: {
  639. matchingHost: ['github.com'],
  640. // exclude on Gothub page itself, linking out
  641. matchingBody: ['<a href="https://codeberg.org/gothub/gothub">Source code</a>'],
  642. },
  643. },
  644. 'gist.github.com': {
  645. excludeLinks: {
  646. // gist single directory paths, e.g. users, /discover, /starred
  647. matchingPath: [/^\/[A-Za-z0-9_.-]+\/?$/],
  648. },
  649. inherit: 'github.com',
  650. },
  651.  
  652. // imdb.com
  653. 'imdb.com': {
  654. excludeOn: {
  655. matchingHost: ['imdb.com'],
  656. // exclude on libremdb itself, linking out
  657. matchingHead: ['<meta property="og:site_name" content="libremdb">'],
  658. },
  659. },
  660.  
  661. // genius.com
  662. // /artist and other pages may not work, but not blacklisting any paths for now
  663. 'genius.com': { excludeOn: { matchingHost: ['genius.com'] } },
  664.  
  665. // medium.com
  666. 'medium.com': { excludeOn: { matchingHost: ['medium.com'] } },
  667.  
  668. // fandom.com
  669. 'fandom.com': { excludeOn: { matchingHost: ['fandom.com'] } },
  670.  
  671. // wikipedia.org
  672. 'wikipedia.org': { excludeOn: { matchingHost: ['wikipedia.org'] } },
  673. };
  674.  
  675. /**
  676. * Begin script logic
  677. *
  678. * Do not modify below if adding or making changes to available proxies
  679. */
  680. const DEBUG = true; // Console logging enabled when true
  681.  
  682. // Tag names of hovered link elements that can be proxified
  683. const PROXIFY_ON = ['A', 'IFRAME'];
  684. // Tag names of hovered elements that should also lookup to the parent node for links
  685. // TODO: per-tag and per-site configuration
  686. const PARENT_LOOKUP_ON = [
  687. 'BUTTON',
  688. 'IMG',
  689. 'SVG',
  690. 'H1',
  691. 'H2',
  692. 'H3',
  693. 'H4',
  694. 'H5',
  695. 'H6',
  696. 'B',
  697. 'I',
  698. 'EM',
  699. 'STRONG',
  700. 'SMALL',
  701. 'SUP',
  702. 'SUB',
  703. 'S',
  704. 'U',
  705. 'LI',
  706. ];
  707. // Default depth limit to ancestor lookup recursion
  708. const PARENT_LOOKUP_STEPS = 1;
  709. /**
  710. * Additional per-site parent lookup config, to fix site-specific proxification
  711. * TODO: further segment sites on matchesPath or other rules and per-nodeName configs
  712. * @type {{ [host: string]: { nodeNames: string[], steps: number } }}
  713. */
  714. const PARENT_LOOKUP_ON_SITE = {
  715. // google.com mobile search results div
  716. // TODO: improve mobile search inline media touch events swallowed
  717. 'google.com': { nodeNames: ['DIV'], steps: 4 },
  718. // duckduckgo.com search results span, hero summary SVGs and title container div
  719. 'duckduckgo.com': { nodeNames: ['SPAN', 'DIV', 'PATH', 'RECT'], steps: 4 },
  720. // startpage.com search results inline images/videos
  721. 'startpage.com': { nodeNames: ['DIV'], steps: 2 },
  722. };
  723.  
  724. // Tag names of hovered elements that should also globally search all hovered elements for links
  725. const HOVER_LOOKUP_ON = ['P', 'SPAN', 'LI', 'LABEL', /*'DIV',*/ 'BUTTON', 'IMG', 'SVG'];
  726. // Additional tag names to search per-site, to fix site-specific proxificaton
  727. /** @type {{ [host: string]: [tags: string[]] }} */
  728. const HOVER_LOOKUP_ON_HOST = {
  729. 'duckduckgo.com': ['DIV'],
  730. };
  731.  
  732. const CLASS_PROXIFIED = 'proxi-fied';
  733. const CLASS_PROXIFIED_LIVE = 'proxi-live';
  734. const CLASS_BODY_FARSIDE = 'proxi-side';
  735. const CLASS_BODY_BONAFIDE = 'proxi-fide';
  736. const CLASS_BODY_BITE = 'proxi-bite';
  737. const CLASS_BODY_FRAMEBITE = 'proxi-framebite';
  738. const STYLE_HIGHLIGHT_ID = 'proxi-highlite';
  739. const ATTR_PROXIFIED_SITE = 'proxi-site';
  740. const ATTR_PROXIFIED_FARSIDE = 'proxi-site-side';
  741. const ATTR_PROXIFIED_DENIED = 'proxi-nied';
  742. const ATTR_BONAFIDE_SITE = 'proxi-site-bonafide';
  743. const ATTR_IS_FARSIDE = 'proxi-side';
  744.  
  745. // https://uibakery.io/regex-library/url
  746. const REGEX_URL =
  747. /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/;
  748.  
  749. // Key event modifiers for selecting destination
  750. document.body.addEventListener('keydown', e => handleModifierKey(e));
  751. document.body.addEventListener('keyup', e => handleModifierKey(e));
  752. document.body.addEventListener('touchstart', e => handleModifierGesture(e));
  753.  
  754. /**
  755. * Lazy load link processing when user interacts per-element
  756. *
  757. * Handle hovering over links and completed keyboard navigation over link
  758. * Does not handle other basic redirects such as form action or onclick attributes
  759. */
  760. document.body.addEventListener('mouseover', handleElement); // link hover
  761. document.body.addEventListener('touchstart', handleElement); // link touch
  762. document.body.addEventListener('keyup', handleElement); // keyboard navigation
  763.  
  764. /**
  765. * Automatic page onload and DOM modifications
  766. */
  767. window.addEventListener('load', () => {
  768. if (ENABLE_SEARCH_GET) {
  769. let searchEngine = getByHost(ENABLE_SEARCH_GET_ON_SITE, window.location.host, false);
  770. if (!!searchEngine && !!searchEngine.formSelector) {
  771. const searchEngineFormEls = document.querySelectorAll(searchEngine.formSelector);
  772. for (let formEl of searchEngineFormEls) {
  773. formEl.method = 'get';
  774. }
  775. }
  776. }
  777. });
  778.  
  779. /**
  780. * Hover event handler to find an anchor hyperlink to check
  781. * @param {Event} e
  782. */
  783. function handleElement(e) {
  784. // Currently support specific events, and optional key nav filter
  785. if (
  786. e.type !== 'mouseover' &&
  787. e.type !== 'touchstart' &&
  788. (e.type !== 'keyup' || (!!KEY_NAVIGATE && e.key !== KEY_NAVIGATE))
  789. ) {
  790. return;
  791. }
  792.  
  793. // Ignore target on multi-touch touch event
  794. if (e.type === 'touchstart' && e.touches.length !== 1) {
  795. return;
  796. }
  797.  
  798. // Look for any element or elements that this event could be trying to proxify
  799. const proxifiableEls = getProxifiableElements(e);
  800. if (!proxifiableEls || !proxifiableEls.length) return;
  801.  
  802. // Prioritize anchor links
  803. // Take the first anchor, even if it has already been processed
  804. const targetAnchor = proxifiableEls.find(el => el.nodeName === 'A');
  805. if (!!targetAnchor) {
  806. // Hovered anchor found, process it unless it was already processed before
  807. if (!isElementProxified(targetAnchor)) {
  808. handleAnchorEl(targetAnchor);
  809. } else {
  810. // Otherwise, trigger an update on the link element to ensure it is current
  811. setTimeout(() => {
  812. updateProxifiedElement(targetAnchor, 'href');
  813. }, 0);
  814. }
  815.  
  816. // Only handle one element per event for now to avoid overeager proxification
  817. return;
  818. }
  819.  
  820. // Handle iframe, if enabled
  821. // If both or either bite key is empty, handle on any interaction
  822. if (ENABLE_IFRAME_PROXIFIED) {
  823. const isProxibite = document.body.classList.contains(CLASS_BODY_BITE);
  824. const isFramebite = document.body.classList.contains(CLASS_BODY_FRAMEBITE);
  825. const canBite =
  826. (!KEY_PROXIBITE || isProxibite || e.key === KEY_PROXIBITE || e.touches?.length === TOUCH_PROXIBITE) &&
  827. (!KEY_FRAMEBITE || isFramebite || e.key === KEY_FRAMEBITE || e.touches?.length === TOUCH_FRAMEBITE);
  828. const targetIframe = canBite && proxifiableEls.find(el => el.nodeName === 'IFRAME');
  829. if (!!targetIframe) {
  830. if (!isElementProxified(targetIframe)) {
  831. handleIframe(targetIframe);
  832. } else {
  833. setTimeout(() => {
  834. updateProxifiedElement(targetIframe, 'src');
  835. }, 0);
  836. }
  837. }
  838.  
  839. return;
  840. }
  841. }
  842.  
  843. /**
  844. * Handle an anchor element to be proxified
  845. * Allows re-proxifying the anchor
  846. *
  847. * @param {HTMLAnchorElement} el Anchor element
  848. * @returns {void}
  849. */
  850. function handleAnchorEl(el) {
  851. // Perform element and url validation and returns a valid proxy
  852. const { url, proxy } = preproxifyElement(el, 'href') || {};
  853.  
  854. // Perform any global link modifications including non-proxied links
  855. if (ENABLE_REFERER_HIDE && ENABLE_REFERER_HIDE_PAGEWIDE) el.rel = 'noreferrer';
  856. let optSmite;
  857. if (
  858. ENABLE_ATTRIBUTES_SMITE &&
  859. !!el.href &&
  860. !!(optSmite = getByHost(ENABLE_ATTRIBUTES_SMITE_ON_SITE, window.location.host, true))
  861. ) {
  862. // If enabled on the current site, strip any click event attributes from this anchor element
  863. // Resulting hyperlink is intended to be a primitive link with no events intercepting navigation
  864. // TODO: Support reverting events on bonafide
  865. for (const attr of optSmite.attributes) {
  866. el.removeAttribute(attr);
  867. }
  868. }
  869.  
  870. // Proxify the link
  871. // Error handling will have been done on preproxification
  872. if (!!proxy) {
  873. proxifyElement(
  874. el,
  875. url,
  876. 'href',
  877. proxy,
  878. /** @param {HTMLAnchorElement} el */ el => {
  879. // Trigger an update on the link now that it has been proxified
  880. updateProxifiedElement(el, 'href');
  881.  
  882. // Upgrade undefined or noopener relationship to norefererer
  883. // If `ENABLE_REFERER_HIDE`, override on all proxified links
  884. // TODO: Support modifier key to revert change?
  885. if (!el.rel || el.rel === 'noopener' || ENABLE_REFERER_HIDE) el.rel = 'noreferrer';
  886. }
  887. );
  888. }
  889. }
  890.  
  891. /**
  892. * Handle an iframe to be proxified
  893. * Allows re-proxifying the frame
  894. *
  895. * @param {HTMLIFrameElement} el
  896. * @returns {void}
  897. */
  898. function handleIframe(el) {
  899. const { url, proxy } = preproxifyElement(el, 'src') || {};
  900. if (!!proxy)
  901. proxifyElement(
  902. el,
  903. url,
  904. 'src',
  905. proxy,
  906. /** @param {HTMLIframeElement} el */ el => {
  907. // Trigger an update on the link now that it has been proxified
  908. updateProxifiedElement(el, 'src');
  909. }
  910. );
  911. }
  912.  
  913. /**
  914. * Get the element or elements that can be proxified, including those already proxified,
  915. * from an event where the user is hovering or making a selection
  916. *
  917. * @param {MouseEvent | KeyboardEvent | TouchEvent} e
  918. * @returns {Element[]?} If none, null
  919. */
  920. function getProxifiableElements(e) {
  921. if (!(e instanceof MouseEvent || e instanceof KeyboardEvent || e instanceof TouchEvent)) {
  922. error('Invalid event');
  923. return null;
  924. }
  925.  
  926. if (!e.target) return null;
  927. const nodeName = e.target.nodeName.toUpperCase();
  928.  
  929. // Anchor links get top priority if directly targeted
  930. if (nodeName === 'A') return [e.target];
  931.  
  932. // Otherwise, build a list of candidates in the order of probable priority
  933. // Add in other directly targeted non-anchor links to the top
  934. const proxifiableEls = [];
  935. if (PROXIFY_ON.includes(nodeName)) proxifiableEls.push(e.target);
  936.  
  937. // Parents may not receive propagated events, so use this event now to check if nested under a link
  938. let optLookup = getByHost(PARENT_LOOKUP_ON_SITE, window.location.host, true);
  939. if (PARENT_LOOKUP_ON.includes(nodeName) || optLookup?.nodeNames?.includes(nodeName)) {
  940. // Step up through ancestors
  941. // Or use target.closest() to search all ancestors
  942. const lookupSteps = Math.max(optLookup?.steps ? optLookup.steps : 1, PARENT_LOOKUP_STEPS);
  943. let currentNode = e.target;
  944. for (let step = 0; step < lookupSteps; step++) {
  945. if (!!currentNode.parentNode) {
  946. currentNode = currentNode.parentNode;
  947.  
  948. // take processed or unprocessed proxifiable elements
  949. if (PROXIFY_ON.includes(currentNode.nodeName.toUpperCase())) {
  950. proxifiableEls.push(currentNode);
  951. }
  952. } else {
  953. break;
  954. }
  955. }
  956. }
  957.  
  958. // A link may be a sibling or other relative, or not even a relative, that does not receive propagated
  959. // For mouse hover or bite event, find any link element that is currently being hovered
  960. // TODO: Skip previously lookup elements and handle updateProxifiedElement rerender separately from proxifying
  961.  
  962. const isHoverLookup =
  963. e instanceof MouseEvent &&
  964. (HOVER_LOOKUP_ON.includes(nodeName) || HOVER_LOOKUP_ON_HOST[window.location.host]?.includes(nodeName));
  965. const isProxiBite = !!KEY_PROXIBITE && document.body.classList.contains(CLASS_BODY_BITE);
  966. if (isHoverLookup || isProxiBite) {
  967. // Select all hovered elements and take the processed or unprocessed proxifiable elements
  968. const hoveredEls = document.querySelectorAll(':hover');
  969. for (const el of hoveredEls) {
  970. if (PROXIFY_ON.includes(el.nodeName.toUpperCase())) {
  971. proxifiableEls.push(el);
  972. }
  973. }
  974. }
  975.  
  976. return !!proxifiableEls.length ? proxifiableEls : null;
  977. }
  978.  
  979. /**
  980. * Perform element and destination url validation and return a valid proxy
  981. *
  982. * @param {HTMLElement} el Target element to be validated for proxification
  983. * @param {string} destinationAttr Element link destination attribute name
  984. * @returns {{url, Proxy} | null} Returns a random proxy for the element, null if none
  985. */
  986. function preproxifyElement(el, destinationAttr) {
  987. const destination = el[destinationAttr];
  988. const text = el.outerText;
  989.  
  990. // Validate anchor is intended to be a hyperlink
  991. if (!destination.length) {
  992. proxifyDenyElement(el, 'Invalid, empty, or undefined destination attribute');
  993. return null;
  994. }
  995. if (!REGEX_URL.test(destination)) {
  996. proxifyDenyElement(el, 'Hyperlink destination is not a valid absolute URL');
  997. return null;
  998. }
  999.  
  1000. // Clean up any proxify attrs before proxifying
  1001. el.removeAttribute(ATTR_PROXIFIED_SITE);
  1002. el.removeAttribute(ATTR_PROXIFIED_FARSIDE);
  1003. el.removeAttribute(ATTR_PROXIFIED_DENIED);
  1004. el.removeAttribute(ATTR_BONAFIDE_SITE);
  1005.  
  1006. // Parse the destination as a URL
  1007. let url;
  1008. try {
  1009. url = new URL(destination);
  1010. } catch (ex) {
  1011. // Exit on malformed URL
  1012. error(`[${text}](${destination}) error parsing URL: ${ex}`);
  1013. proxifyDenyElement(el, 'Error parsing URL');
  1014. return null;
  1015. }
  1016.  
  1017. // Exit if the original link is excluded for any reason
  1018. // Page-wide exclusions to each destination host are memoized so may save unnecessary checking
  1019. let excludedReason;
  1020. if ((excludedReason = getExclusionReasonForLink(url, text))) {
  1021. proxifyDenyElement(el, excludedReason);
  1022. return null;
  1023. }
  1024.  
  1025. // Get a flattened proxy rule that can be applied for this link
  1026. let proxy = getProxyForLink(url);
  1027.  
  1028. // If no proxy can be found for the link url, optionally search its query string
  1029. // Find a proxy match for any query param that is whitelisted, if used, and is a valid url
  1030. if (!proxy && ENABLE_QUERY_PROXIFIED) {
  1031. for (const [paramKey, paramVal] of url.searchParams) {
  1032. // skip blacklisted query params
  1033. if (ENABLE_QUERY_PROXIFIED_OFF.includes(paramKey)) {
  1034. continue;
  1035. }
  1036.  
  1037. // if query param whitelist is empty, allow all
  1038. if (!ENABLE_QUERY_PROXIFIED_ON?.length || ENABLE_QUERY_PROXIFIED_ON.includes(paramKey)) {
  1039. let queryStringLinkCandidate = paramVal;
  1040. let queryStringURLCandidate, queryStringProxyCandidate;
  1041. try {
  1042. queryStringLinkCandidate = decodeURI(paramVal); // attempt to decode a full URI encoded to the query string
  1043. } catch (ex) {
  1044. warn(`Failed to decode query string param: ${ex}`);
  1045. continue;
  1046. }
  1047.  
  1048. // skip param if it doesn't look like an absolute url to proxify
  1049. if (!REGEX_URL.test(queryStringLinkCandidate)) {
  1050. continue;
  1051. }
  1052.  
  1053. try {
  1054. queryStringURLCandidate = new URL(queryStringLinkCandidate);
  1055. } catch (ex) {
  1056. error(`Failed to parse validated url for query param "${paramKey}"`);
  1057. continue;
  1058. }
  1059.  
  1060. if (getExclusionReasonForLink(queryStringURLCandidate, text)) {
  1061. continue;
  1062. }
  1063.  
  1064. queryStringProxyCandidate = getProxyForLink(queryStringURLCandidate);
  1065.  
  1066. // if successful, break on query string and take first match
  1067. if (!!queryStringProxyCandidate) {
  1068. url = queryStringURLCandidate;
  1069. proxy = queryStringProxyCandidate;
  1070. break;
  1071. }
  1072. }
  1073. }
  1074. }
  1075.  
  1076. // Automatic exclusion if no proxy found on this pass
  1077. if (!proxy) {
  1078. proxifyDenyElement(el, 'Not found in proxies list');
  1079. return null;
  1080. }
  1081.  
  1082. return { url, proxy };
  1083. }
  1084.  
  1085. /**
  1086. * Proxify the link element with the specified proxy settings
  1087. * Supports anchor and iframe elements for now
  1088. *
  1089. * @param {HTMLAnchorElement | HTMLIFrameElement} el
  1090. * @param {URL} url
  1091. * @param {string} destinationAttr Element link destination attribute name
  1092. * @param {Proxy} proxy
  1093. * @param {Function<HTMLAnchorElement | HTMLIFrameElement>?} onProxification Optional success callback
  1094. * @returns {void}
  1095. */
  1096. function proxifyElement(el, url, destinationAttr, proxy, onProxification) {
  1097. if (!(el instanceof HTMLAnchorElement) && !(el instanceof HTMLIFrameElement)) {
  1098. error(`${el} is not a supported element type`);
  1099. return;
  1100. }
  1101.  
  1102. if (!isProxyValid(proxy)) {
  1103. error(`Invalid proxy settings for proxifying ${url}`);
  1104. return;
  1105. }
  1106.  
  1107. let proxifyDenied;
  1108.  
  1109. // Build a url to a random instance
  1110. if (!!proxy.redirect) {
  1111. const instanceList = proxy.redirect.replacements;
  1112. const instanceSuffix = proxy.redirect.suffix;
  1113. const instanceRoutes = proxy.redirect.routes;
  1114. const urlInstanceProxified = getProxifiedUrl(url, instanceList, instanceSuffix, instanceRoutes, 'proxy');
  1115.  
  1116. if (urlInstanceProxified instanceof URL) {
  1117. el.setAttribute(ATTR_PROXIFIED_SITE, urlInstanceProxified);
  1118. } else if (typeof urlInstanceProxified === 'string') {
  1119. proxifyDenied ||= urlInstanceProxified;
  1120. }
  1121. }
  1122.  
  1123. // Build a url to a random Farside service redirect
  1124. if (!!proxy.redirectToFarside) {
  1125. const farsideList = proxy.redirectToFarside.replacements;
  1126. const farsideSuffix = proxy.redirectToFarside.suffix;
  1127. const farsideRoutes = proxy.redirectToFarside.routes;
  1128. const urlFarsideProxified = getProxifiedUrl(url, farsideList, farsideSuffix, farsideRoutes, 'farside');
  1129.  
  1130. if (urlFarsideProxified instanceof URL) {
  1131. el.setAttribute(ATTR_PROXIFIED_FARSIDE, urlFarsideProxified);
  1132. } else if (typeof urlFarsideProxified === 'string') {
  1133. proxifyDenied ||= urlFarsideProxified;
  1134. }
  1135. }
  1136.  
  1137. // Complete the proxification
  1138. if (el.hasAttribute(ATTR_PROXIFIED_SITE) || el.hasAttribute(ATTR_PROXIFIED_FARSIDE)) {
  1139. // Mark the link as successfully proxified
  1140. el.classList.add(CLASS_PROXIFIED);
  1141.  
  1142. // Store the original anchor href
  1143. if (!el.hasAttribute(ATTR_BONAFIDE_SITE)) {
  1144. el.setAttribute(ATTR_BONAFIDE_SITE, el[destinationAttr]);
  1145. }
  1146.  
  1147. // Success callback
  1148. if (!!onProxification) {
  1149. onProxification(el);
  1150. }
  1151. } else if (!!proxifyDenied) {
  1152. // No effect proxifying, but a denial reason was given
  1153. proxifyDenyElement(el, proxifyDenied);
  1154. } else {
  1155. // Log unexpected failure past validated inputs
  1156. error(`Failure proxifying ${url}`);
  1157. }
  1158. }
  1159.  
  1160. /**
  1161. * Process but mark the link element as being invalid
  1162. *
  1163. * @param {HTMLAnchorElement | HTMLIFrameElement} el
  1164. * @param {string} reason
  1165. */
  1166. function proxifyDenyElement(el, reason) {
  1167. if (!(el instanceof HTMLAnchorElement) && !(el instanceof HTMLIFrameElement)) {
  1168. error(`${el} is not a supported element type`);
  1169. return;
  1170. }
  1171.  
  1172. // Set denial reason as attr value
  1173. el.setAttribute(ATTR_PROXIFIED_DENIED, reason);
  1174.  
  1175. // Bonafide link is still stored for posterity
  1176. el.setAttribute(ATTR_BONAFIDE_SITE, getElementDest(el));
  1177. }
  1178.  
  1179. /**
  1180. * Return a proxified URL given a list of replacement hosts and optional routes
  1181. *
  1182. * @param {URL} url
  1183. * @param {string[]} replacements
  1184. * @param {string?} suffix Optional static suffix on the replacement host
  1185. * @param {ProxyRoute[]?} routes Optional route list specifying a regex match with its corresponding options
  1186. * @param {string} key Optional unique key used for proxifying
  1187. * @returns {URL | string | null} Proxified URL, string reason if denied, null if failed
  1188. */
  1189. const replacementLast = {};
  1190. function getProxifiedUrl(url, replacements, suffix, routes, key = '') {
  1191. if (!url) {
  1192. error('URL null or undefined');
  1193. return null;
  1194. }
  1195.  
  1196. if (!replacements) {
  1197. error('Replacements list null or undefined');
  1198. return null;
  1199. }
  1200.  
  1201. // Get a cleaned list of replacement strings
  1202. const replacementList = replacements.filter(
  1203. replacement => typeof replacement === 'string' && REGEX_URL.test(replacement)
  1204. );
  1205.  
  1206. // Proxify the url with one of the replacements
  1207. if (replacementList.length > 0) {
  1208. // get a random replacement site
  1209. const hostKey = url.host + key;
  1210. const replacementIndex = (replacementLast[hostKey] =
  1211. ROTATE_SITE_PROXIFIED && Object.keys(replacementLast).includes(hostKey)
  1212. ? (replacementLast[hostKey] + 1) % replacementList.length // rotate proxies sequentially, if enabled
  1213. : Math.floor(Math.random() * replacementList.length));
  1214.  
  1215. const replacementSite = replacementList[replacementIndex];
  1216. const replacementSuffix = suffix || '';
  1217. const replacementRoute = routes?.find(route => route?.regex instanceof RegExp && route.regex.test(url.href));
  1218.  
  1219. const failedWhitelist = !!routes && !replacementRoute; // proxy routes whitelist defined but none matched
  1220. let urlProxified = new URL(url);
  1221. if (!!replacementRoute) {
  1222. // Regex replacement
  1223. // TODO: Support replacement function as an alternative to the replacement string route suffix
  1224. try {
  1225. const routeRegex = replacementRoute.regex;
  1226. const routeSuffix = replacementRoute.suffix || '';
  1227. const hrefProxified = url.href.replace(routeRegex, replacementSite + replacementSuffix + routeSuffix);
  1228. urlProxified = new URL(hrefProxified);
  1229. } catch (ex) {
  1230. warn(`Invalid regex replaced URL for ${url}`);
  1231. return null;
  1232. }
  1233. } else if (!failedWhitelist) {
  1234. // Default regex replacement on url host and scheme
  1235. // Skip if whitelisted routes were defined but not matched
  1236. try {
  1237. const hrefProxified = url.href.replace(/(https?:\/\/)(.*?)(\/.*)/, replacementSite + replacementSuffix + '$3');
  1238. urlProxified = new URL(hrefProxified);
  1239. } catch (ex) {
  1240. error(`Invalid default replaced URL for ${url}`);
  1241. return null;
  1242. }
  1243. }
  1244.  
  1245. if (url.href !== urlProxified.href) {
  1246. return urlProxified;
  1247. } else if (failedWhitelist) {
  1248. return 'Failed routes whitelist';
  1249. } else {
  1250. error(`No effect proxifying ${url}`);
  1251. return null;
  1252. }
  1253. }
  1254.  
  1255. warn(`Missing or invalid replacement URLs for ${url}`);
  1256. return null;
  1257. }
  1258.  
  1259. /**
  1260. * Returns matching proxy for the specified link url
  1261. *
  1262. * @param {URL} url
  1263. * @returns {Proxy?} Proxy settings by url, null if not found
  1264. */
  1265. function getProxyForLink(url) {
  1266. // Get the redirect rules from the proxy matching url host
  1267. /** @type {Proxy?} */
  1268. const proxy = getByHost(PROXIES, url.host, true);
  1269.  
  1270. // Inherit any redirect rules that were left completely undefined, then inherit
  1271. // any rule properties that were left undefined
  1272. /** @type {Proxy?} */
  1273. let flattenedProxyRule;
  1274. try {
  1275. flattenedProxyRule = flattenInheritance(proxy, PROXIES);
  1276. } catch (ex) {
  1277. error(`Error inheriting proxy rules for ${url}: ${ex}`);
  1278. return false;
  1279. }
  1280.  
  1281. if (!!flattenedProxyRule) {
  1282. return flattenedProxyRule;
  1283. }
  1284.  
  1285. return null;
  1286. }
  1287.  
  1288. /**
  1289. * Returns whether link URL is supported by a listed proxy and not explicitly excluded
  1290. *
  1291. * @param {URL} url
  1292. * @param {string} text
  1293. * @returns {string|null} Reason if link is invalid, null if valid
  1294. */
  1295. function getExclusionReasonForLink(url, text) {
  1296. // Exit if this URL is excluded by path or innertext, or by the current page location
  1297. // Look for exclusion rules on both this host and inherited, if applicable
  1298. /** @type {Exclusion?} */
  1299. const exclusion = getByHost(EXCLUSIONS, url.host, true);
  1300.  
  1301. /** @type {Exclusion?} */
  1302. let flattenedExclusionRule;
  1303. try {
  1304. flattenedExclusionRule = flattenInheritance(exclusion, EXCLUSIONS);
  1305. } catch (ex) {
  1306. // Likely inheriting rule that is unexpectedly undefined, probably due to incorrect or nested inheritance
  1307. warn(`Error inheriting exclusion rules for ${url}: ${ex}`);
  1308. return 'Error inheriting exclusion rule';
  1309. }
  1310.  
  1311. // With the final exclusion rule set, check if the link url is excluded
  1312. if (!!flattenedExclusionRule) {
  1313. const excludedReason = isLinkExcludedByRule(url, text, flattenedExclusionRule);
  1314. if (!!excludedReason) {
  1315. return excludedReason;
  1316. }
  1317. }
  1318.  
  1319. return null;
  1320. }
  1321.  
  1322. /**
  1323. * Flatten generic object upwards with inherited data
  1324. *
  1325. * Only goes one level deep, both for inheritance and nested inheritors
  1326. * @typedef {{
  1327. * [keys: string]: Inheritor,
  1328. * inherit?: string
  1329. * }} Inheritor
  1330. *
  1331. * @param {Inheritor} source
  1332. * @param {Object<string, Inheritor>} dictionary
  1333. * @returns {Inheritor?}
  1334. * @throws {Error} Exception on copying with Object.assign()
  1335. */
  1336. function flattenInheritance(source, dictionary) {
  1337. // Avoid destructive shallow copies on `source` or other objects
  1338. let root = source;
  1339. if (!root || !(typeof root === 'object')) return null;
  1340.  
  1341. const rootInheritance = dictionary[root.inherit];
  1342. if (!!rootInheritance && typeof rootInheritance === 'object') {
  1343. // Merge two rules into one
  1344. root = Object.assign({}, rootInheritance, root);
  1345. }
  1346.  
  1347. // For the final state of inherited nested objects that also inherit,
  1348. // flatten their inheritance as well (non-recursive)
  1349. for (const key of Object.keys(root)) {
  1350. const branch = root[key];
  1351. if (branch?.inherit && dictionary[branch.inherit]) {
  1352. // if inherited object also includes corresponding nested data, merge to
  1353. const branchInheritance = dictionary[branch.inherit]?.[key];
  1354. if (!!branchInheritance && typeof branchInheritance === 'object') {
  1355. root[key] = Object.assign({}, branchInheritance, branch);
  1356. }
  1357. }
  1358. }
  1359.  
  1360. // Warn if it appears that recursive inheritance is configured
  1361. // Inheritance beyond the first level of root and property data is not supported
  1362. if (!!rootInheritance?.inherit) {
  1363. warn(`Multi depth recursion hit ${rootInheritance.inherit} but is not supported`);
  1364. }
  1365.  
  1366. return root;
  1367. }
  1368.  
  1369. /**
  1370. * Validate proxy rules have enough instance or Farside rules to proxify a link
  1371. *
  1372. * @param {Proxy} proxy
  1373. * @returns {boolean} true if valid, false otherwise
  1374. */
  1375. function isProxyValid(proxy) {
  1376. if (!proxy) return false;
  1377.  
  1378. const isInstanceRedirectValid =
  1379. !!proxy.redirect && proxy.redirect.replacements && proxy.redirect.replacements.length > 0;
  1380. const isFarsideRedirectValid =
  1381. !!proxy.redirectToFarside &&
  1382. proxy.redirectToFarside.replacements &&
  1383. proxy.redirectToFarside.replacements.length > 0;
  1384. if (!isInstanceRedirectValid && !isFarsideRedirectValid) {
  1385. // neither instance or Farside rules fully defined
  1386. return false;
  1387. }
  1388.  
  1389. return true;
  1390. }
  1391.  
  1392. /**
  1393. * Handle keydown and keyup event to set modifiers on proxified links
  1394. * @param {KeyboardEvent} e
  1395. * @returns {void}
  1396. */
  1397. function handleModifierKey(e) {
  1398. if (e.type !== 'keyup' && e.type !== 'keydown') {
  1399. error('Invalid modifier key event');
  1400. return;
  1401. }
  1402.  
  1403. let isDomChanged = false;
  1404. toggleModifierKeyState(e, KEY_MODIFIED, CLASS_BODY_FARSIDE) && (isDomChanged = true);
  1405. toggleModifierKeyState(e, KEY_BONAFIDE, CLASS_BODY_BONAFIDE) && (isDomChanged = true);
  1406. toggleModifierKeyState(e, KEY_PROXIBITE, CLASS_BODY_BITE) && (isDomChanged = true);
  1407. toggleModifierKeyState(e, KEY_FRAMEBITE, CLASS_BODY_FRAMEBITE) && (isDomChanged = true);
  1408.  
  1409. // If DOM state changed, trigger an render update on proxified elements
  1410. if (isDomChanged) {
  1411. setTimeout(() => {
  1412. updateAllProxifiedAnchors(true);
  1413. }, 0);
  1414. }
  1415. }
  1416.  
  1417. /**
  1418. * Handle multi-touch gesture event to set modifiers on proxified links, for touch-only devices
  1419. * @param {TouchEvent} e
  1420. * @returns {void}
  1421. */
  1422. function handleModifierGesture(e) {
  1423. if (!(e instanceof TouchEvent)) {
  1424. error('Invalid modifier touch gesture event');
  1425. return;
  1426. }
  1427.  
  1428. // only support multi-touch to trigger/reset modifiers
  1429. if (e.touches.length <= 1) {
  1430. return;
  1431. }
  1432.  
  1433. let isDomChanged = false;
  1434. toggleModifierGestureState(e, TOUCH_MODIFIED, CLASS_BODY_FARSIDE) && (isDomChanged = true);
  1435. toggleModifierGestureState(e, TOUCH_BONAFIDE, CLASS_BODY_BONAFIDE) && (isDomChanged = true);
  1436. toggleModifierGestureState(e, TOUCH_PROXIBITE, CLASS_BODY_BITE, true) && (isDomChanged = true);
  1437. toggleModifierGestureState(e, TOUCH_FRAMEBITE, CLASS_BODY_FRAMEBITE, true) && (isDomChanged = true);
  1438.  
  1439. // If DOM state changed, trigger an render update on proxified elements
  1440. if (isDomChanged) {
  1441. setTimeout(() => {
  1442. updateAllProxifiedAnchors(true);
  1443. }, 0);
  1444. }
  1445. }
  1446.  
  1447. /**
  1448. * Toggle a page-wide class by checking KeyboardEvent matches the specified toggleKey
  1449. *
  1450. * @param {KeyboardEvent} e
  1451. * @param {string} toggleKey
  1452. * @param {string} toggleClass Classname to toggle in DOM
  1453. * @returns {boolean} True if changed, false if no change to DOM
  1454. */
  1455. function toggleModifierKeyState(e, toggleKey, toggleClass) {
  1456. if (!(e instanceof KeyboardEvent)) {
  1457. error('Event is not KeyboardEvent');
  1458. return;
  1459. }
  1460.  
  1461. if (e.type !== 'keyup' && e.type !== 'keydown') {
  1462. error('Invalid modifier key event');
  1463. return false;
  1464. }
  1465.  
  1466. if (e.key === toggleKey) {
  1467. const setModifierOn = e.type === 'keydown';
  1468. const originalState = document.body.classList.contains(toggleClass);
  1469.  
  1470. if (setModifierOn) {
  1471. if (!originalState) {
  1472. document.body.classList.add(toggleClass);
  1473. return true;
  1474. }
  1475. } else {
  1476. if (originalState) {
  1477. document.body.classList.remove(toggleClass);
  1478. return true;
  1479. }
  1480. }
  1481. }
  1482.  
  1483. return false;
  1484. }
  1485.  
  1486. /**
  1487. * Toggle a page-wide class by checking TouchEvent matches the specified touch gesture
  1488. *
  1489. * @param {ToggleEvent} e
  1490. * @param {number} toggleGesture Simple gesture based on number of fingers
  1491. * @param {string} toggleClass Classname to toggle in DOM
  1492. * @param {boolean} isManualOff Optional setting to require manually repeating gesture to toggle off
  1493. * @returns {boolean} True if changed, false if no change to DOM
  1494. */
  1495. function toggleModifierGestureState(e, toggleGesture, toggleClass, isManualOff = false) {
  1496. if (!(e instanceof TouchEvent)) {
  1497. error('Event is not TouchEvent');
  1498. return;
  1499. }
  1500.  
  1501. // detect gesture
  1502. // only simple touch count gesture is supported currently
  1503. const gestureTouchCount = toggleGesture;
  1504. const isGestured = gestureTouchCount !== TOUCH_PROXIFIED && e.touches.length === gestureTouchCount;
  1505.  
  1506. // handle gesture to toggle modifier
  1507. const originalState = document.body.classList.contains(toggleClass);
  1508. if (isGestured) {
  1509. if (!originalState) {
  1510. document.body.classList.add(toggleClass);
  1511. return true;
  1512. } else if (isManualOff) {
  1513. // gesture off, if supported by this gesture
  1514. document.body.classList.remove(toggleClass);
  1515. return true;
  1516. }
  1517. } else if (!isManualOff) {
  1518. // automatic off, if supported by this gesture
  1519. if (originalState) {
  1520. document.body.classList.remove(toggleClass);
  1521. return true;
  1522. }
  1523. }
  1524.  
  1525. return false;
  1526. }
  1527.  
  1528. /**
  1529. * Rerender proxified changes to the specified link element
  1530. *
  1531. * @param {HTMLElement} el
  1532. * @param {string} destinationAttr Element link destination attribute name
  1533. * @returns {void}
  1534. */
  1535. function updateProxifiedElement(el, destinationAttr) {
  1536. if (!(el instanceof HTMLElement)) {
  1537. error(`${el} is not an HTMLElement`);
  1538. return;
  1539. }
  1540.  
  1541. if (el.hasAttribute(ATTR_PROXIFIED_DENIED)) {
  1542. return; // element has been proxified but denied
  1543. }
  1544.  
  1545. if (!el[destinationAttr]) {
  1546. error(`Invalid attribute ${destinationAttr}`);
  1547. return;
  1548. }
  1549.  
  1550. if (!isElementProxified(el)) {
  1551. error(`Anchor [${destinationAttr}=${el[destinationAttr]}] is not proxified`);
  1552. return;
  1553. }
  1554.  
  1555. // Check DOM modifier state
  1556. // Instead of using internal state, follow what the rendered DOM has
  1557. const isBonafide = document.body.classList.contains(CLASS_BODY_BONAFIDE);
  1558. // attempt to use Farside automatically if direct site is not found
  1559. const isFarside = document.body.classList.contains(CLASS_BODY_FARSIDE) || !el.hasAttribute(ATTR_PROXIFIED_SITE);
  1560.  
  1561. // Reset active states before re-applying as necessary
  1562. el.removeAttribute(ATTR_IS_FARSIDE);
  1563. el.classList.remove(CLASS_PROXIFIED_LIVE);
  1564.  
  1565. // Update link destination
  1566. // The original, bonafide link takes precedence
  1567. if (isBonafide && el.hasAttribute(ATTR_BONAFIDE_SITE)) {
  1568. if (el[destinationAttr] !== el.getAttribute(ATTR_BONAFIDE_SITE))
  1569. el[destinationAttr] = el.getAttribute(ATTR_BONAFIDE_SITE);
  1570. } else if (isFarside && el.hasAttribute(ATTR_PROXIFIED_FARSIDE)) {
  1571. if (el[destinationAttr] !== el.getAttribute(ATTR_PROXIFIED_FARSIDE))
  1572. el[destinationAttr] = el.getAttribute(ATTR_PROXIFIED_FARSIDE);
  1573. el.setAttribute(ATTR_IS_FARSIDE, true);
  1574. el.classList.add(CLASS_PROXIFIED_LIVE);
  1575. } else if (el.hasAttribute(ATTR_PROXIFIED_SITE)) {
  1576. if (el[destinationAttr] !== el.getAttribute(ATTR_PROXIFIED_SITE))
  1577. el[destinationAttr] = el.getAttribute(ATTR_PROXIFIED_SITE);
  1578. el.classList.add(CLASS_PROXIFIED_LIVE);
  1579. }
  1580.  
  1581. // Optional: Re-render element to encourage browsers to reflect proxified link
  1582. // TODO: Consider aria-live for screenreaders
  1583. if (ENABLE_RERENDER) {
  1584. const isElSelected = document.activeElement == el || el.contains(document.activeElement);
  1585. const elStyleDisplay = el.style.display;
  1586.  
  1587. // hide the element and replace with a placeholder clone to prevent 1-frame flash
  1588. const CLONE_CLASSNAME = 'proxi-tied';
  1589. const elClone = el.cloneNode(true); // deep clone to preserve DOM, at cost of perf
  1590. elClone.classList.add(CLONE_CLASSNAME);
  1591. elClone.style = 'color: black !important; background: black !important;';
  1592. if (!el.parentNode?.querySelector(`.${CLONE_CLASSNAME}`) && elStyleDisplay !== 'none') {
  1593. el.parentNode?.insertBefore(elClone, el.nextSibling);
  1594.  
  1595. if (isElSelected) el.blur();
  1596. el.style.display = 'none';
  1597.  
  1598. setTimeout(() => {
  1599. // show and select the original element
  1600. el.style.display = elStyleDisplay;
  1601. if (isElSelected) el.focus();
  1602.  
  1603. if (elClone.parentNode === el.parentNode) el.parentNode?.removeChild(elClone);
  1604. }, 0);
  1605. }
  1606. }
  1607. }
  1608.  
  1609. /**
  1610. * Rerender state to all proxified anchor elements in the document
  1611. *
  1612. * @param {boolean} doHoveredOnly Only render proxified elements that are being hovered
  1613. * @returns {void}
  1614. */
  1615. function updateAllProxifiedAnchors(doHoveredOnly) {
  1616. const proxifiedEls = document.querySelectorAll(
  1617. `a.${CLASS_PROXIFIED}${doHoveredOnly ? ':is(:hover, :focus-within)' : ''}`
  1618. );
  1619. for (const proxifiedEl of proxifiedEls) {
  1620. updateProxifiedElement(proxifiedEl, 'href');
  1621. }
  1622. }
  1623.  
  1624. /**
  1625. * Add proxified link styles to indicate functionality and readiness
  1626. *
  1627. * Highlight proxied links yellow
  1628. * Highlight Farside-redirected links green
  1629. */
  1630. // TODO: Manually apply inline element styles to avoid style-src CSP
  1631. const isHoverOnly = true; // toggle styling on when hovering on link
  1632. const _important = true ? '!important' : ''; // toggle overriding page styles as much as possible
  1633. // Style anchor element and children under anchor element
  1634. const selectorAnchor = isHoverOnly
  1635. ? `a.${CLASS_PROXIFIED_LIVE}:is(:hover, :focus-within)`
  1636. : `a.${CLASS_PROXIFIED_LIVE}`;
  1637. // add additional styles at higher specificity to child containers with non-empty content
  1638. const selectorChildren = ':is(p, span, h1, h2, h3, h4, h5, h6, label, div, button):not(:empty)';
  1639. addPageStyle(
  1640. `${selectorAnchor} { \
  1641. color: black ${_important}; \
  1642. background-color: yellow ${_important}; \
  1643. text-shadow: none ${_important}; \
  1644. \
  1645. font-style: oblique ${_important}; \
  1646. font-weight: bold ${_important}; \
  1647. \
  1648. ${selectorChildren} { \
  1649. color: yellow ${_important}; \
  1650. background-color: black ${_important}; \
  1651. text-shadow: none ${_important}; \
  1652. \
  1653. font-style: oblique ${_important}; \
  1654. font-weight: bold ${_important}; \
  1655. } \
  1656. :is(img) { \
  1657. filter: sepia(1) hue-rotate(20deg) contrast(1.25) brightness(1.25); ${_important}; \
  1658. }\
  1659. }`
  1660. );
  1661. // Add optional highlighter padding at lowest specificity :where()
  1662. addPageStyle(
  1663. `:where(${selectorAnchor}) { \
  1664. // padding: 0 0.3em; \
  1665. \
  1666. ${selectorChildren} { \
  1667. padding: 0 0.3em; \
  1668. } \
  1669. }`
  1670. );
  1671. // Change colors when links redirect through Farside
  1672. addPageStyle(
  1673. `${selectorAnchor}[${ATTR_IS_FARSIDE}="true"] { \
  1674. color: yellowgreen ${_important}; \
  1675. background-color: black ${_important}; \
  1676. \
  1677. ${selectorChildren} { \
  1678. color: black ${_important}; \
  1679. background-color: yellowgreen ${_important}; \
  1680. } \
  1681. :is(img) { \
  1682. filter: invert(1) sepia(1) hue-rotate(45deg) contrast(1.25) brightness(1.25) ${_important}; \
  1683. } \
  1684. }`
  1685. );
  1686. // Highlight all iframes when frame bite is enabled
  1687. addPageStyle(
  1688. `body.proxi-bite.proxi-framebite { \
  1689. iframe { \
  1690. border: yellow solid 0.3em ${_important}; \
  1691. } \
  1692. iframe[${ATTR_IS_FARSIDE}="true"] { \
  1693. border-color: yellowgreen ${_important}; \
  1694. } \
  1695. }`
  1696. );
  1697.  
  1698. /**
  1699. * Insert a page-level style
  1700. * Adds a document stylesheet to write to when needed
  1701. *
  1702. * @param {string} css
  1703. * @returns {void}
  1704. */
  1705. function addPageStyle(css) {
  1706. const style =
  1707. document.getElementById(STYLE_HIGHLIGHT_ID) ||
  1708. (function () {
  1709. const style = document.createElement('style');
  1710. style.id = STYLE_HIGHLIGHT_ID;
  1711. document.head.appendChild(style);
  1712. return style;
  1713. })();
  1714.  
  1715. const sheet = style.sheet;
  1716. try {
  1717. sheet.insertRule(css, (sheet.rules || sheet.cssRules || []).length);
  1718. } catch (ex) {
  1719. // Likely stylesheet is null from failing to add to DOM
  1720. // Continue on, even without styling
  1721. warn(`Failed to apply style: ${ex}`);
  1722. }
  1723. }
  1724.  
  1725. /**
  1726. * Check given link url and text against provided exclusion rule
  1727. *
  1728. * @param {URL} url
  1729. * @param {string} text
  1730. * @param {Exclusion} rule
  1731. * @returns {string?} First found exclusion reason, null if false
  1732. */
  1733. function isLinkExcludedByRule(url, text, rule) {
  1734. if (!rule) {
  1735. error(`Invalid flattened exclusion rule for ${url}`);
  1736. return 'Invalid flatted exclusion rule';
  1737. }
  1738.  
  1739. // Return reason if excluding the current page that this link is on
  1740. // Check first to short circuit link processing on pages proxifying is excluded
  1741. // Results are memoized by page after being processed once, manually invalidate `_excludedOn` if needed
  1742. const currentDirection = `${window.location.href}=>${url.host}`;
  1743. if (!!_excludedOn[currentDirection]) {
  1744. return _excludedOn[currentDirection];
  1745. }
  1746. if (!!rule.excludeOn && _excludedOn[currentDirection] === undefined) {
  1747. /** @type {URL | null} */
  1748. let currentUrl = null;
  1749. try {
  1750. currentUrl = new URL(currentDirection);
  1751. } catch (ex) {
  1752. // unexpected failure on parsing current URL, error and continue
  1753. error(`Error parsing page URL: ${ex}`);
  1754. }
  1755.  
  1756. if (!!rule.excludeOn.matchingPath) {
  1757. for (const m of rule.excludeOn.matchingPath) {
  1758. if (!!currentUrl.pathname.match(m)) {
  1759. _excludedOn[currentDirection] = `excludeOn.matchingPath[${m}]`;
  1760. }
  1761. }
  1762. }
  1763.  
  1764. if (!!rule.excludeOn.matchingHost) {
  1765. for (const m of rule.excludeOn.matchingHost) {
  1766. if (!!currentUrl.host.match(m)) {
  1767. _excludedOn[currentDirection] = `excludeOn.matchingHost[${m}]`;
  1768. }
  1769. }
  1770. }
  1771.  
  1772. if (!!rule.excludeOn.matchingUrl) {
  1773. for (const m of rule.excludeOn.matchingUrl) {
  1774. if (!!currentUrl.href.match(m)) {
  1775. _excludedOn[currentDirection] = `excludeOn.matchingUrl[${m}]`;
  1776. }
  1777. }
  1778. }
  1779.  
  1780. if (!!rule.excludeOn.matchingBody) {
  1781. for (const m of rule.excludeOn.matchingBody) {
  1782. if (document.getElementsByTagName('body')[0].innerHTML.match(m)) {
  1783. _excludedOn[currentDirection] = `excludeOn.matchingBody[${m}]`;
  1784. }
  1785. }
  1786. }
  1787.  
  1788. if (!!rule.excludeOn.matchingHead) {
  1789. // clean before searching
  1790. // if the userscript extension mounts to head, may false positive on the userscript itself
  1791. if (_documentHeadCleaned === undefined) {
  1792. const documentHeadTemp = document.createElement('head');
  1793. documentHeadTemp.innerHTML = document.getElementsByTagName('head')[0]?.innerHTML;
  1794. let s;
  1795. for (const scriptNodes = documentHeadTemp.getElementsByTagName('script'); (s = scriptNodes[0]); ) {
  1796. // cut down scriptNodes queue
  1797. s.parentNode.removeChild(s);
  1798. }
  1799. _documentHeadCleaned = documentHeadTemp.innerHTML;
  1800. }
  1801.  
  1802. for (const m of rule.excludeOn.matchingHead) {
  1803. if (_documentHeadCleaned.match(m)) {
  1804. _excludedOn[currentDirection] = `excludeOn.matchingHead[${m}]`;
  1805. }
  1806. }
  1807. }
  1808.  
  1809. // process this page url once even if no matches found for this page
  1810. // return any non-nullish match found, or continue to find other link exclusions
  1811. if (_excludedOn[currentDirection] === undefined) {
  1812. _excludedOn[currentDirection] = null;
  1813. } else if (!!_excludedOn[currentDirection]) {
  1814. return _excludedOn[currentDirection];
  1815. }
  1816. }
  1817.  
  1818. // Return reason if excluding this link
  1819. if (!!rule.excludeLinks) {
  1820. if (!!rule.excludeLinks.matchingPath) {
  1821. for (const m of rule.excludeLinks.matchingPath) {
  1822. if (!!url.pathname.match(m)) return `excludeLinks.matchingPath[${m}]`;
  1823. }
  1824. }
  1825.  
  1826. if (!!rule.excludeLinks.matchingHost) {
  1827. for (const m of rule.excludeLinks.matchingHost) {
  1828. if (!!url.host.match(m)) return `excludeLinks.matchingHost[${m}]`;
  1829. }
  1830. }
  1831.  
  1832. if (!!rule.excludeLinks.matchingUrl) {
  1833. for (const m of rule.excludeLinks.matchingUrl) {
  1834. if (!!url.href.match(m)) return `excludeLinks.matchingUrl[${m}]`;
  1835. }
  1836. }
  1837.  
  1838. if (!!rule.excludeLinks.matchingText) {
  1839. for (const m of rule.excludeLinks.matchingText) {
  1840. if (!!text.match(m)) return `excludeLinks.matchingText[${m}]`;
  1841. }
  1842. }
  1843. }
  1844. }
  1845. const _excludedOn = {}; // memoized excludeOn results
  1846. let _documentHeadCleaned; // memoized cleaned <head>
  1847.  
  1848. /**
  1849. * Check if element has been fully processed already
  1850. * @param {HTMLElement} el
  1851. * @returns {boolean}
  1852. */
  1853. function isElementProxified(el) {
  1854. // bonafide attr signals completed proxification
  1855. // successful: all proxified attrs set
  1856. // denied: denied + bonafide attrs set
  1857. const elBonafide = el?.getAttribute(ATTR_BONAFIDE_SITE);
  1858.  
  1859. // proxification is outdated if current link does not match any stored destination
  1860. // this can occur when the same link el is reused dynamically by the site scripts and must be invalidated
  1861. const linkDest = getElementDest(el);
  1862. const isUpToDate =
  1863. linkDest === elBonafide ||
  1864. linkDest === el?.getAttribute(ATTR_PROXIFIED_SITE) ||
  1865. linkDest === el?.getAttribute(ATTR_PROXIFIED_FARSIDE);
  1866.  
  1867. return elBonafide && isUpToDate;
  1868. }
  1869.  
  1870. /**
  1871. * Get the link destination
  1872. * @param {HTMLElement} el
  1873. * @returns {string?}
  1874. */
  1875. function getElementDest(el) {
  1876. switch (el.constructor) {
  1877. case HTMLAnchorElement:
  1878. return el.href;
  1879. case HTMLIFrameElement:
  1880. return el.src;
  1881. default:
  1882. return null;
  1883. }
  1884. }
  1885.  
  1886. /**
  1887. * Returns the dictionary entry for the specified host, agnostic to WWW
  1888. * Take explicit host match if found, but optionally try searching for SLD+TLD only (strip subdomains)
  1889. *
  1890. * @param {Object<string, T?>} dictionary
  1891. * @param {string} host
  1892. * @param {boolean} allowFuzzy
  1893. * @returns {T?}
  1894. */
  1895. function getByHost(dictionary, host, allowFuzzy) {
  1896. if (typeof dictionary !== 'object') {
  1897. error('Invalid dictionary used to look up host');
  1898. return null;
  1899. }
  1900.  
  1901. // Find the explicit entry for the host key
  1902. const explicit = dictionary[host];
  1903. if (!!explicit) {
  1904. return explicit;
  1905. }
  1906.  
  1907. // Strip WWW and try again
  1908. const agnosticWWW = dictionary[host.replace(/^www\./, '')];
  1909. if (!!agnosticWWW) {
  1910. return agnosticWWW;
  1911. }
  1912.  
  1913. // Non-WWW subdomains in hostname may be causing misses
  1914. // Too many TLDs to handle easily, so allow fuzzy matching on SLD+TLD if enabled
  1915. // Search for dictionary keys matching to the end of host and try the first hit
  1916. if (allowFuzzy) {
  1917. // try any dictionary key that has opted into fuzzy matching
  1918. const candidateHostKey = Object.keys(dictionary)
  1919. .filter(hostKey => !!dictionary[hostKey].allowFuzzy)
  1920. .find(hostKey => {
  1921. // try to fit the key to the end of the matching host
  1922. const SLDTLDRegex = new RegExp(escapeRegExp(hostKey) + '$');
  1923. return SLDTLDRegex.test(host);
  1924. });
  1925.  
  1926. const agnosticSubdomain = dictionary[candidateHostKey];
  1927. if (!!agnosticSubdomain) {
  1928. return agnosticSubdomain;
  1929. }
  1930. }
  1931.  
  1932. return null;
  1933. }
  1934.  
  1935. /**
  1936. * Escape regex special characters in a string
  1937. * TC39 X-standard
  1938. *
  1939. * @param {string} string
  1940. * @returns {string}
  1941. */
  1942. function escapeRegExp(string) {
  1943. return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  1944. }
  1945.  
  1946. /**
  1947. * Print to console as error
  1948. *
  1949. * @param {...any} data
  1950. */
  1951. function error(...data) {
  1952. if (DEBUG) {
  1953. console.error(...data);
  1954. }
  1955. }
  1956.  
  1957. /**
  1958. * Print to console as warning
  1959. *
  1960. * @param {...any} data
  1961. */
  1962. function warn(...data) {
  1963. if (DEBUG) {
  1964. console.warn(...data);
  1965. }
  1966. }