水源解码

可在水源论坛 base64 解码选中内容

  1. // ==UserScript==
  2. // @name 水源解码
  3. // @namespace CCCC_David
  4. // @version 0.1.3
  5. // @description 可在水源论坛 base64 解码选中内容
  6. // @author CCCC_David
  7. // @match https://shuiyuan.sjtu.edu.cn/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (() => {
  13. 'use strict';
  14.  
  15. // From Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com
  16. // License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc.
  17. // Modified class attribute to fit in.
  18. const DECODE_ICON = '<svg class="fa d-icon svg-icon svg-string" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M336 352c97.2 0 176-78.8 176-176S433.2 0 336 0S160 78.8 160 176c0 18.7 2.9 36.8 8.3 53.7L7 391c-4.5 4.5-7 10.6-7 17v80c0 13.3 10.7 24 24 24h80c13.3 0 24-10.7 24-24V448h40c13.3 0 24-10.7 24-24V384h40c6.4 0 12.5-2.5 17-7l33.3-33.3c16.9 5.4 35 8.3 53.7 8.3zm40-176c-22.1 0-40-17.9-40-40s17.9-40 40-40s40 17.9 40 40s-17.9 40-40 40z"/></svg>';
  19.  
  20. // Parameters.
  21. const APPEND_DECODE_BUTTON_TARGET_CLASS = 'buttons';
  22.  
  23. // Utility functions.
  24. const escapeRegExpOutsideCharacterClass = (s) => s.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
  25.  
  26. const allowedPolicy = window.trustedTypes?.createPolicy?.('allowedPolicy', {createHTML: (x) => x});
  27. const createTrustedHTML = (html) => (allowedPolicy ? allowedPolicy.createHTML(html) : html);
  28.  
  29. const utf8Decoder = new TextDecoder('utf-8', {fatal: true});
  30. const htmlParser = new DOMParser();
  31.  
  32. const isBinaryString = (s) => s.split('').every((c) => c.charCodeAt(0) < 256);
  33.  
  34. const decodeUTF8BinaryString = (s) => {
  35. // Assuming input is binary string.
  36. const byteArray = new Uint8Array(s.split('').map((c) => c.charCodeAt(0)));
  37. try {
  38. return utf8Decoder.decode(byteArray);
  39. } catch {
  40. return null;
  41. }
  42. };
  43.  
  44. const decodeBase64AndURI = (data) => {
  45. let result = data, prevResult = data;
  46. // eslint-disable-next-line no-constant-condition
  47. while (true) {
  48. const tempResult = result;
  49. try {
  50. result = atob(result);
  51. } catch {
  52. break;
  53. }
  54. prevResult = tempResult;
  55. if (result === prevResult) {
  56. break;
  57. }
  58. }
  59. if (isBinaryString(result)) {
  60. result = decodeUTF8BinaryString(result) ?? prevResult;
  61. }
  62. try {
  63. result = decodeURIComponent(result);
  64. } catch {
  65. }
  66. return result;
  67. };
  68.  
  69. const lookupShortURLs = async (shortURLs) => {
  70. if (!shortURLs) {
  71. return new Map();
  72. }
  73. try {
  74. const response = await fetch('/uploads/lookup-urls', {
  75. method: 'POST',
  76. body: shortURLs.map((url) => `short_urls%5B%5D=${encodeURIComponent(url)}`).join('&'),
  77. headers: {
  78. 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
  79. 'Discourse-Present': 'true',
  80. 'Discourse-Logged-In': 'true',
  81. 'X-Requested-With': 'XMLHttpRequest',
  82. 'X-CSRF-Token': document.querySelector('meta[name=csrf-token]').content,
  83. },
  84. mode: 'same-origin',
  85. credentials: 'include',
  86. });
  87. if (!response.ok) {
  88. // eslint-disable-next-line no-console
  89. console.error(`lookupShortURLs fetch failure: ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`);
  90. return new Map();
  91. }
  92. const result = await response.json();
  93. return new Map(result.map((item) => [item.short_url, item.url]));
  94. } catch (e) {
  95. // eslint-disable-next-line no-console
  96. console.error(e);
  97. return new Map();
  98. }
  99. };
  100.  
  101. const renderContent = async (content) => {
  102. // First cook the content.
  103. const cookedContent = await window.require('discourse/lib/text').cookAsync(content);
  104. let tree;
  105. try {
  106. tree = htmlParser.parseFromString(cookedContent, 'text/html');
  107. } catch (e) {
  108. // eslint-disable-next-line no-console
  109. console.error(e);
  110. return '<font color="red">(Parse error)</font>';
  111. }
  112.  
  113. // Extract all short URLs and look up in batch.
  114. const shortURLs = [];
  115. for (const el of tree.querySelectorAll('img[data-orig-src], source[data-orig-src]')) {
  116. shortURLs.push(el.getAttribute('data-orig-src'));
  117. }
  118. for (const el of tree.querySelectorAll('a[data-orig-href]')) {
  119. shortURLs.push(el.getAttribute('data-orig-href'));
  120. }
  121. const shortURLMapping = await lookupShortURLs(shortURLs);
  122.  
  123. // Replace short URLs with real URLs.
  124. for (const el of tree.querySelectorAll('img[data-orig-src], source[data-orig-src]')) {
  125. const src = el.getAttribute('data-orig-src');
  126. if (shortURLMapping.has(src)) {
  127. el.src = shortURLMapping.get(src);
  128. el.removeAttribute('data-orig-src');
  129. }
  130. }
  131. for (const el of tree.querySelectorAll('a[data-orig-href]')) {
  132. const href = el.getAttribute('data-orig-href');
  133. if (shortURLMapping.has(href)) {
  134. el.href = shortURLMapping.get(href);
  135. el.removeAttribute('data-orig-href');
  136. }
  137. }
  138. return tree.body.innerHTML;
  139. };
  140.  
  141. const convertSelection = async () => {
  142. const selection = window.getSelection();
  143. const selectionString = selection.toString();
  144. const {anchorNode, focusNode} = selection;
  145. if (!selectionString || !anchorNode || !focusNode) {
  146. return;
  147. }
  148. let targetNode;
  149. if (anchorNode === focusNode) {
  150. targetNode = anchorNode;
  151. } else if (anchorNode.contains(focusNode)) {
  152. targetNode = focusNode;
  153. } else if (focusNode.contains(anchorNode)) {
  154. targetNode = anchorNode;
  155. } else {
  156. targetNode = focusNode;
  157. }
  158. if (targetNode.outerHTML === undefined) {
  159. targetNode = targetNode.parentNode;
  160. }
  161. targetNode.outerHTML = createTrustedHTML(await renderContent(decodeBase64AndURI(selectionString)));
  162. selection.removeAllRanges();
  163. };
  164.  
  165. const addDecodeButton = (quoteButtonContainer) => {
  166. if (!quoteButtonContainer?.matches?.('div') ||
  167. quoteButtonContainer.closest('.error-page') ||
  168. document.getElementById('decode-selection-button')) {
  169. return;
  170. }
  171. const decodeButtonContainer = document.createElement('span');
  172. decodeButtonContainer.innerHTML = createTrustedHTML(`
  173. <button title="解码" id="decode-selection-button" class="btn-flat btn btn-icon-text" type="button">
  174. ${DECODE_ICON}
  175. <span class="d-button-label">解码</span>
  176. </button>
  177. `);
  178. quoteButtonContainer.appendChild(decodeButtonContainer);
  179. const decodeSelectionButton = document.getElementById('decode-selection-button');
  180. decodeSelectionButton.addEventListener('click', () => {
  181. decodeSelectionButton.disabled = true;
  182. setTimeout(() => {
  183. decodeSelectionButton.disabled = false;
  184. }, 1000);
  185. convertSelection();
  186. });
  187. };
  188.  
  189. const matchRegExp = new RegExp(`(?:^|\\s)${escapeRegExpOutsideCharacterClass(APPEND_DECODE_BUTTON_TARGET_CLASS)}(?:\\s|$)`, 'u');
  190.  
  191. const observer = new MutationObserver((mutationsList) => {
  192. for (const mutation of mutationsList) {
  193. if (mutation.type === 'childList') {
  194. for (const node of mutation.addedNodes) {
  195. if (node.matches?.(`.${APPEND_DECODE_BUTTON_TARGET_CLASS}`)) {
  196. addDecodeButton(node);
  197. }
  198. for (const el of node.getElementsByClassName?.(APPEND_DECODE_BUTTON_TARGET_CLASS) || []) {
  199. addDecodeButton(el);
  200. }
  201. }
  202. } else if (mutation.type === 'attributes') {
  203. if (mutation.attributeName === 'class') {
  204. if (!matchRegExp.test(mutation.oldValue ?? '') &&
  205. mutation.target.matches?.(`.${APPEND_DECODE_BUTTON_TARGET_CLASS}`)) {
  206. addDecodeButton(mutation.target);
  207. }
  208. }
  209. }
  210. }
  211. });
  212.  
  213. observer.observe(document.documentElement, {
  214. subtree: true,
  215. childList: true,
  216. attributeFilter: ['class'],
  217. attributeOldValue: true,
  218. });
  219.  
  220. for (const el of document.getElementsByClassName(APPEND_DECODE_BUTTON_TARGET_CLASS)) {
  221. addDecodeButton(el);
  222. }
  223. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址