aboutsummaryrefslogtreecommitdiff
path: root/background/google-drive-folders.js
diff options
context:
space:
mode:
authorW. Kosior <koszko@koszko.org>2026-08-06 23:19:37 +0200
committerW. Kosior <koszko@koszko.org>2026-08-06 23:22:59 +0200
commit98becbe8032816869a1d7e06121ed72b636c89be (patch)
tree42c5266f2a950a38338bf97c048fe6c3c5773c71 /background/google-drive-folders.js
parent01fc4dc8a88a91567231e0096961de10f924ae6a (diff)
downloadsimple-browser-extension-magister.tar.gz
simple-browser-extension-magister.zip
google-drive: Change folders fix from content to background script.HEADmagister
This also removes helpers for blocking CSS from `background.js' (nothing shall be using them now).
Diffstat (limited to 'background/google-drive-folders.js')
-rw-r--r--background/google-drive-folders.js396
1 files changed, 396 insertions, 0 deletions
diff --git a/background/google-drive-folders.js b/background/google-drive-folders.js
new file mode 100644
index 0000000..4e6a9c6
--- /dev/null
+++ b/background/google-drive-folders.js
@@ -0,0 +1,396 @@
+/**
+ * SPDX-License-Identifier: CC0-1.0
+ *
+ * Copyright (C) 2021, 2026 Woj. Kosior <koszko@koszko.org>
+ */
+
+(() => {
+
+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('<html>', '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 '<!DOCTYPE html>\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"]
+);
+
+})();