您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
僅供PDF下載
当前为
// ==UserScript== // @name 下載北科i學員PDF // @namespace http://tampermonkey.net/ // @version 1.0.4 // @description 僅供PDF下載 // @author Umeow // @match https://istudy.ntut.edu.tw/learn/index.php // @connect istream.ntut.edu.tw // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant unsafeWindow // @license MIT // ==/UserScript== const parser = new DOMParser(); const ErrorFileChar = [ "/" , "|" ,'\\',"?",'"' ,'*' ,":" ,"<" ,">" , "/" , ":"]; const GM_fetch = getGM_fetch(); const saveData = (function () { const a = document.createElement("a"); document.body.appendChild(a); a.style = "display: none"; return function (blob, fileName) { const url = window.URL.createObjectURL(blob); a.href = url; a.download = fileName; a.click(); window.URL.revokeObjectURL(url); }; }()); const GET = async (url, old = false, headers = {}, args = {}) => { const res = await GM_fetch(url, { ...args, headers, method: "GET" }); if (old) { const data = await res.text(); const status = res.status; return { data, status } } return res; } const POST = async (url, data = {}, dataType = "form", headers = {}, args = {}) => { const ContentType = dataType === "form" ? "application/x-www-form-urlencoded" : "application/json" const body = dataType === "form" ? new URLSearchParams(data).toString() : JSON.stringify(data); const res = await GM_fetch(url, { ...args, body, headers: { ...headers, "Content-Type": ContentType, "Access-Control-Allow-Origin": "*" }, method: "POST" }); return res; } const get_cid = async () => { const cidURL = "https://istudy.ntut.edu.tw/learn/path/launch.php"; const cidResponse = await GET(cidURL, old = true); const cidHTML = parser.parseFromString(cidResponse['data'], 'text/html'); const cidScriptHTML = cidHTML.getElementsByTagName('script')[0].innerHTML; const cidLine = cidScriptHTML.split("\n").filter(str => str.includes('cid'))[0]; const cidStartIndex = cidLine.indexOf('/learn'); const cidResultURL = 'https://istudy.ntut.edu.tw' + cidLine.slice(cidStartIndex).slice(0, -3); const cidResultURLObj = new URL(cidResultURL); const cid = cidResultURLObj.searchParams.get('cid'); return cid; } const getDownloadArguments = async (cid) => { const URL = `https://istudy.ntut.edu.tw/learn/path/pathtree.php?cid=${cid}`; const DownloadData = { 'is_player' : false, 'href' : '', 'prev_href' : '', 'prev_node_id' : '', 'prev_node_title' : '', 'is_download' : false, 'begin_time' : '', 'course_id' : '', 'read_key' : '' } const Response = await GET(URL, old = true); const HTML = parser.parseFromString(Response['data'], 'text/html'); const FormElement = HTML.getElementById('fetchResourceForm'); const InputList = [...FormElement.getElementsByTagName('input')]; InputList.forEach((InputElement) => { const key = InputElement.getAttribute('name'); if(key === "is_download" || key === "is_player") return; const value = InputElement.getAttribute('value') || ''; DownloadData[key] = value; }); return DownloadData } const getFileList = async () => { const FileList = []; const FileListURL = 'https://istudy.ntut.edu.tw/learn/path/SCORM_loadCA.php'; const FileListResponse = await GET(FileListURL, old = true); const FileListXML = parser.parseFromString(FileListResponse['data'], "text/xml"); const FileListItems = [...FileListXML.getElementsByTagName('item')].filter((ele) => ele.getAttribute('identifierref')); const FileListElements = [...FileListXML.getElementsByTagName('resource')].filter((ele) => ele.getAttribute('identifier')); FileListElements.forEach((element) => { const identifier = element.getAttribute('identifier'); const href = '@' + element.getAttribute('href'); const item = FileListItems.filter((ele) => ele.getAttribute('identifierref') === identifier)[0]; const name = item.getElementsByTagName('title')[0].innerHTML.split("\t")[0].replace("\n" , ""); const file = { href, name }; FileList.push(file); }); return FileList; } const getFileType = async (html) => { const LocationStartIndex = html.indexOf('location.replace('); if(LocationStartIndex !== -1) { console.log(html); let ResponseLocation = html.slice(LocationStartIndex + 18); const EndIndex = ResponseLocation.indexOf(')'); ResponseLocation = ResponseLocation.slice(0, EndIndex - 1); console.log(ResponseLocation); if(ResponseLocation.includes("viewPDF.php")) { const ViewPDFURL = 'https://istudy.ntut.edu.tw/learn/path/' + ResponseLocation; const ViewPDF = await GET(ViewPDFURL, old = true); const getPDFLine = ViewPDF['data'].split('\n').filter(str => str.includes('getPDF.php'))[0]; const StartIndex = getPDFLine.indexOf('"'); const EndIndex = getPDFLine.lastIndexOf('"'); const getPDFURL = getPDFLine.slice(StartIndex + 1, EndIndex); const DownloadLink = 'https://istudy.ntut.edu.tw/learn/path/' + getPDFURL; return { type: "pdf", DownloadLink , headers: { referer: ViewPDFURL } } } if(ResponseLocation.includes("player.php")) { const PlayerResponse = await GET(ResponseLocation, false); const PlayerHTML = await PlayerResponse.text(); const PlayerHTMLObj = parser.parseFromString(PlayerHTML, "text/html"); const PlayerList = [...PlayerHTMLObj.getElementsByTagName("video")]; const DownloadLink = PlayerList.map(ele => { const source = ele.getElementsByTagName('source')[0] const href = source.getAttribute('src') return 'https://istream.ntut.edu.tw/videoplayer/' + href; }); return { type: "record", DownloadLink } } return { type: "link", DownloadLink: ResponseLocation } } if(html.includes(`<button onClick="download('`)) { const type = "file"; const ButtonStartIndex = html.indexOf(`<button onClick="download('`); const pathStart = html.slice(ButtonStartIndex + 27); const ButtonEndIndex = pathStart.indexOf("'"); const path = pathStart.slice(0, ButtonEndIndex); const DownloadLink = "https://istudy.ntut.edu.tw/learn/path/download.php?path=" + encodeURIComponent(path); return { type, DownloadLink } } return { type: null } } const DownloadFile = async (DownloadData, file) => { let filename = file['name']; while(ErrorFileChar.map( c => filename.includes(c) ).filter(bool => bool).length) ErrorFileChar.forEach( c => filename = filename.replace(c, ' ')); DownloadData['href'] = file['href']; const URL = "https://istudy.ntut.edu.tw/learn/path/SCORM_fetchResource.php"; const Response = await POST(URL, DownloadData); const ResponseText = await Response.text(); const FileData = await getFileType(ResponseText); const headers = FileData["headers"] || {}; console.log("headers: ", headers); console.log(FileData); if(!FileData['type']) { alert("無法擷取檔案連結"); return; } if(FileData['type'] === 'record') { if(!confirm(`即將開啟 ${FileData['DownloadLink'].length} 個錄影影像分頁 是否確定?`)) return; for(let i = 0 ; i < FileData['DownloadLink'].length ; i++) { const url = FileData['DownloadLink'][i]; window.open(url); } return; } if(FileData['type'] === 'link') { if(!confirm(`此教材為外部連結 是否開啟?`)) return; window.open(FileData['DownloadLink']); return; } const FileResponse = await GET(FileData['DownloadLink'], old = false, headers, ); console.log(FileResponse); const FileBlob = await FileResponse.blob(); saveData(FileBlob, filename); } const getFileListWithDownload = async () => { const cid = await get_cid(); const DownloadData = await getDownloadArguments(cid); const FileList = await getFileList(); FileList.forEach( file => { file['download'] = () => DownloadFile(DownloadData, file) }); return FileList; } const makeHTML = (FileList) => { const doc = createElement({ tag: "html" }); createElement({ tag: "head", childs: [ createElement({ tag: "meta", charset: "utf-8", }) ], appendTo: doc }); const body = createElement({ tag: "body", style: 'display: flex;flex-direction: column;', appendTo: doc }); FileList.forEach(file => createElement({ tag: "div", childs: [ createElement({ tag: "span", innerHTML: file['name'], style: "margin-right: 10px;" }), createElement({ tag: "button", innerHTML: "下載", onclick: file.download }), createElement({ tag: "br" }) ], style: "margin: 8px;", appendTo: body }) ); return doc; } const main = async () => { try { const FileList = await getFileListWithDownload(); const html = makeHTML(FileList); window.open('', 'i學員檔案列表', config='height=500,width=500') .document.body.appendChild(html); } catch(err) { console.error(err); alert("請確認是否位於正確的頁面"); return; } } GM_registerMenuCommand('擷取目前頁面的檔案', main, 'r'); //前端 Element Manager author: Umeow function createElement({ tag = 'div', classes = [], innerHTML = '', childs = [], appendTo = null, onclick = null, ...attrs }) { const result = document.createElement(tag) classes.filter((c) => c).forEach((c) => result.classList.add(c)) Object.keys(attrs) .filter((attr) => attrs[attr] !== null) .forEach((attr) => result.setAttribute(attr, attrs[attr])) childs.filter((c) => c).forEach((c) => result.appendChild(c)) if (innerHTML) result.innerHTML += innerHTML if (appendTo instanceof HTMLElement) appendTo.appendChild(result) if (onclick instanceof Function) result.onclick = onclick return result } /*! GM_fetch — v0.3.6-2022.06.04-dev — https://github.com/AlttiRi/gm_fetch */ function getGM_fetch() { const GM_XHR = (typeof GM_xmlhttpRequest === "function") ? GM_xmlhttpRequest : (GM?.xmlHttpRequest); const isStreamSupported = GM_XHR?.RESPONSE_TYPE_STREAM; let firefoxFixedFetch = false; const fetch = getWebPageFetch(); const crError = new Error().stack.startsWith("Error"); // Chromium Error // In Chromium original `DOMException` contains stack trace, however, manually created does not have it. /** * @param {string, URL, Request} resource * @param fetchInit */ async function handleBaseParams(resource, fetchInit = {}) { let url; if (resource?.url) { const {url: u, init} = await destroyRequest(resource); url = u; fetchInit = {...init, ...fetchInit}; } else { url = new URL(resource, location).href; } return {url, fetchInit}; } /** @param {Request} request */ async function destroyRequest(request) { const url = request.url; const method = request.method; const headers = request.headers; const signal = request.signal; const referrer = request.referrer !== "referrer" ? request.referrer : undefined; // todo test let body; if (!["GET", "HEAD"].includes(method)) { body = await request.blob(); } return {url, init: {method, signal, headers, body}}; } function getWebPageFetch() { let fetch = globalThis.fetch; // [VM/GM/FM + Firefox with "Enhanced Tracking Protection" set to "Strict" (Or "Custom" with enabled "Fingerprinters" option) // on sites with CSP (like Twitter, GitHub)] requires this fix. // They run the code as a content script. TM disables CSP with extra HTTP headers. // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts function fixFirefoxFetchOnPageWithCSP() { const wrappedJSObject = globalThis.wrappedJSObject; const fixRequired = wrappedJSObject && typeof wrappedJSObject.fetch === "function"; if (!fixRequired) { return; } const isTM = (function() { const request = new wrappedJSObject.Request(""); // Firefox content script's `Request` does not support relative URLs try { return request === cloneInto(request); } catch { console.log("[ujs][fixFirefoxFetchOnPageWithCSP] Request:", Request); return false; } })(); if (isTM) { return; } async function fixedFetch(resource, opts = {}) { const {url, fetchInit: init} = await handleBaseParams(resource, opts); if (init.headers instanceof Headers) { console.log("[ujs][fixedFetch] Headers", init.headers); // Since `Headers` are not allowed for structured cloning. init.headers = Object.fromEntries(init.headers.entries()); } if (/** @type {AbortSignal} */ init.signal) { if (init.signal.aborted) { throw new DOMException("The user aborted a request." + (crError ? new Error().stack.slice(5) : ""), "AbortError"); } console.warn("[ujs][fixedFetch] delete signal"); delete init.signal; // Can't be structured cloned } return wrappedJSObject.fetch(cloneInto(url, document), cloneInto(init, document/*, {cloneFunctions: true}*/)); } fetch = fixedFetch; firefoxFixedFetch = true; } fixFirefoxFetchOnPageWithCSP(); console.log({firefoxFixedFetch}); async function enhancedFetch(resource, opts) { const onprogress = opts.extra?.onprogress; delete opts.extra; const response = await fetch(resource, opts); if (onprogress) { return responseProgressProxy(response, onprogress); } return response; } return enhancedFetch; } /** The default Response always has {type: "default", redirected: false, url: ""} */ class ResponseEx extends Response { [Symbol.toStringTag] = "ResponseEx"; constructor(body, {headers, status, statusText, url, redirected, type, ok}) { super(body, {status, statusText, headers: { ...headers, "content-type": headers.get("content-type")?.split("; ")[0] // Fixes Blob type ("text/html; charset=UTF-8") in TM }}); this._type = type; this._url = url; this._redirected = redirected; this._ok = ok; this._headers = headers; // `HeadersLike` is more user-friendly for debug than the original `Headers` object } get redirected() { return this._redirected; } get url() { return this._url; } get type() { return this._type || "basic"; } // todo: if "cors" get ok() { return this._ok; } /** @returns {HeadersLike} */ get headers() { return this._headers; } } class HeadersLike { // Note: the original `Headers` throws an error if `key` requires `.trim()` constructor(headers) { headers && Object.entries(headers).forEach(([key, value]) => { this.append(key, value); }); } get(key) { const value = this[key.trim().toLowerCase()]; return value === undefined ? null : value; } append(key, value) { this[key.trim().toLowerCase()] = value.trim(); } has(key) { return this.get(key) !== null; } } /** * Parses headers from `XMLHttpRequest.getAllResponseHeaders()` string * @returns {HeadersLike} */ function parseHeaders(headersString) { const headers = new HeadersLike(); for (const line of headersString.trim().split("\n")) { const [key, ...valueParts] = line.split(":"); // last-modified: Fri, 21 May 2021 14:46:56 GMT const value = valueParts.join(":"); headers.append(key, value); } return headers; } class ReaderLike { constructor(blobPromise, body) { /** @type {Promise<Blob>} */ this._blobPromise = blobPromise; /** @type {ReadableStreamDefaultReader} */ this._reader = null; /** @type {ReadableStreamLike} */ this._body = body; this._released = false; } /** @return {Promise<{value: Uint8Array, done: boolean}>} */ read() { if (this._released) { throw new TypeError("This readable stream reader has been released and cannot be used to read from its previous owner stream"); } this._body._used = true; if (this._reader === null) { return new Promise(async (resolve) => { const blob = await this._blobPromise; const response = new Response(blob); this._reader = response.body.getReader(); const result = await this._reader.read(); resolve(result); }); } return this._reader.read(); } releaseLock() { this._body.locked = false; this._released = true; } } class ReadableStreamLike { // BodyLike constructor(blobPromise) { this.locked = false; this._used = false; this._blobPromise = blobPromise; } getReader() { if (this.locked) { throw new TypeError("ReadableStreamReader constructor can only accept readable streams that are not yet locked to a reader"); } this._reader = new ReaderLike(this._blobPromise, this); this.locked = true; return this._reader; } } class ResponseLike { constructor(blobPromise, {headers, status, statusText, url, finalUrl}) { /** @type {Promise<Blob>} */ this._blobPromise = blobPromise; this.headers = headers; this.status = status; this.statusText = statusText; this.url = finalUrl; this.redirected = url !== finalUrl; this.type = "basic"; // todo: if "cors" this.ok = status.toString().startsWith("2"); this._bodyUsed = false; this.body = new ReadableStreamLike(blobPromise); } get bodyUsed() { return this._bodyUsed || this.body._used; } blob() { if (this.bodyUsed) { throw new TypeError("body stream already read"); } if (this.body.locked) { throw new TypeError("body stream is locked"); } this._bodyUsed = true; this.body.locked = true; return this._blobPromise; } arrayBuffer() { return this.blob().then(blob => blob.arrayBuffer()); } text() { return this.blob().then(blob => blob.text()); } json() { return this.text().then(text => JSON.parse(text)); } } const identityContentEncodings = new Set([null, "identity", "no encoding"]); function getOnProgressProps(response) { const {headers, status, statusText, url, redirected, ok} = response; const isIdentity = identityContentEncodings.has(headers.get("Content-Encoding")); const compressed = !isIdentity; const _contentLength = parseInt(headers.get("Content-Length")); // `get()` returns `null` if no header present const contentLength = isNaN(_contentLength) ? null : _contentLength; const lengthComputable = isIdentity && _contentLength !== null; // Original XHR behaviour; in TM it equals to `contentLength`, or `-1` if `contentLength` is `null` (and `0`?). const total = lengthComputable ? contentLength : 0; const gmTotal = contentLength > 0 ? contentLength : -1; // Like `total` is in TM and GM. return { gmTotal, total, lengthComputable, compressed, contentLength, headers, status, statusText, url, redirected, ok }; } function responseProgressProxy(response, onProgress) { const onProgressProps = getOnProgressProps(response); let loaded = 0; const reader = response.body.getReader(); const readableStream = new ReadableStream({ async start(controller) { while (true) { const {done, /** @type {Uint8Array} */ value} = await reader.read(); if (done) { break; } loaded += value.length; try { onProgress({loaded, ...onProgressProps}); } catch (e) { console.error("[onProgress]:", e); } controller.enqueue(value); } controller.close(); reader.releaseLock(); }, cancel() { void reader.cancel(); } }); return new ResponseEx(readableStream, response); } /** * The simplified `fetch` — a wrapper for `GM_xmlHttpRequest`. * @example // @grant GM_xmlhttpRequest const response = await fetch(url); const {status, statusText} = response; const lastModified = response.headers.get("last-modified"); const blob = await response.blob(); * @return {Promise<Response>} */ async function GM_fetch(url, fetchInit = {}) { ({url, fetchInit} = await handleBaseParams(url, fetchInit)); if (fetchInit.extra?.webContext) { delete fetchInit.extra; return fetch(url, fetchInit); } function handleParams(fetchInit) { const defaultFetchInit = {method: "GET", headers: {}}; const defaultExtra = {useStream: true, onprogress: null}; const opts = { ...defaultFetchInit, ...fetchInit, extra: { ...defaultExtra, ...fetchInit.extra } }; const {headers, method, body, referrer, signal, extra: {useStream, onprogress}} = opts; delete opts.extra.useStream; delete opts.extra.onprogress; const _headers = new HeadersLike(headers); if (referrer && !_headers.has("referer")) { _headers.append("referer", referrer); // todo: handle referrer } return { method, headers: _headers, body, signal, useStream, onprogress, extra: opts.extra }; } const { method, headers, body, signal, useStream, onprogress, extra } = handleParams(fetchInit); if (signal?.aborted) { throw new DOMException("The user aborted a request." + (crError ? new Error().stack.slice(5) : ""), "AbortError"); } let abortCallback; let done = false; function handleAbort(gmAbort) { if (!signal) { return; } if (signal.aborted) { gmAbort(); const id = setInterval(() => done ? clearInterval(id) : gmAbort(), 1); // VM fix. return; } abortCallback = () => gmAbort(); signal.addEventListener("abort", abortCallback); } function onDone() { signal?.removeEventListener("abort", abortCallback); done = true; } const HEADERS_RECEIVED = 2; const DONE = 4; function getOnReadyStateChange({onHeadersReceived}) { return function onReadyStatechange(gmResponse) { const {readyState} = gmResponse; if (readyState === HEADERS_RECEIVED) { onHeadersReceived(gmResponse); } // It does not trigger on `abort` and `error`, while native XHR does. (In both TM and VM) // Fires only on `onload`. Is a bug? // Also it fires (`readyState === DONE`) multiple times in non the latest VM beta. // else if (readyState === DONE) { // onDone(); // } } } function getOnDones({resolve, reject}) { return { onload(gmResponse) { onDone(); resolve?.(gmResponse.response); // Not required for `responseType: "stream"` }, onerror() { onDone(); reject(new TypeError("Failed to fetch")); }, onabort() { onDone(); reject(new DOMException("The user aborted a request." + (crError ? new Error().stack.slice(5) : ""), "AbortError")); } }; } function nonStreamFetch() { const _onprogress = onprogress; let onProgressProps = {}; // Will be inited on HEADERS_RECEIVED. It used to have the same behaviour in TM and VM. return new Promise((resolve, _reject) => { function onHeadersReceived(gmResponse) { const {responseHeaders, status, statusText, finalUrl} = gmResponse; const headers = parseHeaders(responseHeaders); const response = new ResponseLike(blobPromise, { headers, status, statusText, url, finalUrl }); onProgressProps = getOnProgressProps(response); resolve(response); } const onreadystatechange = getOnReadyStateChange({onHeadersReceived}); const blobPromise = new Promise((resolve, reject) => { const {onload, onabort, onerror} = getOnDones({resolve, reject}); const {abort} = GM_XHR({ ...extra, url, method, headers, responseType: "blob", onreadystatechange, onprogress: _onprogress ? ({loaded/*, total, lengthComputable*/}) => { _onprogress({loaded, ...onProgressProps}); } : undefined, onload, onerror, onabort, data: body, }); handleAbort(abort); }); blobPromise.catch(_reject); }); } function streamFetch() { return new Promise((resolve, reject) => { function onHeadersReceived(gmResponse) { const { responseHeaders, status, statusText, finalUrl, response: readableStream } = gmResponse; const headers = parseHeaders(responseHeaders); const redirected = url !== finalUrl; let response = new ResponseEx(readableStream, {headers, status, statusText, url: finalUrl, redirected}); if (onprogress) { response = responseProgressProxy(response, onprogress); } resolve(response); } const onreadystatechange = getOnReadyStateChange({onHeadersReceived}); const {onload, onabort, onerror} = getOnDones({reject}); const {abort} = GM_XHR({ ...extra, url, method, headers, responseType: "stream", /* fetch: true, */ // Not required, since it already has `responseType: "stream"`. onreadystatechange, onload, onerror, onabort, data: body, }); handleAbort(abort); }); } if (!isStreamSupported || !useStream) { return nonStreamFetch(); } else { return streamFetch(); } } GM_fetch.isStreamSupported = isStreamSupported; GM_fetch.webContextFetch = fetch; GM_fetch.firefoxFixedFetch = firefoxFixedFetch; return GM_fetch; }