mirror of
https://github.com/gchq/CyberChef.git
synced 2025-04-25 09:16:17 -04:00
Move waiters and workers into separate folders.
This commit is contained in:
parent
31a3af1f84
commit
b77239fc15
16 changed files with 31 additions and 31 deletions
157
src/web/waiters/BackgroundWorkerWaiter.mjs
Normal file
157
src/web/waiters/BackgroundWorkerWaiter.mjs
Normal file
|
@ -0,0 +1,157 @@
|
|||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import ChefWorker from "worker-loader?inline&fallback=false!../../core/ChefWorker";
|
||||
|
||||
/**
|
||||
* Waiter to handle conversations with a ChefWorker in the background.
|
||||
*/
|
||||
class BackgroundWorkerWaiter {
|
||||
|
||||
/**
|
||||
* BackgroundWorkerWaiter constructor.
|
||||
*
|
||||
* @param {App} app - The main view object for CyberChef.
|
||||
* @param {Manager} manager - The CyberChef event manager.
|
||||
*/
|
||||
constructor(app, manager) {
|
||||
this.app = app;
|
||||
this.manager = manager;
|
||||
|
||||
this.callbacks = {};
|
||||
this.callbackID = 0;
|
||||
this.completedCallback = -1;
|
||||
this.timeout = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets up the ChefWorker and associated listeners.
|
||||
*/
|
||||
registerChefWorker() {
|
||||
log.debug("Registering new background ChefWorker");
|
||||
this.chefWorker = new ChefWorker();
|
||||
this.chefWorker.addEventListener("message", this.handleChefMessage.bind(this));
|
||||
|
||||
let docURL = document.location.href.split(/[#?]/)[0];
|
||||
const index = docURL.lastIndexOf("/");
|
||||
if (index > 0) {
|
||||
docURL = docURL.substring(0, index);
|
||||
}
|
||||
this.chefWorker.postMessage({"action": "docURL", "data": docURL});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for messages sent back by the ChefWorker.
|
||||
*
|
||||
* @param {MessageEvent} e
|
||||
*/
|
||||
handleChefMessage(e) {
|
||||
const r = e.data;
|
||||
log.debug("Receiving '" + r.action + "' from ChefWorker in the background");
|
||||
|
||||
switch (r.action) {
|
||||
case "bakeComplete":
|
||||
case "bakeError":
|
||||
if (typeof r.data.id !== "undefined") {
|
||||
clearTimeout(this.timeout);
|
||||
this.callbacks[r.data.id].bind(this)(r.data);
|
||||
this.completedCallback = r.data.id;
|
||||
}
|
||||
break;
|
||||
case "workerLoaded":
|
||||
log.debug("Background ChefWorker loaded");
|
||||
break;
|
||||
case "optionUpdate":
|
||||
case "statusMessage":
|
||||
// Ignore these messages
|
||||
break;
|
||||
default:
|
||||
log.error("Unrecognised message from background ChefWorker", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cancels the current bake by terminating the ChefWorker and creating a new one.
|
||||
*/
|
||||
cancelBake() {
|
||||
if (this.chefWorker)
|
||||
this.chefWorker.terminate();
|
||||
this.registerChefWorker();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asks the ChefWorker to bake the input using the specified recipe.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {Object[]} recipeConfig
|
||||
* @param {Object} options
|
||||
* @param {number} progress
|
||||
* @param {boolean} step
|
||||
* @param {Function} callback
|
||||
*/
|
||||
bake(input, recipeConfig, options, progress, step, callback) {
|
||||
const id = this.callbackID++;
|
||||
this.callbacks[id] = callback;
|
||||
|
||||
this.chefWorker.postMessage({
|
||||
action: "bake",
|
||||
data: {
|
||||
input: input,
|
||||
recipeConfig: recipeConfig,
|
||||
options: options,
|
||||
progress: progress,
|
||||
step: step,
|
||||
id: id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Asks the Magic operation what it can do with the input data.
|
||||
*
|
||||
* @param {string|ArrayBuffer} input
|
||||
*/
|
||||
magic(input) {
|
||||
// If we're still working on the previous bake, cancel it before starting a new one.
|
||||
if (this.completedCallback + 1 < this.callbackID) {
|
||||
clearTimeout(this.timeout);
|
||||
this.cancelBake();
|
||||
}
|
||||
|
||||
this.bake(input, [
|
||||
{
|
||||
"op": "Magic",
|
||||
"args": [3, false, false]
|
||||
}
|
||||
], {}, 0, false, this.magicComplete);
|
||||
|
||||
// Cancel this bake if it takes too long.
|
||||
this.timeout = setTimeout(this.cancelBake.bind(this), 3000);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for completed Magic bakes.
|
||||
*
|
||||
* @param {Object} response
|
||||
*/
|
||||
magicComplete(response) {
|
||||
log.debug("--- Background Magic Bake complete ---");
|
||||
if (!response || response.error) return;
|
||||
|
||||
this.manager.output.backgroundMagicResult(response.dish.value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default BackgroundWorkerWaiter;
|
260
src/web/waiters/BindingsWaiter.mjs
Executable file
260
src/web/waiters/BindingsWaiter.mjs
Executable file
|
@ -0,0 +1,260 @@
|
|||
/**
|
||||
* @author Matt C [matt@artemisbot.uk]
|
||||
* @copyright Crown Copyright 2016
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Waiter to handle keybindings to CyberChef functions (i.e. Bake, Step, Save, Load etc.)
|
||||
*/
|
||||
class BindingsWaiter {
|
||||
|
||||
/**
|
||||
* BindingsWaiter constructor.
|
||||
*
|
||||
* @param {App} app - The main view object for CyberChef.
|
||||
* @param {Manager} manager - The CyberChef event manager.
|
||||
*/
|
||||
constructor(app, manager) {
|
||||
this.app = app;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for all keydown events
|
||||
* Checks whether valid keyboard shortcut has been instated
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
* @param {event} e
|
||||
*/
|
||||
parseInput(e) {
|
||||
const modKey = this.app.options.useMetaKey ? e.metaKey : e.altKey;
|
||||
|
||||
if (e.ctrlKey && modKey) {
|
||||
let elem;
|
||||
switch (e.code) {
|
||||
case "KeyF": // Focus search
|
||||
e.preventDefault();
|
||||
document.getElementById("search").focus();
|
||||
break;
|
||||
case "KeyI": // Focus input
|
||||
e.preventDefault();
|
||||
document.getElementById("input-text").focus();
|
||||
break;
|
||||
case "KeyO": // Focus output
|
||||
e.preventDefault();
|
||||
document.getElementById("output-text").focus();
|
||||
break;
|
||||
case "Period": // Focus next operation
|
||||
e.preventDefault();
|
||||
try {
|
||||
elem = document.activeElement.closest(".operation") || document.querySelector("#rec-list .operation");
|
||||
if (elem.parentNode.lastChild === elem) {
|
||||
// If operation is last in recipe, loop around to the top operation's first argument
|
||||
elem.parentNode.firstChild.querySelectorAll(".arg")[0].focus();
|
||||
} else {
|
||||
// Focus first argument of next operation
|
||||
elem.nextSibling.querySelectorAll(".arg")[0].focus();
|
||||
}
|
||||
} catch (e) {
|
||||
// do nothing, just don't throw an error
|
||||
}
|
||||
break;
|
||||
case "KeyB": // Set breakpoint
|
||||
e.preventDefault();
|
||||
try {
|
||||
elem = document.activeElement.closest(".operation").querySelectorAll(".breakpoint")[0];
|
||||
if (elem.getAttribute("break") === "false") {
|
||||
elem.setAttribute("break", "true"); // add break point if not already enabled
|
||||
elem.classList.add("breakpoint-selected");
|
||||
} else {
|
||||
elem.setAttribute("break", "false"); // remove break point if already enabled
|
||||
elem.classList.remove("breakpoint-selected");
|
||||
}
|
||||
window.dispatchEvent(this.manager.statechange);
|
||||
} catch (e) {
|
||||
// do nothing, just don't throw an error
|
||||
}
|
||||
break;
|
||||
case "KeyD": // Disable operation
|
||||
e.preventDefault();
|
||||
try {
|
||||
elem = document.activeElement.closest(".operation").querySelectorAll(".disable-icon")[0];
|
||||
if (elem.getAttribute("disabled") === "false") {
|
||||
elem.setAttribute("disabled", "true"); // disable operation if enabled
|
||||
elem.classList.add("disable-elem-selected");
|
||||
elem.parentNode.parentNode.classList.add("disabled");
|
||||
} else {
|
||||
elem.setAttribute("disabled", "false"); // enable operation if disabled
|
||||
elem.classList.remove("disable-elem-selected");
|
||||
elem.parentNode.parentNode.classList.remove("disabled");
|
||||
}
|
||||
this.app.progress = 0;
|
||||
window.dispatchEvent(this.manager.statechange);
|
||||
} catch (e) {
|
||||
// do nothing, just don't throw an error
|
||||
}
|
||||
break;
|
||||
case "Space": // Bake
|
||||
e.preventDefault();
|
||||
this.manager.controls.bakeClick();
|
||||
break;
|
||||
case "Quote": // Step through
|
||||
e.preventDefault();
|
||||
this.manager.controls.stepClick();
|
||||
break;
|
||||
case "KeyC": // Clear recipe
|
||||
e.preventDefault();
|
||||
this.manager.recipe.clearRecipe();
|
||||
break;
|
||||
case "KeyS": // Save output to file
|
||||
e.preventDefault();
|
||||
this.manager.output.saveClick();
|
||||
break;
|
||||
case "KeyL": // Load recipe
|
||||
e.preventDefault();
|
||||
this.manager.controls.loadClick();
|
||||
break;
|
||||
case "KeyM": // Switch input and output
|
||||
e.preventDefault();
|
||||
this.manager.output.switchClick();
|
||||
break;
|
||||
case "KeyT": // New tab
|
||||
e.preventDefault();
|
||||
this.manager.input.addInputClick();
|
||||
break;
|
||||
case "KeyW": // Close tab
|
||||
e.preventDefault();
|
||||
this.manager.input.removeInput(this.manager.input.getActiveTab());
|
||||
break;
|
||||
case "ArrowLeft": // Go to previous tab
|
||||
e.preventDefault();
|
||||
this.manager.input.changeTabLeft();
|
||||
break;
|
||||
case "ArrowRight": // Go to next tab
|
||||
e.preventDefault();
|
||||
this.manager.input.changeTabRight();
|
||||
break;
|
||||
default:
|
||||
if (e.code.match(/Digit[0-9]/g)) { // Select nth operation
|
||||
e.preventDefault();
|
||||
try {
|
||||
// Select the first argument of the operation corresponding to the number pressed
|
||||
document.querySelector(`li:nth-child(${e.code.substr(-1)}) .arg`).focus();
|
||||
} catch (e) {
|
||||
// do nothing, just don't throw an error
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates keybinding list when metaKey option is toggled
|
||||
*/
|
||||
updateKeybList() {
|
||||
let modWinLin = "Alt";
|
||||
let modMac = "Opt";
|
||||
if (this.app.options.useMetaKey) {
|
||||
modWinLin = "Win";
|
||||
modMac = "Cmd";
|
||||
}
|
||||
document.getElementById("keybList").innerHTML = `
|
||||
<tr>
|
||||
<td><b>Command</b></td>
|
||||
<td><b>Shortcut (Win/Linux)</b></td>
|
||||
<td><b>Shortcut (Mac)</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Place cursor in search field</td>
|
||||
<td>Ctrl+${modWinLin}+f</td>
|
||||
<td>Ctrl+${modMac}+f</td>
|
||||
<tr>
|
||||
<td>Place cursor in input box</td>
|
||||
<td>Ctrl+${modWinLin}+i</td>
|
||||
<td>Ctrl+${modMac}+i</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Place cursor in output box</td>
|
||||
<td>Ctrl+${modWinLin}+o</td>
|
||||
<td>Ctrl+${modMac}+o</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Place cursor in first argument field of the next operation in the recipe</td>
|
||||
<td>Ctrl+${modWinLin}+.</td>
|
||||
<td>Ctrl+${modMac}+.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Place cursor in first argument field of the nth operation in the recipe</td>
|
||||
<td>Ctrl+${modWinLin}+[1-9]</td>
|
||||
<td>Ctrl+${modMac}+[1-9]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Disable current operation</td>
|
||||
<td>Ctrl+${modWinLin}+d</td>
|
||||
<td>Ctrl+${modMac}+d</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Set/clear breakpoint</td>
|
||||
<td>Ctrl+${modWinLin}+b</td>
|
||||
<td>Ctrl+${modMac}+b</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bake</td>
|
||||
<td>Ctrl+${modWinLin}+Space</td>
|
||||
<td>Ctrl+${modMac}+Space</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Step</td>
|
||||
<td>Ctrl+${modWinLin}+'</td>
|
||||
<td>Ctrl+${modMac}+'</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Clear recipe</td>
|
||||
<td>Ctrl+${modWinLin}+c</td>
|
||||
<td>Ctrl+${modMac}+c</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Save to file</td>
|
||||
<td>Ctrl+${modWinLin}+s</td>
|
||||
<td>Ctrl+${modMac}+s</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Load recipe</td>
|
||||
<td>Ctrl+${modWinLin}+l</td>
|
||||
<td>Ctrl+${modMac}+l</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Move output to input</td>
|
||||
<td>Ctrl+${modWinLin}+m</td>
|
||||
<td>Ctrl+${modMac}+m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create a new tab</td>
|
||||
<td>Ctrl+${modWinLin}+t</td>
|
||||
<td>Ctrl+${modMac}+t</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Close the current tab</td>
|
||||
<td>Ctrl+${modWinLin}+w</td>
|
||||
<td>Ctrl+${modMac}+w</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Go to next tab</td>
|
||||
<td>Ctrl+${modWinLin}+RightArrow</td>
|
||||
<td>Ctrl+${modMac}+RightArrow</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Go to previous tab</td>
|
||||
<td>Ctrl+${modWinLin}+LeftArrow</td>
|
||||
<td>Ctrl+${modMac}+LeftArrow</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BindingsWaiter;
|
426
src/web/waiters/ControlsWaiter.mjs
Executable file
426
src/web/waiters/ControlsWaiter.mjs
Executable file
|
@ -0,0 +1,426 @@
|
|||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2016
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Utils from "../../core/Utils";
|
||||
|
||||
|
||||
/**
|
||||
* Waiter to handle events related to the CyberChef controls (i.e. Bake, Step, Save, Load etc.)
|
||||
*/
|
||||
class ControlsWaiter {
|
||||
|
||||
/**
|
||||
* ControlsWaiter constructor.
|
||||
*
|
||||
* @param {App} app - The main view object for CyberChef.
|
||||
* @param {Manager} manager - The CyberChef event manager.
|
||||
*/
|
||||
constructor(app, manager) {
|
||||
this.app = app;
|
||||
this.manager = manager;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialise Bootstrap componenets
|
||||
*/
|
||||
initComponents() {
|
||||
$("body").bootstrapMaterialDesign();
|
||||
$("[data-toggle=tooltip]").tooltip({
|
||||
animation: false,
|
||||
container: "body",
|
||||
boundary: "viewport",
|
||||
trigger: "hover"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks or unchecks the Auto Bake checkbox based on the given value.
|
||||
*
|
||||
* @param {boolean} value - The new value for Auto Bake.
|
||||
*/
|
||||
setAutoBake(value) {
|
||||
const autoBakeCheckbox = document.getElementById("auto-bake");
|
||||
|
||||
if (autoBakeCheckbox.checked !== value) {
|
||||
autoBakeCheckbox.click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler to trigger baking.
|
||||
*/
|
||||
bakeClick() {
|
||||
const btnBake = document.getElementById("bake");
|
||||
if (btnBake.textContent.indexOf("Bake") > 0) {
|
||||
this.app.manager.input.bakeAll();
|
||||
} else if (btnBake.textContent.indexOf("Cancel") > 0) {
|
||||
this.manager.worker.cancelBake(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for the 'Step through' command. Executes the next step of the recipe.
|
||||
*/
|
||||
async stepClick() {
|
||||
if (this.app.baking) return;
|
||||
// Reset status using cancelBake
|
||||
this.manager.worker.cancelBake(true, false);
|
||||
const activeTab = this.manager.input.getActiveTab();
|
||||
let progress = 0;
|
||||
if (this.manager.output.outputs[activeTab].progress !== false) {
|
||||
progress = this.manager.output.outputs[activeTab].progress;
|
||||
}
|
||||
|
||||
this.manager.input.inputWorker.postMessage({
|
||||
action: "step",
|
||||
data: {
|
||||
activeTab: activeTab,
|
||||
progress: progress + 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for changes made to the Auto Bake checkbox.
|
||||
*/
|
||||
autoBakeChange() {
|
||||
this.app.autoBake_ = document.getElementById("auto-bake").checked;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for the 'Clear recipe' command. Removes all operations from the recipe.
|
||||
*/
|
||||
clearRecipeClick() {
|
||||
this.manager.recipe.clearRecipe();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Populates the save dialog box with a URL incorporating the recipe and input.
|
||||
*
|
||||
* @param {Object[]} [recipeConfig] - The recipe configuration object array.
|
||||
*/
|
||||
initialiseSaveLink(recipeConfig) {
|
||||
recipeConfig = recipeConfig || this.app.getRecipeConfig();
|
||||
|
||||
const includeRecipe = document.getElementById("save-link-recipe-checkbox").checked;
|
||||
const includeInput = document.getElementById("save-link-input-checkbox").checked;
|
||||
const saveLinkEl = document.getElementById("save-link");
|
||||
const saveLink = this.generateStateUrl(includeRecipe, includeInput, recipeConfig);
|
||||
|
||||
saveLinkEl.innerHTML = Utils.truncate(saveLink, 120);
|
||||
saveLinkEl.setAttribute("href", saveLink);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a URL containing the current recipe and input state.
|
||||
*
|
||||
* @param {boolean} includeRecipe - Whether to include the recipe in the URL.
|
||||
* @param {boolean} includeInput - Whether to include the input in the URL.
|
||||
* @param {string} input
|
||||
* @param {Object[]} [recipeConfig] - The recipe configuration object array.
|
||||
* @param {string} [baseURL] - The CyberChef URL, set to the current URL if not included
|
||||
* @returns {string}
|
||||
*/
|
||||
generateStateUrl(includeRecipe, includeInput, input, recipeConfig, baseURL) {
|
||||
recipeConfig = recipeConfig || this.app.getRecipeConfig();
|
||||
|
||||
const link = baseURL || window.location.protocol + "//" +
|
||||
window.location.host +
|
||||
window.location.pathname;
|
||||
const recipeStr = Utils.generatePrettyRecipe(recipeConfig);
|
||||
|
||||
includeRecipe = includeRecipe && (recipeConfig.length > 0);
|
||||
|
||||
// If we don't get passed an input, get it from the current URI
|
||||
if (input === null) {
|
||||
const params = this.app.getURIParams();
|
||||
if (params.input) {
|
||||
includeInput = true;
|
||||
input = params.input;
|
||||
}
|
||||
}
|
||||
|
||||
const params = [
|
||||
includeRecipe ? ["recipe", recipeStr] : undefined,
|
||||
includeInput ? ["input", input] : undefined,
|
||||
];
|
||||
|
||||
const hash = params
|
||||
.filter(v => v)
|
||||
.map(([key, value]) => `${key}=${Utils.encodeURIFragment(value)}`)
|
||||
.join("&");
|
||||
|
||||
if (hash) {
|
||||
return `${link}#${hash}`;
|
||||
}
|
||||
|
||||
return link;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for changes made to the save dialog text area. Re-initialises the save link.
|
||||
*/
|
||||
saveTextChange(e) {
|
||||
try {
|
||||
const recipeConfig = Utils.parseRecipeConfig(e.target.value);
|
||||
this.initialiseSaveLink(recipeConfig);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for the 'Save' command. Pops up the save dialog box.
|
||||
*/
|
||||
saveClick() {
|
||||
const recipeConfig = this.app.getRecipeConfig();
|
||||
const recipeStr = JSON.stringify(recipeConfig);
|
||||
|
||||
document.getElementById("save-text-chef").value = Utils.generatePrettyRecipe(recipeConfig, true);
|
||||
document.getElementById("save-text-clean").value = JSON.stringify(recipeConfig, null, 2)
|
||||
.replace(/{\n\s+"/g, "{ \"")
|
||||
.replace(/\[\n\s{3,}/g, "[")
|
||||
.replace(/\n\s{3,}]/g, "]")
|
||||
.replace(/\s*\n\s*}/g, " }")
|
||||
.replace(/\n\s{6,}/g, " ");
|
||||
document.getElementById("save-text-compact").value = recipeStr;
|
||||
|
||||
this.initialiseSaveLink(recipeConfig);
|
||||
$("#save-modal").modal();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for the save link recipe checkbox change event.
|
||||
*/
|
||||
slrCheckChange() {
|
||||
this.initialiseSaveLink();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for the save link input checkbox change event.
|
||||
*/
|
||||
sliCheckChange() {
|
||||
this.initialiseSaveLink();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for the 'Load' command. Pops up the load dialog box.
|
||||
*/
|
||||
loadClick() {
|
||||
this.populateLoadRecipesList();
|
||||
$("#load-modal").modal();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves the recipe specified in the save textarea to local storage.
|
||||
*/
|
||||
saveButtonClick() {
|
||||
if (!this.app.isLocalStorageAvailable()) {
|
||||
this.app.alert(
|
||||
"Your security settings do not allow access to local storage so your recipe cannot be saved.",
|
||||
5000
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const recipeName = Utils.escapeHtml(document.getElementById("save-name").value);
|
||||
const recipeStr = document.querySelector("#save-texts .tab-pane.active textarea").value;
|
||||
|
||||
if (!recipeName) {
|
||||
this.app.alert("Please enter a recipe name", 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
const savedRecipes = localStorage.savedRecipes ?
|
||||
JSON.parse(localStorage.savedRecipes) : [];
|
||||
let recipeId = localStorage.recipeId || 0;
|
||||
|
||||
savedRecipes.push({
|
||||
id: ++recipeId,
|
||||
name: recipeName,
|
||||
recipe: recipeStr
|
||||
});
|
||||
|
||||
localStorage.savedRecipes = JSON.stringify(savedRecipes);
|
||||
localStorage.recipeId = recipeId;
|
||||
|
||||
this.app.alert(`Recipe saved as "${recipeName}".`, 3000);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Populates the list of saved recipes in the load dialog box from local storage.
|
||||
*/
|
||||
populateLoadRecipesList() {
|
||||
if (!this.app.isLocalStorageAvailable()) return false;
|
||||
|
||||
const loadNameEl = document.getElementById("load-name");
|
||||
|
||||
// Remove current recipes from select
|
||||
let i = loadNameEl.options.length;
|
||||
while (i--) {
|
||||
loadNameEl.remove(i);
|
||||
}
|
||||
|
||||
// Add recipes to select
|
||||
const savedRecipes = localStorage.savedRecipes ?
|
||||
JSON.parse(localStorage.savedRecipes) : [];
|
||||
|
||||
for (i = 0; i < savedRecipes.length; i++) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = savedRecipes[i].id;
|
||||
// Unescape then re-escape in case localStorage has been corrupted
|
||||
opt.innerHTML = Utils.escapeHtml(Utils.unescapeHtml(savedRecipes[i].name));
|
||||
|
||||
loadNameEl.appendChild(opt);
|
||||
}
|
||||
|
||||
// Populate textarea with first recipe
|
||||
const loadText = document.getElementById("load-text");
|
||||
const evt = new Event("change");
|
||||
loadText.value = savedRecipes.length ? savedRecipes[0].recipe : "";
|
||||
loadText.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes the currently selected recipe from local storage.
|
||||
*/
|
||||
loadDeleteClick() {
|
||||
if (!this.app.isLocalStorageAvailable()) return false;
|
||||
|
||||
const id = parseInt(document.getElementById("load-name").value, 10);
|
||||
const rawSavedRecipes = localStorage.savedRecipes ?
|
||||
JSON.parse(localStorage.savedRecipes) : [];
|
||||
|
||||
const savedRecipes = rawSavedRecipes.filter(r => r.id !== id);
|
||||
|
||||
localStorage.savedRecipes = JSON.stringify(savedRecipes);
|
||||
this.populateLoadRecipesList();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays the selected recipe in the load text box.
|
||||
*/
|
||||
loadNameChange(e) {
|
||||
if (!this.app.isLocalStorageAvailable()) return false;
|
||||
|
||||
const el = e.target;
|
||||
const savedRecipes = localStorage.savedRecipes ?
|
||||
JSON.parse(localStorage.savedRecipes) : [];
|
||||
const id = parseInt(el.value, 10);
|
||||
|
||||
const recipe = savedRecipes.find(r => r.id === id);
|
||||
|
||||
document.getElementById("load-text").value = recipe.recipe;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads the selected recipe and populates the Recipe with its operations.
|
||||
*/
|
||||
loadButtonClick() {
|
||||
try {
|
||||
const recipeConfig = Utils.parseRecipeConfig(document.getElementById("load-text").value);
|
||||
this.app.setRecipeConfig(recipeConfig);
|
||||
this.app.autoBake();
|
||||
|
||||
$("#rec-list [data-toggle=popover]").popover();
|
||||
} catch (e) {
|
||||
this.app.alert("Invalid recipe", 2000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Populates the bug report information box with useful technical info.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
supportButtonClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const reportBugInfo = document.getElementById("report-bug-info");
|
||||
const saveLink = this.generateStateUrl(true, true, null, null, "https://gchq.github.io/CyberChef/");
|
||||
|
||||
if (reportBugInfo) {
|
||||
reportBugInfo.innerHTML = `* Version: ${PKG_VERSION}
|
||||
* Compile time: ${COMPILE_TIME}
|
||||
* User-Agent:
|
||||
${navigator.userAgent}
|
||||
* [Link to reproduce](${saveLink})
|
||||
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows the stale indicator to show that the input or recipe has changed
|
||||
* since the last bake.
|
||||
*/
|
||||
showStaleIndicator() {
|
||||
const staleIndicator = document.getElementById("stale-indicator");
|
||||
staleIndicator.classList.remove("hidden");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hides the stale indicator to show that the input or recipe has not changed
|
||||
* since the last bake.
|
||||
*/
|
||||
hideStaleIndicator() {
|
||||
const staleIndicator = document.getElementById("stale-indicator");
|
||||
staleIndicator.classList.add("hidden");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Switches the Bake button between 'Bake', 'Cancel' and 'Loading' functions.
|
||||
*
|
||||
* @param {boolean} cancel - Whether to change to cancel or not
|
||||
* @param {boolean} loading - Whether to change to loading or not
|
||||
*/
|
||||
toggleBakeButtonFunction(cancel, loading) {
|
||||
const bakeButton = document.getElementById("bake"),
|
||||
btnText = bakeButton.querySelector("span");
|
||||
|
||||
if (cancel) {
|
||||
btnText.innerText = "Cancel";
|
||||
bakeButton.classList.remove("btn-success");
|
||||
bakeButton.classList.remove("btn-warning");
|
||||
bakeButton.classList.add("btn-danger");
|
||||
} else if (loading) {
|
||||
bakeButton.style.background = "";
|
||||
btnText.innerText = "Loading...";
|
||||
bakeButton.classList.remove("btn-success");
|
||||
bakeButton.classList.remove("btn-danger");
|
||||
bakeButton.classList.add("btn-warning");
|
||||
} else {
|
||||
bakeButton.style.background = "";
|
||||
btnText.innerText = "Bake!";
|
||||
bakeButton.classList.remove("btn-danger");
|
||||
bakeButton.classList.remove("btn-warning");
|
||||
bakeButton.classList.add("btn-success");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ControlsWaiter;
|
470
src/web/waiters/HighlighterWaiter.mjs
Executable file
470
src/web/waiters/HighlighterWaiter.mjs
Executable file
|
@ -0,0 +1,470 @@
|
|||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2016
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* HighlighterWaiter data type enum for the input.
|
||||
* @enum
|
||||
*/
|
||||
const INPUT = 0;
|
||||
|
||||
/**
|
||||
* HighlighterWaiter data type enum for the output.
|
||||
* @enum
|
||||
*/
|
||||
const OUTPUT = 1;
|
||||
|
||||
|
||||
/**
|
||||
* Waiter to handle events related to highlighting in CyberChef.
|
||||
*/
|
||||
class HighlighterWaiter {
|
||||
|
||||
/**
|
||||
* HighlighterWaiter constructor.
|
||||
*
|
||||
* @param {App} app - The main view object for CyberChef.
|
||||
* @param {Manager} manager - The CyberChef event manager.
|
||||
*/
|
||||
constructor(app, manager) {
|
||||
this.app = app;
|
||||
this.manager = manager;
|
||||
|
||||
this.mouseButtonDown = false;
|
||||
this.mouseTarget = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determines if the current text selection is running backwards or forwards.
|
||||
* StackOverflow answer id: 12652116
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_isSelectionBackwards() {
|
||||
let backwards = false;
|
||||
const sel = window.getSelection();
|
||||
|
||||
if (!sel.isCollapsed) {
|
||||
const range = document.createRange();
|
||||
range.setStart(sel.anchorNode, sel.anchorOffset);
|
||||
range.setEnd(sel.focusNode, sel.focusOffset);
|
||||
backwards = range.collapsed;
|
||||
range.detach();
|
||||
}
|
||||
return backwards;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Calculates the text offset of a position in an HTML element, ignoring HTML tags.
|
||||
*
|
||||
* @private
|
||||
* @param {element} node - The parent HTML node.
|
||||
* @param {number} offset - The offset since the last HTML element.
|
||||
* @returns {number}
|
||||
*/
|
||||
_getOutputHtmlOffset(node, offset) {
|
||||
const sel = window.getSelection();
|
||||
const range = document.createRange();
|
||||
|
||||
range.selectNodeContents(document.getElementById("output-html"));
|
||||
range.setEnd(node, offset);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
|
||||
return sel.toString().length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the current selection offsets in the output HTML, ignoring HTML tags.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object} pos
|
||||
* @returns {number} pos.start
|
||||
* @returns {number} pos.end
|
||||
*/
|
||||
_getOutputHtmlSelectionOffsets() {
|
||||
const sel = window.getSelection();
|
||||
let range,
|
||||
start = 0,
|
||||
end = 0,
|
||||
backwards = false;
|
||||
|
||||
if (sel.rangeCount) {
|
||||
range = sel.getRangeAt(sel.rangeCount - 1);
|
||||
backwards = this._isSelectionBackwards();
|
||||
start = this._getOutputHtmlOffset(range.startContainer, range.startOffset);
|
||||
end = this._getOutputHtmlOffset(range.endContainer, range.endOffset);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
|
||||
if (backwards) {
|
||||
// If selecting backwards, reverse the start and end offsets for the selection to
|
||||
// prevent deselecting as the drag continues.
|
||||
sel.collapseToEnd();
|
||||
sel.extend(sel.anchorNode, range.startOffset);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
start: start,
|
||||
end: end
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for input scroll events.
|
||||
* Scrolls the highlighter pane to match the input textarea position.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
inputScroll(e) {
|
||||
const el = e.target;
|
||||
document.getElementById("input-highlighter").scrollTop = el.scrollTop;
|
||||
document.getElementById("input-highlighter").scrollLeft = el.scrollLeft;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for output scroll events.
|
||||
* Scrolls the highlighter pane to match the output textarea position.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
outputScroll(e) {
|
||||
const el = e.target;
|
||||
document.getElementById("output-highlighter").scrollTop = el.scrollTop;
|
||||
document.getElementById("output-highlighter").scrollLeft = el.scrollLeft;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for input mousedown events.
|
||||
* Calculates the current selection info, and highlights the corresponding data in the output.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
inputMousedown(e) {
|
||||
this.mouseButtonDown = true;
|
||||
this.mouseTarget = INPUT;
|
||||
this.removeHighlights();
|
||||
|
||||
const el = e.target;
|
||||
const start = el.selectionStart;
|
||||
const end = el.selectionEnd;
|
||||
|
||||
if (start !== 0 || end !== 0) {
|
||||
document.getElementById("input-selection-info").innerHTML = this.selectionInfo(start, end);
|
||||
this.highlightOutput([{start: start, end: end}]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for output mousedown events.
|
||||
* Calculates the current selection info, and highlights the corresponding data in the input.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
outputMousedown(e) {
|
||||
this.mouseButtonDown = true;
|
||||
this.mouseTarget = OUTPUT;
|
||||
this.removeHighlights();
|
||||
|
||||
const el = e.target;
|
||||
const start = el.selectionStart;
|
||||
const end = el.selectionEnd;
|
||||
|
||||
if (start !== 0 || end !== 0) {
|
||||
document.getElementById("output-selection-info").innerHTML = this.selectionInfo(start, end);
|
||||
this.highlightInput([{start: start, end: end}]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for output HTML mousedown events.
|
||||
* Calculates the current selection info.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
outputHtmlMousedown(e) {
|
||||
this.mouseButtonDown = true;
|
||||
this.mouseTarget = OUTPUT;
|
||||
|
||||
const sel = this._getOutputHtmlSelectionOffsets();
|
||||
if (sel.start !== 0 || sel.end !== 0) {
|
||||
document.getElementById("output-selection-info").innerHTML = this.selectionInfo(sel.start, sel.end);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for input mouseup events.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
inputMouseup(e) {
|
||||
this.mouseButtonDown = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for output mouseup events.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
outputMouseup(e) {
|
||||
this.mouseButtonDown = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for output HTML mouseup events.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
outputHtmlMouseup(e) {
|
||||
this.mouseButtonDown = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for input mousemove events.
|
||||
* Calculates the current selection info, and highlights the corresponding data in the output.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
inputMousemove(e) {
|
||||
// Check that the left mouse button is pressed
|
||||
if (!this.mouseButtonDown ||
|
||||
e.which !== 1 ||
|
||||
this.mouseTarget !== INPUT)
|
||||
return;
|
||||
|
||||
const el = e.target;
|
||||
const start = el.selectionStart;
|
||||
const end = el.selectionEnd;
|
||||
|
||||
if (start !== 0 || end !== 0) {
|
||||
document.getElementById("input-selection-info").innerHTML = this.selectionInfo(start, end);
|
||||
this.highlightOutput([{start: start, end: end}]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for output mousemove events.
|
||||
* Calculates the current selection info, and highlights the corresponding data in the input.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
outputMousemove(e) {
|
||||
// Check that the left mouse button is pressed
|
||||
if (!this.mouseButtonDown ||
|
||||
e.which !== 1 ||
|
||||
this.mouseTarget !== OUTPUT)
|
||||
return;
|
||||
|
||||
const el = e.target;
|
||||
const start = el.selectionStart;
|
||||
const end = el.selectionEnd;
|
||||
|
||||
if (start !== 0 || end !== 0) {
|
||||
document.getElementById("output-selection-info").innerHTML = this.selectionInfo(start, end);
|
||||
this.highlightInput([{start: start, end: end}]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for output HTML mousemove events.
|
||||
* Calculates the current selection info.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
outputHtmlMousemove(e) {
|
||||
// Check that the left mouse button is pressed
|
||||
if (!this.mouseButtonDown ||
|
||||
e.which !== 1 ||
|
||||
this.mouseTarget !== OUTPUT)
|
||||
return;
|
||||
|
||||
const sel = this._getOutputHtmlSelectionOffsets();
|
||||
if (sel.start !== 0 || sel.end !== 0) {
|
||||
document.getElementById("output-selection-info").innerHTML = this.selectionInfo(sel.start, sel.end);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given start and end offsets, writes the HTML for the selection info element with the correct
|
||||
* padding.
|
||||
*
|
||||
* @param {number} start - The start offset.
|
||||
* @param {number} end - The end offset.
|
||||
* @returns {string}
|
||||
*/
|
||||
selectionInfo(start, end) {
|
||||
const len = end.toString().length;
|
||||
const width = len < 2 ? 2 : len;
|
||||
const startStr = start.toString().padStart(width, " ").replace(/ /g, " ");
|
||||
const endStr = end.toString().padStart(width, " ").replace(/ /g, " ");
|
||||
const lenStr = (end-start).toString().padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
return "start: " + startStr + "<br>end: " + endStr + "<br>length: " + lenStr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes highlighting and selection information.
|
||||
*/
|
||||
removeHighlights() {
|
||||
document.getElementById("input-highlighter").innerHTML = "";
|
||||
document.getElementById("output-highlighter").innerHTML = "";
|
||||
document.getElementById("input-selection-info").innerHTML = "";
|
||||
document.getElementById("output-selection-info").innerHTML = "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Highlights the given offsets in the output.
|
||||
* We will only highlight if:
|
||||
* - input hasn't changed since last bake
|
||||
* - last bake was a full bake
|
||||
* - all operations in the recipe support highlighting
|
||||
*
|
||||
* @param {Object} pos - The position object for the highlight.
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
*/
|
||||
highlightOutput(pos) {
|
||||
if (!this.app.autoBake_ || this.app.baking) return false;
|
||||
this.manager.worker.highlight(this.app.getRecipeConfig(), "forward", pos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Highlights the given offsets in the input.
|
||||
* We will only highlight if:
|
||||
* - input hasn't changed since last bake
|
||||
* - last bake was a full bake
|
||||
* - all operations in the recipe support highlighting
|
||||
*
|
||||
* @param {Object} pos - The position object for the highlight.
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
*/
|
||||
highlightInput(pos) {
|
||||
if (!this.app.autoBake_ || this.app.baking) return false;
|
||||
this.manager.worker.highlight(this.app.getRecipeConfig(), "reverse", pos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays highlight offsets sent back from the Chef.
|
||||
*
|
||||
* @param {Object} pos - The position object for the highlight.
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
* @param {string} direction
|
||||
*/
|
||||
displayHighlights(pos, direction) {
|
||||
if (!pos) return;
|
||||
|
||||
if (this.manager.input.getActiveTab() !== this.manager.output.getActiveTab()) return;
|
||||
|
||||
const io = direction === "forward" ? "output" : "input";
|
||||
|
||||
document.getElementById(io + "-selection-info").innerHTML = this.selectionInfo(pos[0].start, pos[0].end);
|
||||
this.highlight(
|
||||
document.getElementById(io + "-text"),
|
||||
document.getElementById(io + "-highlighter"),
|
||||
pos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds the relevant HTML to the specified highlight element such that highlighting appears
|
||||
* underneath the correct offset.
|
||||
*
|
||||
* @param {element} textarea - The input or output textarea.
|
||||
* @param {element} highlighter - The input or output highlighter element.
|
||||
* @param {Object} pos - The position object for the highlight.
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
*/
|
||||
async highlight(textarea, highlighter, pos) {
|
||||
if (!this.app.options.showHighlighter) return false;
|
||||
if (!this.app.options.attemptHighlight) return false;
|
||||
|
||||
// Check if there is a carriage return in the output dish as this will not
|
||||
// be displayed by the HTML textarea and will mess up highlighting offsets.
|
||||
if (await this.manager.output.containsCR()) return false;
|
||||
|
||||
const startPlaceholder = "[startHighlight]";
|
||||
const startPlaceholderRegex = /\[startHighlight\]/g;
|
||||
const endPlaceholder = "[endHighlight]";
|
||||
const endPlaceholderRegex = /\[endHighlight\]/g;
|
||||
let text = textarea.value;
|
||||
|
||||
// Put placeholders in position
|
||||
// If there's only one value, select that
|
||||
// If there are multiple, ignore the first one and select all others
|
||||
if (pos.length === 1) {
|
||||
if (pos[0].end < pos[0].start) return;
|
||||
text = text.slice(0, pos[0].start) +
|
||||
startPlaceholder + text.slice(pos[0].start, pos[0].end) + endPlaceholder +
|
||||
text.slice(pos[0].end, text.length);
|
||||
} else {
|
||||
// O(n^2) - Can anyone improve this without overwriting placeholders?
|
||||
let result = "",
|
||||
endPlaced = true;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
for (let j = 1; j < pos.length; j++) {
|
||||
if (pos[j].end < pos[j].start) continue;
|
||||
if (pos[j].start === i) {
|
||||
result += startPlaceholder;
|
||||
endPlaced = false;
|
||||
}
|
||||
if (pos[j].end === i) {
|
||||
result += endPlaceholder;
|
||||
endPlaced = true;
|
||||
}
|
||||
}
|
||||
result += text[i];
|
||||
}
|
||||
if (!endPlaced) result += endPlaceholder;
|
||||
text = result;
|
||||
}
|
||||
|
||||
const cssClass = "hl1";
|
||||
//if (colour) cssClass += "-"+colour;
|
||||
|
||||
// Remove HTML tags
|
||||
text = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\n/g, " ")
|
||||
// Convert placeholders to tags
|
||||
.replace(startPlaceholderRegex, "<span class=\""+cssClass+"\">")
|
||||
.replace(endPlaceholderRegex, "</span>") + " ";
|
||||
|
||||
// Adjust width to allow for scrollbars
|
||||
highlighter.style.width = textarea.clientWidth + "px";
|
||||
highlighter.innerHTML = text;
|
||||
highlighter.scrollTop = textarea.scrollTop;
|
||||
highlighter.scrollLeft = textarea.scrollLeft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default HighlighterWaiter;
|
1542
src/web/waiters/InputWaiter.mjs
Normal file
1542
src/web/waiters/InputWaiter.mjs
Normal file
File diff suppressed because it is too large
Load diff
288
src/web/waiters/OperationsWaiter.mjs
Executable file
288
src/web/waiters/OperationsWaiter.mjs
Executable file
|
@ -0,0 +1,288 @@
|
|||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2016
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import HTMLOperation from "../HTMLOperation";
|
||||
import Sortable from "sortablejs";
|
||||
|
||||
|
||||
/**
|
||||
* Waiter to handle events related to the operations.
|
||||
*/
|
||||
class OperationsWaiter {
|
||||
|
||||
/**
|
||||
* OperationsWaiter constructor.
|
||||
*
|
||||
* @param {App} app - The main view object for CyberChef.
|
||||
* @param {Manager} manager - The CyberChef event manager.
|
||||
*/
|
||||
constructor(app, manager) {
|
||||
this.app = app;
|
||||
this.manager = manager;
|
||||
|
||||
this.options = {};
|
||||
this.removeIntent = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for search events.
|
||||
* Finds operations which match the given search term and displays them under the search box.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
searchOperations(e) {
|
||||
let ops, selected;
|
||||
|
||||
if (e.type === "search" || e.keyCode === 13) { // Search or Return
|
||||
e.preventDefault();
|
||||
ops = document.querySelectorAll("#search-results li");
|
||||
if (ops.length) {
|
||||
selected = this.getSelectedOp(ops);
|
||||
if (selected > -1) {
|
||||
this.manager.recipe.addOperation(ops[selected].innerHTML);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (e.keyCode === 40) { // Down
|
||||
e.preventDefault();
|
||||
ops = document.querySelectorAll("#search-results li");
|
||||
if (ops.length) {
|
||||
selected = this.getSelectedOp(ops);
|
||||
if (selected > -1) {
|
||||
ops[selected].classList.remove("selected-op");
|
||||
}
|
||||
if (selected === ops.length-1) selected = -1;
|
||||
ops[selected+1].classList.add("selected-op");
|
||||
}
|
||||
} else if (e.keyCode === 38) { // Up
|
||||
e.preventDefault();
|
||||
ops = document.querySelectorAll("#search-results li");
|
||||
if (ops.length) {
|
||||
selected = this.getSelectedOp(ops);
|
||||
if (selected > -1) {
|
||||
ops[selected].classList.remove("selected-op");
|
||||
}
|
||||
if (selected === 0) selected = ops.length;
|
||||
ops[selected-1].classList.add("selected-op");
|
||||
}
|
||||
} else {
|
||||
const searchResultsEl = document.getElementById("search-results");
|
||||
const el = e.target;
|
||||
const str = el.value;
|
||||
|
||||
while (searchResultsEl.firstChild) {
|
||||
try {
|
||||
$(searchResultsEl.firstChild).popover("dispose");
|
||||
} catch (err) {}
|
||||
searchResultsEl.removeChild(searchResultsEl.firstChild);
|
||||
}
|
||||
|
||||
$("#categories .show").collapse("hide");
|
||||
if (str) {
|
||||
const matchedOps = this.filterOperations(str, true);
|
||||
const matchedOpsHtml = matchedOps
|
||||
.map(v => v.toStubHtml())
|
||||
.join("");
|
||||
|
||||
searchResultsEl.innerHTML = matchedOpsHtml;
|
||||
searchResultsEl.dispatchEvent(this.manager.oplistcreate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filters operations based on the search string and returns the matching ones.
|
||||
*
|
||||
* @param {string} searchStr
|
||||
* @param {boolean} highlight - Whether or not to highlight the matching string in the operation
|
||||
* name and description
|
||||
* @returns {string[]}
|
||||
*/
|
||||
filterOperations(inStr, highlight) {
|
||||
const matchedOps = [];
|
||||
const matchedDescs = [];
|
||||
|
||||
const searchStr = inStr.toLowerCase();
|
||||
|
||||
for (const opName in this.app.operations) {
|
||||
const op = this.app.operations[opName];
|
||||
const namePos = opName.toLowerCase().indexOf(searchStr);
|
||||
const descPos = op.description.toLowerCase().indexOf(searchStr);
|
||||
|
||||
if (namePos >= 0 || descPos >= 0) {
|
||||
const operation = new HTMLOperation(opName, this.app.operations[opName], this.app, this.manager);
|
||||
if (highlight) {
|
||||
operation.highlightSearchString(searchStr, namePos, descPos);
|
||||
}
|
||||
|
||||
if (namePos < 0) {
|
||||
matchedOps.push(operation);
|
||||
} else {
|
||||
matchedDescs.push(operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchedDescs.concat(matchedOps);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds the operation which has been selected using keyboard shortcuts. This will have the class
|
||||
* 'selected-op' set. Returns the index of the operation within the given list.
|
||||
*
|
||||
* @param {element[]} ops
|
||||
* @returns {number}
|
||||
*/
|
||||
getSelectedOp(ops) {
|
||||
for (let i = 0; i < ops.length; i++) {
|
||||
if (ops[i].classList.contains("selected-op")) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for oplistcreate events.
|
||||
*
|
||||
* @listens Manager#oplistcreate
|
||||
* @param {event} e
|
||||
*/
|
||||
opListCreate(e) {
|
||||
this.manager.recipe.createSortableSeedList(e.target);
|
||||
this.enableOpsListPopovers(e.target);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets up popovers, allowing the popover itself to gain focus which enables scrolling
|
||||
* and other interactions.
|
||||
*
|
||||
* @param {Element} el - The element to start selecting from
|
||||
*/
|
||||
enableOpsListPopovers(el) {
|
||||
$(el).find("[data-toggle=popover]").addBack("[data-toggle=popover]")
|
||||
.popover({trigger: "manual"})
|
||||
.on("mouseenter", function(e) {
|
||||
if (e.buttons > 0) return; // Mouse button held down - likely dragging an opertion
|
||||
const _this = this;
|
||||
$(this).popover("show");
|
||||
$(".popover").on("mouseleave", function () {
|
||||
$(_this).popover("hide");
|
||||
});
|
||||
}).on("mouseleave", function () {
|
||||
const _this = this;
|
||||
setTimeout(function() {
|
||||
// Determine if the popover associated with this element is being hovered over
|
||||
if ($(_this).data("bs.popover") &&
|
||||
($(_this).data("bs.popover").tip && !$($(_this).data("bs.popover").tip).is(":hover"))) {
|
||||
$(_this).popover("hide");
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for operation doubleclick events.
|
||||
* Adds the operation to the recipe and auto bakes.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
operationDblclick(e) {
|
||||
const li = e.target;
|
||||
|
||||
this.manager.recipe.addOperation(li.textContent);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for edit favourites click events.
|
||||
* Sets up the 'Edit favourites' pane and displays it.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
editFavouritesClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Add favourites to modal
|
||||
const favCat = this.app.categories.filter(function(c) {
|
||||
return c.name === "Favourites";
|
||||
})[0];
|
||||
|
||||
let html = "";
|
||||
for (let i = 0; i < favCat.ops.length; i++) {
|
||||
const opName = favCat.ops[i];
|
||||
const operation = new HTMLOperation(opName, this.app.operations[opName], this.app, this.manager);
|
||||
html += operation.toStubHtml(true);
|
||||
}
|
||||
|
||||
const editFavouritesList = document.getElementById("edit-favourites-list");
|
||||
editFavouritesList.innerHTML = html;
|
||||
this.removeIntent = false;
|
||||
|
||||
const editableList = Sortable.create(editFavouritesList, {
|
||||
filter: ".remove-icon",
|
||||
onFilter: function (evt) {
|
||||
const el = editableList.closest(evt.item);
|
||||
if (el && el.parentNode) {
|
||||
$(el).popover("dispose");
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
},
|
||||
onEnd: function(evt) {
|
||||
if (this.removeIntent) {
|
||||
$(evt.item).popover("dispose");
|
||||
evt.item.remove();
|
||||
}
|
||||
}.bind(this),
|
||||
});
|
||||
|
||||
Sortable.utils.on(editFavouritesList, "dragleave", function() {
|
||||
this.removeIntent = true;
|
||||
}.bind(this));
|
||||
|
||||
Sortable.utils.on(editFavouritesList, "dragover", function() {
|
||||
this.removeIntent = false;
|
||||
}.bind(this));
|
||||
|
||||
$("#edit-favourites-list [data-toggle=popover]").popover();
|
||||
$("#favourites-modal").modal();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for save favourites click events.
|
||||
* Saves the selected favourites and reloads them.
|
||||
*/
|
||||
saveFavouritesClick() {
|
||||
const favs = document.querySelectorAll("#edit-favourites-list li");
|
||||
const favouritesList = Array.from(favs, e => e.childNodes[0].textContent);
|
||||
|
||||
this.app.saveFavourites(favouritesList);
|
||||
this.app.loadFavourites();
|
||||
this.app.populateOperationsList();
|
||||
this.manager.recipe.initialiseOperationDragNDrop();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for reset favourites click events.
|
||||
* Resets favourites to their defaults.
|
||||
*/
|
||||
resetFavouritesClick() {
|
||||
this.app.resetFavourites();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default OperationsWaiter;
|
174
src/web/waiters/OptionsWaiter.mjs
Executable file
174
src/web/waiters/OptionsWaiter.mjs
Executable file
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* Waiter to handle events related to the CyberChef options.
|
||||
*
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2016
|
||||
* @license Apache-2.0
|
||||
*
|
||||
* @constructor
|
||||
* @param {App} app - The main view object for CyberChef.
|
||||
*/
|
||||
const OptionsWaiter = function(app, manager) {
|
||||
this.app = app;
|
||||
this.manager = manager;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Loads options and sets values of switches and inputs to match them.
|
||||
*
|
||||
* @param {Object} options
|
||||
*/
|
||||
OptionsWaiter.prototype.load = function(options) {
|
||||
for (const option in options) {
|
||||
this.app.options[option] = options[option];
|
||||
}
|
||||
|
||||
// Set options to match object
|
||||
const cboxes = document.querySelectorAll("#options-body input[type=checkbox]");
|
||||
let i;
|
||||
for (i = 0; i < cboxes.length; i++) {
|
||||
cboxes[i].checked = this.app.options[cboxes[i].getAttribute("option")];
|
||||
}
|
||||
|
||||
const nboxes = document.querySelectorAll("#options-body input[type=number]");
|
||||
for (i = 0; i < nboxes.length; i++) {
|
||||
nboxes[i].value = this.app.options[nboxes[i].getAttribute("option")];
|
||||
nboxes[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
|
||||
}
|
||||
|
||||
const selects = document.querySelectorAll("#options-body select");
|
||||
for (i = 0; i < selects.length; i++) {
|
||||
const val = this.app.options[selects[i].getAttribute("option")];
|
||||
if (val) {
|
||||
selects[i].value = val;
|
||||
selects[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
|
||||
} else {
|
||||
selects[i].selectedIndex = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for options click events.
|
||||
* Dispays the options pane.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
OptionsWaiter.prototype.optionsClick = function(e) {
|
||||
e.preventDefault();
|
||||
$("#options-modal").modal();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for reset options click events.
|
||||
* Resets options back to their default values.
|
||||
*/
|
||||
OptionsWaiter.prototype.resetOptionsClick = function() {
|
||||
this.load(this.app.doptions);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for switch change events.
|
||||
* Modifies the option state and saves it to local storage.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
OptionsWaiter.prototype.switchChange = function(e) {
|
||||
const el = e.target;
|
||||
const option = el.getAttribute("option");
|
||||
const state = el.checked;
|
||||
|
||||
log.debug(`Setting ${option} to ${state}`);
|
||||
this.app.options[option] = state;
|
||||
|
||||
if (this.app.isLocalStorageAvailable())
|
||||
localStorage.setItem("options", JSON.stringify(this.app.options));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for number change events.
|
||||
* Modifies the option value and saves it to local storage.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
OptionsWaiter.prototype.numberChange = function(e) {
|
||||
const el = e.target;
|
||||
const option = el.getAttribute("option");
|
||||
const val = parseInt(el.value, 10);
|
||||
|
||||
log.debug(`Setting ${option} to ${val}`);
|
||||
this.app.options[option] = val;
|
||||
|
||||
if (this.app.isLocalStorageAvailable())
|
||||
localStorage.setItem("options", JSON.stringify(this.app.options));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for select change events.
|
||||
* Modifies the option value and saves it to local storage.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
OptionsWaiter.prototype.selectChange = function(e) {
|
||||
const el = e.target;
|
||||
const option = el.getAttribute("option");
|
||||
|
||||
log.debug(`Setting ${option} to ${el.value}`);
|
||||
this.app.options[option] = el.value;
|
||||
|
||||
if (this.app.isLocalStorageAvailable())
|
||||
localStorage.setItem("options", JSON.stringify(this.app.options));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets or unsets word wrap on the input and output depending on the wordWrap option value.
|
||||
*/
|
||||
OptionsWaiter.prototype.setWordWrap = function() {
|
||||
document.getElementById("input-text").classList.remove("word-wrap");
|
||||
document.getElementById("output-text").classList.remove("word-wrap");
|
||||
document.getElementById("output-html").classList.remove("word-wrap");
|
||||
document.getElementById("input-highlighter").classList.remove("word-wrap");
|
||||
document.getElementById("output-highlighter").classList.remove("word-wrap");
|
||||
|
||||
if (!this.app.options.wordWrap) {
|
||||
document.getElementById("input-text").classList.add("word-wrap");
|
||||
document.getElementById("output-text").classList.add("word-wrap");
|
||||
document.getElementById("output-html").classList.add("word-wrap");
|
||||
document.getElementById("input-highlighter").classList.add("word-wrap");
|
||||
document.getElementById("output-highlighter").classList.add("word-wrap");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Changes the theme by setting the class of the <html> element.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
OptionsWaiter.prototype.themeChange = function (e) {
|
||||
const themeClass = e.target.value;
|
||||
|
||||
document.querySelector(":root").className = themeClass;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Changes the console logging level.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
OptionsWaiter.prototype.logLevelChange = function (e) {
|
||||
const level = e.target.value;
|
||||
log.setLevel(level, false);
|
||||
this.manager.worker.setLogLevel();
|
||||
this.manager.input.setLogLevel();
|
||||
};
|
||||
|
||||
export default OptionsWaiter;
|
1521
src/web/waiters/OutputWaiter.mjs
Executable file
1521
src/web/waiters/OutputWaiter.mjs
Executable file
File diff suppressed because it is too large
Load diff
619
src/web/waiters/RecipeWaiter.mjs
Executable file
619
src/web/waiters/RecipeWaiter.mjs
Executable file
|
@ -0,0 +1,619 @@
|
|||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2016
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import HTMLOperation from "../HTMLOperation";
|
||||
import Sortable from "sortablejs";
|
||||
import Utils from "../../core/Utils";
|
||||
|
||||
|
||||
/**
|
||||
* Waiter to handle events related to the recipe.
|
||||
*/
|
||||
class RecipeWaiter {
|
||||
|
||||
/**
|
||||
* RecipeWaiter constructor.
|
||||
*
|
||||
* @param {App} app - The main view object for CyberChef.
|
||||
* @param {Manager} manager - The CyberChef event manager.
|
||||
*/
|
||||
constructor(app, manager) {
|
||||
this.app = app;
|
||||
this.manager = manager;
|
||||
this.removeIntent = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets up the drag and drop capability for operations in the operations and recipe areas.
|
||||
*/
|
||||
initialiseOperationDragNDrop() {
|
||||
const recList = document.getElementById("rec-list");
|
||||
|
||||
// Recipe list
|
||||
Sortable.create(recList, {
|
||||
group: "recipe",
|
||||
sort: true,
|
||||
animation: 0,
|
||||
delay: 0,
|
||||
filter: ".arg",
|
||||
preventOnFilter: false,
|
||||
setData: function(dataTransfer, dragEl) {
|
||||
dataTransfer.setData("Text", dragEl.querySelector(".op-title").textContent);
|
||||
},
|
||||
onEnd: function(evt) {
|
||||
if (this.removeIntent) {
|
||||
evt.item.remove();
|
||||
evt.target.dispatchEvent(this.manager.operationremove);
|
||||
}
|
||||
}.bind(this),
|
||||
onSort: function(evt) {
|
||||
if (evt.from.id === "rec-list") {
|
||||
document.dispatchEvent(this.manager.statechange);
|
||||
}
|
||||
}.bind(this)
|
||||
});
|
||||
|
||||
Sortable.utils.on(recList, "dragover", function() {
|
||||
this.removeIntent = false;
|
||||
}.bind(this));
|
||||
|
||||
Sortable.utils.on(recList, "dragleave", function() {
|
||||
this.removeIntent = true;
|
||||
this.app.progress = 0;
|
||||
}.bind(this));
|
||||
|
||||
Sortable.utils.on(recList, "touchend", function(e) {
|
||||
const loc = e.changedTouches[0];
|
||||
const target = document.elementFromPoint(loc.clientX, loc.clientY);
|
||||
|
||||
this.removeIntent = !recList.contains(target);
|
||||
}.bind(this));
|
||||
|
||||
// Favourites category
|
||||
document.querySelector("#categories a").addEventListener("dragover", this.favDragover.bind(this));
|
||||
document.querySelector("#categories a").addEventListener("dragleave", this.favDragleave.bind(this));
|
||||
document.querySelector("#categories a").addEventListener("drop", this.favDrop.bind(this));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a drag-n-droppable seed list of operations.
|
||||
*
|
||||
* @param {element} listEl - The list to initialise
|
||||
*/
|
||||
createSortableSeedList(listEl) {
|
||||
Sortable.create(listEl, {
|
||||
group: {
|
||||
name: "recipe",
|
||||
pull: "clone",
|
||||
put: false,
|
||||
},
|
||||
sort: false,
|
||||
setData: function(dataTransfer, dragEl) {
|
||||
dataTransfer.setData("Text", dragEl.textContent);
|
||||
},
|
||||
onStart: function(evt) {
|
||||
// Removes popover element and event bindings from the dragged operation but not the
|
||||
// event bindings from the one left in the operations list. Without manually removing
|
||||
// these bindings, we cannot re-initialise the popover on the stub operation.
|
||||
$(evt.item)
|
||||
.popover("dispose")
|
||||
.removeData("bs.popover")
|
||||
.off("mouseenter")
|
||||
.off("mouseleave")
|
||||
.attr("data-toggle", "popover-disabled");
|
||||
$(evt.clone)
|
||||
.off(".popover")
|
||||
.removeData("bs.popover");
|
||||
},
|
||||
onEnd: this.opSortEnd.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for operation sort end events.
|
||||
* Removes the operation from the list if it has been dropped outside. If not, adds it to the list
|
||||
* at the appropriate place and initialises it.
|
||||
*
|
||||
* @fires Manager#operationadd
|
||||
* @param {event} evt
|
||||
*/
|
||||
opSortEnd(evt) {
|
||||
if (this.removeIntent) {
|
||||
if (evt.item.parentNode.id === "rec-list") {
|
||||
evt.item.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Reinitialise the popover on the original element in the ops list because for some reason it
|
||||
// gets destroyed and recreated.
|
||||
this.manager.ops.enableOpsListPopovers(evt.clone);
|
||||
|
||||
if (evt.item.parentNode.id !== "rec-list") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildRecipeOperation(evt.item);
|
||||
evt.item.dispatchEvent(this.manager.operationadd);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for favourite dragover events.
|
||||
* If the element being dragged is an operation, displays a visual cue so that the user knows it can
|
||||
* be dropped here.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
favDragover(e) {
|
||||
if (e.dataTransfer.effectAllowed !== "move")
|
||||
return false;
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (e.target.className && e.target.className.indexOf("category-title") > -1) {
|
||||
// Hovering over the a
|
||||
e.target.classList.add("favourites-hover");
|
||||
} else if (e.target.parentNode.className && e.target.parentNode.className.indexOf("category-title") > -1) {
|
||||
// Hovering over the Edit button
|
||||
e.target.parentNode.classList.add("favourites-hover");
|
||||
} else if (e.target.parentNode.parentNode.className && e.target.parentNode.parentNode.className.indexOf("category-title") > -1) {
|
||||
// Hovering over the image on the Edit button
|
||||
e.target.parentNode.parentNode.classList.add("favourites-hover");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for favourite dragleave events.
|
||||
* Removes the visual cue.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
favDragleave(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
document.querySelector("#categories a").classList.remove("favourites-hover");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for favourite drop events.
|
||||
* Adds the dragged operation to the favourites list.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
favDrop(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.target.classList.remove("favourites-hover");
|
||||
|
||||
const opName = e.dataTransfer.getData("Text");
|
||||
this.app.addFavourite(opName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for ingredient change events.
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
*/
|
||||
ingChange(e) {
|
||||
if (e && e.target && e.target.classList.contains("no-state-change")) return;
|
||||
window.dispatchEvent(this.manager.statechange);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for disable click events.
|
||||
* Updates the icon status.
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
* @param {event} e
|
||||
*/
|
||||
disableClick(e) {
|
||||
const icon = e.target;
|
||||
|
||||
if (icon.getAttribute("disabled") === "false") {
|
||||
icon.setAttribute("disabled", "true");
|
||||
icon.classList.add("disable-icon-selected");
|
||||
icon.parentNode.parentNode.classList.add("disabled");
|
||||
} else {
|
||||
icon.setAttribute("disabled", "false");
|
||||
icon.classList.remove("disable-icon-selected");
|
||||
icon.parentNode.parentNode.classList.remove("disabled");
|
||||
}
|
||||
|
||||
this.app.progress = 0;
|
||||
window.dispatchEvent(this.manager.statechange);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for breakpoint click events.
|
||||
* Updates the icon status.
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
* @param {event} e
|
||||
*/
|
||||
breakpointClick(e) {
|
||||
const bp = e.target;
|
||||
|
||||
if (bp.getAttribute("break") === "false") {
|
||||
bp.setAttribute("break", "true");
|
||||
bp.classList.add("breakpoint-selected");
|
||||
} else {
|
||||
bp.setAttribute("break", "false");
|
||||
bp.classList.remove("breakpoint-selected");
|
||||
}
|
||||
|
||||
window.dispatchEvent(this.manager.statechange);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for operation doubleclick events.
|
||||
* Removes the operation from the recipe and auto bakes.
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
* @param {event} e
|
||||
*/
|
||||
operationDblclick(e) {
|
||||
e.target.remove();
|
||||
this.opRemove(e);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for operation child doubleclick events.
|
||||
* Removes the operation from the recipe.
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
* @param {event} e
|
||||
*/
|
||||
operationChildDblclick(e) {
|
||||
e.target.parentNode.remove();
|
||||
this.opRemove(e);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a configuration object to represent the current recipe.
|
||||
*
|
||||
* @returns {recipeConfig}
|
||||
*/
|
||||
getConfig() {
|
||||
const config = [];
|
||||
let ingredients, ingList, disabled, bp, item;
|
||||
const operations = document.querySelectorAll("#rec-list li.operation");
|
||||
|
||||
for (let i = 0; i < operations.length; i++) {
|
||||
ingredients = [];
|
||||
disabled = operations[i].querySelector(".disable-icon");
|
||||
bp = operations[i].querySelector(".breakpoint");
|
||||
ingList = operations[i].querySelectorAll(".arg");
|
||||
|
||||
for (let j = 0; j < ingList.length; j++) {
|
||||
if (ingList[j].getAttribute("type") === "checkbox") {
|
||||
// checkbox
|
||||
ingredients[j] = ingList[j].checked;
|
||||
} else if (ingList[j].classList.contains("toggle-string")) {
|
||||
// toggleString
|
||||
ingredients[j] = {
|
||||
option: ingList[j].parentNode.parentNode.querySelector("button").textContent,
|
||||
string: ingList[j].value
|
||||
};
|
||||
} else if (ingList[j].getAttribute("type") === "number") {
|
||||
// number
|
||||
ingredients[j] = parseFloat(ingList[j].value, 10);
|
||||
} else {
|
||||
// all others
|
||||
ingredients[j] = ingList[j].value;
|
||||
}
|
||||
}
|
||||
|
||||
item = {
|
||||
op: operations[i].querySelector(".op-title").textContent,
|
||||
args: ingredients
|
||||
};
|
||||
|
||||
if (disabled && disabled.getAttribute("disabled") === "true") {
|
||||
item.disabled = true;
|
||||
}
|
||||
|
||||
if (bp && bp.getAttribute("break") === "true") {
|
||||
item.breakpoint = true;
|
||||
}
|
||||
|
||||
config.push(item);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Moves or removes the breakpoint indicator in the recipe based on the position.
|
||||
*
|
||||
* @param {number|boolean} position - If boolean, turn off all indicators
|
||||
*/
|
||||
updateBreakpointIndicator(position) {
|
||||
const operations = document.querySelectorAll("#rec-list li.operation");
|
||||
if (typeof position === "boolean") position = operations.length;
|
||||
for (let i = 0; i < operations.length; i++) {
|
||||
if (i === position) {
|
||||
operations[i].classList.add("break");
|
||||
} else {
|
||||
operations[i].classList.remove("break");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given an operation stub element, this function converts it into a full recipe element with
|
||||
* arguments.
|
||||
*
|
||||
* @param {element} el - The operation stub element from the operations pane
|
||||
*/
|
||||
buildRecipeOperation(el) {
|
||||
const opName = el.textContent;
|
||||
const op = new HTMLOperation(opName, this.app.operations[opName], this.app, this.manager);
|
||||
el.innerHTML = op.toFullHtml();
|
||||
|
||||
if (this.app.operations[opName].flowControl) {
|
||||
el.classList.add("flow-control-op");
|
||||
}
|
||||
|
||||
// Disable auto-bake if this is a manual op
|
||||
if (op.manualBake && this.app.autoBake_) {
|
||||
this.manager.controls.setAutoBake(false);
|
||||
this.app.alert("Auto-Bake is disabled by default when using this operation.", 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds the specified operation to the recipe.
|
||||
*
|
||||
* @fires Manager#operationadd
|
||||
* @param {string} name - The name of the operation to add
|
||||
* @returns {element}
|
||||
*/
|
||||
addOperation(name) {
|
||||
const item = document.createElement("li");
|
||||
|
||||
item.classList.add("operation");
|
||||
item.innerHTML = name;
|
||||
this.buildRecipeOperation(item);
|
||||
document.getElementById("rec-list").appendChild(item);
|
||||
|
||||
item.dispatchEvent(this.manager.operationadd);
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes all operations from the recipe.
|
||||
*
|
||||
* @fires Manager#operationremove
|
||||
*/
|
||||
clearRecipe() {
|
||||
const recList = document.getElementById("rec-list");
|
||||
while (recList.firstChild) {
|
||||
recList.removeChild(recList.firstChild);
|
||||
}
|
||||
recList.dispatchEvent(this.manager.operationremove);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for operation dropdown events from toggleString arguments.
|
||||
* Sets the selected option as the name of the button.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
dropdownToggleClick(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const el = e.target;
|
||||
const button = el.parentNode.parentNode.querySelector("button");
|
||||
|
||||
button.innerHTML = el.textContent;
|
||||
this.ingChange();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Triggers various change events for operation arguments that have just been initialised.
|
||||
*
|
||||
* @param {HTMLElement} op
|
||||
*/
|
||||
triggerArgEvents(op) {
|
||||
// Trigger populateOption and argSelector events
|
||||
const triggerableOptions = op.querySelectorAll(".populate-option, .arg-selector");
|
||||
const evt = new Event("change", {bubbles: true});
|
||||
if (triggerableOptions.length) {
|
||||
for (const el of triggerableOptions) {
|
||||
el.dispatchEvent(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for operationadd events.
|
||||
*
|
||||
* @listens Manager#operationadd
|
||||
* @fires Manager#statechange
|
||||
* @param {event} e
|
||||
*/
|
||||
opAdd(e) {
|
||||
log.debug(`'${e.target.querySelector(".op-title").textContent}' added to recipe`);
|
||||
|
||||
this.triggerArgEvents(e.target);
|
||||
window.dispatchEvent(this.manager.statechange);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for operationremove events.
|
||||
*
|
||||
* @listens Manager#operationremove
|
||||
* @fires Manager#statechange
|
||||
* @param {event} e
|
||||
*/
|
||||
opRemove(e) {
|
||||
log.debug("Operation removed from recipe");
|
||||
window.dispatchEvent(this.manager.statechange);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for text argument dragover events.
|
||||
* Gives the user a visual cue to show that items can be dropped here.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
textArgDragover (e) {
|
||||
// This will be set if we're dragging an operation
|
||||
if (e.dataTransfer.effectAllowed === "move")
|
||||
return false;
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.target.closest("textarea.arg").classList.add("dropping-file");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for text argument dragleave events.
|
||||
* Removes the visual cue.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
textArgDragLeave (e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.target.classList.remove("dropping-file");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for text argument drop events.
|
||||
* Loads the dragged data into the argument textarea.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
textArgDrop(e) {
|
||||
// This will be set if we're dragging an operation
|
||||
if (e.dataTransfer.effectAllowed === "move")
|
||||
return false;
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
const targ = e.target;
|
||||
const file = e.dataTransfer.files[0];
|
||||
const text = e.dataTransfer.getData("Text");
|
||||
|
||||
targ.classList.remove("dropping-file");
|
||||
|
||||
if (text) {
|
||||
targ.value = text;
|
||||
return;
|
||||
}
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
const self = this;
|
||||
reader.onload = function (e) {
|
||||
targ.value = e.target.result;
|
||||
// Trigger floating label move
|
||||
const changeEvent = new Event("change");
|
||||
targ.dispatchEvent(changeEvent);
|
||||
window.dispatchEvent(self.manager.statechange);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets register values.
|
||||
*
|
||||
* @param {number} opIndex
|
||||
* @param {number} numPrevRegisters
|
||||
* @param {string[]} registers
|
||||
*/
|
||||
setRegisters(opIndex, numPrevRegisters, registers) {
|
||||
const op = document.querySelector(`#rec-list .operation:nth-child(${opIndex + 1})`),
|
||||
prevRegList = op.querySelector(".register-list");
|
||||
|
||||
// Remove previous div
|
||||
if (prevRegList) prevRegList.remove();
|
||||
|
||||
const registerList = [];
|
||||
for (let i = 0; i < registers.length; i++) {
|
||||
registerList.push(`$R${numPrevRegisters + i} = ${Utils.escapeHtml(Utils.truncate(Utils.printable(registers[i]), 100))}`);
|
||||
}
|
||||
const registerListEl = `<div class="register-list">
|
||||
${registerList.join("<br>")}
|
||||
</div>`;
|
||||
|
||||
op.insertAdjacentHTML("beforeend", registerListEl);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adjusts the number of ingredient columns as the width of the recipe changes.
|
||||
*/
|
||||
adjustWidth() {
|
||||
const recList = document.getElementById("rec-list");
|
||||
|
||||
if (!this.ingredientRuleID) {
|
||||
this.ingredientRuleID = null;
|
||||
this.ingredientChildRuleID = null;
|
||||
|
||||
// Find relevant rules in the stylesheet
|
||||
// try/catch for chrome 64+ CORS error on cssRules.
|
||||
try {
|
||||
for (const i in document.styleSheets[0].cssRules) {
|
||||
if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients") {
|
||||
this.ingredientRuleID = i;
|
||||
}
|
||||
if (document.styleSheets[0].cssRules[i].selectorText === ".ingredients > div") {
|
||||
this.ingredientChildRuleID = i;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.ingredientRuleID || !this.ingredientChildRuleID) return;
|
||||
|
||||
const ingredientRule = document.styleSheets[0].cssRules[this.ingredientRuleID];
|
||||
const ingredientChildRule = document.styleSheets[0].cssRules[this.ingredientChildRuleID];
|
||||
|
||||
if (recList.clientWidth < 450) {
|
||||
ingredientRule.style.gridTemplateColumns = "auto auto";
|
||||
ingredientChildRule.style.gridColumn = "1 / span 2";
|
||||
} else if (recList.clientWidth < 620) {
|
||||
ingredientRule.style.gridTemplateColumns = "auto auto auto";
|
||||
ingredientChildRule.style.gridColumn = "1 / span 3";
|
||||
} else {
|
||||
ingredientRule.style.gridTemplateColumns = "auto auto auto auto";
|
||||
ingredientChildRule.style.gridColumn = "1 / span 4";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default RecipeWaiter;
|
349
src/web/waiters/SeasonalWaiter.mjs
Executable file
349
src/web/waiters/SeasonalWaiter.mjs
Executable file
|
@ -0,0 +1,349 @@
|
|||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2016
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import clippy from "clippyjs";
|
||||
import "../static/clippy_assets/agents/Clippy/agent.js";
|
||||
import clippyMap from "../static/clippy_assets/agents/Clippy/map.png";
|
||||
|
||||
/**
|
||||
* Waiter to handle seasonal events and easter eggs.
|
||||
*/
|
||||
class SeasonalWaiter {
|
||||
|
||||
/**
|
||||
* SeasonalWaiter contructor.
|
||||
*
|
||||
* @param {App} app - The main view object for CyberChef.
|
||||
* @param {Manager} manager - The CyberChef event manager.
|
||||
*/
|
||||
constructor(app, manager) {
|
||||
this.app = app;
|
||||
this.manager = manager;
|
||||
|
||||
this.clippyAgent = null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads all relevant items depending on the current date.
|
||||
*/
|
||||
load() {
|
||||
// Konami code
|
||||
this.kkeys = [];
|
||||
window.addEventListener("keydown", this.konamiCodeListener.bind(this));
|
||||
|
||||
// Clippy
|
||||
const now = new Date();
|
||||
if (now.getMonth() === 3 && now.getDate() === 1) {
|
||||
this.addClippyOption();
|
||||
this.manager.addDynamicListener(".option-item #clippy", "change", this.setupClippy, this);
|
||||
this.setupClippy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Listen for the Konami code sequence of keys. Turn the page upside down if they are all heard in
|
||||
* sequence.
|
||||
* #konamicode
|
||||
*/
|
||||
konamiCodeListener(e) {
|
||||
this.kkeys.push(e.keyCode);
|
||||
const konami = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
|
||||
for (let i = 0; i < this.kkeys.length; i++) {
|
||||
if (this.kkeys[i] !== konami[i]) {
|
||||
this.kkeys = [];
|
||||
break;
|
||||
}
|
||||
if (i === konami.length - 1) {
|
||||
$("body").children().toggleClass("konami");
|
||||
this.kkeys = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an option in the Options menu for turning Clippy on or off
|
||||
*/
|
||||
addClippyOption() {
|
||||
const optionsBody = document.getElementById("options-body"),
|
||||
optionItem = document.createElement("span");
|
||||
|
||||
optionItem.className = "bmd-form-group is-filled";
|
||||
optionItem.innerHTML = `<div class="checkbox option-item">
|
||||
<label for="clippy">
|
||||
<input type="checkbox" option="clippy" id="clippy" checked="">
|
||||
Use the Clippy helper
|
||||
</label>
|
||||
</div>`;
|
||||
optionsBody.appendChild(optionItem);
|
||||
|
||||
if (!this.app.options.hasOwnProperty("clippy")) {
|
||||
this.app.options.clippy = true;
|
||||
}
|
||||
|
||||
this.manager.options.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up Clippy for April Fools Day
|
||||
*/
|
||||
setupClippy() {
|
||||
// Destroy any previous agents
|
||||
if (this.clippyAgent) {
|
||||
this.clippyAgent.closeBalloonImmediately();
|
||||
this.clippyAgent.hide();
|
||||
}
|
||||
|
||||
if (!this.app.options.clippy) {
|
||||
if (this.clippyTimeouts) this.clippyTimeouts.forEach(t => clearTimeout(t));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set base path to # to prevent external network requests
|
||||
const clippyAssets = "#";
|
||||
// Shim the library to prevent external network requests
|
||||
shimClippy(clippy);
|
||||
|
||||
const self = this;
|
||||
clippy.load("Clippy", (agent) => {
|
||||
shimClippyAgent(agent);
|
||||
self.clippyAgent = agent;
|
||||
agent.show();
|
||||
agent.speak("Hello, I'm Clippy, your personal cyber assistant!");
|
||||
}, undefined, clippyAssets);
|
||||
|
||||
// Watch for the Auto Magic button appearing
|
||||
const magic = document.getElementById("magic");
|
||||
const observer = new MutationObserver((mutationsList, observer) => {
|
||||
// Read in message and recipe
|
||||
let msg, recipe;
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.attributeName === "data-original-title") {
|
||||
msg = magic.getAttribute("data-original-title");
|
||||
}
|
||||
if (mutation.attributeName === "data-recipe") {
|
||||
recipe = magic.getAttribute("data-recipe");
|
||||
}
|
||||
}
|
||||
|
||||
// Close balloon if it is currently showing a magic hint
|
||||
const balloon = self.clippyAgent._balloon._balloon;
|
||||
if (balloon.is(":visible") && balloon.text().indexOf("That looks like encoded data") >= 0) {
|
||||
self.clippyAgent._balloon.hide(true);
|
||||
this.clippyAgent._balloon._hidden = true;
|
||||
}
|
||||
|
||||
// If a recipe was found, get Clippy to tell the user
|
||||
if (recipe) {
|
||||
recipe = this.manager.controls.generateStateUrl(true, true, JSON.parse(recipe));
|
||||
msg = `That looks like encoded data!<br><br>${msg}<br><br>Click <a class="clippyMagicRecipe" href="${recipe}">here</a> to load this recipe.`;
|
||||
|
||||
// Stop current balloon activity immediately and trigger speak again
|
||||
this.clippyAgent.closeBalloonImmediately();
|
||||
self.clippyAgent.speak(msg, true);
|
||||
// self.clippyAgent._queue.next();
|
||||
}
|
||||
});
|
||||
observer.observe(document.getElementById("magic"), {attributes: true});
|
||||
|
||||
// Play animations for various things
|
||||
this.manager.addListeners("#search", "click", () => {
|
||||
this.clippyAgent.play("Searching");
|
||||
}, this);
|
||||
this.manager.addListeners("#save,#save-to-file", "click", () => {
|
||||
this.clippyAgent.play("Save");
|
||||
}, this);
|
||||
this.manager.addListeners("#clr-recipe,#clr-io", "click", () => {
|
||||
this.clippyAgent.play("EmptyTrash");
|
||||
}, this);
|
||||
this.manager.addListeners("#bake", "click", e => {
|
||||
if (e.target.closest("button").textContent.toLowerCase().indexOf("bake") >= 0) {
|
||||
this.clippyAgent.play("Thinking");
|
||||
} else {
|
||||
this.clippyAgent.play("EmptyTrash");
|
||||
}
|
||||
this.clippyAgent._queue.clear();
|
||||
}, this);
|
||||
this.manager.addListeners("#input-text", "keydown", () => {
|
||||
this.clippyAgent.play("Writing");
|
||||
this.clippyAgent._queue.clear();
|
||||
}, this);
|
||||
this.manager.addDynamicListener("a.clippyMagicRecipe", "click", (e) => {
|
||||
this.clippyAgent.play("Congratulate");
|
||||
}, this);
|
||||
|
||||
this.clippyTimeouts = [];
|
||||
// Show challenge after timeout
|
||||
this.clippyTimeouts.push(setTimeout(() => {
|
||||
const hex = "1f 8b 08 00 ae a1 9b 5c 00 ff 05 40 a1 12 00 10 0c fd 26 61 5b 76 aa 9d 26 a8 02 02 37 84 f7 fb bb c5 a4 5f 22 c6 09 e5 6e c5 4c 2d 3f e9 30 a6 ea 41 a2 f2 ac 1c 00 00 00";
|
||||
self.clippyAgent.speak(`How about a fun challenge?<br><br>Try decoding this (click to load):<br><a href="#recipe=[]&input=${encodeURIComponent(btoa(hex))}">${hex}</a>`, true);
|
||||
self.clippyAgent.play("GetAttention");
|
||||
}, 1 * 60 * 1000));
|
||||
|
||||
this.clippyTimeouts.push(setTimeout(() => {
|
||||
self.clippyAgent.speak("<i>Did you know?</i><br><br>You can load files into CyberChef up to around 500MB using drag and drop or the load file button.", 15000);
|
||||
self.clippyAgent.play("Wave");
|
||||
}, 2 * 60 * 1000));
|
||||
|
||||
this.clippyTimeouts.push(setTimeout(() => {
|
||||
self.clippyAgent.speak("<i>Did you know?</i><br><br>You can use the 'Fork' operation to split up your input and run the recipe over each branch separately.<br><br><a class='clippyMagicRecipe' href=\"#recipe=Fork('%5C%5Cn','%5C%5Cn',false)From_UNIX_Timestamp('Seconds%20(s)')&input=OTc4MzQ2ODAwCjEwMTI2NTEyMDAKMTA0NjY5NjQwMAoxMDgxMDg3MjAwCjExMTUzMDUyMDAKMTE0OTYwOTYwMA\">Here's an example</a>.", 15000);
|
||||
self.clippyAgent.play("Print");
|
||||
}, 3 * 60 * 1000));
|
||||
|
||||
this.clippyTimeouts.push(setTimeout(() => {
|
||||
self.clippyAgent.speak("<i>Did you know?</i><br><br>The 'Magic' operation uses a number of methods to detect encoded data and the operations which can be used to make sense of it. A technical description of these methods can be found <a href=\"https://github.com/gchq/CyberChef/wiki/Automatic-detection-of-encoded-data-using-CyberChef-Magic\">here</a>.", 15000);
|
||||
self.clippyAgent.play("Alert");
|
||||
}, 4 * 60 * 1000));
|
||||
|
||||
this.clippyTimeouts.push(setTimeout(() => {
|
||||
self.clippyAgent.speak("<i>Did you know?</i><br><br>You can use parts of the input as arguments to operations.<br><br><a class='clippyMagicRecipe' href=\"#recipe=Register('key%3D(%5B%5C%5Cda-f%5D*)',true,false)Find_/_Replace(%7B'option':'Regex','string':'.*data%3D(.*)'%7D,'$1',true,false,true)RC4(%7B'option':'Hex','string':'$R0'%7D,'Hex','Latin1')&input=aHR0cDovL21hbHdhcmV6LmJpei9iZWFjb24ucGhwP2tleT0wZTkzMmE1YyZkYXRhPThkYjdkNWViZTM4NjYzYTU0ZWNiYjMzNGUzZGIxMQ\">Click here for an example</a>.", 15000);
|
||||
self.clippyAgent.play("CheckingSomething");
|
||||
}, 5 * 60 * 1000));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shims various ClippyJS functions to modify behaviour.
|
||||
*
|
||||
* @param {Clippy} clippy - The Clippy library
|
||||
*/
|
||||
function shimClippy(clippy) {
|
||||
// Shim _loadSounds so that it doesn't actually try to load any sounds
|
||||
clippy.load._loadSounds = function _loadSounds (name, path) {
|
||||
let dfd = clippy.load._sounds[name];
|
||||
if (dfd) return dfd;
|
||||
|
||||
// set dfd if not defined
|
||||
dfd = clippy.load._sounds[name] = $.Deferred();
|
||||
|
||||
// Resolve immediately without loading
|
||||
dfd.resolve({});
|
||||
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// Shim _loadMap so that it uses the local copy
|
||||
clippy.load._loadMap = function _loadMap (path) {
|
||||
let dfd = clippy.load._maps[path];
|
||||
if (dfd) return dfd;
|
||||
|
||||
// set dfd if not defined
|
||||
dfd = clippy.load._maps[path] = $.Deferred();
|
||||
|
||||
const src = clippyMap;
|
||||
const img = new Image();
|
||||
|
||||
img.onload = dfd.resolve;
|
||||
img.onerror = dfd.reject;
|
||||
|
||||
// start loading the map;
|
||||
img.setAttribute("src", src);
|
||||
|
||||
return dfd.promise();
|
||||
};
|
||||
|
||||
// Make sure we don't request the remote map
|
||||
clippy.Animator.prototype._setupElement = function _setupElement (el) {
|
||||
const frameSize = this._data.framesize;
|
||||
el.css("display", "none");
|
||||
el.css({ width: frameSize[0], height: frameSize[1] });
|
||||
el.css("background", "url('" + clippyMap + "') no-repeat");
|
||||
|
||||
return el;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Shims various ClippyJS Agent functions to modify behaviour.
|
||||
*
|
||||
* @param {Agent} agent - The Clippy Agent
|
||||
*/
|
||||
function shimClippyAgent(agent) {
|
||||
// Turn off all sounds
|
||||
agent._animator._playSound = () => {};
|
||||
|
||||
// Improve speak function to support HTML markup
|
||||
const self = agent._balloon;
|
||||
agent._balloon.speak = (complete, text, hold) => {
|
||||
self._hidden = false;
|
||||
self.show();
|
||||
const c = self._content;
|
||||
// set height to auto
|
||||
c.height("auto");
|
||||
c.width("auto");
|
||||
// add the text
|
||||
c.html(text);
|
||||
// set height
|
||||
c.height(c.height());
|
||||
c.width(c.width());
|
||||
c.text("");
|
||||
self.reposition();
|
||||
|
||||
self._complete = complete;
|
||||
self._sayWords(text, hold, complete);
|
||||
if (hold) agent._queue.next();
|
||||
};
|
||||
|
||||
// Improve the _sayWords function to allow HTML and support timeouts
|
||||
agent._balloon.WORD_SPEAK_TIME = 60;
|
||||
agent._balloon._sayWords = (text, hold, complete) => {
|
||||
self._active = true;
|
||||
self._hold = hold;
|
||||
const words = text.split(/[^\S-]/);
|
||||
const time = self.WORD_SPEAK_TIME;
|
||||
const el = self._content;
|
||||
let idx = 1;
|
||||
clearTimeout(self.holdTimeout);
|
||||
|
||||
self._addWord = $.proxy(function () {
|
||||
if (!self._active) return;
|
||||
if (idx > words.length) {
|
||||
delete self._addWord;
|
||||
self._active = false;
|
||||
if (!self._hold) {
|
||||
complete();
|
||||
self.hide();
|
||||
} else if (typeof hold === "number") {
|
||||
self.holdTimeout = setTimeout(() => {
|
||||
self._hold = false;
|
||||
complete();
|
||||
self.hide();
|
||||
}, hold);
|
||||
}
|
||||
} else {
|
||||
el.html(words.slice(0, idx).join(" "));
|
||||
idx++;
|
||||
self._loop = window.setTimeout($.proxy(self._addWord, self), time);
|
||||
}
|
||||
}, self);
|
||||
|
||||
self._addWord();
|
||||
};
|
||||
|
||||
// Add break-word to balloon CSS
|
||||
agent._balloon._balloon.css("word-break", "break-word");
|
||||
|
||||
// Close the balloon on click (unless it was a link)
|
||||
agent._balloon._balloon.click(e => {
|
||||
if (e.target.nodeName !== "A") {
|
||||
agent._balloon.hide(true);
|
||||
agent._balloon._hidden = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Add function to immediately close the balloon even if it is currently doing something
|
||||
agent.closeBalloonImmediately = () => {
|
||||
agent._queue.clear();
|
||||
agent._balloon.hide(true);
|
||||
agent._balloon._hidden = true;
|
||||
agent._queue.next();
|
||||
};
|
||||
}
|
||||
|
||||
export default SeasonalWaiter;
|
61
src/web/waiters/WindowWaiter.mjs
Executable file
61
src/web/waiters/WindowWaiter.mjs
Executable file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2016
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Waiter to handle events related to the window object.
|
||||
*/
|
||||
class WindowWaiter {
|
||||
|
||||
/**
|
||||
* WindowWaiter constructor.
|
||||
*
|
||||
* @param {App} app - The main view object for CyberChef.
|
||||
*/
|
||||
constructor(app) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for window resize events.
|
||||
* Resets the layout of CyberChef's panes after 200ms (so that continuous resizing doesn't cause
|
||||
* continuous resetting).
|
||||
*/
|
||||
windowResize() {
|
||||
this.app.debounce(this.app.resetLayout, 200, "windowResize", this.app, [])();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for window blur events.
|
||||
* Saves the current time so that we can calculate how long the window was unfocussed for when
|
||||
* focus is returned.
|
||||
*/
|
||||
windowBlur() {
|
||||
this.windowBlurTime = new Date().getTime();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for window focus events.
|
||||
*
|
||||
* When a browser tab is unfocused and the browser has to run lots of dynamic content in other
|
||||
* tabs, it swaps out the memory for that tab.
|
||||
* If the CyberChef tab has been unfocused for more than a minute, we run a silent bake which will
|
||||
* force the browser to load and cache all the relevant JavaScript code needed to do a real bake.
|
||||
* This will stop baking taking a long time when the CyberChef browser tab has been unfocused for
|
||||
* a long time and the browser has swapped out all its memory.
|
||||
*/
|
||||
windowFocus() {
|
||||
const unfocusedTime = new Date().getTime() - this.windowBlurTime;
|
||||
if (unfocusedTime > 60000) {
|
||||
this.app.silentBake();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default WindowWaiter;
|
716
src/web/waiters/WorkerWaiter.mjs
Normal file
716
src/web/waiters/WorkerWaiter.mjs
Normal file
|
@ -0,0 +1,716 @@
|
|||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import ChefWorker from "worker-loader?inline&fallback=false!../../core/ChefWorker";
|
||||
|
||||
/**
|
||||
* Waiter to handle conversations with the ChefWorker
|
||||
*/
|
||||
class WorkerWaiter {
|
||||
|
||||
/**
|
||||
* WorkerWaiter constructor
|
||||
*
|
||||
* @param {App} app - The main view object for CyberChef
|
||||
* @param {Manager} manager - The CyberChef event manager
|
||||
*/
|
||||
constructor(app, manager) {
|
||||
this.app = app;
|
||||
this.manager = manager;
|
||||
|
||||
this.loaded = false;
|
||||
this.chefWorkers = [];
|
||||
this.dishWorker = null;
|
||||
this.maxWorkers = navigator.hardwareConcurrency || 4;
|
||||
this.inputs = [];
|
||||
this.inputNums = [];
|
||||
this.totalOutputs = 0;
|
||||
this.loadingOutputs = 0;
|
||||
this.bakeId = 0;
|
||||
this.callbacks = {};
|
||||
this.callbackID = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates any existing ChefWorkers and sets up a new worker
|
||||
*/
|
||||
setupChefWorker() {
|
||||
for (let i = this.chefWorkers.length - 1; i >= 0; i--) {
|
||||
this.removeChefWorker(this.chefWorkers[i]);
|
||||
}
|
||||
|
||||
this.addChefWorker();
|
||||
this.setupDishWorker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a separate ChefWorker for performing dish operations.
|
||||
* Using a separate worker so that we can run dish operations without
|
||||
* affecting a bake which may be in progress.
|
||||
*/
|
||||
setupDishWorker() {
|
||||
if (this.dishWorker !== null) {
|
||||
this.dishWorker.terminate();
|
||||
}
|
||||
log.debug("Adding new ChefWorker (DishWorker)");
|
||||
|
||||
this.dishWorker = new ChefWorker();
|
||||
this.dishWorker.addEventListener("message", this.handleChefMessage.bind(this));
|
||||
|
||||
let docURL = document.location.href.split(/[#?]/)[0];
|
||||
const index = docURL.lastIndexOf("/");
|
||||
if (index > 0) {
|
||||
docURL = docURL.substring(0, index);
|
||||
}
|
||||
|
||||
this.dishWorker.postMessage({"action": "docURL", "data": docURL});
|
||||
this.dishWorker.postMessage({
|
||||
action: "setLogLevel",
|
||||
data: log.getLevel()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new ChefWorker
|
||||
*
|
||||
* @returns {number} The index of the created worker
|
||||
*/
|
||||
addChefWorker() {
|
||||
if (this.chefWorkers.length === this.maxWorkers) {
|
||||
// Can't create any more workers
|
||||
return -1;
|
||||
}
|
||||
|
||||
log.debug("Adding new ChefWorker");
|
||||
|
||||
// Create a new ChefWorker and send it the docURL
|
||||
const newWorker = new ChefWorker();
|
||||
newWorker.addEventListener("message", this.handleChefMessage.bind(this));
|
||||
let docURL = document.location.href.split(/[#?]/)[0];
|
||||
const index = docURL.lastIndexOf("/");
|
||||
if (index > 0) {
|
||||
docURL = docURL.substring(0, index);
|
||||
}
|
||||
|
||||
newWorker.postMessage({"action": "docURL", "data": docURL});
|
||||
newWorker.postMessage({
|
||||
action: "setLogLevel",
|
||||
data: log.getLevel()
|
||||
});
|
||||
|
||||
// Store the worker, whether or not it's active, and the inputNum as an object
|
||||
const newWorkerObj = {
|
||||
worker: newWorker,
|
||||
active: false,
|
||||
inputNum: -1
|
||||
};
|
||||
|
||||
this.chefWorkers.push(newWorkerObj);
|
||||
return this.chefWorkers.indexOf(newWorkerObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an inactive ChefWorker to be used for baking
|
||||
*
|
||||
* @param {boolean} [setActive=true] - If true, set the worker status to active
|
||||
* @returns {number} - The index of the ChefWorker
|
||||
*/
|
||||
getInactiveChefWorker(setActive=true) {
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
if (!this.chefWorkers[i].active) {
|
||||
this.chefWorkers[i].active = setActive;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a ChefWorker
|
||||
*
|
||||
* @param {Object} workerObj
|
||||
*/
|
||||
removeChefWorker(workerObj) {
|
||||
const index = this.chefWorkers.indexOf(workerObj);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.chefWorkers.length > 1 || this.chefWorkers[index].active) {
|
||||
log.debug(`Removing ChefWorker at index ${index}`);
|
||||
this.chefWorkers[index].worker.terminate();
|
||||
this.chefWorkers.splice(index, 1);
|
||||
}
|
||||
|
||||
// There should always be a ChefWorker loaded
|
||||
if (this.chefWorkers.length === 0) {
|
||||
this.addChefWorker();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the object for the ChefWorker of a given inputNum
|
||||
*
|
||||
* @param {number} inputNum
|
||||
*/
|
||||
getChefWorker(inputNum) {
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
if (this.chefWorkers[i].inputNum === inputNum) {
|
||||
return this.chefWorkers[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for messages sent back by the ChefWorkers
|
||||
*
|
||||
* @param {MessageEvent} e
|
||||
*/
|
||||
handleChefMessage(e) {
|
||||
const r = e.data;
|
||||
let inputNum = 0;
|
||||
log.debug(`Receiving ${r.action} from ChefWorker.`);
|
||||
|
||||
if (r.data.hasOwnProperty("inputNum")) {
|
||||
inputNum = r.data.inputNum;
|
||||
}
|
||||
|
||||
const currentWorker = this.getChefWorker(inputNum);
|
||||
|
||||
switch (r.action) {
|
||||
case "bakeComplete":
|
||||
log.debug(`Bake ${inputNum} complete.`);
|
||||
|
||||
if (r.data.error) {
|
||||
this.app.handleError(r.data.error);
|
||||
this.manager.output.updateOutputError(r.data.error, inputNum, r.data.progress);
|
||||
} else {
|
||||
this.updateOutput(r.data, r.data.inputNum, r.data.bakeId, r.data.progress);
|
||||
}
|
||||
this.app.progress = r.data.progress;
|
||||
|
||||
if (r.data.progress === this.recipeConfig.length) {
|
||||
this.step = false;
|
||||
}
|
||||
|
||||
this.workerFinished(currentWorker);
|
||||
break;
|
||||
case "bakeError":
|
||||
this.app.handleError(r.data.error);
|
||||
this.manager.output.updateOutputError(r.data.error, inputNum, r.data.progress);
|
||||
this.app.progress = r.data.progress;
|
||||
this.workerFinished(currentWorker);
|
||||
break;
|
||||
case "dishReturned":
|
||||
this.callbacks[r.data.id](r.data);
|
||||
this.dishWorker.terminate();
|
||||
this.dishWorker = null;
|
||||
break;
|
||||
case "silentBakeComplete":
|
||||
break;
|
||||
case "workerLoaded":
|
||||
this.app.workerLoaded = true;
|
||||
log.debug("ChefWorker loaded.");
|
||||
if (!this.loaded) {
|
||||
this.app.loaded();
|
||||
this.loaded = true;
|
||||
} else {
|
||||
this.bakeNextInput(this.getInactiveChefWorker(false));
|
||||
}
|
||||
break;
|
||||
case "statusMessage":
|
||||
// Status message should be done per output
|
||||
this.manager.output.updateOutputMessage(r.data.message, r.data.inputNum, true);
|
||||
break;
|
||||
case "optionUpdate":
|
||||
log.debug(`Setting ${r.data.option} to ${r.data.value}`);
|
||||
this.app.options[r.data.option] = r.data.value;
|
||||
break;
|
||||
case "setRegisters":
|
||||
this.manager.recipe.setRegisters(r.data.opIndex, r.data.numPrevRegisters, r.data.registers);
|
||||
break;
|
||||
case "highlightsCalculated":
|
||||
this.manager.highlighter.displayHighlights(r.data.pos, r.data.direction);
|
||||
break;
|
||||
default:
|
||||
log.error("Unrecognised message from ChefWorker", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the value of an output
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {number} inputNum
|
||||
* @param {number} bakeId
|
||||
* @param {number} progress
|
||||
*/
|
||||
updateOutput(data, inputNum, bakeId, progress) {
|
||||
this.manager.output.updateOutputBakeId(bakeId, inputNum);
|
||||
if (progress === this.recipeConfig.length) {
|
||||
progress = false;
|
||||
}
|
||||
this.manager.output.updateOutputProgress(progress, inputNum);
|
||||
this.manager.output.updateOutputValue(data, inputNum, false);
|
||||
this.manager.output.updateOutputStatus("baked", inputNum);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI to show if baking is in process or not.
|
||||
*
|
||||
* @param {boolean} bakingStatus
|
||||
*/
|
||||
setBakingStatus(bakingStatus) {
|
||||
this.app.baking = bakingStatus;
|
||||
this.manager.controls.toggleBakeButtonFunction(bakingStatus);
|
||||
|
||||
if (bakingStatus) this.manager.output.hideMagicButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the progress of the ChefWorkers
|
||||
*/
|
||||
getBakeProgress() {
|
||||
const pendingInputs = this.inputNums.length + this.loadingOutputs + this.inputs.length;
|
||||
let bakingInputs = 0;
|
||||
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
if (this.chefWorkers[i].active) {
|
||||
bakingInputs++;
|
||||
}
|
||||
}
|
||||
|
||||
const total = this.totalOutputs;
|
||||
const bakedInputs = total - pendingInputs - bakingInputs;
|
||||
|
||||
return {
|
||||
total: total,
|
||||
pending: pendingInputs,
|
||||
baking: bakingInputs,
|
||||
baked: bakedInputs
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the current bake by terminating and removing all ChefWorkers
|
||||
*
|
||||
* @param {boolean} [silent=false] - If true, don't set the output
|
||||
* @param {boolean} killAll - If true, kills all chefWorkers regardless of status
|
||||
*/
|
||||
cancelBake(silent, killAll) {
|
||||
for (let i = this.chefWorkers.length - 1; i >= 0; i--) {
|
||||
if (this.chefWorkers[i].active || killAll) {
|
||||
const inputNum = this.chefWorkers[i].inputNum;
|
||||
this.removeChefWorker(this.chefWorkers[i]);
|
||||
this.manager.output.updateOutputStatus("inactive", inputNum);
|
||||
}
|
||||
}
|
||||
this.setBakingStatus(false);
|
||||
|
||||
for (let i = 0; i < this.inputs.length; i++) {
|
||||
this.manager.output.updateOutputStatus("inactive", this.inputs[i].inputNum);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.inputNums.length; i++) {
|
||||
this.manager.output.updateOutputStatus("inactive", this.inputNums[i]);
|
||||
}
|
||||
|
||||
this.inputs = [];
|
||||
this.inputNums = [];
|
||||
this.totalOutputs = 0;
|
||||
this.loadingOutputs = 0;
|
||||
if (!silent) this.manager.output.set(this.manager.output.getActiveTab());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a worker completing baking
|
||||
*
|
||||
* @param {object} workerObj - Object containing the worker information
|
||||
* @param {ChefWorker} workerObj.worker - The actual worker object
|
||||
* @param {number} workerObj.inputNum - The inputNum of the input being baked by the worker
|
||||
* @param {boolean} workerObj.active - If true, the worker is currrently baking an input
|
||||
*/
|
||||
workerFinished(workerObj) {
|
||||
const workerIdx = this.chefWorkers.indexOf(workerObj);
|
||||
this.chefWorkers[workerIdx].active = false;
|
||||
if (this.inputs.length > 0) {
|
||||
this.bakeNextInput(workerIdx);
|
||||
} else if (this.inputNums.length === 0 && this.loadingOutputs === 0) {
|
||||
// The ChefWorker is no longer needed
|
||||
log.debug("No more inputs to bake.");
|
||||
const progress = this.getBakeProgress();
|
||||
if (progress.total === progress.baked) {
|
||||
this.bakingComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for completed bakes
|
||||
*/
|
||||
bakingComplete() {
|
||||
this.setBakingStatus(false);
|
||||
let duration = new Date().getTime() - this.bakeStartTime;
|
||||
duration = duration.toLocaleString() + "ms";
|
||||
const progress = this.getBakeProgress();
|
||||
|
||||
if (progress.total > 1) {
|
||||
let width = progress.total.toLocaleString().length;
|
||||
if (duration.length > width) {
|
||||
width = duration.length;
|
||||
}
|
||||
width = width < 2 ? 2 : width;
|
||||
|
||||
const totalStr = progress.total.toLocaleString().padStart(width, " ").replace(/ /g, " ");
|
||||
const durationStr = duration.padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
const inputNums = Object.keys(this.manager.output.outputs);
|
||||
let avgTime = 0,
|
||||
numOutputs = 0;
|
||||
for (let i = 0; i < inputNums.length; i++) {
|
||||
const output = this.manager.output.outputs[inputNums[i]];
|
||||
if (output.status === "baked") {
|
||||
numOutputs++;
|
||||
avgTime += output.data.duration;
|
||||
}
|
||||
}
|
||||
avgTime = Math.round(avgTime / numOutputs).toLocaleString() + "ms";
|
||||
avgTime = avgTime.padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
const msg = `total: ${totalStr}<br>time: ${durationStr}<br>average: ${avgTime}`;
|
||||
|
||||
const bakeInfo = document.getElementById("bake-info");
|
||||
bakeInfo.innerHTML = msg;
|
||||
bakeInfo.style.display = "";
|
||||
} else {
|
||||
document.getElementById("bake-info").style.display = "none";
|
||||
}
|
||||
document.getElementById("bake").style.background = "";
|
||||
this.totalOutputs = 0; // Reset for next time
|
||||
log.debug("--- Bake complete ---");
|
||||
}
|
||||
|
||||
/**
|
||||
* Bakes the next input and tells the inputWorker to load the next input
|
||||
*
|
||||
* @param {number} workerIdx - The index of the worker to bake with
|
||||
*/
|
||||
bakeNextInput(workerIdx) {
|
||||
if (this.inputs.length === 0) return;
|
||||
if (workerIdx === -1) return;
|
||||
if (!this.chefWorkers[workerIdx]) return;
|
||||
this.chefWorkers[workerIdx].active = true;
|
||||
const nextInput = this.inputs.splice(0, 1)[0];
|
||||
if (typeof nextInput.inputNum === "string") nextInput.inputNum = parseInt(nextInput.inputNum, 10);
|
||||
|
||||
log.debug(`Baking input ${nextInput.inputNum}.`);
|
||||
this.manager.output.updateOutputMessage(`Baking input ${nextInput.inputNum}...`, nextInput.inputNum, false);
|
||||
this.manager.output.updateOutputStatus("baking", nextInput.inputNum);
|
||||
|
||||
this.chefWorkers[workerIdx].inputNum = nextInput.inputNum;
|
||||
const input = nextInput.input,
|
||||
recipeConfig = this.recipeConfig;
|
||||
|
||||
if (this.step) {
|
||||
// Remove all breakpoints from the recipe up to progress
|
||||
for (let i = 0; i < this.app.progress; i++) {
|
||||
if (recipeConfig[i].hasOwnProperty("breakpoint")) {
|
||||
delete recipeConfig[i].breakpoint;
|
||||
}
|
||||
}
|
||||
|
||||
// Set a breakpoint at the next operation so we stop baking there
|
||||
if (recipeConfig[this.app.progress]) recipeConfig[this.app.progress].breakpoint = true;
|
||||
}
|
||||
|
||||
let transferable;
|
||||
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
|
||||
transferable = [input];
|
||||
}
|
||||
this.chefWorkers[workerIdx].worker.postMessage({
|
||||
action: "bake",
|
||||
data: {
|
||||
input: input,
|
||||
recipeConfig: recipeConfig,
|
||||
options: this.options,
|
||||
inputNum: nextInput.inputNum,
|
||||
bakeId: this.bakeId
|
||||
}
|
||||
}, transferable);
|
||||
|
||||
if (this.inputNums.length > 0) {
|
||||
this.manager.input.inputWorker.postMessage({
|
||||
action: "bakeNext",
|
||||
data: {
|
||||
inputNum: this.inputNums.splice(0, 1)[0],
|
||||
bakeId: this.bakeId
|
||||
}
|
||||
});
|
||||
this.loadingOutputs++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bakes the current input using the current recipe.
|
||||
*
|
||||
* @param {Object[]} recipeConfig
|
||||
* @param {Object} options
|
||||
* @param {number} progress
|
||||
* @param {boolean} step
|
||||
*/
|
||||
bake(recipeConfig, options, progress, step) {
|
||||
this.setBakingStatus(true);
|
||||
this.manager.recipe.updateBreakpointIndicator(false);
|
||||
this.bakeStartTime = new Date().getTime();
|
||||
this.bakeId++;
|
||||
this.recipeConfig = recipeConfig;
|
||||
this.options = options;
|
||||
this.progress = progress;
|
||||
this.step = step;
|
||||
|
||||
this.displayProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues an input ready to be baked
|
||||
*
|
||||
* @param {object} inputData
|
||||
* @param {string | ArrayBuffer} inputData.input
|
||||
* @param {number} inputData.inputNum
|
||||
* @param {number} inputData.bakeId
|
||||
*/
|
||||
queueInput(inputData) {
|
||||
this.loadingOutputs--;
|
||||
if (this.app.baking && inputData.bakeId === this.bakeId) {
|
||||
this.inputs.push(inputData);
|
||||
this.bakeNextInput(this.getInactiveChefWorker(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles if an error is thrown by QueueInput
|
||||
*
|
||||
* @param {object} inputData
|
||||
* @param {number} inputData.inputNum
|
||||
* @param {number} inputData.bakeId
|
||||
*/
|
||||
queueInputError(inputData) {
|
||||
this.loadingOutputs--;
|
||||
if (this.app.baking && inputData.bakeId === this.bakeId) {
|
||||
this.manager.output.updateOutputError("Error queueing the input for a bake.", inputData.inputNum, 0);
|
||||
|
||||
if (this.inputNums.length === 0) return;
|
||||
|
||||
// Load the next input
|
||||
this.manager.input.inputWorker.postMessage({
|
||||
action: "bakeNext",
|
||||
data: {
|
||||
inputNum: this.inputNums.splice(0, 1)[0],
|
||||
bakeId: this.bakeId
|
||||
}
|
||||
});
|
||||
this.loadingOutputs++;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a list of inputNums to be baked by ChefWorkers, and begins baking
|
||||
*
|
||||
* @param {object} inputData
|
||||
* @param {number[]} inputData.nums - The inputNums to be queued for baking
|
||||
* @param {boolean} inputData.step - If true, only execute the next operation in the recipe
|
||||
* @param {number} inputData.progress - The current progress through the recipe. Used when stepping
|
||||
*/
|
||||
async bakeAllInputs(inputData) {
|
||||
return await new Promise(resolve => {
|
||||
if (this.app.baking) return;
|
||||
const inputNums = inputData.nums;
|
||||
const step = inputData.step;
|
||||
|
||||
// Use cancelBake to clear out the inputs
|
||||
this.cancelBake(true, false);
|
||||
|
||||
this.inputNums = inputNums;
|
||||
this.totalOutputs = inputNums.length;
|
||||
this.app.progress = inputData.progress;
|
||||
|
||||
let inactiveWorkers = 0;
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
if (!this.chefWorkers[i].active) {
|
||||
inactiveWorkers++;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < inputNums.length - inactiveWorkers; i++) {
|
||||
if (this.addChefWorker() === -1) break;
|
||||
}
|
||||
|
||||
this.app.bake(step);
|
||||
|
||||
for (let i = 0; i < this.inputNums.length; i++) {
|
||||
this.manager.output.updateOutputMessage(`Input ${inputNums[i]} has not been baked yet.`, inputNums[i], false);
|
||||
this.manager.output.updateOutputStatus("pending", inputNums[i]);
|
||||
}
|
||||
|
||||
let numBakes = this.chefWorkers.length;
|
||||
if (this.inputNums.length < numBakes) {
|
||||
numBakes = this.inputNums.length;
|
||||
}
|
||||
for (let i = 0; i < numBakes; i++) {
|
||||
this.manager.input.inputWorker.postMessage({
|
||||
action: "bakeNext",
|
||||
data: {
|
||||
inputNum: this.inputNums.splice(0, 1)[0],
|
||||
bakeId: this.bakeId
|
||||
}
|
||||
});
|
||||
this.loadingOutputs++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the ChefWorker to run a silent bake, forcing the browser to load and cache all the relevant
|
||||
* JavaScript code needed to do a real bake.
|
||||
*
|
||||
* @param {Object[]} [recipeConfig]
|
||||
*/
|
||||
silentBake(recipeConfig) {
|
||||
// If there aren't any active ChefWorkers, try to add one
|
||||
let workerId = this.getInactiveChefWorker();
|
||||
if (workerId === -1) {
|
||||
workerId = this.addChefWorker();
|
||||
}
|
||||
if (workerId === -1) return;
|
||||
this.chefWorkers[workerId].worker.postMessage({
|
||||
action: "silentBake",
|
||||
data: {
|
||||
recipeConfig: recipeConfig
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the ChefWorker to return the dish as the specified type
|
||||
*
|
||||
* @param {Dish} dish
|
||||
* @param {string} type
|
||||
* @param {Function} callback
|
||||
*/
|
||||
getDishAs(dish, type, callback) {
|
||||
const id = this.callbackID++;
|
||||
|
||||
this.callbacks[id] = callback;
|
||||
|
||||
if (this.dishWorker === null) this.setupDishWorker();
|
||||
|
||||
this.dishWorker.postMessage({
|
||||
action: "getDishAs",
|
||||
data: {
|
||||
dish: dish,
|
||||
type: type,
|
||||
id: id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the console log level in the workers.
|
||||
*
|
||||
* @param {string} level
|
||||
*/
|
||||
setLogLevel(level) {
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
this.chefWorkers[i].worker.postMessage({
|
||||
action: "setLogLevel",
|
||||
data: log.getLevel()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the bake progress in the output bar and bake button
|
||||
*/
|
||||
displayProgress() {
|
||||
const progress = this.getBakeProgress();
|
||||
if (progress.total === progress.baked) return;
|
||||
|
||||
const percentComplete = ((progress.pending + progress.baking) / progress.total) * 100;
|
||||
const bakeButton = document.getElementById("bake");
|
||||
if (this.app.baking) {
|
||||
if (percentComplete < 100) {
|
||||
document.getElementById("bake").style.background = `linear-gradient(to left, #fea79a ${percentComplete}%, #f44336 ${percentComplete}%)`;
|
||||
} else {
|
||||
bakeButton.style.background = "";
|
||||
}
|
||||
} else {
|
||||
// not baking
|
||||
bakeButton.style.background = "";
|
||||
}
|
||||
|
||||
const bakeInfo = document.getElementById("bake-info");
|
||||
if (progress.total > 1) {
|
||||
let width = progress.total.toLocaleString().length;
|
||||
width = width < 2 ? 2 : width;
|
||||
|
||||
const totalStr = progress.total.toLocaleString().padStart(width, " ").replace(/ /g, " ");
|
||||
const bakedStr = progress.baked.toLocaleString().padStart(width, " ").replace(/ /g, " ");
|
||||
const pendingStr = progress.pending.toLocaleString().padStart(width, " ").replace(/ /g, " ");
|
||||
const bakingStr = progress.baking.toLocaleString().padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
let msg = "total: " + totalStr;
|
||||
msg += "<br>baked: " + bakedStr;
|
||||
|
||||
if (progress.pending > 0) {
|
||||
msg += "<br>pending: " + pendingStr;
|
||||
} else if (progress.baking > 0) {
|
||||
msg += "<br>baking: " + bakingStr;
|
||||
}
|
||||
bakeInfo.innerHTML = msg;
|
||||
bakeInfo.style.display = "";
|
||||
} else {
|
||||
bakeInfo.style.display = "none";
|
||||
}
|
||||
|
||||
if (progress.total !== progress.baked) {
|
||||
setTimeout(function() {
|
||||
this.displayProgress();
|
||||
}.bind(this), 100);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the ChefWorker to calculate highlight offsets if possible.
|
||||
*
|
||||
* @param {Object[]} recipeConfig
|
||||
* @param {string} direction
|
||||
* @param {Object} pos - The position object for the highlight.
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
*/
|
||||
highlight(recipeConfig, direction, pos) {
|
||||
let workerIdx = this.getInactiveChefWorker(false);
|
||||
if (workerIdx === -1) {
|
||||
workerIdx = this.addChefWorker();
|
||||
}
|
||||
if (workerIdx === -1) return;
|
||||
this.chefWorkers[workerIdx].worker.postMessage({
|
||||
action: "highlight",
|
||||
data: {
|
||||
recipeConfig: recipeConfig,
|
||||
direction: direction,
|
||||
pos: pos
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkerWaiter;
|
Loading…
Add table
Add a link
Reference in a new issue