1/** 2 * - DOMPurifyé¢é£å¦ç 3 */ 4 5const DOMPURIFY_SRC = "/shared/js/purify.min.js"; 6const DOMPURIFY_LOAD_TIMEOUT = 5000; 7let domPurifyLoadingPromise = null; 8 9/** 10 * - DOMPurifyã®å©ç¨å¯å¦ãå¤å® 11 * 12 * @returns {boolean} 13 */ 14const isDOMPurifyAvailable = () => 15 typeof window.DOMPurify?.sanitize === "function"; 16 17/** 18 * - jQueryãåå¾ 19 * 20 * @returns {JQueryStatic} 21 */ 22const getJQuery = () => { 23 if (typeof window.jQuery !== "function") { 24 throw new Error("jQueryã確èªã§ãã¾ãã"); 25 } 26 return window.jQuery; 27}; 28 29/** 30 * - æå®ãããURLã¨ä¸è´ããscriptè¦ç´ ãåå¾ 31 * 32 * @param {string} src 33 * @returns {HTMLScriptElement | undefined} 34 */ 35const findScriptBySrc = (src) => { 36 const targetUrl = new URL(src, document.baseURI).href; 37 38 return Array.from(document.querySelectorAll("script[src]")).find((script) => { 39 try { 40 const scriptUrl = new URL(script.src, document.baseURI).href; 41 return scriptUrl === targetUrl; 42 } catch { 43 return false; 44 } 45 }); 46}; 47 48/** 49 * - DOMPurifyãåçã«èªã¿è¾¼ã¿ 50 * - è¤æ°åå¼ã³åºãããå ´åã¯åãPromiseãå ±æ 51 * 52 * @returns {Promise<object>} 53 */ 54export const loadDOMPurify = () => { 55 if (isDOMPurifyAvailable()) { 56 return Promise.resolve(window.DOMPurify); 57 } 58 if (domPurifyLoadingPromise) { 59 return domPurifyLoadingPromise; 60 } 61 62 domPurifyLoadingPromise = new Promise((resolve, reject) => { 63 const existingScript = findScriptBySrc(DOMPURIFY_SRC); 64 const script = existingScript ?? document.createElement("script"); 65 let timeoutId; 66 67 const cleanup = () => { 68 window.clearTimeout(timeoutId); 69 script.removeEventListener("load", handleLoad); 70 script.removeEventListener("error", handleError); 71 }; 72 73 const handleLoad = () => { 74 cleanup(); 75 if (!isDOMPurifyAvailable()) { 76 if (!existingScript) { 77 script.remove(); 78 } 79 reject(new Error("DOMPurify sanitizeã確èªã§ãã¾ãã")); 80 return; 81 } 82 resolve(window.DOMPurify); 83 }; 84 85 const handleError = () => { 86 cleanup(); 87 if (!existingScript) { 88 script.remove(); 89 } 90 reject(new Error(`DOMPurifyã®èªã¿è¾¼ã¿ã«å¤±æ ${DOMPURIFY_SRC}`)); 91 }; 92 93 script.addEventListener("load", handleLoad, { 94 once: true, 95 }); 96 97 script.addEventListener("error", handleError, { 98 once: true, 99 }); 100 101 timeoutId = window.setTimeout(() => { 102 cleanup(); 103 if (!existingScript) { 104 script.remove(); 105 } 106 reject( 107 new Error( 108 `DOMPurifyã®èªã¿è¾¼ã¿ãã¿ã¤ã ã¢ã¦ã ${DOMPURIFY_LOAD_TIMEOUT}ms`, 109 ), 110 ); 111 }, DOMPURIFY_LOAD_TIMEOUT); 112 113 if (!existingScript) { 114 script.src = DOMPURIFY_SRC; 115 script.async = true; 116 script.dataset.dompurifyLoader = "dompurify-helper"; 117 document.head.appendChild(script); 118 } else if (isDOMPurifyAvailable()) { 119 handleLoad(); 120 } 121 }).catch((error) => { 122 domPurifyLoadingPromise = null; 123 throw error; 124 }); 125 return domPurifyLoadingPromise; 126}; 127 128/** 129 * - HTMLæååããµãã¿ã¤ãº 130 * - DOMPurifyãå é¨ã§èªã¿è¾¼ã¿ 131 * 132 * @param {string} html 133 * @returns {Promise<string>} 134 */ 135export const sanitizeHtml = async (html) => { 136 if (typeof html !== "string") { 137 throw new TypeError("ãµãã¿ã¤ãºå¯¾è±¡ã¯æååã§æå®ãã¦ãã ãã"); 138 } 139 const DOMPurify = await loadDOMPurify(); 140 return DOMPurify.sanitize(html, { 141 USE_PROFILES: { 142 html: true, 143 }, 144 ADD_ATTR: ["accept-charset"], 145 RETURN_TRUSTED_TYPE: false, 146 }); 147}; 148 149/** 150 * - Ajaxã§HTMLãåå¾ 151 * - åå¾ããHTMLããµãã¿ã¤ãº 152 * 153 * @param {object} options 154 * @param {string} options.url 155 * @param {number} options.timeout 156 * @returns {Promise<string>} 157 */ 158export const fetchSanitizedHtml = ({ url, timeout }) => { 159 const jQuery = getJQuery(); 160 161 return new Promise((resolve, reject) => { 162 jQuery 163 .ajax(url, { 164 timeout, 165 dataType: "html", 166 }) 167 .done(async (data) => { 168 try { 169 const sanitizedHtml = await sanitizeHtml(data); 170 resolve(sanitizedHtml); 171 } catch (error) { 172 reject(error); 173 } 174 }) 175 .fail((jqXHR, textStatus, errorThrown) => { 176 const statusCode = jqXHR?.status ?? "unknown"; 177 reject( 178 new Error( 179 `${url}ã®åå¾ã«å¤±æ ` + 180 `statusCode=${statusCode} ` + 181 `textStatus=${textStatus} ` + 182 `error=${errorThrown || "unknown"}`, 183 ), 184 ); 185 }); 186 }); 187}; 188 189/** 190 * - ãµãã¿ã¤ãºæ¸ã¿HTMLãDocumentFragmentã¸å¤æ 191 * 192 * @param {string} sanitizedHtml 193 * @returns {DocumentFragment} 194 */ 195export const createSanitizedFragment = (sanitizedHtml) => { 196 const template = document.createElement("template"); 197 template.innerHTML = sanitizedHtml; 198 return template.content; 199}; 200 201/** 202 * - ãµãã¿ã¤ãºæ¸ã¿HTMLããæå®è¦ç´ ãåå¾ 203 * 204 * @param {string} sanitizedHtml 205 * @param {string} selector 206 * @returns {Element | null} 207 */ 208export const findSanitizedElement = (sanitizedHtml, selector) => { 209 const fragment = createSanitizedFragment(sanitizedHtml); 210 return fragment.querySelector(selector); 211};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.