/**
* 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();
}