PageSourceSearch

https://www.saishin.co.jp/shared/js/dompurify_helper.js

js saishin.co.jp collected 2026-10-04 16:31:39 UTC 5,426 bytes, 211 lines download raw bytes

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.