/** * SPDX-License-Identifier: CC0-1.0 * * Copyright (C) 2021, 2026 Woj. Kosior */ (() => { blockJsOnUrl("https://drive.google.com/drive/folders/*"); /* Define how to handle various mime types used by Google. */ const defaultLinkProducer = id => ({ view: `https://drive.google.com/file/d/${id}`, download: `https://drive.google.com/uc?export=download&id=${id}` }); const knownMimes = { "application/vnd.google-apps.folder": { links: id => ({ view: `https://drive.google.com/drive/folders/${id}` }), type: "folder", is_folder: true }, "application/vnd.google-apps.shortcut": { links: id => ({ view: `https://drive.google.com/drive/folders/${id}` }), type: "shortcut", is_folder: true }, "application/vnd.google-apps.document": { links: id => ({ view: `https://docs.google.com/document/d/${id}`, download: `https://docs.google.com/document/d/${id}/export?format=odt`, }), type: "Google text document", newMime: "application/vnd.oasis.opendocument.text" }, "application/vnd.google-apps.spreadsheet": { links: id => ({ view: `https://docs.google.com/spreadsheets/d/${id}`, download: `https://docs.google.com/spreadsheets/d/${id}/export?format=ods` }), type: "Google spreadsheet", newMime: "application/vnd.oasis.opendocument.spreadsheet" }, "application/vnd.google-apps.presentation": { links: id => ({ view: `https://docs.google.com/presentation/d/${id}`, download: `https://docs.google.com/presentation/d/${id}/export/pptx` }), type: "Google presentation", newMime: "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, "application/vnd.google-apps.drawing": { links: id => ({ view: `https://docs.google.com/drawings/d/${id}`, download: `https://docs.google.com/drawings/d/${id}/export/jpeg` }), type: "Google drawing", newMime: "image/jpeg" }, "application/vnd.google-apps.script": { links: id => ({ download: `https://script.google.com/feeds/download/export?format=json&id=${id}` }), type: "Google script", newMime: "application/vnd.google-apps.script+json" }, "application/vnd.google-apps.jam": { links: id => ({ download: `https://jamboard.google.com/export?id=${id}` }), type: "Google jam", newMime: "application/pdf" }, /* * Human-friendly names defined below will be displayed to the user instead * of raw mime types. Please add more here. */ ...[ [ "image/jpeg", "JPEG image"], ["application/octet-stream", "binary data"], [ "application/pdf", "PDF document"], [ "application/rar", "RAR archive"], [ "application/zip", "ZIP archive"] ].map(([mime, displayName]) => [ mime, { links: id => defaultLinkProducer, type: displayName, newMime: mime } ]) }; function getMimeInfo(mime) { return knownMimes[mime] || { links: defaultLinkProducer, type: mime || "", newMime: mime || "application/octet-stream" }; } function transformDocument(origDoc) { /* * Prepare folder contents data as well as data regarding the folder itself. */ const contents = new Map(); function add_contents_item(item) { const old_item = contents.get(item.id) || {}; Object.assign(old_item, item); contents.set(item.id, old_item); } const this_folder = {}; function replace_string_escape(match, group) { return String.fromCharCode(`0x${group}`); } function try_parse(data, replace_x_escapes) { if (!data) return null; if (replace_x_escapes) data = data.replaceAll(/\\x([0-9a-f]{2})/g, replace_string_escape); try { return JSON.parse(data); } catch (e) { console.log(e); } return null; } function process_file_data(file_data, callback) { if (!Array.isArray(file_data) || !typeof file_data[0] === "string") { console.log("cannot process the following file data object:", file_data); return; } const result = {id: file_data[0], folders: []}; if (Array.isArray(file_data[1])) { for (const item of file_data[1]) { if (typeof item === "string" && item !== this_folder.id) result.folders.push(item); } } if (typeof file_data[2] === "string") result.filename = file_data[2]; if (typeof file_data[3] === "string" && file_data[3].search("/") >= 0) result.mime = file_data[3]; if (typeof file_data[9] === "number") result.date1 = new Date(file_data[9]); if (typeof file_data[10] === "number") result.date2 = new Date(file_data[10]); callback(result); } /* * By searching for scripts with calls to AF_initDataCallback we get about 7 * matches. All arguments to this function seem to be arrays parseable as * JSON, but their contents are very different and only two of those ~7 * arrays actually hold some data useful to us. Here we try to filter out * the other cases and then extract the useful data. */ function process_af_init_data(data) { if (!Array.isArray(data) || !/^driveweb/.test(data[0]) || !Array.isArray(data[1])) return; /* * First useful "kind" of object we can encounter is this folder's data. */ if (typeof data[1][0] === "string") { process_file_data(data[1], item => Object.assign(this_folder, item)); return; } /* * Second "kind" of object holds data about all items in the folder. * Folders and Files are stored in separate lists (doesn't matter to us, * since we distinguish them based on mime type anyway). */ for (const data_sub of data[1]) { if (!Array.isArray(data_sub) || !/^driveweb/.test(data_sub[0]) || !Array.isArray(data_sub[1])) continue; for (const item of data_sub[1]) process_file_data(item, add_contents_item); } } /* * Folder items data actually exists in both of the 2 kinds of scripts we * search for. In case of both of the regexes below we call * `process_file_data' in the end. As a result, we process the same file * multiple time as it appears in 2 scripts. This is, however, a good thing, * because it makes a change less likely to break our fix. */ const ivd_data_regex = /\s*window\s*\['_DRIVE_ivd'\]\s*=\s*'(\\x5b[^']+)'(.*)$/; const af_init_data_regex = /AF_initDataCallback\s*\(.+data\s*:\s*(\[.*\])[^\]]+$/; for (const script of origDoc.scripts) { const ivd_data_match = ivd_data_regex.exec(script.textContent); if (ivd_data_match) { const ivd_data = try_parse(ivd_data_match[1], true); if (ivd_data && Array.isArray(ivd_data) && Array.isArray(ivd_data[0])) { for (const item of ivd_data[0]) process_file_data(item, add_contents_item); } } const af_init_data_match = af_init_data_regex.exec(script.textContent); if (af_init_data_match) { const af_init_data = try_parse(af_init_data_match[1], false); if (af_init_data) process_af_init_data(af_init_data); } } /* Construct our own user interface. */ const newDoc = new DOMParser().parseFromString('', 'text/html'); const body = newDoc.createElement("body"); const folders = newDoc.createElement("div"); const files = newDoc.createElement("div"); const folders_heading = newDoc.createElement("h2"); const files_heading = newDoc.createElement("h2"); folders_heading.textContent = "Folders"; files_heading.textContent = "Files"; let has_folders = false; let has_files = false; folders.appendChild(folders_heading); files.appendChild(files_heading); body.setAttribute("style", "width: 100vw; height: 100vh; overflow: scroll; color: #555; margin: 15px; -webkit-user-select: initial;"); const drive_folder_regex = /application\/vnd.google-apps.(folder|shortcut)/; function add_item_to_view(item_data) { const item_div = newDoc.createElement("div"); const item_heading = newDoc.createElement("h4"); item_div.setAttribute("style", "border: 2px solid #999; border-radius: 8px; padding: 10px; display: inline-block; margin: 2px;"); let item_heading_style = "margin: 8px 4px;"; if (item_data.filename) { item_heading.textContent = item_data.filename; } else { item_heading.textContent = "(no name)"; item_heading_style += " font-style:italic;"; } item_heading.setAttribute("style", item_heading_style); item_div.appendChild(item_heading); const mime_info = getMimeInfo(item_data.mime); if (mime_info.type) { const type_div = newDoc.createElement("div"); type_div.setAttribute("style", "margin-bottom: 5px;"); type_div.textContent = mime_info.type; item_div.appendChild(type_div); } if (mime_info.is_folder) has_folders = true; else has_files = true; const links = {}; if (item_data.id) Object.assign(links, mime_info.links(item_data.id)); if (links.view) { const view_button = newDoc.createElement("a"); view_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block; margin: 5px;"); view_button.textContent = "view"; view_button.href = links.view; item_div.appendChild(view_button); } if (links.download) { const download_button = newDoc.createElement("a"); download_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block; margin: 5px;"); download_button.textContent = "download"; download_button.href = links.download; download_button.type = mime_info.newMime; item_div.appendChild(download_button); } (mime_info.is_folder ? folders : files).appendChild(item_div); } for (const item of contents.values()) add_item_to_view(item); if (this_folder.filename) { const heading = newDoc.createElement("h1"); heading.textContent = this_folder.filename; body.appendChild(heading); } if (this_folder.folders && this_folder.folders.length > 0) { for (const parent_id of this_folder.folders) { const up_button = newDoc.createElement("a"); let text = "go up"; up_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block; margin: 5px;"); up_button.href = `https://drive.google.com/drive/folders/${parent_id}`; if (this_folder.folders.length > 1) text = `${text} (${parent_id})`; up_button.textContent = text; body.appendChild(up_button); } } if (has_folders) body.appendChild(folders); if (has_files) body.appendChild(files); if (!(has_files || has_folders)) { const no_files_message = newDoc.createElement("h3"); no_files_message.textContent = "No files found."; body.appendChild(no_files_message); } newDoc.documentElement.replaceChild(body, newDoc.body); return '\n' + newDoc.documentElement.outerHTML; } function alterGoogleDriveFolderResponse(details) { if (String(details.statusCode)[0] !== '2') return; for (const header of details.responseHeaders) { if (/^content-type$/i.test(header.name) && !/^text[/]html; *charset *= *utf-8/i.test(header.value)) return; } const filter = browser.webRequest.filterResponseData(details.requestId); const decoder = new TextDecoder("utf-8"); const pieces = []; filter.ondata = event => pieces.push( decoder.decode(event.data, { stream: true }) ); filter.onstop = event => { const origDoc = new DOMParser() .parseFromString(pieces.join(''), 'text/html'); filter.write(new TextEncoder().encode(transformDocument(origDoc))); filter.close(); }; } browser.webRequest.onHeadersReceived.addListener( alterGoogleDriveFolderResponse, { urls: ["https://drive.google.com/drive/folders/*"], types: ["main_frame", "sub_frame"] }, ["blocking", "responseHeaders"] ); })();