Project

General

Profile

Download (12.4 KB) Statistics
| Branch: | Revision:

hydrilla-fixes-bundle / src / google_drive_folders.js @ bac96457

1
/**
2
 * SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-WITH-js-exceptions
3
 *
4
 * Make folders on drive.google.com browsable without nonfree js
5
 *
6
 * Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
7
 *
8
 * This program is free software: you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * As additional permission under GNU GPL version 3 section 7, you
19
 * may distribute forms of that code without the copy of the GNU
20
 * GPL normally required by section 4, provided you include this
21
 * license notice and, in case of non-source distribution, a URL
22
 * through which recipients can access the Corresponding Source.
23
 * If you modify file(s) with this exception, you may extend this
24
 * exception to your version of the file(s), but you are not
25
 * obligated to do so. If you do not wish to do so, delete this
26
 * exception statement from your version.
27
 *
28
 * As a special exception to the GPL, any HTML file which merely
29
 * makes function calls to this code, and for that purpose
30
 * includes it by reference shall be deemed a separate work for
31
 * copyright law purposes. If you modify this code, you may extend
32
 * this exception to your version of the code, but you are not
33
 * obligated to do so. If you do not wish to do so, delete this
34
 * exception statement from your version.
35
 *
36
 * You should have received a copy of the GNU General Public License
37
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
38
 *
39
 * I, Wojtek Kosior, thereby promise not to sue for violation of this file's
40
 * license. Although I request that you do not make use of this code in a
41
 * proprietary program, I am not going to enforce this in court.
42
 */
43

    
44
/* Use with https://drive.google.com/drive/folders/*** */
45

    
46
/* Define how to handle various mime types used by Google. */
47

    
48
const known_mimes = {
49
    "application/vnd.google-apps.folder": {
50
	links: id => ({
51
	    view: `https://drive.google.com/drive/folders/${id}`
52
	}),
53
	type: "folder",
54
	is_folder: true
55
    },
56
    "application/vnd.google-apps.shortcut": {
57
	links: id => ({
58
	    view: `https://drive.google.com/drive/folders/${id}`
59
	}),
60
	type: "shortcut",
61
	is_folder: true
62
    },
63
    "application/vnd.google-apps.document": {
64
	links: id => ({
65
	    view: `https://docs.google.com/document/d/${id}`,
66
	    download: `https://docs.google.com/document/d/${id}/export?format=odt`,
67
	}),
68
	type: "Google text document",
69
	new_mime: "application/vnd.oasis.opendocument.text"
70
    },
71
    "application/vnd.google-apps.spreadsheet": {
72
	links: id => ({
73
	    view: `https://docs.google.com/spreadsheets/d/${id}`,
74
	    download: `https://docs.google.com/spreadsheets/d/${id}/export?format=ods`
75
	}),
76
	type: "Google spreadsheet",
77
	new_mime: "application/vnd.oasis.opendocument.spreadsheet"
78
    },
79
    "application/vnd.google-apps.presentation": {
80
	links: id => ({
81
	    view: `https://docs.google.com/presentation/d/${id}`,
82
	    download: `https://docs.google.com/presentation/d/${id}/export/pptx`
83
	}),
84
	type: "Google presentation",
85
	new_mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
86
    },
87
    "application/vnd.google-apps.drawing": {
88
	links: id => ({
89
	    view: `https://docs.google.com/drawings/d/${id}`,
90
	    download: `https://docs.google.com/drawings/d/${id}/export/jpeg`
91
	}),
92
	type: "Google drawing",
93
	new_mime: "image/jpeg"
94
    },
95
    "application/vnd.google-apps.script": {
96
	links: id => ({
97
	    download: `https://script.google.com/feeds/download/export?format=json&id=${id}`
98
	}),
99
	type: "Google script",
100
	new_mime: "application/vnd.google-apps.script+json"
101
    },
102
    "application/vnd.google-apps.jam": {
103
	links: id => ({
104
	    download: `https://jamboard.google.com/export?id=${id}`
105
	}),
106
	type: "Google jam",
107
	new_mime: "application/pdf"
108
    }
109
};
110

    
111
/*
112
 * Human-friendly names defined here will be displayed to the user instead of
113
 * raw mime types. Please add more here.
114
 */
115
let mime_display_overrides = {
116
    "image/jpeg": "JPEG image",
117
    "application/octet-stream": "binary data",
118
    "application/pdf": "PDF document",
119
    "application/rar": "RAR archive",
120
    "application/zip": "ZIP archive"
121
};
122

    
123
let default_link_producer = id => ({
124
    view: `https://drive.google.com/file/d/${id}`,
125
    download: `https://drive.google.com/uc?export=download&id=${id}`
126
});
127

    
128
for (const [mime, display_name] of Object.entries(mime_display_overrides)) {
129
    known_mimes[mime] = {
130
	links: default_link_producer,
131
	type: display_name,
132
	new_mime: mime
133
    }
134
}
135

    
136
delete mime_display_overrides;
137

    
138
function get_mime_info(mime) {
139
    return known_mimes[mime] || {
140
	links: default_link_producer,
141
	type: mime || "",
142
	new_mime: mime || "application/octet-stream"
143
    }
144
}
145

    
146
/* Prepare folder contents data as well as data regarding the folder itself. */
147

    
148
const content = new Map();
149

    
150
function add_content_item(item)
151
{
152
    const old_item = content.get(item.id) || {};
153
    Object.assign(old_item, item);
154
    content.set(item.id, old_item);
155
}
156

    
157
const this_folder = {};
158

    
159
function replace_string_escape(match, group)
160
{
161
    return String.fromCharCode(`0x${group}`);
162
}
163

    
164
function try_parse(data, replace_x_escapes)
165
{
166
    if (!data)
167
	return null;
168

    
169
    if (replace_x_escapes)
170
	data = data.replaceAll(/\\x([0-9a-f]{2})/g, replace_string_escape);
171

    
172
    try {
173
	return JSON.parse(data);
174
    } catch (e) {
175
	console.log(e);
176
    }
177

    
178
    return null;
179
}
180

    
181
function process_file_data(file_data, callback)
182
{
183
    if (!Array.isArray(file_data) || !typeof file_data[0] === "string") {
184
	console.log("cannot process the following file data object:",
185
		    file_data);
186
	return;
187
    }
188

    
189
    const result = {id: file_data[0], folders: []};
190

    
191
    if (Array.isArray(file_data[1])) {
192
	for (const item of file_data[1]) {
193
	    if (typeof item === "string" && item !== this_folder.id)
194
		result.folders.push(item);
195
	}
196
    }
197
    if (typeof file_data[2] === "string")
198
	result.filename = file_data[2];
199
    if (typeof file_data[3] === "string" && file_data[3].search("/") >= 0)
200
	result.mime = file_data[3];
201
    if (typeof file_data[9] === "number")
202
	result.date1 = new Date(file_data[9]);
203
    if (typeof file_data[10] === "number")
204
	result.date2 = new Date(file_data[10]);
205

    
206
    callback(result);
207
}
208

    
209
/*
210
 * By searching for scripts with calls to AF_initDataCallback we get about 7
211
 * matches. All arguments to this function seem to be arrays parseable as JSON,
212
 * but their contents are very different and only two of those ~7 arrays
213
 * actually hold some data useful to us. Here we try to filter out the other
214
 * cases and then extract the useful data.
215
 */
216
function process_af_init_data(data)
217
{
218
    if (!Array.isArray(data) || !/^driveweb/.test(data[0]) ||
219
	!Array.isArray(data[1]))
220
	return;
221

    
222
    /* First useful "kind" of object we can encounter is this folder's data. */
223
    if (typeof data[1][0] === "string") {
224
	process_file_data(data[1], item => Object.assign(this_folder, item));
225
	return;
226
    }
227

    
228
    /*
229
     * Second "kind" of object holds data about all items in the folder.
230
     * Folders and Files are stored in separate lists (doesn't matter to us,
231
     * since we distinguish them based on mime type anyway).
232
     */
233
    for (const data_sub of data[1]) {
234
	if (!Array.isArray(data_sub) || !/^driveweb/.test(data_sub[0]) ||
235
	    !Array.isArray(data_sub[1]))
236
	    continue;
237
	for (const item of data_sub[1])
238
	    process_file_data(item, add_content_item);
239
    }
240
}
241

    
242
/*
243
 * Folder items data actually exists in both of the 2 kinds of scripts we search
244
 * for. In case of both of the regexes below we call `process_file_data' in the
245
 * end. As a result we process the same file multiple time as it appears in 2
246
 * scripts. This is, however, a good thing, because it makes a change less
247
 * likely to break our fix.
248
 */
249

    
250
const ivd_data_regex = /\s*window\s*\['_DRIVE_ivd'\]\s*=\s*'(\\x5b[^']+)'(.*)$/;
251
const af_init_data_regex = /AF_initDataCallback\s*\(.+data\s*:\s*(\[.*\])[^\]]+$/;
252

    
253
for (const script of document.scripts) {
254
    const ivd_data_match = ivd_data_regex.exec(script.textContent);
255
    if (ivd_data_match) {
256
	const ivd_data = try_parse(ivd_data_match[1], true);
257
	if (ivd_data && Array.isArray(ivd_data) && Array.isArray(ivd_data[0])) {
258
	    for (const item of ivd_data[0])
259
		process_file_data(item, add_content_item);
260
	}
261
    }
262

    
263
    const af_init_data_match = af_init_data_regex.exec(script.textContent);
264
    if (af_init_data_match) {
265
	const af_init_data = try_parse(af_init_data_match[1], false);
266
	if (af_init_data)
267
	    process_af_init_data(af_init_data);
268
    }
269
}
270

    
271
/* Construct our own user interface. */
272

    
273
const body = document.createElement("body");
274
const folders = document.createElement("div");
275
const files = document.createElement("div");
276
const folders_heading = document.createElement("h2");
277
const files_heading = document.createElement("h2");
278

    
279
folders_heading.textContent = "Folders";
280
files_heading.textContent = "Files";
281

    
282
let has_folders = false;
283
let has_files = false;
284

    
285
folders.appendChild(folders_heading);
286
files.appendChild(files_heading);
287

    
288
body.setAttribute("style", "width: 100vw; height: 100vh; overflow: scroll; color: #555; margin: 15px; -webkit-user-select: initial;");
289

    
290
const drive_folder_regex = /application\/vnd.google-apps.(folder|shortcut)/;
291

    
292
function add_item_to_view(item_data)
293
{
294
    const item_div = document.createElement("div");
295
    const item_heading = document.createElement("h4");
296

    
297
    item_div.setAttribute("style", "border: 2px solid #999; border-radius: 8px; padding: 10px; display: inline-block; margin: 2px;");
298

    
299
    let item_heading_style = "margin: 8px 4px;";
300

    
301
    if (item_data.filename) {
302
	item_heading.textContent = item_data.filename;
303
    } else {
304
	item_heading.textContent = "(no name)";
305
	item_heading_style += " font-style:italic;";
306
    }
307
    item_heading.setAttribute("style", item_heading_style);
308
    item_div.appendChild(item_heading);
309

    
310
    const mime_info = get_mime_info(item_data.mime);
311

    
312
    if (mime_info.type) {
313
	const type_div = document.createElement("div");
314
	type_div.setAttribute("style", "margin-bottom: 5px;");
315
	type_div.textContent = mime_info.type;
316

    
317
	item_div.appendChild(type_div);
318
    }
319

    
320
    if (mime_info.is_folder)
321
	has_folders = true;
322
    else
323
	has_files = true;
324

    
325
    const links = {};
326
    if (item_data.id)
327
	Object.assign(links, mime_info.links(item_data.id));
328

    
329
    if (links.view) {
330
	const view_button = document.createElement("a");
331
	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;");
332
	view_button.textContent = "view";
333
	view_button.href = links.view;
334

    
335
	item_div.appendChild(view_button);
336
    }
337

    
338
    if (links.download) {
339
	const download_button = document.createElement("a");
340
	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;");
341
	download_button.textContent = "download";
342
	download_button.href = links.download;
343
	download_button.type = mime_info.new_mime;
344

    
345
	item_div.appendChild(download_button);
346
    }
347

    
348
    (mime_info.is_folder ? folders : files).appendChild(item_div);
349
}
350

    
351
for (const item of content.values())
352
    add_item_to_view(item);
353

    
354
if (this_folder.filename) {
355
    const heading = document.createElement("h1");
356
    heading.textContent = this_folder.filename;
357
    body.appendChild(heading);
358
}
359
if (this_folder.folders && this_folder.folders.length > 0) {
360
    for (const parent_id of this_folder.folders) {
361
	const up_button = document.createElement("a");
362
	let text = "go up";
363

    
364
	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;");
365
	up_button.href = `https://drive.google.com/drive/folders/${parent_id}`;
366
	if (this_folder.folders.length > 1)
367
	    text = `${text} (${parent_id})`;
368
	up_button.textContent = text;
369

    
370
	body.appendChild(up_button);
371
    }
372
}
373
if (has_folders)
374
    body.appendChild(folders);
375
if (has_files)
376
    body.appendChild(files);
377
if (!(has_files || has_folders)) {
378
    const no_files_message = document.createElement("h3");
379
    no_files_message.textContent = "No files found.";
380
    body.appendChild(no_files_message);
381
}
382

    
383
if (document.body.firstChild.id !== "af-error-container")
384
    document.documentElement.replaceChild(body, document.body);
(6-6/22)