aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/accuweather.js19
-rw-r--r--src/bandcamp.js31
-rw-r--r--src/box.js300
-rw-r--r--src/fedoraaccounts.js37
-rw-r--r--src/google_drive_files.js168
-rw-r--r--src/google_drive_folders.js384
-rw-r--r--src/google_forms.js66
-rw-r--r--src/google_sheets_download.js210
-rw-r--r--src/internet_archive_video.js91
-rw-r--r--src/odysee.js425
-rw-r--r--src/opencores.js46
-rw-r--r--src/pcspecialist_cookie_notice.js21
-rw-r--r--src/pcspecialist_display_prices.js57
-rw-r--r--src/phoronix_benchmarks.js38
-rw-r--r--src/royal_geographical_society.js20
-rw-r--r--src/santander_centrum24.js25
-rw-r--r--src/stack_exchange_cookienotice.js20
-rw-r--r--src/sumofus.js56
-rw-r--r--src/vaticannews_videos.js24
-rw-r--r--src/worldcat.js67
-rw-r--r--src/yewtube_urls.js58
-rw-r--r--src/youtube_yewtube_redirection.js21
22 files changed, 2184 insertions, 0 deletions
diff --git a/src/accuweather.js b/src/accuweather.js
new file mode 100644
index 0000000..d178451
--- /dev/null
+++ b/src/accuweather.js
@@ -0,0 +1,19 @@
+/**
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2021 jahoti <jahoti@tilde.team>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+for (var nonAd of document.querySelectorAll('.ads-not-loaded .non-ad')) nonAd.style.visibility = 'visible';
diff --git a/src/bandcamp.js b/src/bandcamp.js
new file mode 100644
index 0000000..eafcc1e
--- /dev/null
+++ b/src/bandcamp.js
@@ -0,0 +1,31 @@
+/**
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2021 jahoti <jahoti@tilde.team>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var div, player, playerBox = document.querySelector('.inline_player');
+playerBox.innerHTML = '';
+
+for (var track of JSON.parse(document.querySelector('[data-tralbum]').dataset.tralbum).trackinfo) {
+ div = document.createElement('div');
+ player = document.createElement('audio');
+ player.controls = 'controls';
+
+ div.innerText = track.title + ': ';
+ player.src = track.file['mp3-128']; // Is this always available?
+ div.append(player);
+ playerBox.append(div);
+}
diff --git a/src/box.js b/src/box.js
new file mode 100644
index 0000000..f02bf3a
--- /dev/null
+++ b/src/box.js
@@ -0,0 +1,300 @@
+/**
+ * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions
+ *
+ * Copyright 2022 Jacob K
+ * Copyright 2022 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+ * license. Although I request that you do not make use of this code in a
+ * proprietary program, I am not going to enforce this in court.
+ */
+
+// meta: match should be https://***.app.box.com/s/* (*** instead of * for the first section because otherwise plain app.box.com URLs won't work)
+// meta: some test cases (mostly found at https://old.reddit.com/search?q="box.com"&include_over_18=on&sort=new)
+ // https://uwmadison.app.box.com/s/ydht2incbdmw1lhpjg5t40adguc0fm14
+ // umadison's enrollment report
+ // pdf
+ // https://app.box.com/s/gc4ygloi4qtimeh98dq9mmydyuydawcn
+ // password-protected 7z file (nsfw)
+ // https://app.box.com/shared/static/su6xx6zx50cd68zdtbm3wfxhh9kwke8x.zip
+ // a soundtrack in a zip file
+ // This is a static download, so it works without this script.
+ // https://app.box.com/s/vysdh2u78yih3c8leetgq82il954a3g3
+ // some gambling ad
+ // pptx
+ // https://app.box.com/s/nnlplkmjhimau404qohh9my10pwmo8es
+ // a list of books(?)
+ // txt
+ // https://ucla.app.box.com/s/mv32q624ojihohzh8d0mhhj0b3xluzbz
+ // "COVID-19 Pivot Plan Decision Matrix"
+ // If you load the proprietary scripts on this page, you'll see that there is no download button
+ // TODO: find a public folder link (the private links I have seem to work)
+ // TODO: find a (preferably public) link with a folder inside a folder, as these may need to be handled differently
+
+/* Extract data from a script that sets multiple variables. */ // from here: https://api-demo.hachette-hydrilla.org/content/sgoogle_sheets_download/google_sheets_download.js
+
+let prefetchedData = null; // This variable isn't actually used.
+for (const script of document.scripts) {
+ const match = /Box.prefetchedData = ({([^;]|[^}];)+})/.exec(script.textContent); // looks for "Box.prefetchedData = " in the script files and then grabs the json text after that.
+ if (!match)
+ continue;
+ prefetchedData = JSON.parse(match[1]);
+}
+
+let config = null;
+for (const script of document.scripts) {
+ const match = /Box.config = ({([^;]|[^}];)+})/.exec(script.textContent); // looks for "Box.config = " in the script files and then grabs the json text after that.
+ if (!match)
+ continue;
+ config = JSON.parse(match[1]);
+}
+
+let postStreamData = null;
+for (const script of document.scripts) {
+ const match = /Box.postStreamData = ({([^;]|[^}];)+})/.exec(script.textContent); // looks for "Box.postStreamData = " in the script files and then grabs the json text after that.
+ if (!match)
+ continue;
+ postStreamData = JSON.parse(match[1]);
+}
+
+// get domain from URL
+const domain = document.location.href.split("/")[2];
+
+/* Replace current page contents. */
+const replacement_html = `\
+<!DOCTYPE html>
+<html>
+ <head>
+ <style>
+ button, .button {
+ border-radius: 10px;
+ padding: 20px;
+ color: #333;
+ background-color:
+ lightgreen;
+ text-decoration: none;
+ display: inline-block;
+ }
+ button:hover, .button:hover {
+ box-shadow: -4px 8px 8px #888;
+ }
+
+ .hide {
+ display: none;
+ }
+
+ #download_button .unofficial, #download_button .red_note {
+ display: none;
+ }
+ #download_button.unofficial .unofficial {
+ display: inline;
+ }
+ #download_button.unofficial .red_note {
+ display: block;
+ }
+ .red_note {
+ font-size: 75%;
+ color: #c55;
+ font-style: italic;
+ text-align: center;
+ }
+ </style>
+ </head>
+ <body>
+ <h1 id="loading" class="hide">loading...</h1>
+ <h1 id="error" class="hide">error occured :(</h1>
+ <h1 id="title" class="hide"></h1>
+ <div id="single_file_section" class="hide">
+ <a id="download_button" class="button">
+ <span class="unofficial">unofficial</span> download
+ <aside class="red_note">(officially disallowed)</aside>
+ </a>
+ <aside></aside>
+ <h2>File info</h2>
+ <div id="file_info"></div>
+ </div>
+ </body>
+</html>
+`;
+
+/*
+ * We could instead use document.write(), but browser's debugging tools would
+ * not see the new page contents.
+ */
+const parser = new DOMParser();
+const alt_doc = parser.parseFromString(replacement_html, "text/html");
+document.documentElement.replaceWith(alt_doc.documentElement);
+
+const nodes = {};
+document.querySelectorAll('[id]').forEach(node => nodes[node.id] = node);
+
+function show_error() {
+ nodes.loading.classList.add("hide");
+ nodes.error.classList.remove("hide");
+}
+
+function show_title(text) {
+ nodes.title.innerText = text;
+ nodes.loading.classList.add("hide");
+ nodes.title.classList.remove("hide");
+}
+
+async function hack_file() {
+ nodes.loading.classList.remove("hide");
+
+ const tokens_url = "/app-api/enduserapp/elements/tokens";
+ const file_nr = postStreamData["/app-api/enduserapp/shared-item"].itemID;
+ const file_id = `file_${file_nr}`;
+ const shared_name = postStreamData["/app-api/enduserapp/shared-item"].sharedName;
+
+ /*
+ * We need to perform a POST to obtain a token that will be used later to
+ * authenticate against Box's API endpoint.
+ */
+ const tokens_response = await fetch(tokens_url, {
+ method: "POST",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "Request-Token": config.requestToken,
+ "X-Box-Client-Name": "enduserapp",
+ "X-Box-Client-Version": "20.712.2",
+ "X-Box-EndUser-API": `sharedName=${shared_name}`,
+ "X-Request-Token": config.requestToken
+ },
+ body: JSON.stringify({"fileIDs": [file_id]})
+ });
+ console.log("tokens_response", tokens_response);
+
+ const access_token = (await tokens_response.json())[file_id].read;
+ console.log("access_token", access_token);
+
+ const fields = [
+ "permissions", "shared_link", "sha1", "file_version", "name", "size",
+ "extension", "representations", "watermark_info",
+ "authenticated_download_url", "is_download_available",
+ "content_created_at", "content_modified_at", "created_at", "created_by",
+ "modified_at", "modified_by", "owned_by", "description",
+ "metadata.global.boxSkillsCards", "expires_at", "version_limit",
+ "version_number", "is_externally_owned", "restored_from",
+ "uploader_display_name"
+ ];
+
+ const file_info_url =
+ `https://api.box.com/2.0/files/${file_nr}?fields=${fields.join()}`;
+
+ /*
+ * We need to perform a GET to obtain file metadata. The fields we curently
+ * make use of are "authenticated_download_url" and "file_version", but in
+ * the request we also include names of other fields that the original Box
+ * client would include. The metadata is then dumped as JSON on the page, so
+ * the user, if curious, can look at it.
+ */
+ const file_info_response = await fetch(file_info_url, {
+ headers: {
+ "Accept": "application/json",
+ "Authorization": `Bearer ${access_token}`,
+ "BoxApi": `shared_link=${document.URL}`,
+ "X-Box-Client-Name": "ContentPreview",
+ "X-Rep-Hints": "[3d][pdf][text][mp3][json][jpg?dimensions=1024x1024&paged=false][jpg?dimensions=2048x2048,png?dimensions=2048x2048][dash,mp4][filmstrip]"
+ },
+ });
+ console.log("file_info_response", file_info_response);
+
+ const file_info = await file_info_response.json();
+ console.log("file_info", file_info);
+
+ const params = new URLSearchParams();
+ params.set("preview", true);
+ params.set("version", file_info.file_version.id);
+ params.set("access_token", access_token);
+ params.set("shared_link", document.URL);
+ params.set("box_client_name", "box-content-preview");
+ params.set("box_client_version", "2.82.0");
+ params.set("encoding", "gzip");
+
+ /* We use file metadata from earlier requests to construct the link. */
+ const download_url =
+ `${file_info.authenticated_download_url}?${params.toString()}`;
+ console.log("download_url", download_url);
+
+ show_title(file_info.name);
+
+ nodes.download_button.href = download_url;
+ if (!file_info.permissions.can_download)
+ nodes.download_button.classList.add("unofficial");
+ nodes.file_info.innerText = JSON.stringify(file_info);
+ nodes.single_file_section.classList.remove("hide");
+}
+
+if (postStreamData["/app-api/enduserapp/shared-item"].itemType == "file") {
+ /*
+ * We call hack_file and in case it asynchronously throws an exception, we
+ * make an error message appear.
+ */
+ hack_file().then(() => {}, show_error);
+} else if (postStreamData["/app-api/enduserapp/shared-item"].itemType == "folder") {
+ show_title(postStreamData["/app-api/enduserapp/shared-folder"].currentFolderName);
+
+ // TODO: implement a download folder button (included in proprietary app)
+ /*
+ The original download folder button sends a GET request that gets 2 URLs
+ in the response. 1 of those URLs downloads the file, and a POST request
+ is sent after (or maybe while in some cases?) a file is downloaded, to
+ let the server know how much is downloaded.
+ */
+ // for each item in the folder, show a button with a link to download it
+ postStreamData["/app-api/enduserapp/shared-folder"].items.forEach(function(item) {
+ console.log("item", item);
+
+ const file_direct_url = "https://"+domain+"/index.php?rm=box_download_shared_file&shared_name="+postStreamData["/app-api/enduserapp/shared-item"].sharedName+"&file_id="+item.typedID;
+
+ const folderButton = nodes.download_button.cloneNode(false);
+ folderButton.removeAttribute("id");
+
+ if (item.type == "file") {
+ folderButton.href = file_direct_url;
+ folderButton.innerText = item.name; // show the name of the file
+ } else if (item.type == "folder") {
+ folderButton.innerText = "[folders inside folders not yet supported]";
+ } else {
+ folderButton.innerText = "[this item type is not supported]";
+ }
+
+ document.body.appendChild(folderButton);
+ });
+} else {
+ console.log('expected "folder" or "file" as the item type (postStreamData["/app-api/enduserapp/shared-item"].itemType) but got ' + postStreamData["/app-api/enduserapp/shared-item"].itemType + ' instead; this item type is not implemented');
+ show_error();
+}
diff --git a/src/fedoraaccounts.js b/src/fedoraaccounts.js
new file mode 100644
index 0000000..27e76d9
--- /dev/null
+++ b/src/fedoraaccounts.js
@@ -0,0 +1,37 @@
+/**
+ * SPDX-License-Identifier: CC0-1.0
+ *
+ * Fix registration of a Fedora account
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the CC0 1.0 Universal License as published by
+ * the Creative Commons Corporation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * CC0 1.0 Universal License for more details.
+ */
+
+/* Haketilo fix to use with https://accounts.fedoraproject.org */
+
+var by_id = id => document.getElementById(id);
+
+var login_register_tabs = ['login', 'register'].map(by_id);
+var login_register_buttons = login_register_tabs.map(e => by_id(`${e.id}-tab`))
+
+function switch_tab(i)
+{
+ login_register_buttons[i].classList.add('active');
+ login_register_buttons[1 - i].classList.remove('active');
+
+ login_register_tabs[i].classList.add('show', 'active');
+ login_register_tabs[1 - i].classList.remove('show', 'active');
+}
+
+for (const i of [0, 1]) {
+ login_register_buttons[i].addEventListener('click', () => switch_tab(i));
+ login_register_buttons[i].href = '#'
+}
diff --git a/src/google_drive_files.js b/src/google_drive_files.js
new file mode 100644
index 0000000..f93194b
--- /dev/null
+++ b/src/google_drive_files.js
@@ -0,0 +1,168 @@
+/**
+ * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions
+ *
+ * Make files on drive.google.com downloadable without nonfree js
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+ * license. Although I request that you do not make use of this code in a
+ * proprietary program, I am not going to enforce this in court.
+ */
+
+/* Use with: https://drive.google.com/file/d/** */
+
+const og = {};
+
+for (const node of document.head.childNodes) {
+ if (node.tagName === "STYLE") {
+ document.head.removeChild(node);
+ continue;
+ }
+
+ if (node.tagName !== "META")
+ continue;
+
+ const match = /^og:(.+)/.exec(node.getAttribute("property"));
+ if (!match)
+ continue;
+
+ og[match[1]] = node.getAttribute("content");
+}
+
+const match = new RegExp("/file/d/([^/]+)").exec(document.URL);
+const file_id = match && match[1];
+
+/* Extract file's mime type from script. */
+
+let mime_type;
+
+for (const script of document.scripts) {
+ const match = /itemJson\s*:\s*(\[.*\])\s*}\s*;$/.exec(script.textContent);
+ if (!match)
+ continue;
+
+ let data;
+ try {
+ data = JSON.parse(match[1]);
+ } catch(e) {
+ console.log(e);
+ continue;
+ }
+
+ if (/^[^\/]+\/[^\/]+$/.test(data[11]))
+ mime_type = data[11];
+}
+
+/* If file is folder, redirect to its page. */
+
+const redirect = file_id &&
+ /^application\/vnd.google-apps.(folder|shortcut)$/.test(mime_type);
+if (redirect)
+ window.location.href = `https://drive.google.com/drive/folders/${file_id}`;
+
+const download_link =
+ file_id && `https://drive.google.com/uc?export=download&id=${file_id}`;
+
+const body = document.createElement("body");
+const name_div = document.createElement("div");
+const download_div = document.createElement("div");
+const download_button = document.createElement("a");
+const type_div = document.createElement("div");
+const type_span = document.createElement("span");
+const show_image_div = document.createElement("div");
+const show_image_button = document.createElement("button");
+const image = document.createElement("img");
+
+
+let button_text = "download";
+
+if (!og.title)
+ button_text += " file";
+else
+ name_div.textContent = og.title;
+
+name_div.setAttribute("style", "font-weight: bold; display:inline-block;");
+
+
+if (download_link)
+ download_button.setAttribute("href", download_link);
+else
+ button_text += " (unavailable)";
+
+download_button.textContent = button_text;
+download_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: black; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888;");
+
+
+download_div.appendChild(download_button);
+download_div.setAttribute("style", "padding: 10px; display: inline-block; margin: 0 0 10px;");
+
+
+type_span.textContent = "type: ";
+type_span.setAttribute("style", "font-weight: bold; display:inline-block; white-space: pre;");
+
+type_div.setAttribute("style", "margin: 0 0 10px;");
+type_div.appendChild(type_span);
+if (mime_type || og.type)
+ type_div.append(mime_type || og.type);
+
+
+function show_image()
+{
+ const image = document.createElement("img");
+ image.setAttribute("src", og.image);
+ image.setAttribute("width", og["image:width"]);
+ image.setAttribute("height", og["image:height"]);
+ image.setAttribute("style", "border: 1px solid #eee;");
+
+ document.body.replaceChild(image, show_image_div);
+}
+
+show_image_button.setAttribute("style", "border-radius: 5px; padding: 10px; color: black; background-color: lightgreen; box-shadow: -4px 8px 8px #888; font-family: inherit; font-size: inherit; border: none;");
+show_image_button.textContent = "show image";
+show_image_button.addEventListener("click", show_image);
+show_image_div.appendChild(show_image_button);
+
+
+body.setAttribute("style", "margin: 20px;");
+if (og.title)
+ body.appendChild(name_div);
+body.appendChild(download_div);
+if (og.type)
+ body.appendChild(type_div);
+if (og.image && og["image:width"] && og["image:height"])
+ body.appendChild(show_image_div);
+
+if (!redirect)
+ document.documentElement.replaceChild(body, document.body);
diff --git a/src/google_drive_folders.js b/src/google_drive_folders.js
new file mode 100644
index 0000000..7d37032
--- /dev/null
+++ b/src/google_drive_folders.js
@@ -0,0 +1,384 @@
+/**
+ * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions
+ *
+ * Make folders on drive.google.com browsable without nonfree js
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+ * license. Although I request that you do not make use of this code in a
+ * proprietary program, I am not going to enforce this in court.
+ */
+
+/* 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/src/google_forms.js b/src/google_forms.js
new file mode 100644
index 0000000..5d8826d
--- /dev/null
+++ b/src/google_forms.js
@@ -0,0 +1,66 @@
+/**
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * (Incomplete) Fix for Google Forms
+ *
+ * Copyright © 2021 jahoti <jahoti@tilde.team>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var form = document.forms[0];
+for (let div of form.querySelectorAll('div[data-params]')) {
+ var data = JSON.parse('[' + div.dataset.params.substring(4));
+ var name = 'entry.' + data[0][4][0][0];
+ var input = div.querySelector('input');
+
+ if (input.name === name + '_sentinel') { // Radio
+ for (input of div.querySelectorAll('.appsMaterialWizToggleRadiogroupElContainer')) {
+ div = document.createElement('input');
+ div.type = 'radio';
+ div.name = name;
+ div.value = input.nextElementSibling.innerText.trim();
+ input.parentNode.replaceChild(div, input);
+ }
+ } else {
+ input.removeAttribute('disabled');
+ input.name = name;
+ }
+}
+
+for (div of document.querySelectorAll('.quantumWizTextinputPaperinputPlaceholder'))
+ div.remove();
+
+function goToNext()
+{
+ var next = document.createElement('input');
+ next.type = 'hidden';
+ next.name = 'continue';
+ next.value = '1';
+ form.appendChild(next);
+ form.submit();
+}
+
+for (div of document.querySelectorAll('.freebirdFormviewerViewNavigationNoSubmitButton')) {
+ input = document.createElement('button');
+
+ data = div.innerText.trim();
+ input.innerText = data;
+ if (data.toLowerCase() === 'next')
+ input.onclick = goToNext;
+ else if (data.toLowerCase() === 'submit')
+ input.type = 'submit';
+ div.parentNode.replaceChild(input, div);
+}
+
+// TODO: back, instate previous entries
diff --git a/src/google_sheets_download.js b/src/google_sheets_download.js
new file mode 100644
index 0000000..24149ca
--- /dev/null
+++ b/src/google_sheets_download.js
@@ -0,0 +1,210 @@
+/**
+ * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions
+ *
+ * Make spreadsheets on drive.google.com browsable without nonfree js
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+ * license. Although I request that you do not make use of this code in a
+ * proprietary program, I am not going to enforce this in court.
+ */
+
+/* Use with https://docs.google.com/spreadsheets/d/** */
+
+/* Make the view scrollable. */
+
+document.body.setAttribute("style",
+ "width: 100vw; height: 100vh; overflow: scroll;" +
+ (document.body.getAttribute("style") || ""));
+
+let container = document.querySelectorAll(".waffle")[0];
+let main_gid = null;
+
+while (container) {
+ container = container.parentElement;
+ console.log(container);
+ if (container === document.body || !container)
+ break;
+
+ const match = /([0-9]+)-grid-container/.exec(container.id);
+ if (match)
+ main_gid = match[1];
+
+ container.setAttribute("style",
+ "width: fit-content; width: -moz-fit-content;");
+}
+
+
+/* Remove editor toolbars and bottom bar - these don't work anyway. */
+
+const docs_chrome = document.getElementById("docs-chrome");
+if (docs_chrome)
+ docs_chrome.remove()
+const grid_bottom_bar = document.getElementById("grid-bottom-bar");
+if (grid_bottom_bar)
+ grid_bottom_bar.remove()
+
+
+/* Remove no Javascript warning. */
+
+for (const no_js_warning of document.querySelectorAll("noscript"))
+ no_js_warning.remove();
+
+
+/* Get opengraph data. */
+
+const og = {};
+
+for (const node of document.head.childNodes) {
+ if (node.tagName === "STYLE") {
+ document.head.removeChild(node);
+ continue;
+ }
+
+ if (node.tagName !== "META")
+ continue;
+
+ const match = /^og:(.+)/.exec(node.getAttribute("property"));
+ if (!match)
+ continue;
+
+ og[match[1]] = node.getAttribute("content");
+}
+
+
+/* Construct download link. */
+
+let download_link = null;
+
+const match = new RegExp("/spreadsheets/d/([^/]+)").exec(document.URL);
+if (match)
+ download_link = `https://docs.google.com/spreadsheets/d/${match[1]}/export`;
+
+
+/* Add title bar with sheet name and download button. */
+
+const title_bar = document.createElement("div");
+const title_heading = document.createElement("h1");
+const title_text = document.createElement("span");
+const main_download_button = document.createElement("a");
+
+main_download_button.textContent = "download";
+main_download_button.setAttribute("style", "border-radius: 10px; padding: 20px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block;");
+
+if (og.title) {
+ title_text.textContent = og.title;
+ title_heading.appendChild(title_text);
+}
+
+title_text.setAttribute("style", "margin-right: 10px;");
+
+if (download_link) {
+ main_download_button.setAttribute("href", download_link);
+ title_heading.appendChild(main_download_button);
+}
+
+title_bar.setAttribute("style", "padding: 0 20px; color: #555;");
+
+title_bar.appendChild(title_heading);
+
+document.body.insertBefore(title_bar, document.body.firstElementChild);
+
+
+/* Extract sheet data from a script that sets the `bootstrapData' variable. */
+
+let data = null;
+for (const script of document.scripts) {
+ const match = /bootstrapData = ({([^;]|[^}];)+})/.exec(script.textContent);
+ if (!match)
+ continue;
+ data = JSON.parse(match[1]);
+}
+
+/*
+ * Add download buttons for individual sheets belonging to this spreadsheet.
+ * Data schema has been observed by looking at various spreadsheets.
+ */
+
+function add_sheet_download(data)
+{
+ if (!Array.isArray(data))
+ return;
+
+ const gid = data[2];
+ if (!["string", "number"].includes(typeof gid))
+ return;
+
+ const sheet_download_link = `${download_link}?gid=${gid}`;
+ const sheet_download_button = document.createElement("a");
+
+ sheet_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: 0 5px 5px 0;");
+ sheet_download_button.setAttribute("href", sheet_download_link);
+
+ let sheet_name = null;
+ if (Array.isArray(data[3]) &&
+ data[3][0] && typeof data[3][0] === "object"
+ && Array.isArray(data[3][0][1]) &&
+ Array.isArray(data[3][0][1][0]) &&
+ typeof data[3][0][1][0][2] === "string") {
+
+ const sheet_name = data[3][0][1][0][2];
+ sheet_download_button.textContent = sheet_name;
+ if (gid == main_gid)
+ title_text.textContent = `${title_text.textContent} - ${sheet_name}`;
+ } else {
+ sheet_download_button.textContent = `<sheet gid=${gid}>`;
+ }
+
+ title_bar.appendChild(sheet_download_button);
+}
+
+if (download_link) {
+ for (const entry of data.changes.topsnapshot) {
+ if (!Array.isArray(entry) || entry[0] !== 21350203 ||
+ typeof entry[1] !== "string")
+ continue;
+
+ let entry_data = null;
+
+ try {
+ entry_data = JSON.parse(entry[1]);
+ } catch (e) {
+ console.log(e);
+ continue;
+ }
+
+ add_sheet_download(entry_data);
+ }
+}
diff --git a/src/internet_archive_video.js b/src/internet_archive_video.js
new file mode 100644
index 0000000..f7d7198
--- /dev/null
+++ b/src/internet_archive_video.js
@@ -0,0 +1,91 @@
+/**
+ * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions
+ *
+ * Make videos on archive.org playable inline without relying on site-served js
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+ * license. Although I request that you do not make use of this code in a
+ * proprietary program, I am not going to enforce this in court.
+ */
+
+const theatre_ia = document.getElementById("theatre-ia");
+
+let srcs = [];
+
+let ogv_src = null;
+let webm_src = null;
+let mp4_src = null;
+
+function process_link(link)
+{
+ ogv_src = ogv_src || (/\.ogv$/.test(link) && link);
+ webm_src = webm_src || (/\.webm$/.test(link) && link);
+ mp4_src = mp4_src || (/\.mp4$/.test(link) && link);
+}
+
+if (theatre_ia) {
+ for (const a of
+ document.querySelectorAll(".item-download-options a.download-pill"))
+ process_link(a.href);
+
+ for (const link of document.querySelectorAll("link[itemprop=contentUrl]"))
+ process_link(link.href);
+
+ srcs = [
+ {src: ogv_src, type: "video/ogg"},
+ {src: webm_src, type: "video/webm"},
+ {src: mp4_src, type: "video/mp4"}
+ ].filter(src => src.src);
+}
+
+if (srcs.length > 0) {
+ const video = document.createElement("video");
+
+ for (const src of srcs) {
+ const source = document.createElement("source");
+ Object.assign(source, src);
+ video.appendChild(source);
+ }
+
+ video.setAttribute("width", "100%");
+ video.setAttribute("height", "auto");
+ video.setAttribute("controls", "");
+
+ for (const child of theatre_ia.children)
+ child.remove();
+
+ theatre_ia.appendChild(video);
+}
diff --git a/src/odysee.js b/src/odysee.js
new file mode 100644
index 0000000..cd1c49c
--- /dev/null
+++ b/src/odysee.js
@@ -0,0 +1,425 @@
+/**
+ * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions
+ *
+ * Make video playback and search on odysee.com functional without nonfree js
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of 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.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+ * license. Although I request that you do not make use of this code in a
+ * proprietary program, I am not going to enforce this in court.
+ */
+
+/* use with https://odysee.com/*** */
+
+/* Helper functions for ajax. */
+
+function ajax_callback(xhttp, cb)
+{
+ cb(xhttp.response);
+}
+
+function perform_ajax(method, url, callback, err_callback, data)
+{
+ const xhttp = new XMLHttpRequest();
+ xhttp.onload = () => ajax_callback(xhttp, callback);
+ xhttp.onerror = err_callback;
+ xhttp.onabort = err_callback;
+ xhttp.open(method, url, true);
+ try {
+ xhttp.send(data);
+ } catch(e) {
+ err_callback();
+ }
+}
+
+/* Helper functions for strings with HTML entities (e.g. `&quot;'). */
+function HTML_decode(text)
+{
+ const tmp_span = document.createElement("span");
+ tmp_span.innerHTML = text;
+ return tmp_span.textContent;
+}
+
+/* Odysee API servers. */
+
+const odysee_resolve_url = "https://api.na-backend.odysee.com/api/v1/proxy?m=resolve";
+const lighthouse_search_url = "https://lighthouse.odysee.com/search";
+
+/*
+ * If we're on a video page, show the video. Use JSON data embedded in <head>
+ * if possible. If not - fetch video data using Odysee API.
+ */
+
+let data = null;
+
+function process_json_script(json_script)
+{
+ try {
+ data = JSON.parse(json_script.textContent);
+ } catch (e) {
+ console.log("Error parsing content data", e);
+ }
+}
+
+for (const json_script of document.querySelectorAll("head script")) {
+ if (["blocked-type", "type"].map(a => json_script.getAttribute(a))
+ .includes("application/ld+json"))
+ process_json_script(json_script);
+}
+
+const body = document.createElement("body");
+const video_container = document.createElement("div");
+
+body.appendChild(video_container);
+
+function show_video(content_url, title, upload_date, description)
+{
+ if (content_url) {
+ const video = document.createElement("video");
+ const source = document.createElement("source");
+
+ source.src = content_url;
+
+ video.setAttribute("width", "100%");
+ video.setAttribute("height", "auto");
+ video.setAttribute("controls", "");
+
+ video.appendChild(source);
+
+ video_container.appendChild(video);
+ }
+
+ if (title) {
+ const h1 = document.createElement("h1");
+
+ h1.textContent = HTML_decode(title);
+ h1.setAttribute("style", "color: #555;");
+
+ video_container.appendChild(h1);
+ }
+
+ if (upload_date) {
+ try {
+ const date = new Date(upload_date).toString();
+ const date_div = document.createElement("div");
+
+ date_div.textContent = `Uploaded: ${date}`;
+ date_div.setAttribute("style", "font-size: 14px; font-weight: bold; margin-bottom: 5px;");
+
+ video_container.appendChild(date_div);
+ } catch(e) {
+ console.log("Error parsing content upload date", e);
+ }
+ }
+
+ if (description) {
+ const description_div = document.createElement("div");
+
+ description_div.textContent = HTML_decode(description);
+ description_div.setAttribute("style", "white-space: pre;");
+
+ video_container.appendChild(description_div);
+ }
+}
+
+function show_video_from_query(response)
+{
+ try {
+ var result = Object.values(JSON.parse(response).result)[0];
+
+ if (result.value_type !== "stream")
+ return;
+
+ var date = result.timestamp * 1000;
+ var description = result.value.description;
+ var title = result.value.title;
+ const name = encodeURIComponent(result.name);
+ var url = `https://odysee.com/$/stream/${name}/${result.claim_id}`;
+ } catch (e) {
+ return;
+ }
+
+ show_video(url, title, date, description);
+}
+
+function fetch_show_video(name, claim_id)
+{
+ const payload = {
+ jsonrpc: "2.0",
+ method: "resolve",
+ params: {
+ urls: [`lbry://${decodeURIComponent(name)}#${claim_id}`],
+ include_purchase_receipt: true
+ },
+ id: Math.round(Math.random() * 10**14)
+ };
+
+ perform_ajax("POST", odysee_resolve_url, show_video_from_query,
+ () => null, JSON.stringify(payload));
+}
+
+if (data && typeof data === "object" && data["@type"] === "VideoObject") {
+ show_video(data.contentUrl, data.name, data.uploadDate, data.description);
+} else {
+ const match = /\/([^/]+):([0-9a-f]+)$/.exec(document.URL);
+ if (match)
+ fetch_show_video(match[1], match[2]);
+}
+
+/* Show search. */
+
+const search_input = document.createElement("input");
+const search_submit = document.createElement("button");
+const search_form = document.createElement("form");
+const error_div = document.createElement("div");
+
+search_submit.textContent = "Search Odysee";
+
+search_form.setAttribute("style", "margin: 15px 0 0 0;");
+
+search_form.appendChild(search_input);
+search_form.appendChild(search_submit);
+
+error_div.textContent = "Failed to perform search :c";
+error_div.setAttribute("style", "display: none;");
+
+body.appendChild(search_form);
+body.appendChild(error_div);
+
+/* Replace the UI. */
+
+document.documentElement.replaceChild(body, document.body);
+
+/* Add the logic of performing search and showing results. */
+
+function show_error()
+{
+ error_div.setAttribute("style", "color: #b44;");
+}
+
+function clear_error()
+{
+ error_div.setAttribute("style", "display: none;");
+}
+
+let results_div = null;
+const load_more_but = document.createElement("button");
+
+load_more_but.textContent = "Load more";
+
+function show_search_entries(new_results_div, response)
+{
+ try {
+ var results = Object.values(JSON.parse(response).result);
+ } catch (e) {
+ console.log("Failed to parse search response :c",
+ "Bad response format from api.na-backend.odysee.com.");
+ show_error();
+ return;
+ }
+
+ for (const result of results) {
+ try {
+ if (result.value_type !== "stream")
+ continue;
+
+ let channel_specifier = "";
+ let channel_name = null;
+ try {
+ channel_name = result.signing_channel.name;
+ const channel_name_enc = encodeURIComponent(channel_name);
+ const channel_digit = result.signing_channel.claim_id[0];
+ channel_specifier = `${channel_name_enc}:${channel_digit}`;
+ } catch (e) {
+ }
+ const video_name = encodeURIComponent(result.name);
+ const video_id = result.claim_id[0];
+
+ const result_a = document.createElement("a");
+ const thumbnail = document.createElement("img");
+ const title_span = document.createElement("span");
+ const uploader_div = document.createElement("div");
+ const description_div = document.createElement("div");
+
+ thumbnail.setAttribute("style", "width: 100px; height: auto;");
+ thumbnail.setAttribute("alt", result.value.thumbnail.url);
+ thumbnail.src = result.value.thumbnail.url;
+
+ title_span.setAttribute("style", "font-weight: bold;");
+ title_span.textContent = result.value.title;
+
+ uploader_div.setAttribute("style", "margin-left: 5px; font-size: 21px; color: #555;");
+ uploader_div.textContent = channel_name;
+
+ description_div.setAttribute("style", "white-space: pre;");
+ description_div.textContent = result.value.description;
+
+ result_a.setAttribute("style", "display: block; width: 100%; text-decoration: none; color: #333; margin: 8px; border-style: solid; border-width: 3px 0 0 0; border-color: #7aa;");
+ result_a.href = `https://odysee.com${channel_specifier}/${video_name}:${video_id}`;
+
+ if (result.value.thumbnail.url)
+ result_a.appendChild(thumbnail);
+ result_a.appendChild(title_span);
+ if (channel_name)
+ result_a.appendChild(uploader_div);
+ result_a.appendChild(description_div);
+
+ new_results_div.appendChild(result_a);
+ }
+ catch(e) {
+ console.log(e);
+ }
+ }
+
+ clear_error();
+
+ if (results_div)
+ results_div.remove();
+
+ results_div = new_results_div;
+
+ body.appendChild(results_div);
+ body.appendChild(load_more_but);
+
+ enable_search_form();
+}
+
+function search_ajax_error(url)
+{
+ console.log(`Failed to query ${url} :c`);
+ show_error();
+ enable_search_form();
+}
+
+function get_detailed_search_entries(new_results_div, response)
+{
+ /* TODO: Simplify JSON handling using sanitize_JSON.js from Hachette. */
+ try {
+ var response_data = JSON.parse(response);
+ if (!Array.isArray(response_data))
+ throw "Bad response format from lighthouse.odysee.com.";
+ } catch (e) {
+ show_error();
+ console.log("Failed to parse search response :c", e);
+ enable_search_form();
+ return;
+ }
+
+ const callback = r => show_search_entries(new_results_div, r);
+ const lbry_urls = [];
+
+ for (const search_result of response_data) {
+ if (!search_result.claimId || !search_result.name)
+ continue;
+ lbry_urls.push(`lbry://${search_result.name}#${search_result.claimId}`);
+ }
+
+ const payload = {
+ jsonrpc: "2.0",
+ method: "resolve",
+ params: {
+ urls: lbry_urls,
+ include_purchase_receipt: true
+ },
+ id: Math.round(Math.random() * 10**14)
+ };
+
+ const url = odysee_resolve_url;
+
+ perform_ajax("POST", url, callback, () => search_ajax_error(url),
+ JSON.stringify(payload));
+}
+
+function get_search_entries(new_results_div, query, from)
+{
+ const callback = r => get_detailed_search_entries(new_results_div, r);
+ const url = `${lighthouse_search_url}?s=${encodeURIComponent(query)}&size=20&from=${from}&claimType=file,channel&nsfw=false&free_only=true`;
+
+ new_results_div.setAttribute("data-fetched", parseInt(from) + 20);
+
+ perform_ajax("GET", url, callback, () => search_ajax_error(url));
+}
+
+function search(event)
+{
+ if (event)
+ event.preventDefault();
+
+ if (!/[^\s]/.test(search_input.value))
+ return;
+
+ disable_search_form();
+
+ const new_results_div = document.createElement("div");
+
+ new_results_div.setAttribute("data-query", search_input.value);
+
+ get_search_entries(new_results_div, search_input.value, 0);
+}
+
+function search_more()
+{
+ disable_search_form();
+
+ get_search_entries(results_div, results_div.getAttribute("data-query"),
+ results_div.getAttribute("data-fetched"));
+}
+
+load_more_but.addEventListener("click", search_more);
+
+function enable_search_form()
+{
+ search_form.addEventListener("submit", search);
+ search_submit.removeAttribute("disabled");
+ if (results_div)
+ load_more_but.removeAttribute("disabled");
+}
+
+function disable_search_form()
+{
+ search_form.removeEventListener("submit", search);
+ search_submit.setAttribute("disabled", "");
+ load_more_but.setAttribute("disabled", "");
+}
+
+
+enable_search_form();
+
+
+const match = /^[^?]*search\?q=([^&]+)/.exec(document.URL)
+if (match) {
+ search_input.value = decodeURIComponent(match[1]);
+ search();
+}
diff --git a/src/opencores.js b/src/opencores.js
new file mode 100644
index 0000000..d5b6376
--- /dev/null
+++ b/src/opencores.js
@@ -0,0 +1,46 @@
+/**
+ * SPDX-License-Identifier: CC0-1.0
+ *
+ * View OpenCores projects list without nonfree js
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the CC0 1.0 Universal License as published by
+ * the Creative Commons Corporation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * CC0 1.0 Universal License for more details.
+ */
+
+/* use with https://opencores.org/projects */
+
+let data = JSON.parse(document.getElementById("__NEXT_DATA__").textContent);
+let sections = {};
+for (let h1 of document.getElementsByClassName("cMJCrc")) {
+ let ul = document.createElement("ul");
+ if (h1.nextElementSibling !== null)
+ h1.parentNode.insertBefore(ul, h1.nextElementSibling);
+ else
+ h1.parentNode.appendChild(ul);
+
+ sections[h1.children[1].firstChild.textContent] = ul;
+}
+
+for (let prop of data.props.pageProps.list) {
+ let ul = sections[prop.category];
+ if (ul === undefined) {
+ console.log(`unknown category "${prop.category}" for project "${prop.title}"`);
+ continue;
+ }
+
+ let li = document.createElement("li");
+ let a = document.createElement("a");
+ a.setAttribute("href", "/projects/" + prop.slug);
+ a.textContent = prop.title;
+
+ li.appendChild(a);
+ ul.appendChild(li);
+}
diff --git a/src/pcspecialist_cookie_notice.js b/src/pcspecialist_cookie_notice.js
new file mode 100644
index 0000000..bd61bf7
--- /dev/null
+++ b/src/pcspecialist_cookie_notice.js
@@ -0,0 +1,21 @@
+/**
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2021 jahoti <jahoti@tilde.team>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const jbc = document.querySelector('.cc-policy');
+
+jbc.querySelector('.cc-buttons').onclick = () => jbc.parentNode.removeChild(jbc);
diff --git a/src/pcspecialist_display_prices.js b/src/pcspecialist_display_prices.js
new file mode 100644
index 0000000..c28cb47
--- /dev/null
+++ b/src/pcspecialist_display_prices.js
@@ -0,0 +1,57 @@
+/**
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2021 jahoti <jahoti@tilde.team>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const formId = document.querySelector('input[name="page_id"]').value;
+const form = document.querySelector('form[name="specbuilder"]');
+const noVat = document.getElementById('running_total_ex');
+const incVat = document.getElementById('running_total_inc');
+
+function updatePrice() {
+ const xhr = new XMLHttpRequest();
+
+ var names = [], values = [];
+ for (var inputElement of form.querySelectorAll('select, input[type="radio"]')) {
+ if (inputElement.name && (inputElement.checked || inputElement.tagName === 'SELECT')) {
+ names.push(inputElement.name);
+ values.push(inputElement.value);
+ }
+ }
+
+ const url = 'https://www.pcspecialist.co.uk/ajax/running_total.php?categories=' + names.join('%2C') +
+ '%2C&products=' + values.join('%2C') + '%2C&q=' + form.querySelector('input[name="q"]').value + '&form_id=' + formId;
+
+ xhr.onreadystatechange = priceUpdated;
+ xhr.open('GET', url, true);
+ xhr.send();
+}
+
+function priceUpdated() {
+ if (this.readyState === 4) {
+ if (this.status === 200) {
+ const parts = this.responseText.split("'");
+ noVat.innerText = parts[parts.length - 6];
+ incVat.innerText = parts[parts.length - 2];
+ }
+ else alert('Failed to get data: HTTP status code ' + this.status);
+ }
+}
+
+const button = document.createElement('button');
+button.innerText = 'Update Prices';
+button.onclick = updatePrice;
+document.querySelector('.price-holder.price-finance-holder').append(button);
diff --git a/src/phoronix_benchmarks.js b/src/phoronix_benchmarks.js
new file mode 100644
index 0000000..a332cc0
--- /dev/null
+++ b/src/phoronix_benchmarks.js
@@ -0,0 +1,38 @@
+/**
+ * SPDX-License-Identifier: CC0-1.0
+ *
+ * Fix benchmarks in phoronix.com articles
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the CC0 1.0 Universal License as published by
+ * the Creative Commons Corporation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * CC0 1.0 Universal License for more details.
+ */
+
+/* Use with https://www.phoronix.com/*** */
+
+/*
+ * Phoronix normally includes scripts that call document.write() to inject
+ * <img> tags. The most obvious way o code a fix would then be do download and
+ * parse the contents of those scripts. CORS, however, doesn't allow this.
+ * Instead, we notice that the openbenchmarking embed script url is related to
+ * the actual image url we need, so we can create <img>'s from it straight away.
+ */
+for (const script of document.scripts) {
+ const match = /openbenchmarking.org\/+(embed.php\?.*)p=0$/.exec(script.src);
+ if (!match) continue;
+
+ const img = document.createElement("img");
+ img.src = `https://openbenchmarking.org/${match[1]}p=2`;
+ img.setAttribute("type", "image/svg+xml");
+ img.setAttribute("width", "100%");
+ img.setAttribute("height", "auto");
+
+ script.parentElement.insertBefore(img, script);
+}
diff --git a/src/royal_geographical_society.js b/src/royal_geographical_society.js
new file mode 100644
index 0000000..0e983b0
--- /dev/null
+++ b/src/royal_geographical_society.js
@@ -0,0 +1,20 @@
+/**
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2021 jahoti <jahoti@tilde.team>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+document.documentElement.style.visibility = 'visible';
+document.documentElement.style.opacity = '100';
diff --git a/src/santander_centrum24.js b/src/santander_centrum24.js
new file mode 100644
index 0000000..793888f
--- /dev/null
+++ b/src/santander_centrum24.js
@@ -0,0 +1,25 @@
+/**
+ * SPDX-License-Identifier: CC0-1.0
+ *
+ * Fix SMS code submission on https://acsv.centrum24.pl/ACS/servlet/ACSAuthoriz
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the CC0 1.0 Universal License as published by
+ * the Creative Commons Corporation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * CC0 1.0 Universal License for more details.
+ */
+
+const submit_button = document.getElementById("submit");
+submit_button.classList.remove("disabled");
+submit_button.removeAttribute("disabled");
+
+console.log(document.querySelectorAll("noscript"));
+
+for (const noscript_element of document.querySelectorAll("noscript"))
+ noscript_element.remove();
diff --git a/src/stack_exchange_cookienotice.js b/src/stack_exchange_cookienotice.js
new file mode 100644
index 0000000..37a42ac
--- /dev/null
+++ b/src/stack_exchange_cookienotice.js
@@ -0,0 +1,20 @@
+/**
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2021 jahoti <jahoti@tilde.team>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const jcb = document.querySelector('.js-consent-banner');
+document.querySelector('.js-accept-cookies').onclick = e => jcb.parentNode.removeChild(jcb);
diff --git a/src/sumofus.js b/src/sumofus.js
new file mode 100644
index 0000000..7d0b5a6
--- /dev/null
+++ b/src/sumofus.js
@@ -0,0 +1,56 @@
+/**
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2021 jahoti <jahoti@tilde.team>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+function submitFormItem() {
+ var name, val, queryString = '', xhr = new content.XMLHttpRequest();
+ for (var formItem of this.querySelectorAll('select, input:not([type="radio"]):not([type="checkbox"])' +
+ ':not([type="submit"]):not([type="reset"])')) {
+ queryString += (queryString && '&') + formItem.name + '=' + encodeURIComponent(formItem.value);
+ }
+
+ xhr.onreadystatechange = function () {
+ if (this.readyState === 4) {
+ if (this.status === 200) location.href = JSON.parse(this.responseText).follow_up_url;
+ else if (this.status === 422) {
+ var failMessage = [], response = JSON.parse(this.responseText);
+ for (field in response.errors) for (error of response.errors[field]) {
+ failMessage.push('Field "' + field + '" ' + error);
+ }
+ alert(failMessage.join('\n'));
+ }
+ else alert('Submission failed: response code ' + this.status);
+ }
+ }
+
+ xhr.open('POST', this.action, true); // Manually add the domain, as it's not properly handled in extensions
+ xhr.setRequestHeader('X-CSRF-Token', csrf);
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+ xhr.send(queryString);
+ return false;
+}
+
+// Apply CSS as necessary
+if (notice = document.querySelector('#petition-bar-main > span')) notice.style.display = 'none'; // Hide the totally mistaken (even without this extension) anti-anti-JS warning
+document.querySelector('.script-dependent').style.display = 'block';
+document.querySelector('.button-wrapper').style.position = 'static'; // Stop the "submit" button obscuring the form
+
+
+
+csrf = document.querySelector('meta[name="csrf-token"]').content
+for (var button of document.querySelectorAll('button[type="submit"].button.action-form__submit-button')) button.form.onsubmit = submitFormItem;
diff --git a/src/vaticannews_videos.js b/src/vaticannews_videos.js
new file mode 100644
index 0000000..0967ed5
--- /dev/null
+++ b/src/vaticannews_videos.js
@@ -0,0 +1,24 @@
+/**
+ * SPDX-License-Identifier: CC0-1.0
+ *
+ * Watch vaticannews.va embedded YouTube videos on yewtu.be instead
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the CC0 1.0 Universal License as published by
+ * the Creative Commons Corporation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * CC0 1.0 Universal License for more details.
+ * Available under the terms of Creative Commons Zero.
+ */
+
+/* Use with https://www.vaticannews.va/*** */
+
+for (const iframe of document.querySelectorAll("iframe[data-src]")) {
+ const youtube_url = iframe.getAttribute("data-src");
+ iframe.setAttribute("src", make_yewtube_url(youtube_url));
+}
diff --git a/src/worldcat.js b/src/worldcat.js
new file mode 100644
index 0000000..0a4b531
--- /dev/null
+++ b/src/worldcat.js
@@ -0,0 +1,67 @@
+/**
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * Copyright © 2021 jahoti <jahoti@tilde.team>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var pathParts = location.pathname.split('/'), itemRef = pathParts[pathParts.length - 1];
+
+// Generate a function which, when invoked, loads the catalog holdings starting at i (one-indexed) focused on loc
+function generateGoTo(i, set_loc) {
+ return function () {
+ ; // If this is a new search, "set_loc" won't be set; set it
+ var xhr = new content.XMLHttpRequest(), loc = set_loc || encodeURIComponent(locInput.value);
+ xhr.onreadystatechange = function () {
+ if (this.readyState === 4) {
+ if (this.status === 200) {
+ retrieved.innerHTML = this.responseText;
+
+ var i, node = document.getElementById('libslocator');
+ node.parentNode.removeChild(node);
+ for (node of retrieved.querySelectorAll('a[href^="javascript:findLibs(\'\', "]')) {
+ i = parseInt(node.href.split(',', 2)[1]);
+ node.onclick = generateGoTo(i, loc);
+ }
+ }
+ else alert('Search failed: response code ' + this.status);
+ }
+ }
+
+ xhr.open('GET', 'https://www.worldcat.org/wcpa/servlet/org.oclc.lac.ui.ajax.ServiceServlet?wcoclcnum=' + itemRef + '&start_holding='
+ + i + '&serviceCommand=holdingsdata&loc=' + loc, true);
+ xhr.send();
+ return false; // Make sure the browser doesn't try to submit any holding form
+ };
+}
+
+
+var retriever = document.querySelector('.retrieving'), retrieved = document.getElementById('donelocator');
+
+var locForm = document.createElement('form'), locLabel = document.createElement('label'), locInput = document.createElement('input'),
+ locSubmit = document.createElement('input');
+
+locForm.appendChild(locLabel);
+locForm.appendChild(locInput);
+locForm.appendChild(locSubmit);
+
+locInput.name = locLabel.htmlFor = 'cat_location';
+locInput.type = 'text';
+locInput.required = 'yes';
+locLabel.innerText = 'Find copies closest to: ';
+locSubmit.value = 'Go';
+locSubmit.type = 'submit';
+locForm.onsubmit = generateGoTo(1);
+
+retriever.parentNode.replaceChild(locForm, retriever);
diff --git a/src/yewtube_urls.js b/src/yewtube_urls.js
new file mode 100644
index 0000000..9eca21f
--- /dev/null
+++ b/src/yewtube_urls.js
@@ -0,0 +1,58 @@
+/**
+ * SPDX-License-Identifier: CC0-1.0
+ *
+ * Library to convert youtube.com URLs to yewtu.be URLs.
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the CC0 1.0 Universal License as published by
+ * the Creative Commons Corporation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * CC0 1.0 Universal License for more details.
+ */
+
+function decode_query_options(url)
+{
+ const query_options = {};
+ const match = /^[^?]*\?(.*)/.exec(url);
+ if (!match)
+ return query_options;
+
+ for (const opt of match[1].split("&")) {
+ const [key, val] =
+ /^([^=]*)=?(.*)/.exec(opt).splice(1, 2).map(decodeURIComponent);
+ query_options[key] = val;
+ }
+
+ return query_options;
+}
+
+function encode_query_options(query_options)
+{
+ return Object.entries(query_options)
+ .map(ar => ar.map(encodeURIComponent).join("=")).join("&");
+}
+
+function make_yewtube_url(youtube_url)
+{
+ const query_options = decode_query_options(youtube_url);
+
+ let endpoint = "";
+
+ const match = /^(?:(?:https?:)?\/\/)?[^/]*\/([^?]*)/.exec(youtube_url);
+ if (match)
+ endpoint = match[1];
+
+ if (/^embed\//.test(query_options.v))
+ endpoint = query_options.v;
+
+ if (/^embed\/.+/.test(endpoint))
+ delete query_options.v;
+
+ const encoded_options = encode_query_options(query_options);
+ return `https://yewtu.be/${endpoint}?${encoded_options}`;
+}
diff --git a/src/youtube_yewtube_redirection.js b/src/youtube_yewtube_redirection.js
new file mode 100644
index 0000000..5b90776
--- /dev/null
+++ b/src/youtube_yewtube_redirection.js
@@ -0,0 +1,21 @@
+/**
+ * SPDX-License-Identifier: CC0-1.0
+ *
+ * Redirect youtube.com to yewtu.be
+ *
+ * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the CC0 1.0 Universal License as published by
+ * the Creative Commons Corporation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * CC0 1.0 Universal License for more details.
+ * Available under the terms of Creative Commons Zero.
+ */
+
+/* Use with https://www.youtube.com/*** */
+
+window.location.href = make_yewtube_url(document.URL);