diff options
Diffstat (limited to 'content')
-rw-r--r-- | content/pgoogle_drive_browse/index.json | 5 | ||||
-rw-r--r-- | content/sgoogle_drive_browse/google_drive_browse_folder.js | 373 | ||||
-rw-r--r-- | content/sgoogle_drive_browse/index.json | 6 |
3 files changed, 384 insertions, 0 deletions
diff --git a/content/pgoogle_drive_browse/index.json b/content/pgoogle_drive_browse/index.json new file mode 100644 index 0000000..b1fa688 --- /dev/null +++ b/content/pgoogle_drive_browse/index.json @@ -0,0 +1,5 @@ +{ +"type" : "page", +"pattern" : "https://drive.google.com/drive/folders/***", +"payload" : ["script", "google_drive_browse"] +} diff --git a/content/sgoogle_drive_browse/google_drive_browse_folder.js b/content/sgoogle_drive_browse/google_drive_browse_folder.js new file mode 100644 index 0000000..7843b15 --- /dev/null +++ b/content/sgoogle_drive_browse/google_drive_browse_folder.js @@ -0,0 +1,373 @@ +/** + * Copyright 2021 Wojtek Kosior + * + * This program is free software; you can redistribute it + * and/or modify it under the terms of either: + * - the GNU General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at + * your option) any later version, or + * - the "A" license: <https://koszko.org/alicense.txt>; explained + * at: <https://koszko.org/en/articles/my-new-license.html> + * + * As additional permission under GNU GPL version 3 section 7, you + * may distribute forms of that code without the copy of the GNU + * GPL normally required by section 4, provided you include this + * license notice and, in case of non-source distribution, a URL + * through which recipients can access the Corresponding Source. + * If you modify file(s) with this exception, you may extend this + * exception to your version of the file(s), but you are not + * obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. If you delete this + * exception statement from all source files in the program, then + * also delete it here. + * + * As a special exception to the GPL, any HTML file which merely + * makes function calls to this code, and for that purpose + * includes it by reference shall be deemed a separate work for + * copyright law purposes. If you modify this code, you may extend + * this exception to your version of the code, but you are not + * obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +/* Use with https://drive.google.com/drive/folders/*** */ + +/* Define how to handle various mime types used by Google. */ + +const known_mimes = { + "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", + new_mime: "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", + new_mime: "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", + new_mime: "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", + new_mime: "image/jpeg" + }, + "application/vnd.google-apps.script": { + links: id => ({ + download: `https://script.google.com/feeds/download/export?format=json&id=${id}` + }), + type: "Google script", + new_mime: "application/vnd.google-apps.script+json" + }, + "application/vnd.google-apps.jam": { + links: id => ({ + download: `https://jamboard.google.com/export?id=${id}` + }), + type: "Google jam", + new_mime: "application/pdf" + } +}; + +/* + * Human-friendly names defined here will be displayed to the user instead of + * raw mime types. Please add more here. + */ +let mime_display_overrides = { + "image/jpeg": "JPEG image", + "application/octet-stream": "binary data", + "application/pdf": "PDF document", + "application/rar": "RAR archive", + "application/zip": "ZIP archive" +}; + +let default_link_producer = id => ({ + view: `https://drive.google.com/file/d/${id}`, + download: `https://drive.google.com/uc?export=download&id=${id}` +}); + +for (const [mime, display_name] of Object.entries(mime_display_overrides)) { + known_mimes[mime] = { + links: default_link_producer, + type: display_name, + new_mime: mime + } +} + +delete mime_display_overrides; + +function get_mime_info(mime) { + return known_mimes[mime] || { + links: default_link_producer, + type: mime || "", + new_mime: mime || "application/octet-stream" + } +} + +/* Prepare folder contents data as well as data regarding the folder itself. */ + +const content = new Map(); + +function add_content_item(item) +{ + const old_item = content.get(item.id) || {}; + Object.assign(old_item, item); + content.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_content_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 document.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_content_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 body = document.createElement("body"); +const folders = document.createElement("div"); +const files = document.createElement("div"); +const folders_heading = document.createElement("h2"); +const files_heading = document.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 = document.createElement("div"); + const item_heading = document.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 = get_mime_info(item_data.mime); + + if (mime_info.type) { + const type_div = document.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 = document.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 = document.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.new_mime; + + item_div.appendChild(download_button); + } + + (mime_info.is_folder ? folders : files).appendChild(item_div); +} + +for (const item of content.values()) + add_item_to_view(item); + +if (this_folder.filename) { + const heading = document.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 = document.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 = document.createElement("h3"); + no_files_message.textContent = "No files found."; + body.appendChild(no_files_message); +} + +if (document.body.firstChild.id !== "af-error-container") + document.documentElement.replaceChild(body, document.body); diff --git a/content/sgoogle_drive_browse/index.json b/content/sgoogle_drive_browse/index.json new file mode 100644 index 0000000..148ab65 --- /dev/null +++ b/content/sgoogle_drive_browse/index.json @@ -0,0 +1,6 @@ +{ +"type" : "script", +"name" : "google_drive_browse", +"sha256" : "e1648ba9f01272e2f5594800acf85515346ec0145e90e4dc8557e09f79b17b92", +"location" : "google_drive_browse_folder.js" +} |