Project

General

Profile

Site script request/donation #101 » box-fix.js

koszko, 01/22/2022 02:33 AM

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

    
41
// 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)
42
// meta: some test cases (mostly found at https://old.reddit.com/search?q="box.com"&include_over_18=on&sort=new)
43
	// https://uwmadison.app.box.com/s/ydht2incbdmw1lhpjg5t40adguc0fm14
44
		// umadison's enrollment report
45
		// pdf
46
	// https://app.box.com/s/gc4ygloi4qtimeh98dq9mmydyuydawcn
47
		// password-protected 7z file (nsfw)
48
	// https://app.box.com/shared/static/su6xx6zx50cd68zdtbm3wfxhh9kwke8x.zip
49
		// a soundtrack in a zip file
50
		// This is a static download, so it works without this script.
51
	// https://app.box.com/s/vysdh2u78yih3c8leetgq82il954a3g3
52
		// some gambling add
53
		// pptx
54
	// https://app.box.com/s/nnlplkmjhimau404qohh9my10pwmo8es
55
		// a list of books(?)
56
		// txt
57
	// https://ucla.app.box.com/s/mv32q624ojihohzh8d0mhhj0b3xluzbz
58
		// "COVID-19 Pivot Plan Decision Matrix"
59
		// cannot be downloaded (403 Forbidden): "This user is not allowed to use direct links. Please email "[support address, changes depending on where you are logged in]" for support"
60
		// If you load the proprietary scripts on this page, you'll see that there is no download button
61
	// TODO: find a public folder link (the private links I have seem to work)
62
	// TODO: find a (preferably public) link with a folder inside a folder, as these may need to be handled differently
63

    
64
/* 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
65

    
66
let prefetchedData = null; // This variable isn't actually used.
67
for (const script of document.scripts) {
68
    const match = /Box.prefetchedData = ({([^;]|[^}];)+})/.exec(script.textContent); // looks for "Box.prefetchedData = " in the script files and then grabs the json text after that.
69
    if (!match)
70
	continue;
71
    prefetchedData = JSON.parse(match[1]);
72
}
73

    
74
let config = null;
75
for (const script of document.scripts) {
76
    const match = /Box.config = ({([^;]|[^}];)+})/.exec(script.textContent); // looks for "Box.config = " in the script files and then grabs the json text after that.
77
    if (!match)
78
	continue;
79
    config = JSON.parse(match[1]);
80
}
81

    
82
let postStreamData = null;
83
for (const script of document.scripts) {
84
    const match = /Box.postStreamData = ({([^;]|[^}];)+})/.exec(script.textContent); // looks for "Box.postStreamData = " in the script files and then grabs the json text after that.
85
    if (!match)
86
	continue;
87
    postStreamData = JSON.parse(match[1]);
88
}
89

    
90
// empty the initial document body
91
[...document.body.childNodes].forEach(n => n.remove());
92

    
93
// create div container
94
const divContainer = document.createElement("div");
95
document.body.append(divContainer);
96

    
97
const loadingIcon = document.createElement("h1");
98
loadingIcon.innerText = "loading...";
99
loadingIcon.style.display = "none";
100

    
101
const error_msg = document.createElement("h1");
102
error_msg.innerText = "error occured :(";
103
error_msg.style.display = "none";
104

    
105
divContainer.append(loadingIcon, error_msg);
106

    
107
// get domain from URL
108
const domain = document.location.href.split("/")[2];
109

    
110
async function hack_file() {
111
    loadingIcon.style.display = "initial";
112

    
113
    const tokens_url = "/app-api/enduserapp/elements/tokens";
114
    const file_nr = postStreamData["/app-api/enduserapp/shared-item"].itemID;
115
    const file_id = `file_${file_nr}`;
116
    const shared_name = postStreamData["/app-api/enduserapp/shared-item"].sharedName;
117

    
118
    /*
119
     * We need to perform a POST to obtain a token that will be used later to
120
     * authenticate against Box's API endpoint.
121
     */
122
    const tokens_response = await fetch(tokens_url, {
123
	method: "POST",
124
	headers: {
125
	    "Accept":               "application/json",
126
	    "Content-Type":         "application/json",
127
	    "Request-Token":        config.requestToken,
128
	    "X-Box-Client-Name":    "enduserapp",
129
	    "X-Box-Client-Version": "20.712.2",
130
	    "X-Box-EndUser-API":    `sharedName=${shared_name}`,
131
	    "X-Request-Token":      config.requestToken
132
	},
133
	body: JSON.stringify({"fileIDs": [file_id]})
134
    });
135
    console.log("tokens_response", tokens_response);
136

    
137
    const access_token = (await tokens_response.json())[file_id].read;
138
    console.log("access_token", access_token);
139

    
140
    const fields = [
141
	"permissions", "shared_link", "sha1", "file_version", "name", "size",
142
	"extension", "representations", "watermark_info",
143
	"authenticated_download_url", "is_download_available"
144
    ];
145

    
146
    const file_info_url =
147
	  `https://api.box.com/2.0/files/${file_nr}?fields=${fields.join()}`;
148

    
149
    /*
150
     * We need to perform a GET to obtain file metadata. The fields we curently
151
     * make use of are "authenticated_download_url" and "file_version", but in
152
     * the request we also include names of other fields that the original Box
153
     * client would include. The metadata is then dumped as JSON on the page, so
154
     * the user, if curious, can look at it.
155
     */
156
    const file_info_response = await fetch(file_info_url, {
157
	headers: {
158
	    "Accept":            "application/json",
159
	    "Authorization":     `Bearer ${access_token}`,
160
	    "BoxApi":            `shared_link=${document.URL}`,
161
	    "X-Box-Client-Name": "ContentPreview",
162
	    "X-Rep-Hints":       "[3d][pdf][text][mp3][json][jpg?dimensions=1024x1024&paged=false][jpg?dimensions=2048x2048,png?dimensions=2048x2048][dash,mp4][filmstrip]"
163
	},
164
    });
165
    console.log("file_info_response", file_info_response);
166

    
167
    const file_info = await file_info_response.json();
168
    console.log("file_info", file_info);
169

    
170
    const params = new URLSearchParams();
171
    params.set("preview",            true);
172
    params.set("version",            file_info.file_version.id);
173
    params.set("access_token",       access_token);
174
    params.set("shared_link",        document.URL);
175
    params.set("box_client_name",    "box-content-preview");
176
    params.set("box_client_version", "2.82.0");
177
    params.set("encoding",           "gzip");
178

    
179
    /* We use file metadata from earlier requests to construct the link. */
180
    const download_url =
181
	  `${file_info.authenticated_download_url}?${params.toString()}`;
182
    console.log("download_url", download_url);
183

    
184
    const downloadButton = document.createElement("a");
185
    downloadButton.innerText = "download";
186
    downloadButton.href = download_url;
187
    downloadButton.setAttribute("style", "border-radius: 10px; padding: 20px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block;");
188

    
189
    const file_info_header = document.createElement("h2");
190
    file_info_header.innerText = "File info";
191

    
192
    divContainer.append(downloadButton, file_info_header,
193
			JSON.stringify(file_info));
194

    
195
    loadingIcon.style.display = "none";
196
}
197

    
198
function show_error() {
199
    loadingIcon.style.display = "none";
200
    error_msg.style.display = "initial";
201
}
202

    
203
if (postStreamData["/app-api/enduserapp/shared-item"].itemType == "file") {
204
    /*
205
     * We call hack_file and in case it asynchronously throws an exception, we
206
     * make an error message appear.
207
     */
208
    hack_file().then(() => {}, show_error);
209
} else if (postStreamData["/app-api/enduserapp/shared-item"].itemType == "folder") {
210
	const folderHeader = document.createElement("h1");
211
	folderHeader.innerText = postStreamData["/app-api/enduserapp/shared-folder"].currentFolderName;
212
	divContainer.appendChild(folderHeader);
213
	//console.log(postStreamData["/app-api/enduserapp/shared-folder"]);
214
	postStreamData["/app-api/enduserapp/shared-folder"].items.forEach(function(element) {
215
		console.log(element);
216
		const folderButton = document.createElement("a");
217
		folderButton.setAttribute("style", "border-radius: 10px; padding: 20px; color: #333; background-color: lightgreen; text-decoration: none; box-shadow: -4px 8px 8px #888; display: inline-block;"); // from https://api-demo.hachette-hydrilla.org/content/sgoogle_sheets_download/google_sheets_download.js
218
		if (element.type == "file") {
219
			folderButton.innerText = "loading...";
220
			// craft request
221
			var downloadLinkGet = new XMLHttpRequest();
222
			downloadLinkGet.open("POST", "https://"+domain+"/index.php?rm=box_download_shared_file&shared_name="+postStreamData["/app-api/enduserapp/shared-item"].sharedName+"&file_id="+element.typedID);
223
			downloadLinkGet.setRequestHeader("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:96.0) Gecko/20100101 Firefox/96.0"); // Would this be set automatically otherwise?
224
			downloadLinkGet.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8");
225
			downloadLinkGet.setRequestHeader("Accept-Language",  "en-US,en;q=0.5"); // TODO: find a test case in another language
226
			downloadLinkGet.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
227
			downloadLinkGet.setRequestHeader("Upgrade-Insecure-Requests", "1");
228
			downloadLinkGet.onreadystatechange = function() {
229
				if (downloadLinkGet.readyState === 4) {
230
					//console.log(downloadLinkGet.status);
231
					// configure download button and add it
232
					folderButton.setAttribute("href", downloadLinkGet.responseURL);
233
					folderButton.innerText = element.name; // show the name of the file
234
				}
235
			};
236
			downloadLinkGet.send("request_token="+config.requestToken);
237
		} else {
238
			folderButton.innerText = "[folders inside folders not yet supported]";
239
		}
240
		divContainer.appendChild(folderButton);
241
	})
242
} else {
243
	console.log("Error: not implemented");
244
	// TODO: also display an error on the page
245
}
(2-2/3)