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);
|