您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
HanoiCollab client for Second Generation HanoiCollab server
// ==UserScript== // @name HanoiCollab_v2 // @namespace https://trungnt2910.github.io/ // @version 0.0.2 // @description HanoiCollab client for Second Generation HanoiCollab server // @author trungnt2910 // @license MIT // @icon https://www.google.com/s2/favicons?domain=edu.vn // @connect * // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @require http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.2/signalr.min.js // @match https://forms.office.com/Pages/ResponsePage.aspx?* // @match https://shub.edu.vn/* // @match https://azota.vn/* // @match https://quilgo.com/* // @match https://docs.google.com/forms/* // ==/UserScript== String.prototype.getHashCode = function() { var hash = 0, i, chr; if (this.length === 0) return hash; for (i = 0; i < this.length; i++) { chr = this.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; // Convert to 32bit integer } return hash.toString(); }; var GUID = function () { //------------------ var S4 = function () { return( Math.floor( Math.random() * 0x10000 /* 65536 */ ).toString(16) ); }; //------------------ return ( S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4() ); }; let HanoiCollabGlobals = {}; HanoiCollabGlobals.WindowId = GUID(); function HanoiCollab$(obj) { if (typeof obj === "string") { return $(HanoiCollabGlobals.Document).contents().find(obj); } return $(obj); } var entityMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/', '`': '`', '=': '=' }; function EscapeHtml(string) { return String(string).replace(/[&<>"'`=\/]/g, function (s) { return entityMap[s]; }); } async function ServerPrompt() { var server = prompt("Enter your HanoiCollab server address", (HanoiCollabGlobals.Server) ? HanoiCollabGlobals.Server : "https://hanoicollab.herokuapp.com/"); if (!server.endsWith("/")) { server += "/"; } await GM_setValue("HanoiCollabServer", server); HanoiCollabGlobals.Server = server; return server; } async function LoginPopup(displayText) { if (elem = HanoiCollabGlobals.Document.getElementById("hanoicollab-login-popup-container")) { return await HanoiCollabGlobals.LoginPopupPromise; } HanoiCollabGlobals.LoginPopupPromise = new Promise(async function(resolve, reject) { var oldUsername = await GM_getValue("HanoiCollabUsername", ""); displayText = displayText ? displayText : "Please sign in to use HanoiCollab"; //--- Use jQuery to add the form in a "popup" dialog. HanoiCollab$(HanoiCollabGlobals.Document.body).append (` <div id="hanoicollab-login-popup-container" class="hanoicollab-basic-container"> <p id="hanoicollab-login-popup-background" style="position:fixed;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.9);z-index:9998;"></p> <div id="hanoicollab-login-popup" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:50%;padding:2em;color:white;background-color:rgba(0,127,255,0.75);border-radius:1ex;z-index:9999;"> <p class="hanoicollab-basic-container">${displayText}</p> <p class="hanoicollab-basic-container">Your current HanoiCollab server: <a class="hanoicollab-basic-container" href="${HanoiCollabGlobals.Server}" style="color:orange;">${HanoiCollabGlobals.Server}</a>.</p> <p class="hanoicollab-basic-container">Press Alt+S to change your server.</p> <input class="hanoicollab-basic-container" type="text" id="hanoicollab-username" style="color:black;" value="${oldUsername}"> <input class="hanoicollab-basic-container" type="password" id="hanoicollab-password" style="color:black;" value=""> <p class="hanoicollab-basic-container" id="hanoicollab-login-status">Please enter your HanoiCollab username and password.</p> <button class="hanoicollab-button" id="hanoicollab-login-button">Login</button> <button class="hanoicollab-button" id="hanoicollab-register-button">Register</button> <button class="hanoicollab-button" id="hanoicollab-close-button">Later</button> <button class="hanoicollab-button" id="hanoicollab-close-suppress-button">Don't bother me today</button> </div> </div> `); function EnableFields() { HanoiCollab$("#hanoicollab-username").prop("disabled", false); HanoiCollab$("#hanoicollab-password").prop("disabled", false); } function DisableFields() { HanoiCollab$("#hanoicollab-username").prop("disabled", true); HanoiCollab$("#hanoicollab-password").prop("disabled", true); } HanoiCollab$("#hanoicollab-login-button").click(function() { var username = HanoiCollab$("#hanoicollab-username").val(); var password = HanoiCollab$("#hanoicollab-password").val(); HanoiCollab$("#hanoicollab-login-status").text("Logging in..."); DisableFields(); GM_xmlhttpRequest({ method: "POST", url: HanoiCollabGlobals.Server + "api/Accounts/login", data: JSON.stringify({ Name: username, Password: password }), headers: { "Content-Type": "application/json" }, onload: function(response) { if (response.status === 200) { var data = JSON.parse(response.responseText); if (data.Token) { GM_setValue("HanoiCollabUsername", username); GM_setValue("HanoiCollabIdentity", JSON.stringify(data)); HanoiCollab$("#hanoicollab-login-popup-container").remove(); HanoiCollabGlobals.Identity = data; if (HanoiCollabGlobals.OnIdentityChange) { HanoiCollabGlobals.OnIdentityChange(); } resolve(data); } else { HanoiCollab$("#hanoicollab-login-status").text("Error: Invalid response from server."); EnableFields(); } } else { HanoiCollab$("#hanoicollab-login-status").text("Error: " + response.statusText); EnableFields(); } }, onerror: function(response) { HanoiCollab$("#hanoicollab-login-status").text("Error: " + response.statusText); EnableFields(); } }); }); HanoiCollab$("#hanoicollab-register-button").click(function() { var username = HanoiCollab$("#hanoicollab-username").val(); var password = HanoiCollab$("#hanoicollab-password").val(); HanoiCollab$("#hanoicollab-login-status").text("Registering..."); DisableFields(); GM_xmlhttpRequest({ method: "POST", url: HanoiCollabGlobals.Server + "api/Accounts/register", data: JSON.stringify({ Name: username, Password: password }), headers: { "Content-Type": "application/json" }, onload: function(response) { var data = JSON.parse(response.responseText); HanoiCollab$("#hanoicollab-login-status").text(`${data.Status}: ${data.Message}`); EnableFields(); }, onerror: function(response) { HanoiCollab$("#hanoicollab-login-status").text("Error: " + response.statusText); } }); }); HanoiCollab$("#hanoicollab-close-button").click(function() { HanoiCollab$("#hanoicollab-login-popup-container").remove(); reject("User cancelled"); }); HanoiCollab$("#hanoicollab-close-suppress-button").click(function() { HanoiCollab$("#hanoicollab-login-popup-container").remove(); reject("User cancelled"); }); }); return await HanoiCollabGlobals.LoginPopupPromise; } async function SetupServer() { var storedServer = await GM_getValue("HanoiCollabServer", null); if (!storedServer) { storedServer = ServerPrompt(); } HanoiCollabGlobals.Server = storedServer; return storedServer; } async function SetupIdentity() { var storedIdentity = JSON.parse(await GM_getValue("HanoiCollabIdentity", null)); if (!storedIdentity || !storedIdentity.Token || !storedIdentity.Expiration || Date.parse(storedIdentity.Expiration) <= Date.now()) { return await LoginPopup(); } HanoiCollabGlobals.Identity = storedIdentity; return HanoiCollabGlobals.Identity; } function SetupKeyBindings() { HanoiCollabGlobals.Document.addEventListener('keyup', function (e) { if (e.altKey && e.key === 's') { ServerPrompt(); } if (e.altKey && e.key === 'l') { LoginPopup(); } }, false); } async function GetToken() { var identity = HanoiCollabGlobals.Identity; if (Date.parse(identity.expiration) <= Date.now()) { await LoginPopup("Session expired. Please sign in to continue using HanoiCollab."); } return HanoiCollabGlobals.Identity.Token; } async function SetupChatConnection() { var connection = new signalR .HubConnectionBuilder() .withUrl(HanoiCollabGlobals.Server + "hubs/chat", { accessTokenFactory: GetToken }) .build(); await connection.start(); connection.DeliberateClose = false; connection.onclose(function() { if (connection.DeliberateClose) { return; } console.log("Reconnecting to chat channel..."); var handle = setInterval(async function() { await connection.start(); await HanoiCollabGlobals.ChatConnection.invoke("JoinChannel", HanoiCollabGlobals.Channel); clearInterval(handle); }, 5000); }); HanoiCollabGlobals.ChatConnection = connection; return connection; } async function SetupChatUserInterface() { HanoiCollab$("#hanoicollab-chat-container").remove(); HanoiCollab$("body").append (` <div id="hanoicollab-chat-container" class="hanoicollab-basic-container" style="position:fixed;right:16px;bottom:16px;height:20%;width:30%;border-radius:1ex;background-color:rgba(0,127,255,0.9);z-index:9997;user-select:text"> <div id="hanoicollab-chat-messages" style="width:100%;height:90%;overflow:auto;"></div> <input type="text" autocomplete="off" id="hanoicollab-chat-input" style="width:100%;bottom:-10%;position:absolute"> </div> `); HanoiCollab$("#hanoicollab-chat-input").keyup(async function(e) { if (e.key === "Enter") { var message = HanoiCollab$("#hanoicollab-chat-input").val(); // Prevent empty message spam. if (message) { HanoiCollab$("#hanoicollab-chat-input").val(""); await HanoiCollabGlobals.ChatConnection.invoke("SendMessage", HanoiCollabGlobals.Channel, message); e.preventDefault(); } } }); HanoiCollabGlobals.ChatConnection.on("ReceiveMessage", function(name, message) { HanoiCollab$("#hanoicollab-chat-messages").append(`<p class="hanoicollab-basic-container" style="user-select:text;word-wrap: break-word;"><b>${EscapeHtml(name)}</b>: ${EscapeHtml(message)}</p>`); HanoiCollab$("#hanoicollab-chat-messages").animate({scrollTop: HanoiCollab$("#hanoicollab-chat-messages").prop("scrollHeight")}, 1000); }) await HanoiCollabGlobals.ChatConnection.invoke("JoinChannel", HanoiCollabGlobals.Channel); HanoiCollabGlobals.Document.addEventListener('keyup', function (e) { if (e.altKey && e.key === 'c') { HanoiCollab$("#hanoicollab-chat-container").toggle(); } }, false); } async function WaitForDocumentReady() { if (document.readyState === 'complete') { return; } await new Promise(function(resolve, reject) { $("document").ready(function() { resolve(); }); }); } async function TerminateChatConnection() { if (HanoiCollabGlobals.ChatConnection) { await HanoiCollabGlobals.ChatConnection.invoke("LeaveChannel", HanoiCollabGlobals.Channel); HanoiCollabGlobals.ChatConnection.DeliberateClose = true; await HanoiCollabGlobals.ChatConnection.stop(); HanoiCollabGlobals.ChatConnection = null; } } async function Download(url) { return await new Promise(function (resolve, reject) { GM_xmlhttpRequest({ method: "GET", url: url, onload: function(r) { resolve(r); }, onerror: function(r) { resolve(r); } }); }); } HanoiCollabScriptPatches = { "shub.edu.vn": function(src, code) { if (src.includes("_app")) { // This should block monitor action INSIDE THE IFRAME, however we're not using it because: // - It does NOT block monitor action in the main script. // - It takes a long time to apply this patch. // code = code.replace(`key:"add",value:function(e,t){this.userTestId`, `key:"add",value:function(e,t){console.log("Blocked monitor action");console.log(e);return;this.userTestId`) code = code.replace(`component:"a",href:n`, `component:"a",href:(function(n){if(!window.HanoiCollabExposedVariables)window.HanoiCollabExposedVariables=[];if(!window.HanoiCollabExposedVariables.ExposedFiles)window.HanoiCollabExposedVariables.ExposedFiles=[];window.HanoiCollabExposedVariables.ExposedFiles.push(n);return n;})(n)`) } return code; }, "azota.vn": function(src, code) { if (src.includes("main") || src.includes("runtime")) { code = code.replace(/document\.head\.appendChild/g, `(function(child) { if (child.tagName !== "SCRIPT") { document.head.appendChild(child); return; } if (child.src && child.src.includes("es2015")) { var request = new XMLHttpRequest(); request.open("GET", child.src, false); request.send(null); var scriptContent = request.responseText; scriptContent = scriptContent.replace(/constructor\\([a-zA-Z,]*?\\){/gm, (match) => {return match + "try{if(!window.HanoiCollabExposedVariables)window.HanoiCollabExposedVariables=[];HanoiCollabExposedVariables.push(this);}catch(e){console.log(e);}"}); var scriptBlob = new Blob([scriptContent], {type: "application/javascript"}); var scriptURL = URL.createObjectURL(scriptBlob); child.removeAttribute("integrity"); child.src = scriptURL; } document.head.appendChild(child); })` ); code = code.replace(/sendMonitorAction\([A-Za-z,]*?\){/, match => match + `console.log("Blocked monitor action.");return;`); code = code.replace(/trackInfos:[^,}]*/, `trackInfos: null`); code = code.replace(/resultTrack:[^,}]*/, `resultTrack: null`); } return code; }, "forms.office.com": function(src, code) { if (src.includes("page.min")) { code = code .replace("return(e=e||c.length!==Object.keys(n).length)?i:n", "return(e=e||c.length!==Object.keys(n).length)?(function(i){window.HanoiCollabExposedVariables=window.HanoiCollabExposedVariables||[];window.HanoiCollabExposedVariables.FormState=i;return i})(i):n") .replace("function f(n){var r=(0,o.cF)", "function f(n){window.HanoiCollabExposedVariables=window.HanoiCollabExposedVariables||[];window.HanoiCollabExposedVariables.UpdateLocalStorage=f;var r=(0,o.cF)"); } return code; }, "quilgo.com": function(src, code) { if (src.includes("collector.min")) { code = code // PLASM: Block focus tracking .replace(`this.checkAndHandleUnfocused=()=>{`, `this.checkAndHandleUnfocused=()=>{console.log("Blocked monitor action.");return;`) // PLASM: Allow sharing one tab .replace(/getSelectedArea\([A-Za-z,]*?\){/g, match => match + `return "monitor";`); if (HanoiCollabGlobals.EnableTrackingPrevention) { code = code .replace(/(new Promise\(\()(\(.,.\))/, "$1 async $2") // Here, we hardcode the interval to 5000 to allow students to have more time preparing their screens. .replace(/validateOptions\(t\){/g, "validateOptions(t){if (t.interval) {window.HanoiCollabExposedVariables = window.HanoiCollabExposedVariables || []; window.HanoiCollabExposedVariables.OldIntervals = window.HanoiCollabExposedVariables.OldIntervals || {}; window.HanoiCollabExposedVariables.OldIntervals[t.id] = t.interval; t.interval = 5000;}") .replace(/JSON\.stringify\(([^\)]*?)\.payload\)/, `(await (async function(t){if(!window.HanoiCollabExposedVariables?.HanoiCollabApproveRequest){throw "HanoiCollab blocked";} await window.HanoiCollabExposedVariables.HanoiCollabApproveRequest(t); return JSON.stringify(t.payload);})($1))`) } } if (src.includes("link.js") || src.includes("app.js") || src.includes("form.js")) { code = code .replace(/\/api/g, top.window.location.origin + "/api") .replace(/\/stream/g, top.window.location.origin + "/stream"); } return code; }, "docs.google.com": function(src, code) { // This _should_ allow Google Forms accept the sandbox, // however many stuff are messed up so we dropped the sandbox // for Google Forms. if (src.includes("viewer_base")) { code = ` function setupHook(xhr) { function getter() { console.log('get responseText'); delete xhr.responseText; var ret = xhr.responseText; if (ret.includes("history.replaceState")) { ret = "window.history.replaceState=function(){};console.log(window.history);" + ret; ret = ret .replace(/=history\\.pushState/, "=window.parent.history.pushState") .replace(/=history\\.replaceState/, "=function(){}"); } setup(); return ret; } function setter(str) { console.log('set responseText: %s', str); } function setup() { Object.defineProperty(xhr, 'responseText', { get: getter, set: setter, configurable: true }); } setup(); } XMLHttpRequest.prototype.oldOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function(method, url, async, user, password) { console.log(url); if (!this._HanoiCollabHooked) { this._HanoiCollabHooked = true; setupHook(this); } XMLHttpRequest.prototype.oldOpen.call(this, method, url, async, user); } ` + code; code = code.replace(/impressionBatch:[^}]+/g, "impressionBatch: []") .replace(/this.j.send\(a\)/, "this.j.send((function(a){console.log(a);return a;})(a))") .replace(/document\.getElementById\("base-js\"\)\)&&\([A-Za-z]\=[A-Za-z]\.src\?[A-Za-z]\.src.+?length-1]\.src/g, match => match.replace(/\.src/g, `.getAttribute("originalSrc")`)); } return code; } }; function PatchElement(element) { switch (HanoiCollabGlobals.Provider) { case "quilgo.com": { if (element.classList.contains("form-submitted")) { element.remove(); } if (element.classList.contains("plasm-caliber")) { var parser = new DOMParser(); var doc = parser.parseFromString( ` <div class="hanoicollab-basic-container" style="color:red;"> <b>This is a screen-monitored exam! Please remember to enable Stealth Mode (by pressing Alt + H) before proceeding.</b> </br> <b>Furthermore, to prevent HanoiCollab's sign in popup to appear during the test, if you haven't logged in for more than 20 hours, please renew your session by pressing Alt + L and entering your username and password in the dialog box.</b> <h4>Good luck, and have fun!</h4> </div> ` , "text/html").body.firstChild; element.appendChild(doc); } } break; } } async function SetupSandbox() { // Google is way too complex. Leave it alone. // We don't have to intercept any form state variables: They're all present in the DOM. if (HanoiCollabGlobals.Provider == "docs.google.com") { HanoiCollabGlobals.Document = document; HanoiCollabGlobals.Window = unsafeWindow; return new Promise((resolve) => resolve(document)); } else if (HanoiCollabGlobals.Provider == "quilgo.com") { // There's some _urrh_ Google Captcha bullshit on this page. // We also want to steer clear of it. if (document.querySelector("[class='auth-via-email-submit']")) { HanoiCollabGlobals.Document = document; HanoiCollabGlobals.Window = unsafeWindow; return new Promise((resolve) => resolve(document)); } } var style = window.document.createElement("style"); style.innerText = `body { margin: 0; overflow: hidden; } #iframe1 { position:absolute; left: 0px; width: 100%; top: 0px; height: 100%; }`; window.document.body.textContent = ""; window.document.body.appendChild(style); var frame = window.document.createElement("iframe"); frame.id = "iframe1"; window.document.body.appendChild(frame); var request = await Download(location.href); var parser = new DOMParser(); var htmlDoc = parser.parseFromString(request.responseText, 'text/html'); for (var base of htmlDoc.getElementsByTagName("base")) { base.href = location.origin + "/"; } async function PatchScript(script) { if (!script || script.tagName !== "SCRIPT") { return; } function PatchWindowAccess(code) { return code .replace(/window\.location/g, "window.parent.location") .replace(/window\.history/g, "window.parent.history") .replace(/document\.location\.href/g, "window.parent.document.location.href") .replace(/window\.parent\.location\.href=([^=])/g, function(match) { var index = match.indexOf("="); return match.slice(0, index + 1) + "window.parent.document.location.origin+" + match.slice(index + 1); }); } if (script.src) { var scriptSource = script.getAttribute("src"); var request = await Download(scriptSource); var scriptContent = HanoiCollabScriptPatches[HanoiCollabGlobals.Provider](scriptSource, PatchWindowAccess(request.responseText)); var scriptBlob = new Blob([scriptContent], {type: "application/javascript"}); script.src = URL.createObjectURL(scriptBlob); script.setAttribute("originalSrc", scriptSource); } if (script.textContent) { script.textContent = PatchWindowAccess(script.textContent); } script.removeAttribute("integrity"); } function PatchLocation(elem) { if (!elem.getAttribute) { return; } var src = elem.getAttribute("src"); var href = elem.getAttribute("href"); if (href) { if (!href.startsWith("http")) { elem.setAttribute("href", location.origin + href); } } if (src) { if (src.startsWith("blob") || src.startsWith("data")) { return; } if (!src.startsWith("http")) { // Sus. if (src.startsWith("//")) { elem.setAttribute("src", "https:" + src); elem.setAttribute("originalSrc", src); } else { elem.setAttribute("src", location.origin + src); elem.setAttribute("originalSrc", src); } } } } for (var elem of htmlDoc.documentElement.getElementsByTagName("*")) { PatchLocation(elem); await PatchScript(elem); PatchElement(elem); } var blob = new Blob([htmlDoc.documentElement.outerHTML], {type: "text/html"}); return await new Promise(function (resolve, reject) { frame.onload = function() { new MutationObserver(async function(mutations) { for (var mutation of mutations) { for (var node of mutation.addedNodes) { PatchLocation(node); await PatchScript(node); if (node.getElementsByTagName) { for (var elem of node.getElementsByTagName("*")) { PatchLocation(elem); await PatchScript(elem); } } } } }).observe(frame.contentDocument.documentElement, {childList: true, subtree: true}); HanoiCollabGlobals.Document = frame.contentDocument; HanoiCollabGlobals.Window = frame.contentWindow; resolve({document: frame.contentDocument, window: frame.contentWindow}); } frame.src = URL.createObjectURL(blob); }); } function SetupStyles() { var style = HanoiCollabGlobals.Document.createElement("style"); style.innerText = ` *[id^="hanoicollab-"], *[class^="hanoicollab-"] { box-sizing: border-box; font-family: Segoe UI,Segoe WP,Tahoma,Arial,sans-serif; font-size: 14px; font-stretch: 100%; font-style: normal; font-variant-caps: normal; font-variant-east-asian: normal; font-variant-ligatures: normal; font-variant-numeric: normal; font-weight: 400; letter-spacing: normal; line-height: 1.5; margin: 0; margin-block-start: 1em; margin-block-end: 1em; margin-inline-start: 0px; margin-inline-end: 0px; padding: 0; text-size-adjust: 100%; user-select: none; -webkit-font-smoothing: antialiased; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } .hanoicollab-basic-container p { margin: 0; } .hanoicollab-button { background-color: rgba(0,127,127,0.9); border: 1px solid black; border-radius: 1ex; padding: 0.5em; color: white; cursor: pointer; line-height: 1.5; text-transform: unset !important; } `; HanoiCollabGlobals.Document.body.appendChild(style); } function AppendStealthModeStyle() { var stealthModeStyle = HanoiCollabGlobals.Document.createElement("style"); stealthModeStyle.innerText = ` *[id^="hanoicollab-"], *[class^="hanoicollab-"] { display: none; } `; stealthModeStyle.id = "hanoicollab-stealth-mode-css"; HanoiCollabGlobals.Document.body.appendChild(stealthModeStyle); } function RemoveStealthModeStyle() { HanoiCollab$("#hanoicollab-stealth-mode-css").remove(); } function ToggleStealthMode() { HanoiCollabGlobals.IsSteathMode = !HanoiCollabGlobals.IsSteathMode; SetupStealthMode(true); } function SynchronizeStealthMode(originId) { originId = originId || HanoiCollabGlobals.WindowId; // Stealth mode synchronization if (window.parent) { window.parent.postMessage({ Type: "HanoiCollabStealthMode", Value: HanoiCollabGlobals.IsSteathMode, OriginId: originId }, "*"); } for (var frame in document.querySelectorAll("iframe")) { frame.contentWindow?.postMessage({ Type: "HanoiCollabStealthMode", Value: HanoiCollabGlobals.IsSteathMode, OriginId: originId }, "*"); } } window.addEventListener("message", function(event) { var msg = event.data; if (msg.Type === "HanoiCollabStealthMode") { if (msg.OriginId == HanoiCollabGlobals.WindowId) { return; } console.log(window.location.href + ": Received stealth mode message: " + msg.Value + " from " + msg.OriginId); HanoiCollabGlobals.IsSteathMode = msg.Value; SetupStealthMode(true); SynchronizeStealthMode(msg.OriginId); } }); HanoiCollabGlobals.ChildProviders = { "quilgo.com": ["docs.google.com"] }; async function SetupStealthMode(init = false) { if (!init) { HanoiCollabGlobals.Document.addEventListener('keyup', function (e) { if (e.altKey && e.key === 'h') { ToggleStealthMode(); } }, false); var stealthModeConfig = JSON.parse(await GM_getValue("HanoiCollabStealthConfig", "{}")); if (stealthModeConfig[HanoiCollabGlobals.Provider]) { HanoiCollabGlobals.IsSteathMode = stealthModeConfig[HanoiCollabGlobals.Provider]; } HanoiCollabGlobals.StealthModeConfig = stealthModeConfig; } // Probably a hosted iframe where HanoiCollab is disabled. if (!HanoiCollabGlobals.Document) { return; } if (HanoiCollabGlobals.IsSteathMode) { AppendStealthModeStyle(); HanoiCollabGlobals.StealthModeConfig[HanoiCollabGlobals.Provider] = true; } else { RemoveStealthModeStyle(); HanoiCollabGlobals.StealthModeConfig[HanoiCollabGlobals.Provider] = false; } // Sync with children for (var child of (HanoiCollabGlobals.ChildProviders || {})[HanoiCollabGlobals.Provider] || []) { HanoiCollabGlobals.StealthModeConfig[child] = HanoiCollabGlobals.IsSteathMode; } SynchronizeStealthMode(); await GM_setValue("HanoiCollabStealthConfig", JSON.stringify(HanoiCollabGlobals.StealthModeConfig)); } async function WaitForTestReady() { return await new Promise((resolve, reject) => { switch (HanoiCollabGlobals.Provider) { case "azota.vn": { // Top, not hanoicollab. HanoiCollab's window is a blob, remember? if (!top.window.location.href.includes("take-test")) { resolve(false); return; } var hadFormState = false; var interval = setInterval(function() { if (!hadFormState) { if (HanoiCollabGlobals.Window.HanoiCollabExposedVariables) { for (var v of HanoiCollabGlobals.Window.HanoiCollabExposedVariables) { if (v.saveToStorage && v.questionList) { // Clean the other junk. HanoiCollabGlobals.Window.HanoiCollabExposedVariables = []; // Name from our Office Forms experience. HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState = v; // Prevent new junk. HanoiCollabGlobals.Window.HanoiCollabExposedVariables.push = function(){}; hadFormState = true; break; } } } } else if (HanoiCollab$(".question-content").length) { clearInterval(interval); resolve(true); return; } }, 1000); } break; case "forms.office.com": { if (!top.window.location.href.includes("Pages/ResponsePage.aspx")) { resolve(false); return; } var hadFormState = false; var interval = setInterval(function() { if (!hadFormState) { if (HanoiCollabGlobals.Window.HanoiCollabExposedVariables && HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState) { hadFormState = true; } } else if (HanoiCollab$(".office-form-question-content").length) { clearInterval(interval); resolve(true); return; } }, 1000); } break; case "shub.edu.vn": { if (!top.window.location.href.endsWith("/test")) { resolve(false); return; } var interval = setInterval(function() { if (HanoiCollabGlobals.Document.querySelectorAll("[id^=cell]").length) { clearInterval(interval); resolve(true); return; } }); } break; case "quilgo.com": { // We do not care about Quilgo: It's a freaking iframe, // Tampermonkey will access the iframe. resolve(false); } break; case "docs.google.com": { if (!window.location.href.includes("/viewform")) { resolve(false); return; } var interval = setInterval(function() { if (HanoiCollabGlobals.Document.querySelectorAll("form").length) { clearInterval(interval); resolve(true); return; } }); } break; default: resolve(false); } }) } function GetFormId() { switch (HanoiCollabGlobals.Provider) { case "azota.vn": return "" + HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.exam_obj.id; case "forms.office.com": return "" + HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.$$.$H; case "shub.edu.vn": return "" + top.location.href.match(/homework\/([\d]+?)\/test/)[1]; case "docs.google.com": return "" + window.location.href.match(`https:\/\/docs\.google\.com\/forms\/d\/e\/(.*?)\/viewform.*`)[1]; default: return ""; } } class QuestionInfo { constructor(htmlElement, id, type, index) { this.HtmlElement = htmlElement; this.Id = id; this.Type = type; this.Index = index; var parser = new DOMParser(); this.CommunityAnswersHtml = parser.parseFromString( ` <div class="hanoicollab-community-answers"> <div class="hanoicollab-community-answers-header">Community answers:</div> <div class="hanoicollab-community-answers-multiple-choice"> <div class="hanoicollab-community-answers-multiple-choice-header">Multiple choice:</div> <div class="hanoicollab-community-answers-multiple-choice-contents"></div> </div> <div class="hanoicollab-community-answers-written"> <div class="hanoicollab-community-answers-written-header">Written:</div> <select class="hanoicollab-community-answers-written-select"> </select> <div class="hanoicollab-community-answers-written-content" style="user-select:text;"></div> </div> </div> ` , "text/html").body.firstChild; if (this.Type == "multipleChoice") { this.CommunityAnswersHtml.querySelector(".hanoicollab-community-answers-written").style.display = "none"; } if (this.Type == "written") { this.CommunityAnswersHtml.querySelector(".hanoicollab-community-answers-multiple-choice").style.display = "none"; } var info = this; this.CommunityAnswersHtml.querySelector("select").addEventListener("change", function() { info.UpdateWrittenSelect(this); }); if (this.IsMultipleChoice()) { this.Answers = []; } } IsMultipleChoice() { return this.Type == "multipleChoice" || this.Type == "hybrid"; } IsWritten() { return this.Type == "written" || this.Type == "hybrid"; } GetUserAnswer() { throw "Not implemented"; } SetUserAnswer() { throw "Not implemented"; } async SendUserAnswer(answer) { if (HanoiCollabGlobals.ExamConnection) { var info = this; if (info.SendUserTimeOut) { info.NeedsUpdate = true; return; } info.SendUserTimeOut = true; info.NeedsUpdate = false; await HanoiCollabGlobals.ExamConnection.invoke("UpdateAnswer", GetFormId(), info.Id, answer); setTimeout(function() { info.SendUserTimeOut = false; if (info.NeedsUpdate) { info.SendUserAnswer(info.GetUserAnswer()); } }, 1000); } } ClearUserAnswer() { throw "Not implemented"; } UpdateWrittenSelect(select) { var value = select.value; if (!value) { return; } var contentElement = select.parentElement.querySelector(".hanoicollab-community-answers-written-content"); contentElement.innerText = this.CommunityAnswers.find(function(ans){return ans.User == value}).Answer; } UpdateCommunityAnswersHtml() { var info = this; if (info.IsMultipleChoice()) { var communityAnswers = info.CommunityAnswers; var multipleChoice = communityAnswers.MultipleChoice; var communityAnswersHtml = info.CommunityAnswersHtml; var multipleChoiceHtml = communityAnswersHtml.querySelector(".hanoicollab-community-answers-multiple-choice-contents"); var elem = HanoiCollabGlobals.Document.createElement("div"); for (var key of Object.keys(multipleChoice).sort(function(key1, key2){return multipleChoice[key1].Alpha.localeCompare(multipleChoice[key2].Alpha)})) { if (multipleChoice[key].Alpha) { var p = HanoiCollabGlobals.Document.createElement("p"); p.innerHTML = `<b>${multipleChoice[key].Alpha}</b> (${multipleChoice[key].length}): ${EscapeHtml(multipleChoice[key].slice(0, Math.min(multipleChoice[key].length, 10)).join(", "))}`; elem.appendChild(p); } } multipleChoiceHtml.innerHTML = elem.innerHTML; } if (info.IsWritten()) { var communityAnswers = info.CommunityAnswers; var communityAnswersHtml = info.CommunityAnswersHtml; var writtenSelect = communityAnswersHtml.querySelector(".hanoicollab-community-answers-written-select"); var newSelect = HanoiCollabGlobals.Document.createElement("select"); for (var ans of communityAnswers.sort( function(a, b){return (a.Answer.length != b.Answer.length) ? b.Answer.length - a.Answer.length : a.User.localeCompare(b.User)})) { var option = HanoiCollabGlobals.Document.createElement("option"); option.value = ans.User; option.innerText = `${ans.User} (${ans.Answer.length}) characters`; newSelect.appendChild(option); } writtenSelect.innerHTML = newSelect.innerHTML; info.UpdateWrittenSelect(writtenSelect); } } }; // A unified interface for getting questions. function GetQuestions() { switch (HanoiCollabGlobals.Provider) { case "azota.vn": { var result = []; var elements = HanoiCollab$(".question-content"); var questions = HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.questionList; for (var i = 0; i < questions.length; ++i) { var info = new QuestionInfo(elements[i], "" + questions[i].id, questions[i].answerType === 1 ? "multipleChoice" : "written", i); info.GetUserAnswer = function() { var info = this; var ans = HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.answerList.find(function (a) { return a.questionId == info.Id; }); if (!ans) { return null; } return ans.answerContent[0].content; } info.SetUserAnswer = function(answer) { if (this.SendUserAnswer) { this.SendUserAnswer(answer); } var sheetContentButton = HanoiCollab$(".sheet_content").find(".no-answered")[this.Index]; if (answer) { sheetContentButton.style.backgroundColor = "rgba(60, 141, 188, 1)"; sheetContentButton.style.color = "rgb(255, 255, 255)"; } else { sheetContentButton.style.background = "#fff"; sheetContentButton.style.color = "#111"; } } if (questions[i].answerType === 1) { if (questions[i].answerConfig[0].alpha) { for (var j = 0; j < questions[i].answerConfig.length; ++j) { info.Answers.push({Id: questions[i].answerConfig[j].key, Alpha: questions[i].answerConfig[j].alpha}); } } else { for (var j = 0; j < questions[i].answerConfig[0].answer.length; ++j) { //To-Do: Is this shit shuffled? info.Answers.push({Id: questions[i].answerConfig[0].answer[j].content, Alpha: questions[i].answerConfig[0].answer[j].content}); } } info.ClearUserAnswer = function() { var info = this; var formState = HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState; formState.answerList = formState.answerList.filter(function (a) { return a.questionId != info.Id; }); formState.saveToStorage(formState.answerList, formState.noteList, formState.files); HanoiCollab$(this.HtmlElement).find(".no-answered").each(function (i, button) { button.style.background = "#fff"; button.style.color = "#111"; }); for (var j = 0; j < questions[info.Index].answerConfig.length; ++j) { questions[info.Index].answerConfig[j].checked = false; } } } else { info.ClearUserAnswer = function() { var info = this; var formState = HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState; formState.answerList = formState.answerList.filter(function (a) { return a.questionId != info.Id; }); formState.saveToStorage(formState.answerList, formState.noteList, formState.files); HanoiCollab$(this.HtmlElement).find("textarea").each(function (i, textArea) { textArea.value = null; }); } } result.push(info); } return result; } case "forms.office.com": { var result = []; var elements = HanoiCollab$(".office-form-question-content"); var questions = HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.$$.$e; for (let i = 0; i < elements.length; ++i) { var currentQuestionId = HanoiCollab$(elements[i]).find(".question-title-box")[0].id.substr("QuestionId_".length); var currentQuestion = questions[currentQuestionId]; var info = new QuestionInfo(elements[i], currentQuestionId, currentQuestion.info.type == "Question.Choice" ? "multipleChoice" : "written", i); info.GetUserAnswer = function() { var info = this; var content = HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.$$.$e[info.Id].runtime.$c; if (!content || content.length == 0) { return null; } if (info.IsMultipleChoice()) { // Array of choices. return content[0].getHashCode(); } else { // String return content; } } info.SetUserAnswer = function(answer) { if (this.SendUserAnswer) { this.SendUserAnswer(answer); } } if (info.IsMultipleChoice()) { var alpha = "A"; for (let j = 0; j < currentQuestion.info.choices.length; ++j) { info.Answers.push({Id: currentQuestion.info.choices[j].description.getHashCode(), Alpha: String.fromCharCode(alpha.charCodeAt(0) + j)}); } info.ClearUserAnswer = function() { var info = this; HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.$$.$e[info.Id].runtime.$c = []; for (var input of info.HtmlElement.getElementsByTagName("input")) { input.checked = false; } if (HanoiCollabGlobals.Window.HanoiCollabExposedVariables.UpdateLocalStorage) { HanoiCollabGlobals.Window.HanoiCollabExposedVariables.UpdateLocalStorage( HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.$$ ); } } } else { info.ClearUserAnswer = function() { var info = this; HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.$$.$e[info.Id].runtime.$c = ""; for (var input of info.HtmlElement.getElementsByTagName("input")) { input.value = ""; } if (HanoiCollabGlobals.Window.HanoiCollabExposedVariables.UpdateLocalStorage) { HanoiCollabGlobals.Window.HanoiCollabExposedVariables.UpdateLocalStorage( HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.$$ ); } } } result.push(info); } return result; } case "shub.edu.vn": { var result = []; var elements = HanoiCollab$("[id^=cell]"); for (let i = 0; i < elements.length; ++i) { var currentQuestionId = elements[i].id.substr("cell-".length); var info = new QuestionInfo(elements[i], currentQuestionId, "hybrid", i); info.GetUserAnswer = function() { var info = this; var text = this.HtmlElement.getElementsByTagName("p")[0].innerText; var colonIndex = text.indexOf(":"); if (colonIndex == -1) { return null; } text = text.substr(colonIndex + 1); if (!text) { return null; } return text; } info.SetUserAnswer = function(answer) { if (this.SendUserAnswer) { this.SendUserAnswer(answer); } } info.ClearUserAnswer = function() { // May or may not be implemented using simulation. throw "Not implemented."; } var alpha = "A"; for (let j = 0; j < 4; ++j) { info.Answers.push({Id: String.fromCharCode(alpha.charCodeAt(0) + j), Alpha: String.fromCharCode(alpha.charCodeAt(0) + j)}); } result.push(info); } return result; } case "docs.google.com": { var result = []; var questions = HanoiCollabGlobals.Window.FB_PUBLIC_LOAD_DATA_[1][1]; var questionElements = document.querySelectorAll("[role=\"listitem\"]"); for (let i = 0; i < questions.length; ++i) { var q = questions[i]; var qElem = questionElements[i]; var currentQuestionType = null; switch (q[3]) { case 0: currentQuestionType = "hybrid"; break; case 1: currentQuestionType = "written"; break; case 2: currentQuestionType = "multipleChoice"; break; } if (currentQuestionType == null) { continue; } var currentQuestionId = "" + q[4][0][0]; var info = new QuestionInfo(qElem, currentQuestionId, currentQuestionType, i); if (currentQuestionType == "multipleChoice") { var answers = q[4][0][1]; var alpha = "A"; for (let j = 0; j < answers.length; ++j) { var a = answers[j]; info.Answers.push({Id: "" + a[0].getHashCode(), Alpha: String.fromCharCode(alpha.charCodeAt(0) + j)}); } } else if (currentQuestionType == "hybrid") { var alpha = "A"; for (let j = 0; j < 4; ++j) { info.Answers.push({Id: String.fromCharCode(alpha.charCodeAt(0) + j), Alpha: String.fromCharCode(alpha.charCodeAt(0) + j)}); } } info.GetUserAnswer = function() { var info = this; var input = HanoiCollab$("[name=\"entry." + info.Id + "\"]")[0]; var content = input?.value; if (!content || content.length == 0) { return null; } if (info.Type == "multipleChoice") { return content.getHashCode(); } else { return content; } } info.SetUserAnswer = function(answer) { if (this.SendUserAnswer) { this.SendUserAnswer(answer); } } info.ClearUserAnswer = function() { var info = this; var input = HanoiCollab$("[name=\"entry." + info.Id + "\"]")[0]; // Clear from DOM if (input) { input.value = ""; if (info.Type == "multipleChoice") { input.remove(); } } // Clear from UI if (info.IsWritten()) { var writeArea = info.HtmlElement.querySelector("input,textarea"); if (writeArea) { writeArea.value = ""; writeArea.dispatchEvent(new InputEvent("input", {bubbles: true})); } } else { var elem = info.HtmlElement.querySelector("[aria-checked='true']"); if (elem) { elem.setAttribute("aria-checked", "false"); elem.setAttribute("tabindex", "-1"); var clazz = elem.classList[elem.classList.length - 1]; elem.classList.remove(clazz); } } } result.push(info); } return result; } default: { return []; } } } function SetupElementHooks() { var questions = HanoiCollabGlobals.Questions; function Update(q) { // Set timeout, to wait for other event handlers: setTimeout(function() { var answer = q.GetUserAnswer(); q.SetUserAnswer(answer); }, 100); } function AddButton(q) { var button = HanoiCollabGlobals.Document.createElement("button"); button.innerText = "Clear"; button.className = "hanoicollab-clear-button"; button.type = "button"; button.addEventListener("click", function() {q.ClearUserAnswer();}); q.HtmlElement.appendChild(button); } for (/* new var in each interation */let q of questions) { switch (HanoiCollabGlobals.Provider) { case "azota.vn": { if (q.Type == "multipleChoice") { q.HtmlElement.querySelector(".list-answer").addEventListener("click", function(){Update(q)}); } else { q.HtmlElement.querySelector("textarea").addEventListener("blur", function(){Update(q)}); } AddButton(q); } break; case "forms.office.com": { if (q.Type == "multipleChoice") { for (var row of q.HtmlElement.getElementsByClassName("office-form-question-choice-row")) { row.addEventListener("click", function(){Update(q)}); } } else { q.HtmlElement.querySelector(".office-form-textfield-input").addEventListener("blur", function(){Update(q)}); } AddButton(q); } break; case "shub.edu.vn": { // We don't add clear buttons for SHUB. q.HtmlElement.addEventListener("click", function() { // setTimeout(function() // { // q.HtmlElement.closest(".MuiBox-root").querySelector(".hanoicollab-community-answers").remove(); // q.HtmlElement.closest(".MuiBox-root").appendChild(q.CommunityAnswersHtml); // }, 100); }); } break; case "docs.google.com": { AddButton(q); } default: break; } q.HtmlElement.querySelector(".hanoicollab-clear-button")?.addEventListener("click", function(){Update(q)}); } if (HanoiCollabGlobals.Provider == "shub.edu.vn") { var inputAnsKey = HanoiCollabGlobals.Document.getElementById("inputAnsKey"); new MutationObserver(function(mutations) { for (var mutation of mutations) { Update(HanoiCollabGlobals.Questions[Number.parseInt(mutation.oldValue.substring("Đáp án câu ".length)) - 1]); mutation.target.closest(".MuiBox-root").querySelector(".hanoicollab-community-answers")?.remove(); mutation.target.closest(".MuiBox-root").appendChild(HanoiCollabGlobals.Questions[Number.parseInt(mutation.target.placeholder.substring("Đáp án câu ".length)) - 1].CommunityAnswersHtml); } }).observe(inputAnsKey, {subtree: false, childList: false, attributes: true, attributeOldValue : true, attributeFilter: ["placeholder"]}) // Very annoying and covers community answers. inputAnsKey.setAttribute("autocomplete", "off"); inputAnsKey.addEventListener("blur", function() { for (let q of questions) { if (q.HtmlElement.style.border.startsWith("2px")) { Update(q); } } }); } if (HanoiCollabGlobals.Provider == "docs.google.com") { var inputCollection = HanoiCollabGlobals.Document.querySelector("form").querySelector("input").parentElement; new MutationObserver(function(mutations) { var ids = []; for (var mutate of mutations) { if (mutate.target?.name?.includes("entry")) { ids[mutate.target.name.substring("entry.".length)] = true; } } for (let q of questions) { if (ids[q.Id]) { Update(q); } } }).observe(inputCollection, {subtree: true, childList: true, attributes: true, attributeOldValue : true, attributeFilter: ["value"]}) } } class QuestionLayout { constructor(type, description, id, answers, resources, imageResources) { this.Type = type; this.Description = description; this.Id = id; this.Answers = answers; this.Resources = resources; this.ImageResources = imageResources; } } class AnswerLayout { constructor(description, resources, id, alpha, imageResources) { this.Description = description; this.Resources = resources; this.Id = id; this.Alpha = alpha; this.ImageResources = imageResources; } } class ExamLayout { constructor() { this.OriginalLink = window.location.href; this.Resources = this.ExtractResources(); this.Questions = this.ExtractQuestions(); } ExtractResources() { switch (HanoiCollabGlobals.Provider) { case "shub.edu.vn": { return HanoiCollabGlobals.Window.HanoiCollabExposedVariables?.ExposedFiles?.filter(function (a, b, c) { return c.indexOf(a) === b; }); } case "docs.google.com": { var result = []; // return result; for (var item of Array.from(HanoiCollabGlobals.Document.querySelectorAll("*")).filter(i => i.getAttribute("role") == "listitem")) { // All items without any choices: if (!item.querySelectorAll("input").length && !item.querySelectorAll("textarea").length && !item.querySelectorAll("label").length) { result.push(...Array.from(item.querySelectorAll("img,iframe")).map(i => i.src)); } } return result; } default: { return []; } } } ExtractQuestions() { switch (HanoiCollabGlobals.Provider) { case "forms.office.com": { var result = []; var elements = HanoiCollab$(".office-form-question-content"); var questions = HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.$$.$e; for (let i = 0; i < elements.length; ++i) { var currentQuestionId = HanoiCollab$(elements[i]).find(".question-title-box")[0].id.substr("QuestionId_".length); var currentQuestion = questions[currentQuestionId]; var answers = []; var currentQuestionType = null; if (currentQuestion.info.type == "Question.Choice") { var alpha = "A"; for (var j = 0; j < currentQuestion.info.choices.length; ++j) { var currentAnswer = currentQuestion.info.choices[j]; answers.push(new AnswerLayout(currentAnswer.description, null, currentAnswer.description.getHashCode(), String.fromCharCode(alpha.charCodeAt(0) + j), null)); } currentQuestionType = "multipleChoice"; } else { currentQuestionType = "written"; } var currentQuestionImageResources = null; if (currentQuestion.info.image) { currentQuestionImageResources = [currentQuestion.info.image]; } var currentQuestionDescription = currentQuestion.info.title; if (currentQuestion.info.subtitle) { currentQuestionDescription += "\n" + currentQuestion.info.subtitle; } result.push(new QuestionLayout(currentQuestionType, currentQuestionDescription, currentQuestionId, answers, null, currentQuestionImageResources)); } return result; } case "azota.vn": { var result = []; var questions = HanoiCollabGlobals.Window.HanoiCollabExposedVariables.FormState.questionList; for (let i = 0; i < questions.length; ++i) { var currentQuestion = questions[i]; var currentQuestionId = "" + currentQuestion.id; var answers = []; var currentQuestionType = currentQuestion.answerType == 1 ? "multipleChoice" : "written"; if (currentQuestion.answerType == 1) { if (currentQuestion.answerConfig[0].alpha) { for (var j = 0; j < currentQuestion.answerConfig.length; ++j) { answers.push(new AnswerLayout(currentQuestion.answerConfig[j].content.replace("<br>", "\n"), null, currentQuestion.answerConfig[j].key, currentQuestion.answerConfig[j].alpha, null)); } } else { for (var j = 0; j < currentQuestion.answerConfig[0].answer.length; ++j) { //To-Do: Is this shit shuffled? answers.push(new AnswerLayout(null, null, currentQuestion.answerConfig[0].answer[j].content, currentQuestion.answerConfig[0].answer[j].content, null)); } } } var currentQuestionResources = []; var currentQuestionImageResources = []; var currentQuestionDescription = currentQuestion.questionText; for (var content of currentQuestion.questionContent) { if (["jpg", "png", "bmp", "svg"].includes(content.extension)) { currentQuestionImageResources.push(content.url); } else if (content.extension == "text") { currentQuestionDescription += content.content.replace("<br>", "\n"); } else { currentQuestionResources.push(content.url); } } result.push(new QuestionLayout(currentQuestionType, currentQuestionDescription, currentQuestionId, answers, currentQuestionResources, currentQuestionImageResources)); } return result; } break; case "shub.edu.vn": { // To-do: SHUB questions may have multimedia resources. var result = []; // To-do: SHUB questions might also have descriptions. for (var q of HanoiCollabGlobals.Questions) { var answers = []; for (var a of q.Answers) { answers.push(new AnswerLayout("", null, a.Id, a.Alpha, null)); } result.push(new QuestionLayout("hybrid", "SHUB question #" + (q.Index + 1), q.Id, answers, null, null)); } return result; } break; case "docs.google.com": { var result = []; var questions = HanoiCollabGlobals.Window.FB_PUBLIC_LOAD_DATA_[1][1]; var questionElements = document.querySelectorAll("[role=\"listitem\"]"); for (let i = 0; i < questions.length; ++i) { var q = questions[i]; var qElem = questionElements[i]; var currentQuestionType = null; switch (q[3]) { case 0: currentQuestionType = "hybrid"; break; case 1: currentQuestionType = "written"; break; case 2: currentQuestionType = "multipleChoice"; break; } if (currentQuestionType == null) { continue; } var currentQuestionDescription = q[1]; var currentQuestionId = "" + q[4][0][0]; var currentQuestionAnswers = []; var resourcesInAnswers = []; if (currentQuestionType == "multipleChoice") { var answers = q[4][0][1]; var answerElements = qElem.querySelectorAll("label"); var alpha = "A"; for (let j = 0; j < answers.length; ++j) { var a = answers[j]; var aElem = answerElements[j]; var imageResources = Array.from(aElem.querySelectorAll("img")).map(i => i.src); var resources = Array.from(aElem.querySelectorAll("iframe")).map(i => i.src); resourcesInAnswers.push(...imageResources); resourcesInAnswers.push(...resources); currentQuestionAnswers.push(new AnswerLayout(a[0], resources, a[0].getHashCode(), String.fromCharCode(alpha.charCodeAt(0) + j), imageResources)); } } else if (currentQuestionType == "hybrid") { var alpha = "A"; for (let j = 0; j < 4; ++j) { currentQuestionAnswers.push(new AnswerLayout("", null, String.fromCharCode(alpha.charCodeAt(0) + j), String.fromCharCode(alpha.charCodeAt(0) + j), null)); } } var currentQuestionImageResources = Array.from(qElem.querySelectorAll("img")).map(i => i.src).filter(i => !resourcesInAnswers.includes(i)); var currentQuestionResources = Array.from(qElem.querySelectorAll("iframe")).map(i => i.src).filter(i => !resourcesInAnswers.includes(i)); result.push(new QuestionLayout(currentQuestionType, currentQuestionDescription, currentQuestionId, currentQuestionAnswers, currentQuestionResources, currentQuestionImageResources)); } return result; } default: return []; } } } async function SetupExamConnection() { if (HanoiCollabGlobals.ExamConnection) { return HanoiCollabGlobals.ExamConnection; } var connection = new signalR .HubConnectionBuilder() .withUrl(HanoiCollabGlobals.Server + "hubs/exam", { accessTokenFactory: GetToken }) .build(); HanoiCollabGlobals.ExamConnection = connection; var questions = HanoiCollabGlobals.Questions; connection.on("InitializeExam", async function(onlineQuestions) { for (var q of questions) { q.CommunityAnswers = []; q.CommunityAnswers.MultipleChoice = []; if (q.IsMultipleChoice()) { q.CommunityAnswers.MultipleChoice = []; for (var a of q.Answers) { q.CommunityAnswers.MultipleChoice[a.Id] = []; q.CommunityAnswers.MultipleChoice[a.Id].Alpha = a.Alpha; } } } for (var i = 0; i < onlineQuestions.length; ++i) { var question = questions.find(function (q) { return q.Id == onlineQuestions[i].QuestionId; }); if (question.CommunityAnswers.MultipleChoice[onlineQuestions[i].Answer]) { question.CommunityAnswers.MultipleChoice[onlineQuestions[i].Answer].push(onlineQuestions[i].UserId); } else { question.CommunityAnswers.push({User: onlineQuestions[i].UserId, Answer: onlineQuestions[i].Answer}); } } for (var q of questions) { q.UpdateCommunityAnswersHtml(); var ans = q.GetUserAnswer(); if (ans != null) { await q.SendUserAnswer(ans); } } console.log(HanoiCollabGlobals.Questions); }); connection.on("ReceiveAnswer", function(onlineQuestion) { var question = questions.find(function (q) { return q.Id == onlineQuestion.QuestionId; }); if (onlineQuestion.OldAnswer) { if (question.CommunityAnswers.MultipleChoice[onlineQuestion.OldAnswer]) { var arr = question.CommunityAnswers.MultipleChoice[onlineQuestion.OldAnswer]; arr.splice(arr.indexOf(onlineQuestion.UserId), 1); } // null answer, must delete. else if (!onlineQuestion.Answer) { var arr = question.CommunityAnswers; var idx = arr.findIndex(function(ans){return ans?.User == onlineQuestion.UserId;}); if (idx != -1) { arr.splice(idx, 1); } } } if (question.CommunityAnswers.MultipleChoice[onlineQuestion.Answer]) { question.CommunityAnswers.MultipleChoice[onlineQuestion.Answer].push(onlineQuestion.UserId); var idx = question.CommunityAnswers.findIndex(function (ans){return ans?.User == onlineQuestion.UserId;}); if (idx != -1) { question.CommunityAnswers.splice(idx, 1); } } // Prevent null answers, which are actually deletions. else if (onlineQuestion.Answer) { var arr = question.CommunityAnswers; var idx = arr.findIndex(function(ans){return ans.User == onlineQuestion.UserId;}); if (idx != -1) { arr[idx].Answer = onlineQuestion.Answer; } else { question.CommunityAnswers.push({User: onlineQuestion.UserId, Answer: onlineQuestion.Answer}); } } question.UpdateCommunityAnswersHtml(); }); connection.on("RequestExamLayout", function(examId) { if (examId === GetFormId()) { connection.invoke("BroadcastExamLayout", examId, new ExamLayout()); } }); await connection.start(); await connection.invoke("JoinExam", GetFormId()); connection.DeliberateClose = false; connection.onclose(function() { if (connection.DeliberateClose) { return; } console.log("Reconnecting..."); var handle = setInterval(async function() { await connection.start(); await HanoiCollabGlobals.ExamConnection.invoke("JoinExam", GetFormId()); clearInterval(handle); }, 5000); }); return connection; } async function TerminateExamConnection() { if (HanoiCollabGlobals.ExamConnection) { await HanoiCollabGlobals.ExamConnection.invoke("LeaveExam", GetFormId()); HanoiCollabGlobals.ExamConnection.DeliberateClose = true; await HanoiCollabGlobals.ExamConnection.stop(); HanoiCollabGlobals.ExamConnection = null; } } function SetupCommunityAnswersUserInterface() { switch(HanoiCollabGlobals.Provider) { case "shub.edu.vn": { for (var q of HanoiCollabGlobals.Questions) { // Active question. if (q.HtmlElement.style.border.startsWith("2px")) { q.HtmlElement.closest(".MuiBox-root").appendChild(q.CommunityAnswersHtml); return; } } var q = HanoiCollabGlobals.Questions[0]; q.HtmlElement.closest(".MuiBox-root").appendChild(q.CommunityAnswersHtml); } break; case "azota.vn": case "forms.office.com": default: { for (var q of HanoiCollabGlobals.Questions) { q.HtmlElement.appendChild(q.CommunityAnswersHtml); } } break; } } async function DisplayPopup(element, urgent) { while (HanoiCollabGlobals.NoticePopupPromise) { await HanoiCollabGlobals.NoticePopupPromise; } HanoiCollabGlobals.NoticePopupPromise = new Promise(async function(resolve, reject) { var needsReToggle = false; if (urgent && HanoiCollabGlobals.IsSteathMode) { needsReToggle = true; ToggleStealthMode(); } //--- Use jQuery to add the form in a "popup" dialog. HanoiCollab$(HanoiCollabGlobals.Document.body).append (` <div id="hanoicollab-notice-popup-container" class="hanoicollab-basic-container"> <p id="hanoicollab-notice-popup-background" style="position:fixed;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.9);z-index:9998;"></p> <div id="hanoicollab-notice-popup" style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);width:50%;padding:2em;color:white;background-color:rgba(0,127,255,0.75);border-radius:1ex;z-index:9999;"> ${element.outerHTML} </div> </div> `); for (let button of HanoiCollabGlobals.Document.getElementById("hanoicollab-notice-popup").getElementsByTagName("button")) { button.onclick = function() { if (needsReToggle) { ToggleStealthMode(); } HanoiCollab$("#hanoicollab-notice-popup-container").remove(); HanoiCollabGlobals.NoticePopupPromise = null; resolve(button.value); } } }); return await HanoiCollabGlobals.NoticePopupPromise; } function SetupTrackingPrevention() { HanoiCollabGlobals.Window.HanoiCollabExposedVariables = HanoiCollabGlobals.Window.HanoiCollabExposedVariables || []; HanoiCollabGlobals.Window.HanoiCollabExposedVariables.HanoiCollabApproveRequest = async function(data) { switch (HanoiCollabGlobals.Provider) { case "quilgo.com": { if (data.path == "snapshots" || data.path == "screenshots") { if (HanoiCollabGlobals.AlwaysBlockImage && HanoiCollabGlobals.AlwaysBlockImage[data.path]) { throw "Image blocked by HanoiCollab"; } if (HanoiCollabGlobals.AlwaysSendImage && HanoiCollabGlobals.AlwaysSendImage[data.path]) { return; } if (HanoiCollabGlobals.LoopImage && HanoiCollabGlobals.LoopImage[data.path]) { console.log("HanoiCollab is looping an old image for " + data.path); data.payload.image = HanoiCollabGlobals.LoopImage[data.path]; var collectorType = null; switch (data.path) { case "snapshots": collectorType = "camera"; break; case "screenshots": collectorType = "screen"; break; } HanoiCollabGlobals.AlwaysBlockImage = HanoiCollabGlobals.AlwaysBlockImage || {}; HanoiCollabGlobals.AlwaysBlockImage[data.path] = true; console.log("HanoiCollab is restoring original interval...."); setTimeout(function() { HanoiCollabGlobals.AlwaysBlockImage = HanoiCollabGlobals.AlwaysBlockImage || {}; HanoiCollabGlobals.AlwaysBlockImage[data.path] = false; }, HanoiCollabGlobals.Window.HanoiCollabExposedVariables.OldIntervals[collectorType]); return; } var parser = new DOMParser(); var doc = parser.parseFromString( ` <p>Quilgo's PLASM engine is trying to record your ${data.path == "snapshots" ? "snapshot" : "screenshot"}! Choose your action!</p> <div style="display:flex;justify-content:center;align-items:center;width:100%;"> <img src="data:image/jpeg;base64,${data.payload.image}" style="max-width:100%;"></img> </div> <button class="hanoicollab-button" value="loop">Send this image over and over</button> <button class="hanoicollab-button" value="send">Send</button> <button class="hanoicollab-button" value="always-send">Always send ${data.path}</button> <button class="hanoicollab-button" value="block">Block</button> <button class="hanoicollab-button" value="always-block">Always block ${data.path}</button> ` , "text/html").body; var result = await DisplayPopup(doc, true); switch (result) { case "loop": var image = data.payload.image; HanoiCollabGlobals.LoopImage = HanoiCollabGlobals.LoopImage || {}; HanoiCollabGlobals.LoopImage[data.path] = image; return; case "send": return; case "always-send": HanoiCollabGlobals.AlwaysSendImage = HanoiCollabGlobals.AlwaysSendImage || {}; HanoiCollabGlobals.AlwaysSendImage[data.path] = true; return; case "block": throw "Image blocked by HanoiCollab"; case "always-block": HanoiCollabGlobals.AlwaysBlockImage = HanoiCollabGlobals.AlwaysBlockImage || {}; HanoiCollabGlobals.AlwaysBlockImage[data.path] = true; throw "Image blocked by HanoiCollab"; } } return; } } } } (async function() { if (window.location.origin.startsWith("blob")) { return; } HanoiCollabGlobals.Provider = location.hostname; HanoiCollabGlobals.Channel = location.href; await WaitForDocumentReady(); var oldPushState = window.history.pushState; window.history.pushState = function(state, title, url) { if (url) { // Should never return, page reloaded. location.href = url; } oldPushState(state, title, url); } await SetupSandbox(); // We had a nice time implementing this, however, this function still have // some problems: // - A loop of the snapshot seems really suspicious when the student doesn't move during the test. // The teacher might accuse the student of using a virtual background. // - A loop of the screenshot is way more suspicious: The clock does not tick if the screenshot // is looped. // Therefore, the recommended way to disable Quilgo tracking is using OBS studio (or any virtual camera provider) // _and_ using HanoiCollab's website for collaboration. if (HanoiCollabGlobals.EnableTrackingPrevention) { SetupTrackingPrevention(); } await SetupStealthMode(); SetupKeyBindings(); SetupStyles(); await SetupServer(); await SetupIdentity(); await SetupChatConnection(); await SetupChatUserInterface(); var isTest = await WaitForTestReady(); HanoiCollabGlobals.OnIdentityChange = async function() { await TerminateChatConnection(); await SetupChatConnection(); await SetupChatUserInterface(); if (isTest) { await TerminateExamConnection(); await SetupExamConnection(); } } if (isTest) { HanoiCollabGlobals.Questions = GetQuestions(); SetupElementHooks(); await SetupExamConnection(); SetupCommunityAnswersUserInterface(); } $(window).on("beforeunload", async function() { await TerminateChatConnection(); await TerminateExamConnection(); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址