ESM: Rewritten src/web/ in ESM format.

This commit is contained in:
n1474335 2018-05-15 17:36:45 +00:00
parent c90acd24f5
commit 07715bd167
34 changed files with 5207 additions and 4341 deletions

View file

@ -53,6 +53,7 @@ class FromHex extends Operation {
* @returns {Object[]} pos * @returns {Object[]} pos
*/ */
highlight(pos, args) { highlight(pos, args) {
if (args[0] === "Auto") return false;
const delim = Utils.charRep(args[0] || "Space"), const delim = Utils.charRep(args[0] || "Space"),
len = delim === "\r\n" ? 1 : delim.length, len = delim === "\r\n" ? 1 : delim.length,
width = len + 2; width = len + 2;

202
src/web/App.js Executable file → Normal file
View file

@ -1,26 +1,32 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import Utils from "../core/Utils"; import Utils from "../core/Utils";
import {fromBase64} from "../core/lib/Base64"; import {fromBase64} from "../core/lib/Base64";
import Manager from "./Manager.js"; import Manager from "./Manager";
import HTMLCategory from "./HTMLCategory.js"; import HTMLCategory from "./HTMLCategory";
import HTMLOperation from "./HTMLOperation.js"; import HTMLOperation from "./HTMLOperation";
import Split from "split.js"; import Split from "split.js";
/** /**
* HTML view for CyberChef responsible for building the web page and dealing with all user * HTML view for CyberChef responsible for building the web page and dealing with all user
* interactions. * interactions.
*/
class App {
/**
* App constructor.
* *
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {CatConf[]} categories - The list of categories and operations to be populated. * @param {CatConf[]} categories - The list of categories and operations to be populated.
* @param {Object.<string, OpConf>} operations - The list of operation configuration objects. * @param {Object.<string, OpConf>} operations - The list of operation configuration objects.
* @param {String[]} defaultFavourites - A list of default favourite operations. * @param {String[]} defaultFavourites - A list of default favourite operations.
* @param {Object} options - Default setting for app options. * @param {Object} options - Default setting for app options.
*/ */
const App = function(categories, operations, defaultFavourites, defaultOptions) { constructor(categories, operations, defaultFavourites, defaultOptions) {
this.categories = categories; this.categories = categories;
this.operations = operations; this.operations = operations;
this.dfavourites = defaultFavourites; this.dfavourites = defaultFavourites;
@ -34,15 +40,15 @@ const App = function(categories, operations, defaultFavourites, defaultOptions)
this.autoBakePause = false; this.autoBakePause = false;
this.progress = 0; this.progress = 0;
this.ingId = 0; this.ingId = 0;
}; }
/** /**
* This function sets up the stage and creates listeners for all events. * This function sets up the stage and creates listeners for all events.
* *
* @fires Manager#appstart * @fires Manager#appstart
*/ */
App.prototype.setup = function() { setup() {
document.dispatchEvent(this.manager.appstart); document.dispatchEvent(this.manager.appstart);
this.initialiseSplitter(); this.initialiseSplitter();
this.loadLocalStorage(); this.loadLocalStorage();
@ -56,15 +62,15 @@ App.prototype.setup = function() {
this.loadURIParams(); this.loadURIParams();
this.loaded(); this.loaded();
}; }
/** /**
* Fires once all setup activities have completed. * Fires once all setup activities have completed.
* *
* @fires Manager#apploaded * @fires Manager#apploaded
*/ */
App.prototype.loaded = function() { loaded() {
// Check that both the app and the worker have loaded successfully, and that // Check that both the app and the worker have loaded successfully, and that
// we haven't already loaded before attempting to remove the loading screen. // we haven't already loaded before attempting to remove the loading screen.
if (!this.workerLoaded || !this.appLoaded || if (!this.workerLoaded || !this.appLoaded ||
@ -87,29 +93,29 @@ App.prototype.loaded = function() {
window.removeEventListener("error", window.loadingErrorHandler); window.removeEventListener("error", window.loadingErrorHandler);
document.dispatchEvent(this.manager.apploaded); document.dispatchEvent(this.manager.apploaded);
}; }
/** /**
* An error handler for displaying the error to the user. * An error handler for displaying the error to the user.
* *
* @param {Error} err * @param {Error} err
* @param {boolean} [logToConsole=false] * @param {boolean} [logToConsole=false]
*/ */
App.prototype.handleError = function(err, logToConsole) { handleError(err, logToConsole) {
if (logToConsole) log.error(err); if (logToConsole) log.error(err);
const msg = err.displayStr || err.toString(); const msg = err.displayStr || err.toString();
this.alert(msg, "danger", this.options.errorTimeout, !this.options.showErrors); this.alert(msg, "danger", this.options.errorTimeout, !this.options.showErrors);
}; }
/** /**
* Asks the ChefWorker to bake the current input using the current recipe. * Asks the ChefWorker to bake the current input using the current recipe.
* *
* @param {boolean} [step] - Set to true if we should only execute one operation instead of the * @param {boolean} [step] - Set to true if we should only execute one operation instead of the
* whole recipe. * whole recipe.
*/ */
App.prototype.bake = function(step) { bake(step) {
if (this.baking) return; if (this.baking) return;
// Reset attemptHighlight flag // Reset attemptHighlight flag
@ -122,13 +128,13 @@ App.prototype.bake = function(step) {
this.progress, // The current position in the recipe this.progress, // The current position in the recipe
step // Whether or not to take one step or execute the whole recipe step // Whether or not to take one step or execute the whole recipe
); );
}; }
/** /**
* Runs Auto Bake if it is set. * Runs Auto Bake if it is set.
*/ */
App.prototype.autoBake = function() { autoBake() {
// If autoBakePause is set, we are loading a full recipe (and potentially input), so there is no // If autoBakePause is set, we are loading a full recipe (and potentially input), so there is no
// need to set the staleness indicator. Just exit and wait until auto bake is called after loading // need to set the staleness indicator. Just exit and wait until auto bake is called after loading
// has completed. // has completed.
@ -140,17 +146,17 @@ App.prototype.autoBake = function() {
} else { } else {
this.manager.controls.showStaleIndicator(); this.manager.controls.showStaleIndicator();
} }
}; }
/** /**
* Runs a silent bake, forcing the browser to load and cache all the relevant JavaScript code needed * Runs a silent bake, forcing the browser to load and cache all the relevant JavaScript code needed
* to do a real bake. * to do a real bake.
* *
* The output will not be modified (hence "silent" bake). This will only actually execute the recipe * The output will not be modified (hence "silent" bake). This will only actually execute the recipe
* if auto-bake is enabled, otherwise it will just wake up the ChefWorker with an empty recipe. * if auto-bake is enabled, otherwise it will just wake up the ChefWorker with an empty recipe.
*/ */
App.prototype.silentBake = function() { silentBake() {
let recipeConfig = []; let recipeConfig = [];
if (this.autoBake_) { if (this.autoBake_) {
@ -160,36 +166,36 @@ App.prototype.silentBake = function() {
} }
this.manager.worker.silentBake(recipeConfig); this.manager.worker.silentBake(recipeConfig);
}; }
/** /**
* Gets the user's input data. * Gets the user's input data.
* *
* @returns {string} * @returns {string}
*/ */
App.prototype.getInput = function() { getInput() {
return this.manager.input.get(); return this.manager.input.get();
}; }
/** /**
* Sets the user's input data. * Sets the user's input data.
* *
* @param {string} input - The string to set the input to * @param {string} input - The string to set the input to
*/ */
App.prototype.setInput = function(input) { setInput(input) {
this.manager.input.set(input); this.manager.input.set(input);
}; }
/** /**
* Populates the operations accordion list with the categories and operations specified in the * Populates the operations accordion list with the categories and operations specified in the
* view constructor. * view constructor.
* *
* @fires Manager#oplistcreate * @fires Manager#oplistcreate
*/ */
App.prototype.populateOperationsList = function() { populateOperationsList() {
// Move edit button away before we overwrite it // Move edit button away before we overwrite it
document.body.appendChild(document.getElementById("edit-favourites")); document.body.appendChild(document.getElementById("edit-favourites"));
@ -225,13 +231,13 @@ App.prototype.populateOperationsList = function() {
// Add edit button to first category (Favourites) // Add edit button to first category (Favourites)
document.querySelector("#categories a").appendChild(document.getElementById("edit-favourites")); document.querySelector("#categories a").appendChild(document.getElementById("edit-favourites"));
}; }
/** /**
* Sets up the adjustable splitter to allow the user to resize areas of the page. * Sets up the adjustable splitter to allow the user to resize areas of the page.
*/ */
App.prototype.initialiseSplitter = function() { initialiseSplitter() {
this.columnSplitter = Split(["#operations", "#recipe", "#IO"], { this.columnSplitter = Split(["#operations", "#recipe", "#IO"], {
sizes: [20, 30, 50], sizes: [20, 30, 50],
minSize: [240, 325, 450], minSize: [240, 325, 450],
@ -248,14 +254,14 @@ App.prototype.initialiseSplitter = function() {
}); });
this.resetLayout(); this.resetLayout();
}; }
/** /**
* Loads the information previously saved to the HTML5 local storage object so that user options * Loads the information previously saved to the HTML5 local storage object so that user options
* and favourites can be restored. * and favourites can be restored.
*/ */
App.prototype.loadLocalStorage = function() { loadLocalStorage() {
// Load options // Load options
let lOptions; let lOptions;
if (this.isLocalStorageAvailable() && localStorage.options !== undefined) { if (this.isLocalStorageAvailable() && localStorage.options !== undefined) {
@ -265,15 +271,15 @@ App.prototype.loadLocalStorage = function() {
// Load favourites // Load favourites
this.loadFavourites(); this.loadFavourites();
}; }
/** /**
* Loads the user's favourite operations from the HTML5 local storage object and populates the * Loads the user's favourite operations from the HTML5 local storage object and populates the
* Favourites category with them. * Favourites category with them.
* If the user currently has no saved favourites, the defaults from the view constructor are used. * If the user currently has no saved favourites, the defaults from the view constructor are used.
*/ */
App.prototype.loadFavourites = function() { loadFavourites() {
let favourites; let favourites;
if (this.isLocalStorageAvailable()) { if (this.isLocalStorageAvailable()) {
@ -298,17 +304,17 @@ App.prototype.loadFavourites = function() {
ops: favourites ops: favourites
}); });
} }
}; }
/** /**
* Filters the list of favourite operations that the user had stored and removes any that are no * Filters the list of favourite operations that the user had stored and removes any that are no
* longer available. The user is notified if this is the case. * longer available. The user is notified if this is the case.
* @param {string[]} favourites - A list of the user's favourite operations * @param {string[]} favourites - A list of the user's favourite operations
* @returns {string[]} A list of the valid favourites * @returns {string[]} A list of the valid favourites
*/ */
App.prototype.validFavourites = function(favourites) { validFavourites(favourites) {
const validFavs = []; const validFavs = [];
for (let i = 0; i < favourites.length; i++) { for (let i = 0; i < favourites.length; i++) {
if (this.operations.hasOwnProperty(favourites[i])) { if (this.operations.hasOwnProperty(favourites[i])) {
@ -319,15 +325,15 @@ App.prototype.validFavourites = function(favourites) {
} }
} }
return validFavs; return validFavs;
}; }
/** /**
* Saves a list of favourite operations to the HTML5 local storage object. * Saves a list of favourite operations to the HTML5 local storage object.
* *
* @param {string[]} favourites - A list of the user's favourite operations * @param {string[]} favourites - A list of the user's favourite operations
*/ */
App.prototype.saveFavourites = function(favourites) { saveFavourites(favourites) {
if (!this.isLocalStorageAvailable()) { if (!this.isLocalStorageAvailable()) {
this.alert( this.alert(
"Your security settings do not allow access to local storage so your favourites cannot be saved.", "Your security settings do not allow access to local storage so your favourites cannot be saved.",
@ -338,27 +344,27 @@ App.prototype.saveFavourites = function(favourites) {
} }
localStorage.setItem("favourites", JSON.stringify(this.validFavourites(favourites))); localStorage.setItem("favourites", JSON.stringify(this.validFavourites(favourites)));
}; }
/** /**
* Resets favourite operations back to the default as specified in the view constructor and * Resets favourite operations back to the default as specified in the view constructor and
* refreshes the operation list. * refreshes the operation list.
*/ */
App.prototype.resetFavourites = function() { resetFavourites() {
this.saveFavourites(this.dfavourites); this.saveFavourites(this.dfavourites);
this.loadFavourites(); this.loadFavourites();
this.populateOperationsList(); this.populateOperationsList();
this.manager.recipe.initialiseOperationDragNDrop(); this.manager.recipe.initialiseOperationDragNDrop();
}; }
/** /**
* Adds an operation to the user's favourites. * Adds an operation to the user's favourites.
* *
* @param {string} name - The name of the operation * @param {string} name - The name of the operation
*/ */
App.prototype.addFavourite = function(name) { addFavourite(name) {
const favourites = JSON.parse(localStorage.favourites); const favourites = JSON.parse(localStorage.favourites);
if (favourites.indexOf(name) >= 0) { if (favourites.indexOf(name) >= 0) {
@ -371,13 +377,13 @@ App.prototype.addFavourite = function(name) {
this.loadFavourites(); this.loadFavourites();
this.populateOperationsList(); this.populateOperationsList();
this.manager.recipe.initialiseOperationDragNDrop(); this.manager.recipe.initialiseOperationDragNDrop();
}; }
/** /**
* Checks for input and recipe in the URI parameters and loads them if present. * Checks for input and recipe in the URI parameters and loads them if present.
*/ */
App.prototype.loadURIParams = function() { loadURIParams() {
// Load query string or hash from URI (depending on which is populated) // Load query string or hash from URI (depending on which is populated)
// We prefer getting the hash by splitting the href rather than referencing // We prefer getting the hash by splitting the href rather than referencing
// location.hash as some browsers (Firefox) automatically URL decode it, // location.hash as some browsers (Firefox) automatically URL decode it,
@ -421,36 +427,36 @@ App.prototype.loadURIParams = function() {
this.autoBakePause = false; this.autoBakePause = false;
this.autoBake(); this.autoBake();
}; }
/** /**
* Returns the next ingredient ID and increments it for next time. * Returns the next ingredient ID and increments it for next time.
* *
* @returns {number} * @returns {number}
*/ */
App.prototype.nextIngId = function() { nextIngId() {
return this.ingId++; return this.ingId++;
}; }
/** /**
* Gets the current recipe configuration. * Gets the current recipe configuration.
* *
* @returns {Object[]} * @returns {Object[]}
*/ */
App.prototype.getRecipeConfig = function() { getRecipeConfig() {
return this.manager.recipe.getConfig(); return this.manager.recipe.getConfig();
}; }
/** /**
* Given a recipe configuration, sets the recipe to that configuration. * Given a recipe configuration, sets the recipe to that configuration.
* *
* @fires Manager#statechange * @fires Manager#statechange
* @param {Object[]} recipeConfig - The recipe configuration * @param {Object[]} recipeConfig - The recipe configuration
*/ */
App.prototype.setRecipeConfig = function(recipeConfig) { setRecipeConfig(recipeConfig) {
document.getElementById("rec-list").innerHTML = null; document.getElementById("rec-list").innerHTML = null;
// Pause auto-bake while loading but don't modify `this.autoBake_` // Pause auto-bake while loading but don't modify `this.autoBake_`
@ -492,25 +498,25 @@ App.prototype.setRecipeConfig = function(recipeConfig) {
// Unpause auto bake // Unpause auto bake
this.autoBakePause = false; this.autoBakePause = false;
}; }
/** /**
* Resets the splitter positions to default. * Resets the splitter positions to default.
*/ */
App.prototype.resetLayout = function() { resetLayout() {
this.columnSplitter.setSizes([20, 30, 50]); this.columnSplitter.setSizes([20, 30, 50]);
this.ioSplitter.setSizes([50, 50]); this.ioSplitter.setSizes([50, 50]);
this.manager.controls.adjustWidth(); this.manager.controls.adjustWidth();
this.manager.output.adjustWidth(); this.manager.output.adjustWidth();
}; }
/** /**
* Sets the compile message. * Sets the compile message.
*/ */
App.prototype.setCompileMessage = function() { setCompileMessage() {
// Display time since last build and compile message // Display time since last build and compile message
const now = new Date(), const now = new Date(),
timeSinceCompile = Utils.fuzzyTime(now.getTime() - window.compileTime); timeSinceCompile = Utils.fuzzyTime(now.getTime() - window.compileTime);
@ -532,15 +538,15 @@ App.prototype.setCompileMessage = function() {
} }
document.getElementById("notice").innerHTML = compileInfo; document.getElementById("notice").innerHTML = compileInfo;
}; }
/** /**
* Determines whether the browser supports Local Storage and if it is accessible. * Determines whether the browser supports Local Storage and if it is accessible.
* *
* @returns {boolean} * @returns {boolean}
*/ */
App.prototype.isLocalStorageAvailable = function() { isLocalStorageAvailable() {
try { try {
if (!localStorage) return false; if (!localStorage) return false;
return true; return true;
@ -548,10 +554,10 @@ App.prototype.isLocalStorageAvailable = function() {
// Access to LocalStorage is denied // Access to LocalStorage is denied
return false; return false;
} }
}; }
/** /**
* Pops up a message to the user and writes it to the console log. * Pops up a message to the user and writes it to the console log.
* *
* @param {string} str - The message to display (HTML supported) * @param {string} str - The message to display (HTML supported)
@ -574,7 +580,7 @@ App.prototype.isLocalStorageAvailable = function() {
* // that will disappear after 5 seconds. * // that will disappear after 5 seconds.
* this.alert("Happy Christmas!", "info", 5000); * this.alert("Happy Christmas!", "info", 5000);
*/ */
App.prototype.alert = function(str, style, timeout, silent) { alert(str, style, timeout, silent) {
const time = new Date(); const time = new Date();
log.info("[" + time.toLocaleString() + "] " + str); log.info("[" + time.toLocaleString() + "] " + str);
@ -612,10 +618,10 @@ App.prototype.alert = function(str, style, timeout, silent) {
$("#alert").slideUp(100); $("#alert").slideUp(100);
}, timeout); }, timeout);
} }
}; }
/** /**
* Pops up a box asking the user a question and sending the answer to a specified callback function. * Pops up a box asking the user a question and sending the answer to a specified callback function.
* *
* @param {string} title - The title of the box * @param {string} title - The title of the box
@ -628,7 +634,7 @@ App.prototype.alert = function(str, style, timeout, silent) {
* // Pops up a box asking if the user would like a cookie. Prints the answer to the console. * // Pops up a box asking if the user would like a cookie. Prints the answer to the console.
* this.confirm("Question", "Would you like a cookie?", function(answer) {console.log(answer);}); * this.confirm("Question", "Would you like a cookie?", function(answer) {console.log(answer);});
*/ */
App.prototype.confirm = function(title, body, callback, scope) { confirm(title, body, callback, scope) {
scope = scope || this; scope = scope || this;
document.getElementById("confirm-title").innerHTML = title; document.getElementById("confirm-title").innerHTML = title;
document.getElementById("confirm-body").innerHTML = body; document.getElementById("confirm-body").innerHTML = body;
@ -649,26 +655,26 @@ App.prototype.confirm = function(title, body, callback, scope) {
callback.bind(scope)(false); callback.bind(scope)(false);
this.confirmClosed = true; this.confirmClosed = true;
}.bind(this)); }.bind(this));
}; }
/** /**
* Handler for the alert close button click event. * Handler for the alert close button click event.
* Closes the alert box. * Closes the alert box.
*/ */
App.prototype.alertCloseClick = function() { alertCloseClick() {
document.getElementById("alert").style.display = "none"; document.getElementById("alert").style.display = "none";
}; }
/** /**
* Handler for CyerChef statechange events. * Handler for CyerChef statechange events.
* Fires whenever the input or recipe changes in any way. * Fires whenever the input or recipe changes in any way.
* *
* @listens Manager#statechange * @listens Manager#statechange
* @param {event} e * @param {event} e
*/ */
App.prototype.stateChange = function(e) { stateChange(e) {
this.autoBake(); this.autoBake();
// Set title // Set title
@ -693,24 +699,24 @@ App.prototype.stateChange = function(e) {
this.lastStateUrl = this.manager.controls.generateStateUrl(true, true, recipeConfig); this.lastStateUrl = this.manager.controls.generateStateUrl(true, true, recipeConfig);
window.history.replaceState({}, title, this.lastStateUrl); window.history.replaceState({}, title, this.lastStateUrl);
} }
}; }
/** /**
* Handler for the history popstate event. * Handler for the history popstate event.
* Reloads parameters from the URL. * Reloads parameters from the URL.
* *
* @param {event} e * @param {event} e
*/ */
App.prototype.popState = function(e) { popState(e) {
this.loadURIParams(); this.loadURIParams();
}; }
/** /**
* Function to call an external API from this view. * Function to call an external API from this view.
*/ */
App.prototype.callApi = function(url, type, data, dataType, contentType) { callApi(url, type, data, dataType, contentType) {
type = type || "POST"; type = type || "POST";
data = data || {}; data = data || {};
dataType = dataType || undefined; dataType = dataType || undefined;
@ -740,6 +746,8 @@ App.prototype.callApi = function(url, type, data, dataType, contentType) {
success: success, success: success,
response: response response: response
}; };
}; }
}
export default App; export default App;

753
src/web/App.mjs Executable file
View file

@ -0,0 +1,753 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import Utils from "../core/Utils";
import {fromBase64} from "../core/lib/Base64";
import Manager from "./Manager";
import HTMLCategory from "./HTMLCategory";
import HTMLOperation from "./HTMLOperation";
import Split from "split.js";
/**
* HTML view for CyberChef responsible for building the web page and dealing with all user
* interactions.
*/
class App {
/**
* App constructor.
*
* @param {CatConf[]} categories - The list of categories and operations to be populated.
* @param {Object.<string, OpConf>} operations - The list of operation configuration objects.
* @param {String[]} defaultFavourites - A list of default favourite operations.
* @param {Object} options - Default setting for app options.
*/
constructor(categories, operations, defaultFavourites, defaultOptions) {
this.categories = categories;
this.operations = operations;
this.dfavourites = defaultFavourites;
this.doptions = defaultOptions;
this.options = Object.assign({}, defaultOptions);
this.manager = new Manager(this);
this.baking = false;
this.autoBake_ = false;
this.autoBakePause = false;
this.progress = 0;
this.ingId = 0;
}
/**
* This function sets up the stage and creates listeners for all events.
*
* @fires Manager#appstart
*/
setup() {
document.dispatchEvent(this.manager.appstart);
this.initialiseSplitter();
this.loadLocalStorage();
this.populateOperationsList();
this.manager.setup();
this.resetLayout();
this.setCompileMessage();
log.debug("App loaded");
this.appLoaded = true;
this.loadURIParams();
this.loaded();
}
/**
* Fires once all setup activities have completed.
*
* @fires Manager#apploaded
*/
loaded() {
// Check that both the app and the worker have loaded successfully, and that
// we haven't already loaded before attempting to remove the loading screen.
if (!this.workerLoaded || !this.appLoaded ||
!document.getElementById("loader-wrapper")) return;
// Trigger CSS animations to remove preloader
document.body.classList.add("loaded");
// Wait for animations to complete then remove the preloader and loaded style
// so that the animations for existing elements don't play again.
setTimeout(function() {
document.getElementById("loader-wrapper").remove();
document.body.classList.remove("loaded");
}, 1000);
// Clear the loading message interval
clearInterval(window.loadingMsgsInt);
// Remove the loading error handler
window.removeEventListener("error", window.loadingErrorHandler);
document.dispatchEvent(this.manager.apploaded);
}
/**
* An error handler for displaying the error to the user.
*
* @param {Error} err
* @param {boolean} [logToConsole=false]
*/
handleError(err, logToConsole) {
if (logToConsole) log.error(err);
const msg = err.displayStr || err.toString();
this.alert(msg, "danger", this.options.errorTimeout, !this.options.showErrors);
}
/**
* Asks the ChefWorker to bake the current input using the current recipe.
*
* @param {boolean} [step] - Set to true if we should only execute one operation instead of the
* whole recipe.
*/
bake(step) {
if (this.baking) return;
// Reset attemptHighlight flag
this.options.attemptHighlight = true;
this.manager.worker.bake(
this.getInput(), // The user's input
this.getRecipeConfig(), // The configuration of the recipe
this.options, // Options set by the user
this.progress, // The current position in the recipe
step // Whether or not to take one step or execute the whole recipe
);
}
/**
* Runs Auto Bake if it is set.
*/
autoBake() {
// If autoBakePause is set, we are loading a full recipe (and potentially input), so there is no
// need to set the staleness indicator. Just exit and wait until auto bake is called after loading
// has completed.
if (this.autoBakePause) return false;
if (this.autoBake_ && !this.baking) {
log.debug("Auto-baking");
this.bake();
} else {
this.manager.controls.showStaleIndicator();
}
}
/**
* Runs a silent bake, forcing the browser to load and cache all the relevant JavaScript code needed
* to do a real bake.
*
* The output will not be modified (hence "silent" bake). This will only actually execute the recipe
* if auto-bake is enabled, otherwise it will just wake up the ChefWorker with an empty recipe.
*/
silentBake() {
let recipeConfig = [];
if (this.autoBake_) {
// If auto-bake is not enabled we don't want to actually run the recipe as it may be disabled
// for a good reason.
recipeConfig = this.getRecipeConfig();
}
this.manager.worker.silentBake(recipeConfig);
}
/**
* Gets the user's input data.
*
* @returns {string}
*/
getInput() {
return this.manager.input.get();
}
/**
* Sets the user's input data.
*
* @param {string} input - The string to set the input to
*/
setInput(input) {
this.manager.input.set(input);
}
/**
* Populates the operations accordion list with the categories and operations specified in the
* view constructor.
*
* @fires Manager#oplistcreate
*/
populateOperationsList() {
// Move edit button away before we overwrite it
document.body.appendChild(document.getElementById("edit-favourites"));
let html = "";
let i;
for (i = 0; i < this.categories.length; i++) {
const catConf = this.categories[i],
selected = i === 0,
cat = new HTMLCategory(catConf.name, selected);
for (let j = 0; j < catConf.ops.length; j++) {
const opName = catConf.ops[j];
if (!this.operations.hasOwnProperty(opName)) {
log.warn(`${opName} could not be found.`);
continue;
}
const op = new HTMLOperation(opName, this.operations[opName], this, this.manager);
cat.addOperation(op);
}
html += cat.toHtml();
}
document.getElementById("categories").innerHTML = html;
const opLists = document.querySelectorAll("#categories .op-list");
for (i = 0; i < opLists.length; i++) {
opLists[i].dispatchEvent(this.manager.oplistcreate);
}
// Add edit button to first category (Favourites)
document.querySelector("#categories a").appendChild(document.getElementById("edit-favourites"));
}
/**
* Sets up the adjustable splitter to allow the user to resize areas of the page.
*/
initialiseSplitter() {
this.columnSplitter = Split(["#operations", "#recipe", "#IO"], {
sizes: [20, 30, 50],
minSize: [240, 325, 450],
gutterSize: 4,
onDrag: function() {
this.manager.controls.adjustWidth();
this.manager.output.adjustWidth();
}.bind(this)
});
this.ioSplitter = Split(["#input", "#output"], {
direction: "vertical",
gutterSize: 4,
});
this.resetLayout();
}
/**
* Loads the information previously saved to the HTML5 local storage object so that user options
* and favourites can be restored.
*/
loadLocalStorage() {
// Load options
let lOptions;
if (this.isLocalStorageAvailable() && localStorage.options !== undefined) {
lOptions = JSON.parse(localStorage.options);
}
this.manager.options.load(lOptions);
// Load favourites
this.loadFavourites();
}
/**
* Loads the user's favourite operations from the HTML5 local storage object and populates the
* Favourites category with them.
* If the user currently has no saved favourites, the defaults from the view constructor are used.
*/
loadFavourites() {
let favourites;
if (this.isLocalStorageAvailable()) {
favourites = localStorage.favourites && localStorage.favourites.length > 2 ?
JSON.parse(localStorage.favourites) :
this.dfavourites;
favourites = this.validFavourites(favourites);
this.saveFavourites(favourites);
} else {
favourites = this.dfavourites;
}
const favCat = this.categories.filter(function(c) {
return c.name === "Favourites";
})[0];
if (favCat) {
favCat.ops = favourites;
} else {
this.categories.unshift({
name: "Favourites",
ops: favourites
});
}
}
/**
* Filters the list of favourite operations that the user had stored and removes any that are no
* longer available. The user is notified if this is the case.
* @param {string[]} favourites - A list of the user's favourite operations
* @returns {string[]} A list of the valid favourites
*/
validFavourites(favourites) {
const validFavs = [];
for (let i = 0; i < favourites.length; i++) {
if (this.operations.hasOwnProperty(favourites[i])) {
validFavs.push(favourites[i]);
} else {
this.alert("The operation \"" + Utils.escapeHtml(favourites[i]) +
"\" is no longer available. It has been removed from your favourites.", "info");
}
}
return validFavs;
}
/**
* Saves a list of favourite operations to the HTML5 local storage object.
*
* @param {string[]} favourites - A list of the user's favourite operations
*/
saveFavourites(favourites) {
if (!this.isLocalStorageAvailable()) {
this.alert(
"Your security settings do not allow access to local storage so your favourites cannot be saved.",
"danger",
5000
);
return false;
}
localStorage.setItem("favourites", JSON.stringify(this.validFavourites(favourites)));
}
/**
* Resets favourite operations back to the default as specified in the view constructor and
* refreshes the operation list.
*/
resetFavourites() {
this.saveFavourites(this.dfavourites);
this.loadFavourites();
this.populateOperationsList();
this.manager.recipe.initialiseOperationDragNDrop();
}
/**
* Adds an operation to the user's favourites.
*
* @param {string} name - The name of the operation
*/
addFavourite(name) {
const favourites = JSON.parse(localStorage.favourites);
if (favourites.indexOf(name) >= 0) {
this.alert("'" + name + "' is already in your favourites", "info", 2000);
return;
}
favourites.push(name);
this.saveFavourites(favourites);
this.loadFavourites();
this.populateOperationsList();
this.manager.recipe.initialiseOperationDragNDrop();
}
/**
* Checks for input and recipe in the URI parameters and loads them if present.
*/
loadURIParams() {
// Load query string or hash from URI (depending on which is populated)
// We prefer getting the hash by splitting the href rather than referencing
// location.hash as some browsers (Firefox) automatically URL decode it,
// which cause issues.
const params = window.location.search ||
window.location.href.split("#")[1] ||
window.location.hash;
this.uriParams = Utils.parseURIParams(params);
this.autoBakePause = true;
// Read in recipe from URI params
if (this.uriParams.recipe) {
try {
const recipeConfig = Utils.parseRecipeConfig(this.uriParams.recipe);
this.setRecipeConfig(recipeConfig);
} catch (err) {}
} else if (this.uriParams.op) {
// If there's no recipe, look for single operations
this.manager.recipe.clearRecipe();
// Search for nearest match and add it
const matchedOps = this.manager.ops.filterOperations(this.uriParams.op, false);
if (matchedOps.length) {
this.manager.recipe.addOperation(matchedOps[0].name);
}
// Populate search with the string
const search = document.getElementById("search");
search.value = this.uriParams.op;
search.dispatchEvent(new Event("search"));
}
// Read in input data from URI params
if (this.uriParams.input) {
try {
const inputData = fromBase64(this.uriParams.input);
this.setInput(inputData);
} catch (err) {}
}
this.autoBakePause = false;
this.autoBake();
}
/**
* Returns the next ingredient ID and increments it for next time.
*
* @returns {number}
*/
nextIngId() {
return this.ingId++;
}
/**
* Gets the current recipe configuration.
*
* @returns {Object[]}
*/
getRecipeConfig() {
return this.manager.recipe.getConfig();
}
/**
* Given a recipe configuration, sets the recipe to that configuration.
*
* @fires Manager#statechange
* @param {Object[]} recipeConfig - The recipe configuration
*/
setRecipeConfig(recipeConfig) {
document.getElementById("rec-list").innerHTML = null;
// Pause auto-bake while loading but don't modify `this.autoBake_`
// otherwise `manualBake` cannot trigger.
this.autoBakePause = true;
for (let i = 0; i < recipeConfig.length; i++) {
const item = this.manager.recipe.addOperation(recipeConfig[i].op);
// Populate arguments
const args = item.querySelectorAll(".arg");
for (let j = 0; j < args.length; j++) {
if (recipeConfig[i].args[j] === undefined) continue;
if (args[j].getAttribute("type") === "checkbox") {
// checkbox
args[j].checked = recipeConfig[i].args[j];
} else if (args[j].classList.contains("toggle-string")) {
// toggleString
args[j].value = recipeConfig[i].args[j].string;
args[j].previousSibling.children[0].innerHTML =
Utils.escapeHtml(recipeConfig[i].args[j].option) +
" <span class='caret'></span>";
} else {
// all others
args[j].value = recipeConfig[i].args[j];
}
}
// Set disabled and breakpoint
if (recipeConfig[i].disabled) {
item.querySelector(".disable-icon").click();
}
if (recipeConfig[i].breakpoint) {
item.querySelector(".breakpoint").click();
}
this.progress = 0;
}
// Unpause auto bake
this.autoBakePause = false;
}
/**
* Resets the splitter positions to default.
*/
resetLayout() {
this.columnSplitter.setSizes([20, 30, 50]);
this.ioSplitter.setSizes([50, 50]);
this.manager.controls.adjustWidth();
this.manager.output.adjustWidth();
}
/**
* Sets the compile message.
*/
setCompileMessage() {
// Display time since last build and compile message
const now = new Date(),
timeSinceCompile = Utils.fuzzyTime(now.getTime() - window.compileTime);
// Calculate previous version to compare to
const prev = PKG_VERSION.split(".").map(n => {
return parseInt(n, 10);
});
if (prev[2] > 0) prev[2]--;
else if (prev[1] > 0) prev[1]--;
else prev[0]--;
const compareURL = `https://github.com/gchq/CyberChef/compare/v${prev.join(".")}...v${PKG_VERSION}`;
let compileInfo = `<a href='${compareURL}'>Last build: ${timeSinceCompile.substr(0, 1).toUpperCase() + timeSinceCompile.substr(1)} ago</a>`;
if (window.compileMessage !== "") {
compileInfo += " - " + window.compileMessage;
}
document.getElementById("notice").innerHTML = compileInfo;
}
/**
* Determines whether the browser supports Local Storage and if it is accessible.
*
* @returns {boolean}
*/
isLocalStorageAvailable() {
try {
if (!localStorage) return false;
return true;
} catch (err) {
// Access to LocalStorage is denied
return false;
}
}
/**
* Pops up a message to the user and writes it to the console log.
*
* @param {string} str - The message to display (HTML supported)
* @param {string} style - The colour of the popup
* "danger" = red
* "warning" = amber
* "info" = blue
* "success" = green
* @param {number} timeout - The number of milliseconds before the popup closes automatically
* 0 for never (until the user closes it)
* @param {boolean} [silent=false] - Don't show the message in the popup, only print it to the
* console
*
* @example
* // Pops up a red box with the message "[current time] Error: Something has gone wrong!"
* // that will need to be dismissed by the user.
* this.alert("Error: Something has gone wrong!", "danger", 0);
*
* // Pops up a blue information box with the message "[current time] Happy Christmas!"
* // that will disappear after 5 seconds.
* this.alert("Happy Christmas!", "info", 5000);
*/
alert(str, style, timeout, silent) {
const time = new Date();
log.info("[" + time.toLocaleString() + "] " + str);
if (silent) return;
style = style || "danger";
timeout = timeout || 0;
const alertEl = document.getElementById("alert"),
alertContent = document.getElementById("alert-content");
alertEl.classList.remove("alert-danger");
alertEl.classList.remove("alert-warning");
alertEl.classList.remove("alert-info");
alertEl.classList.remove("alert-success");
alertEl.classList.add("alert-" + style);
// If the box hasn't been closed, append to it rather than replacing
if (alertEl.style.display === "block") {
alertContent.innerHTML +=
"<br><br>[" + time.toLocaleTimeString() + "] " + str;
} else {
alertContent.innerHTML =
"[" + time.toLocaleTimeString() + "] " + str;
}
// Stop the animation if it is in progress
$("#alert").stop();
alertEl.style.display = "block";
alertEl.style.opacity = 1;
if (timeout > 0) {
clearTimeout(this.alertTimeout);
this.alertTimeout = setTimeout(function(){
$("#alert").slideUp(100);
}, timeout);
}
}
/**
* Pops up a box asking the user a question and sending the answer to a specified callback function.
*
* @param {string} title - The title of the box
* @param {string} body - The question (HTML supported)
* @param {function} callback - A function accepting one boolean argument which handles the
* response e.g. function(answer) {...}
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Pops up a box asking if the user would like a cookie. Prints the answer to the console.
* this.confirm("Question", "Would you like a cookie?", function(answer) {console.log(answer);});
*/
confirm(title, body, callback, scope) {
scope = scope || this;
document.getElementById("confirm-title").innerHTML = title;
document.getElementById("confirm-body").innerHTML = body;
document.getElementById("confirm-modal").style.display = "block";
this.confirmClosed = false;
$("#confirm-modal").modal()
.one("show.bs.modal", function(e) {
this.confirmClosed = false;
}.bind(this))
.one("click", "#confirm-yes", function() {
this.confirmClosed = true;
callback.bind(scope)(true);
$("#confirm-modal").modal("hide");
}.bind(this))
.one("hide.bs.modal", function(e) {
if (!this.confirmClosed)
callback.bind(scope)(false);
this.confirmClosed = true;
}.bind(this));
}
/**
* Handler for the alert close button click event.
* Closes the alert box.
*/
alertCloseClick() {
document.getElementById("alert").style.display = "none";
}
/**
* Handler for CyerChef statechange events.
* Fires whenever the input or recipe changes in any way.
*
* @listens Manager#statechange
* @param {event} e
*/
stateChange(e) {
this.autoBake();
// Set title
const recipeConfig = this.getRecipeConfig();
let title = "CyberChef";
if (recipeConfig.length === 1) {
title = `${recipeConfig[0].op} - ${title}`;
} else if (recipeConfig.length > 1) {
// See how long the full recipe is
const ops = recipeConfig.map(op => op.op).join(", ");
if (ops.length < 45) {
title = `${ops} - ${title}`;
} else {
// If it's too long, just use the first one and say how many more there are
title = `${recipeConfig[0].op}, ${recipeConfig.length - 1} more - ${title}`;
}
}
document.title = title;
// Update the current history state (not creating a new one)
if (this.options.updateUrl) {
this.lastStateUrl = this.manager.controls.generateStateUrl(true, true, recipeConfig);
window.history.replaceState({}, title, this.lastStateUrl);
}
}
/**
* Handler for the history popstate event.
* Reloads parameters from the URL.
*
* @param {event} e
*/
popState(e) {
this.loadURIParams();
}
/**
* Function to call an external API from this view.
*/
callApi(url, type, data, dataType, contentType) {
type = type || "POST";
data = data || {};
dataType = dataType || undefined;
contentType = contentType || "application/json";
let response = null,
success = false;
$.ajax({
url: url,
async: false,
type: type,
data: data,
dataType: dataType,
contentType: contentType,
success: function(data) {
success = true;
response = data;
},
error: function(data) {
success = false;
response = data;
},
});
return {
success: success,
response: response
};
}
}
export default App;

View file

@ -1,217 +0,0 @@
/**
* Waiter to handle keybindings to CyberChef functions (i.e. Bake, Step, Save, Load etc.)
*
* @author Matt C [matt@artemisbot.uk]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const BindingsWaiter = function (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
*/
BindingsWaiter.prototype.parseInput = function(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.app.bake();
break;
case "Quote": // Step through
e.preventDefault();
this.app.bake(true);
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;
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
*
*/
BindingsWaiter.prototype.updateKeybList = function() {
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>
`;
};
export default BindingsWaiter;

224
src/web/BindingsWaiter.mjs Executable file
View file

@ -0,0 +1,224 @@
/**
* @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.app.bake();
break;
case "Quote": // Step through
e.preventDefault();
this.app.bake(true);
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;
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>
`;
}
}
export default BindingsWaiter;

View file

@ -1,441 +0,0 @@
import Utils from "../core/Utils";
import {toBase64} from "../core/lib/Base64";
/**
* Waiter to handle events related to the CyberChef controls (i.e. Bake, Step, Save, Load etc.)
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const ControlsWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
};
/**
* Adjusts the display properties of the control buttons so that they fit within the current width
* without wrapping or overflowing.
*/
ControlsWaiter.prototype.adjustWidth = function() {
const controls = document.getElementById("controls");
const step = document.getElementById("step");
const clrBreaks = document.getElementById("clr-breaks");
const saveImg = document.querySelector("#save img");
const loadImg = document.querySelector("#load img");
const stepImg = document.querySelector("#step img");
const clrRecipImg = document.querySelector("#clr-recipe img");
const clrBreaksImg = document.querySelector("#clr-breaks img");
if (controls.clientWidth < 470) {
step.childNodes[1].nodeValue = " Step";
} else {
step.childNodes[1].nodeValue = " Step through";
}
if (controls.clientWidth < 400) {
saveImg.style.display = "none";
loadImg.style.display = "none";
stepImg.style.display = "none";
clrRecipImg.style.display = "none";
clrBreaksImg.style.display = "none";
} else {
saveImg.style.display = "inline";
loadImg.style.display = "inline";
stepImg.style.display = "inline";
clrRecipImg.style.display = "inline";
clrBreaksImg.style.display = "inline";
}
if (controls.clientWidth < 330) {
clrBreaks.childNodes[1].nodeValue = " Clear breaks";
} else {
clrBreaks.childNodes[1].nodeValue = " Clear breakpoints";
}
};
/**
* Checks or unchecks the Auto Bake checkbox based on the given value.
*
* @param {boolean} value - The new value for Auto Bake.
*/
ControlsWaiter.prototype.setAutoBake = function(value) {
const autoBakeCheckbox = document.getElementById("auto-bake");
if (autoBakeCheckbox.checked !== value) {
autoBakeCheckbox.click();
}
};
/**
* Handler to trigger baking.
*/
ControlsWaiter.prototype.bakeClick = function() {
if (document.getElementById("bake").textContent.indexOf("Bake") > 0) {
this.app.bake();
} else {
this.manager.worker.cancelBake();
}
};
/**
* Handler for the 'Step through' command. Executes the next step of the recipe.
*/
ControlsWaiter.prototype.stepClick = function() {
this.app.bake(true);
};
/**
* Handler for changes made to the Auto Bake checkbox.
*/
ControlsWaiter.prototype.autoBakeChange = function() {
const autoBakeLabel = document.getElementById("auto-bake-label");
const autoBakeCheckbox = document.getElementById("auto-bake");
this.app.autoBake_ = autoBakeCheckbox.checked;
if (autoBakeCheckbox.checked) {
autoBakeLabel.classList.add("btn-success");
autoBakeLabel.classList.remove("btn-default");
} else {
autoBakeLabel.classList.add("btn-default");
autoBakeLabel.classList.remove("btn-success");
}
};
/**
* Handler for the 'Clear recipe' command. Removes all operations from the recipe.
*/
ControlsWaiter.prototype.clearRecipeClick = function() {
this.manager.recipe.clearRecipe();
};
/**
* Handler for the 'Clear breakpoints' command. Removes all breakpoints from operations in the
* recipe.
*/
ControlsWaiter.prototype.clearBreaksClick = function() {
const bps = document.querySelectorAll("#rec-list li.operation .breakpoint");
for (let i = 0; i < bps.length; i++) {
bps[i].setAttribute("break", "false");
bps[i].classList.remove("breakpoint-selected");
}
};
/**
* Populates the save disalog box with a URL incorporating the recipe and input.
*
* @param {Object[]} [recipeConfig] - The recipe configuration object array.
*/
ControlsWaiter.prototype.initialiseSaveLink = function(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 {Object[]} [recipeConfig] - The recipe configuration object array.
* @param {string} [baseURL] - The CyberChef URL, set to the current URL if not included
* @returns {string}
*/
ControlsWaiter.prototype.generateStateUrl = function(includeRecipe, includeInput, recipeConfig, baseURL) {
recipeConfig = recipeConfig || this.app.getRecipeConfig();
const link = baseURL || window.location.protocol + "//" +
window.location.host +
window.location.pathname;
const recipeStr = Utils.generatePrettyRecipe(recipeConfig);
const inputStr = toBase64(this.app.getInput(), "A-Za-z0-9+/"); // B64 alphabet with no padding
includeRecipe = includeRecipe && (recipeConfig.length > 0);
// Only inlcude input if it is less than 50KB (51200 * 4/3 as it is Base64 encoded)
includeInput = includeInput && (inputStr.length > 0) && (inputStr.length <= 68267);
const params = [
includeRecipe ? ["recipe", recipeStr] : undefined,
includeInput ? ["input", inputStr] : 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.
*/
ControlsWaiter.prototype.saveTextChange = function(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.
*/
ControlsWaiter.prototype.saveClick = function() {
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.
*/
ControlsWaiter.prototype.slrCheckChange = function() {
this.initialiseSaveLink();
};
/**
* Handler for the save link input checkbox change event.
*/
ControlsWaiter.prototype.sliCheckChange = function() {
this.initialiseSaveLink();
};
/**
* Handler for the 'Load' command. Pops up the load dialog box.
*/
ControlsWaiter.prototype.loadClick = function() {
this.populateLoadRecipesList();
$("#load-modal").modal();
};
/**
* Saves the recipe specified in the save textarea to local storage.
*/
ControlsWaiter.prototype.saveButtonClick = function() {
if (!this.app.isLocalStorageAvailable()) {
this.app.alert(
"Your security settings do not allow access to local storage so your recipe cannot be saved.",
"danger",
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", "danger", 2000);
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 + "\".", "success", 2000);
};
/**
* Populates the list of saved recipes in the load dialog box from local storage.
*/
ControlsWaiter.prototype.populateLoadRecipesList = function() {
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
document.getElementById("load-text").value = savedRecipes.length ? savedRecipes[0].recipe : "";
};
/**
* Removes the currently selected recipe from local storage.
*/
ControlsWaiter.prototype.loadDeleteClick = function() {
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.
*/
ControlsWaiter.prototype.loadNameChange = function(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.
*/
ControlsWaiter.prototype.loadButtonClick = function() {
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", "danger", 2000);
}
};
/**
* Populates the bug report information box with useful technical info.
*
* @param {event} e
*/
ControlsWaiter.prototype.supportButtonClick = function(e) {
e.preventDefault();
const reportBugInfo = document.getElementById("report-bug-info");
const saveLink = this.generateStateUrl(true, true, null, "https://gchq.github.io/CyberChef/");
if (reportBugInfo) {
reportBugInfo.innerHTML = `* Version: ${PKG_VERSION + (typeof INLINE === "undefined" ? "" : "s")}
* 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.
*/
ControlsWaiter.prototype.showStaleIndicator = function() {
const staleIndicator = document.getElementById("stale-indicator");
staleIndicator.style.visibility = "visible";
staleIndicator.style.opacity = 1;
};
/**
* Hides the stale indicator to show that the input or recipe has not changed
* since the last bake.
*/
ControlsWaiter.prototype.hideStaleIndicator = function() {
const staleIndicator = document.getElementById("stale-indicator");
staleIndicator.style.opacity = 0;
staleIndicator.style.visibility = "hidden";
};
/**
* Switches the Bake button between 'Bake' and 'Cancel' functions.
*
* @param {boolean} cancel - Whether to change to cancel or not
*/
ControlsWaiter.prototype.toggleBakeButtonFunction = function(cancel) {
const bakeButton = document.getElementById("bake"),
btnText = bakeButton.querySelector("span");
if (cancel) {
btnText.innerText = "Cancel";
bakeButton.classList.remove("btn-success");
bakeButton.classList.add("btn-danger");
} else {
btnText.innerText = "Bake!";
bakeButton.classList.remove("btn-danger");
bakeButton.classList.add("btn-success");
}
};
export default ControlsWaiter;

449
src/web/ControlsWaiter.mjs Executable file
View file

@ -0,0 +1,449 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import Utils from "../core/Utils";
import {toBase64} from "../core/lib/Base64";
/**
* 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;
}
/**
* Adjusts the display properties of the control buttons so that they fit within the current width
* without wrapping or overflowing.
*/
adjustWidth() {
const controls = document.getElementById("controls");
const step = document.getElementById("step");
const clrBreaks = document.getElementById("clr-breaks");
const saveImg = document.querySelector("#save img");
const loadImg = document.querySelector("#load img");
const stepImg = document.querySelector("#step img");
const clrRecipImg = document.querySelector("#clr-recipe img");
const clrBreaksImg = document.querySelector("#clr-breaks img");
if (controls.clientWidth < 470) {
step.childNodes[1].nodeValue = " Step";
} else {
step.childNodes[1].nodeValue = " Step through";
}
if (controls.clientWidth < 400) {
saveImg.style.display = "none";
loadImg.style.display = "none";
stepImg.style.display = "none";
clrRecipImg.style.display = "none";
clrBreaksImg.style.display = "none";
} else {
saveImg.style.display = "inline";
loadImg.style.display = "inline";
stepImg.style.display = "inline";
clrRecipImg.style.display = "inline";
clrBreaksImg.style.display = "inline";
}
if (controls.clientWidth < 330) {
clrBreaks.childNodes[1].nodeValue = " Clear breaks";
} else {
clrBreaks.childNodes[1].nodeValue = " Clear breakpoints";
}
}
/**
* 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() {
if (document.getElementById("bake").textContent.indexOf("Bake") > 0) {
this.app.bake();
} else {
this.manager.worker.cancelBake();
}
}
/**
* Handler for the 'Step through' command. Executes the next step of the recipe.
*/
stepClick() {
this.app.bake(true);
}
/**
* Handler for changes made to the Auto Bake checkbox.
*/
autoBakeChange() {
const autoBakeLabel = document.getElementById("auto-bake-label");
const autoBakeCheckbox = document.getElementById("auto-bake");
this.app.autoBake_ = autoBakeCheckbox.checked;
if (autoBakeCheckbox.checked) {
autoBakeLabel.classList.add("btn-success");
autoBakeLabel.classList.remove("btn-default");
} else {
autoBakeLabel.classList.add("btn-default");
autoBakeLabel.classList.remove("btn-success");
}
}
/**
* Handler for the 'Clear recipe' command. Removes all operations from the recipe.
*/
clearRecipeClick() {
this.manager.recipe.clearRecipe();
}
/**
* Handler for the 'Clear breakpoints' command. Removes all breakpoints from operations in the
* recipe.
*/
clearBreaksClick() {
const bps = document.querySelectorAll("#rec-list li.operation .breakpoint");
for (let i = 0; i < bps.length; i++) {
bps[i].setAttribute("break", "false");
bps[i].classList.remove("breakpoint-selected");
}
}
/**
* Populates the save disalog 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 {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, recipeConfig, baseURL) {
recipeConfig = recipeConfig || this.app.getRecipeConfig();
const link = baseURL || window.location.protocol + "//" +
window.location.host +
window.location.pathname;
const recipeStr = Utils.generatePrettyRecipe(recipeConfig);
const inputStr = toBase64(this.app.getInput(), "A-Za-z0-9+/"); // B64 alphabet with no padding
includeRecipe = includeRecipe && (recipeConfig.length > 0);
// Only inlcude input if it is less than 50KB (51200 * 4/3 as it is Base64 encoded)
includeInput = includeInput && (inputStr.length > 0) && (inputStr.length <= 68267);
const params = [
includeRecipe ? ["recipe", recipeStr] : undefined,
includeInput ? ["input", inputStr] : 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.",
"danger",
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", "danger", 2000);
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 + "\".", "success", 2000);
}
/**
* 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
document.getElementById("load-text").value = savedRecipes.length ? savedRecipes[0].recipe : "";
}
/**
* 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", "danger", 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, "https://gchq.github.io/CyberChef/");
if (reportBugInfo) {
reportBugInfo.innerHTML = `* Version: ${PKG_VERSION + (typeof INLINE === "undefined" ? "" : "s")}
* 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.style.visibility = "visible";
staleIndicator.style.opacity = 1;
}
/**
* 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.style.opacity = 0;
staleIndicator.style.visibility = "hidden";
}
/**
* Switches the Bake button between 'Bake' and 'Cancel' functions.
*
* @param {boolean} cancel - Whether to change to cancel or not
*/
toggleBakeButtonFunction(cancel) {
const bakeButton = document.getElementById("bake"),
btnText = bakeButton.querySelector("span");
if (cancel) {
btnText.innerText = "Cancel";
bakeButton.classList.remove("btn-success");
bakeButton.classList.add("btn-danger");
} else {
btnText.innerText = "Bake!";
bakeButton.classList.remove("btn-danger");
bakeButton.classList.add("btn-success");
}
}
}
export default ControlsWaiter;

View file

@ -1,52 +0,0 @@
/**
* Object to handle the creation of operation categories.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {string} name - The name of the category.
* @param {boolean} selected - Whether this category is pre-selected or not.
*/
const HTMLCategory = function(name, selected) {
this.name = name;
this.selected = selected;
this.opList = [];
};
/**
* Adds an operation to this category.
*
* @param {HTMLOperation} operation - The operation to add.
*/
HTMLCategory.prototype.addOperation = function(operation) {
this.opList.push(operation);
};
/**
* Renders the category and all operations within it in HTML.
*
* @returns {string}
*/
HTMLCategory.prototype.toHtml = function() {
const catName = "cat" + this.name.replace(/[\s/-:_]/g, "");
let html = "<div class='panel category'>\
<a class='category-title' data-toggle='collapse'\
data-parent='#categories' href='#" + catName + "'>\
" + this.name + "\
</a>\
<div id='" + catName + "' class='panel-collapse collapse\
" + (this.selected ? " in" : "") + "'><ul class='op-list'>";
for (let i = 0; i < this.opList.length; i++) {
html += this.opList[i].toStubHtml();
}
html += "</ul></div></div>";
return html;
};
export default HTMLCategory;

60
src/web/HTMLCategory.mjs Executable file
View file

@ -0,0 +1,60 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
/**
* Object to handle the creation of operation categories.
*/
class HTMLCategory {
/**
* HTMLCategory constructor.
*
* @param {string} name - The name of the category.
* @param {boolean} selected - Whether this category is pre-selected or not.
*/
constructor(name, selected) {
this.name = name;
this.selected = selected;
this.opList = [];
}
/**
* Adds an operation to this category.
*
* @param {HTMLOperation} operation - The operation to add.
*/
addOperation(operation) {
this.opList.push(operation);
}
/**
* Renders the category and all operations within it in HTML.
*
* @returns {string}
*/
toHtml() {
const catName = "cat" + this.name.replace(/[\s/-:_]/g, "");
let html = "<div class='panel category'>\
<a class='category-title' data-toggle='collapse'\
data-parent='#categories' href='#" + catName + "'>\
" + this.name + "\
</a>\
<div id='" + catName + "' class='panel-collapse collapse\
" + (this.selected ? " in" : "") + "'><ul class='op-list'>";
for (let i = 0; i < this.opList.length; i++) {
html += this.opList[i].toStubHtml();
}
html += "</ul></div></div>";
return html;
}
}
export default HTMLCategory;

View file

@ -1,215 +0,0 @@
/**
* Object to handle the creation of operation ingredients.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {Object} config - The configuration object for this ingredient.
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const HTMLIngredient = function(config, app, manager) {
this.app = app;
this.manager = manager;
this.name = config.name;
this.type = config.type;
this.value = config.value;
this.disabled = config.disabled || false;
this.disableArgs = config.disableArgs || false;
this.placeholder = config.placeholder || false;
this.target = config.target;
this.toggleValues = config.toggleValues;
this.id = "ing-" + this.app.nextIngId();
};
/**
* Renders the ingredient in HTML.
*
* @returns {string}
*/
HTMLIngredient.prototype.toHtml = function() {
const inline = (
this.type === "boolean" ||
this.type === "number" ||
this.type === "option" ||
this.type === "shortString" ||
this.type === "binaryShortString"
);
let html = inline ? "" : "<div class='clearfix'>&nbsp;</div>",
i, m;
html += "<div class='arg-group" + (inline ? " inline-args" : "") +
(this.type === "text" ? " arg-group-text" : "") + "'><label class='arg-label' for='" +
this.id + "'>" + this.name + "</label>";
switch (this.type) {
case "string":
case "binaryString":
case "byteArray":
html += "<input type='text' id='" + this.id + "' class='arg arg-input' arg-name='" +
this.name + "' value='" + this.value + "'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
break;
case "shortString":
case "binaryShortString":
html += "<input type='text' id='" + this.id +
"'class='arg arg-input short-string' arg-name='" + this.name + "'value='" +
this.value + "'" + (this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
break;
case "toggleString":
html += "<div class='input-group'><div class='input-group-btn'>\
<button type='button' class='btn btn-default dropdown-toggle' data-toggle='dropdown'\
aria-haspopup='true' aria-expanded='false'" +
(this.disabled ? " disabled='disabled'" : "") + ">" + this.toggleValues[0] +
" <span class='caret'></span></button><ul class='dropdown-menu'>";
for (i = 0; i < this.toggleValues.length; i++) {
html += "<li><a href='#'>" + this.toggleValues[i] + "</a></li>";
}
html += "</ul></div><input type='text' class='arg arg-input toggle-string'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + "></div>";
break;
case "number":
html += "<input type='number' id='" + this.id + "'class='arg arg-input' arg-name='" +
this.name + "'value='" + this.value + "'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
break;
case "boolean":
html += "<input type='checkbox' id='" + this.id + "'class='arg' arg-name='" +
this.name + "'" + (this.value ? " checked='checked' " : "") +
(this.disabled ? " disabled='disabled'" : "") + ">";
if (this.disableArgs) {
this.manager.addDynamicListener("#" + this.id, "click", this.toggleDisableArgs, this);
}
break;
case "option":
html += "<select class='arg' id='" + this.id + "'arg-name='" + this.name + "'" +
(this.disabled ? " disabled='disabled'" : "") + ">";
for (i = 0; i < this.value.length; i++) {
if ((m = this.value[i].match(/\[([a-z0-9 -()^]+)\]/i))) {
html += "<optgroup label='" + m[1] + "'>";
} else if ((m = this.value[i].match(/\[\/([a-z0-9 -()^]+)\]/i))) {
html += "</optgroup>";
} else {
html += "<option>" + this.value[i] + "</option>";
}
}
html += "</select>";
break;
case "populateOption":
html += "<select class='arg' id='" + this.id + "'arg-name='" + this.name + "'" +
(this.disabled ? " disabled='disabled'" : "") + ">";
for (i = 0; i < this.value.length; i++) {
if ((m = this.value[i].name.match(/\[([a-z0-9 -()^]+)\]/i))) {
html += "<optgroup label='" + m[1] + "'>";
} else if ((m = this.value[i].name.match(/\[\/([a-z0-9 -()^]+)\]/i))) {
html += "</optgroup>";
} else {
html += "<option populate-value='" + this.value[i].value + "'>" +
this.value[i].name + "</option>";
}
}
html += "</select>";
this.manager.addDynamicListener("#" + this.id, "change", this.populateOptionChange, this);
break;
case "editableOption":
html += "<div class='editable-option'>";
html += "<select class='editable-option-select' id='sel-" + this.id + "'" +
(this.disabled ? " disabled='disabled'" : "") + ">";
for (i = 0; i < this.value.length; i++) {
html += "<option value='" + this.value[i].value + "'>" + this.value[i].name + "</option>";
}
html += "</select>";
html += "<input class='arg arg-input editable-option-input' id='" + this.id +
"'arg-name='" + this.name + "'" + " value='" + this.value[0].value + "'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
html += "</div>";
this.manager.addDynamicListener("#sel-" + this.id, "change", this.editableOptionChange, this);
break;
case "text":
html += "<textarea id='" + this.id + "' class='arg' arg-name='" +
this.name + "'" + (this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">" +
this.value + "</textarea>";
break;
default:
break;
}
html += "</div>";
return html;
};
/**
* Handler for argument disable toggle.
* Toggles disabled state for all arguments in the disableArgs list for this ingredient.
*
* @param {event} e
*/
HTMLIngredient.prototype.toggleDisableArgs = function(e) {
const el = e.target;
const op = el.parentNode.parentNode;
const args = op.querySelectorAll(".arg-group");
for (let i = 0; i < this.disableArgs.length; i++) {
const els = args[this.disableArgs[i]].querySelectorAll("input, select, button");
for (let j = 0; j < els.length; j++) {
if (els[j].getAttribute("disabled")) {
els[j].removeAttribute("disabled");
} else {
els[j].setAttribute("disabled", "disabled");
}
}
}
this.manager.recipe.ingChange();
};
/**
* Handler for populate option changes.
* Populates the relevant argument with the specified value.
*
* @param {event} e
*/
HTMLIngredient.prototype.populateOptionChange = function(e) {
const el = e.target;
const op = el.parentNode.parentNode;
const target = op.querySelectorAll(".arg-group")[this.target].querySelector("input, select, textarea");
target.value = el.childNodes[el.selectedIndex].getAttribute("populate-value");
this.manager.recipe.ingChange();
};
/**
* Handler for editable option changes.
* Populates the input box with the selected value.
*
* @param {event} e
*/
HTMLIngredient.prototype.editableOptionChange = function(e) {
const select = e.target,
input = select.nextSibling;
input.value = select.childNodes[select.selectedIndex].value;
this.manager.recipe.ingChange();
};
export default HTMLIngredient;

223
src/web/HTMLIngredient.mjs Executable file
View file

@ -0,0 +1,223 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
/**
* Object to handle the creation of operation ingredients.
*/
class HTMLIngredient {
/**
* HTMLIngredient constructor.
*
* @param {Object} config - The configuration object for this ingredient.
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
constructor(config, app, manager) {
this.app = app;
this.manager = manager;
this.name = config.name;
this.type = config.type;
this.value = config.value;
this.disabled = config.disabled || false;
this.disableArgs = config.disableArgs || false;
this.placeholder = config.placeholder || false;
this.target = config.target;
this.toggleValues = config.toggleValues;
this.id = "ing-" + this.app.nextIngId();
}
/**
* Renders the ingredient in HTML.
*
* @returns {string}
*/
toHtml() {
const inline = (
this.type === "boolean" ||
this.type === "number" ||
this.type === "option" ||
this.type === "shortString" ||
this.type === "binaryShortString"
);
let html = inline ? "" : "<div class='clearfix'>&nbsp;</div>",
i, m;
html += "<div class='arg-group" + (inline ? " inline-args" : "") +
(this.type === "text" ? " arg-group-text" : "") + "'><label class='arg-label' for='" +
this.id + "'>" + this.name + "</label>";
switch (this.type) {
case "string":
case "binaryString":
case "byteArray":
html += "<input type='text' id='" + this.id + "' class='arg arg-input' arg-name='" +
this.name + "' value='" + this.value + "'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
break;
case "shortString":
case "binaryShortString":
html += "<input type='text' id='" + this.id +
"'class='arg arg-input short-string' arg-name='" + this.name + "'value='" +
this.value + "'" + (this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
break;
case "toggleString":
html += "<div class='input-group'><div class='input-group-btn'>\
<button type='button' class='btn btn-default dropdown-toggle' data-toggle='dropdown'\
aria-haspopup='true' aria-expanded='false'" +
(this.disabled ? " disabled='disabled'" : "") + ">" + this.toggleValues[0] +
" <span class='caret'></span></button><ul class='dropdown-menu'>";
for (i = 0; i < this.toggleValues.length; i++) {
html += "<li><a href='#'>" + this.toggleValues[i] + "</a></li>";
}
html += "</ul></div><input type='text' class='arg arg-input toggle-string'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + "></div>";
break;
case "number":
html += "<input type='number' id='" + this.id + "'class='arg arg-input' arg-name='" +
this.name + "'value='" + this.value + "'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
break;
case "boolean":
html += "<input type='checkbox' id='" + this.id + "'class='arg' arg-name='" +
this.name + "'" + (this.value ? " checked='checked' " : "") +
(this.disabled ? " disabled='disabled'" : "") + ">";
if (this.disableArgs) {
this.manager.addDynamicListener("#" + this.id, "click", this.toggleDisableArgs, this);
}
break;
case "option":
html += "<select class='arg' id='" + this.id + "'arg-name='" + this.name + "'" +
(this.disabled ? " disabled='disabled'" : "") + ">";
for (i = 0; i < this.value.length; i++) {
if ((m = this.value[i].match(/\[([a-z0-9 -()^]+)\]/i))) {
html += "<optgroup label='" + m[1] + "'>";
} else if ((m = this.value[i].match(/\[\/([a-z0-9 -()^]+)\]/i))) {
html += "</optgroup>";
} else {
html += "<option>" + this.value[i] + "</option>";
}
}
html += "</select>";
break;
case "populateOption":
html += "<select class='arg' id='" + this.id + "'arg-name='" + this.name + "'" +
(this.disabled ? " disabled='disabled'" : "") + ">";
for (i = 0; i < this.value.length; i++) {
if ((m = this.value[i].name.match(/\[([a-z0-9 -()^]+)\]/i))) {
html += "<optgroup label='" + m[1] + "'>";
} else if ((m = this.value[i].name.match(/\[\/([a-z0-9 -()^]+)\]/i))) {
html += "</optgroup>";
} else {
html += "<option populate-value='" + this.value[i].value + "'>" +
this.value[i].name + "</option>";
}
}
html += "</select>";
this.manager.addDynamicListener("#" + this.id, "change", this.populateOptionChange, this);
break;
case "editableOption":
html += "<div class='editable-option'>";
html += "<select class='editable-option-select' id='sel-" + this.id + "'" +
(this.disabled ? " disabled='disabled'" : "") + ">";
for (i = 0; i < this.value.length; i++) {
html += "<option value='" + this.value[i].value + "'>" + this.value[i].name + "</option>";
}
html += "</select>";
html += "<input class='arg arg-input editable-option-input' id='" + this.id +
"'arg-name='" + this.name + "'" + " value='" + this.value[0].value + "'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
html += "</div>";
this.manager.addDynamicListener("#sel-" + this.id, "change", this.editableOptionChange, this);
break;
case "text":
html += "<textarea id='" + this.id + "' class='arg' arg-name='" +
this.name + "'" + (this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">" +
this.value + "</textarea>";
break;
default:
break;
}
html += "</div>";
return html;
}
/**
* Handler for argument disable toggle.
* Toggles disabled state for all arguments in the disableArgs list for this ingredient.
*
* @param {event} e
*/
toggleDisableArgs(e) {
const el = e.target;
const op = el.parentNode.parentNode;
const args = op.querySelectorAll(".arg-group");
for (let i = 0; i < this.disableArgs.length; i++) {
const els = args[this.disableArgs[i]].querySelectorAll("input, select, button");
for (let j = 0; j < els.length; j++) {
if (els[j].getAttribute("disabled")) {
els[j].removeAttribute("disabled");
} else {
els[j].setAttribute("disabled", "disabled");
}
}
}
this.manager.recipe.ingChange();
}
/**
* Handler for populate option changes.
* Populates the relevant argument with the specified value.
*
* @param {event} e
*/
populateOptionChange(e) {
const el = e.target;
const op = el.parentNode.parentNode;
const target = op.querySelectorAll(".arg-group")[this.target].querySelector("input, select, textarea");
target.value = el.childNodes[el.selectedIndex].getAttribute("populate-value");
this.manager.recipe.ingChange();
}
/**
* Handler for editable option changes.
* Populates the input box with the selected value.
*
* @param {event} e
*/
editableOptionChange(e) {
const select = e.target,
input = select.nextSibling;
input.value = select.childNodes[select.selectedIndex].value;
this.manager.recipe.ingChange();
}
}
export default HTMLIngredient;

View file

@ -1,128 +0,0 @@
import HTMLIngredient from "./HTMLIngredient.js";
/**
* Object to handle the creation of operations.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {string} name - The name of the operation.
* @param {Object} config - The configuration object for this operation.
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const HTMLOperation = function(name, config, app, manager) {
this.app = app;
this.manager = manager;
this.name = name;
this.description = config.description;
this.manualBake = config.manualBake || false;
this.config = config;
this.ingList = [];
for (let i = 0; i < config.args.length; i++) {
const ing = new HTMLIngredient(config.args[i], this.app, this.manager);
this.ingList.push(ing);
}
};
/**
* @constant
*/
HTMLOperation.INFO_ICON = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAByElEQVR4XqVTzWoaYRQ9KZJmoVaS1J1QiYTIuOgqi9lEugguQhYhdGs3hTyAi0CWJTvJIks30ZBNsimUtlqkVLoQCuJsphRriyFjabWtEyf/Rv3iWcwwymTlgQuH851z5hu43wRGkEwmXwCIA4hiGAUAmUQikQbhEHwyGCWVSglVVUW73RYmyKnxjB56ncJ6NpsVxHGrI/ZLuniVb3DIqQmCHnrNkgcggNeSJPlisRgyJR2b737j/TcDsQUPwv6H5NR4BnroZcb6Z16N2PvyX6yna9Z8qp6JQ0Uf0ughmGHWBSAuyzJqrQ7eqKewY/dzE363C71e39LoWQq5wUwul4uzIBoIBHD01RgyrkZ8eDbvwUWnj623v2DHx4qB51IAzLIAXq8XP/7W0bUVVJtXWIk8wvlN364TA+/1IDMLwmWK/Hq3axmhaBdoGLeklm73ElaBYRgIzkyifHIOO4QQJKM3oJcZq6CgaVp0OTyHw9K/kQI4FiyHfdC0n2CWe5ApFosIPZ7C2tNpXpcDOehGyD/FIbd0euhlhllzFxRzC3fydbG4XRYbB9/tQ41n9m1U7l3lyp9LkfygiZeZCoecmtMqj/+Yxn7Od3v0j50qCO3zAAAAAElFTkSuQmCC";
/**
* @constant
*/
HTMLOperation.REMOVE_ICON = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABwklEQVR42qRTPU8CQRB9K2CCMRJ6NTQajOUaqfxIbLCRghhjQixosLAgFNBQ3l8wsabxLxBJbCyVUBiMCVQEQkOEKBbCnefM3p4eohWXzM3uvHlv52b2hG3bmOWZw4yPn1/XQkCQ9wFxcgZZ0QLKpifpN8Z1n1L13griBBjHhYK0nMT4b+wom53ClAAFQacZJ/m8rNfrSOZy0vxJjPP6IJ2WzWYTO6mUwiwtILiJJSHUKVSWkchkZK1WQzQaxU2pVGUglkjIbreLUCiEx0qlStlFCpfPiPstYDtVKJH9ZFI2Gw1FGA6H6LTbCAaDeGu1FJl6UuYjpwTGzucokZW1NfnS66kyfT4fXns9RaZmlgNcuhZQU+jowLzuOK/HgwEW3E5ZlhLXVWKk11P3wNYNWw+HZdA0sUgx1zjGmD05nckx0ilGjBJdUq3fr7K5e8bGf43RdL7fOPSQb4lI8SLbrUfkUIuY32VTI1bJn5BqDnh4Dodt9ryPUDzyD7aquWoKQohl2i9sAbubwPkTcHkP3FHsg+yT+7sN7G0AF3Xg6sHB3onbdgWWKBDQg/BcTuVt51dQA/JrnIcyIu6rmPV3/hJgACPc0BMEYTg+AAAAAElFTkSuQmCC";
/**
* Renders the operation in HTML as a stub operation with no ingredients.
*
* @returns {string}
*/
HTMLOperation.prototype.toStubHtml = function(removeIcon) {
let html = "<li class='operation'";
if (this.description) {
html += " data-container='body' data-toggle='popover' data-placement='auto right'\
data-content=\"" + this.description + "\" data-html='true' data-trigger='hover'";
}
html += ">" + this.name;
if (removeIcon) {
html += "<img src='data:image/png;base64," + HTMLOperation.REMOVE_ICON +
"' class='op-icon remove-icon'>";
}
if (this.description) {
html += "<img src='data:image/png;base64," + HTMLOperation.INFO_ICON + "' class='op-icon'>";
}
html += "</li>";
return html;
};
/**
* Renders the operation in HTML as a full operation with ingredients.
*
* @returns {string}
*/
HTMLOperation.prototype.toFullHtml = function() {
let html = "<div class='arg-title'>" + this.name + "</div>";
for (let i = 0; i < this.ingList.length; i++) {
html += this.ingList[i].toHtml();
}
html += "<div class='recip-icons'>\
<div class='breakpoint' title='Set breakpoint' break='false'></div>\
<div class='disable-icon recip-icon' title='Disable operation'\
disabled='false'></div>";
html += "</div>\
<div class='clearfix'>&nbsp;</div>";
return html;
};
/**
* Highlights the searched string in the name and description of the operation.
*
* @param {string} searchStr
* @param {number} namePos - The position of the search string in the operation name
* @param {number} descPos - The position of the search string in the operation description
*/
HTMLOperation.prototype.highlightSearchString = function(searchStr, namePos, descPos) {
if (namePos >= 0) {
this.name = this.name.slice(0, namePos) + "<b><u>" +
this.name.slice(namePos, namePos + searchStr.length) + "</u></b>" +
this.name.slice(namePos + searchStr.length);
}
if (this.description && descPos >= 0) {
// Find HTML tag offsets
const re = /<[^>]+>/g;
let match;
while ((match = re.exec(this.description))) {
// If the search string occurs within an HTML tag, return without highlighting it.
if (descPos >= match.index && descPos <= (match.index + match[0].length))
return;
}
this.description = this.description.slice(0, descPos) + "<b><u>" +
this.description.slice(descPos, descPos + searchStr.length) + "</u></b>" +
this.description.slice(descPos + searchStr.length);
}
};
export default HTMLOperation;

129
src/web/HTMLOperation.mjs Executable file
View file

@ -0,0 +1,129 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import HTMLIngredient from "./HTMLIngredient";
const INFO_ICON = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAByElEQVR4XqVTzWoaYRQ9KZJmoVaS1J1QiYTIuOgqi9lEugguQhYhdGs3hTyAi0CWJTvJIks30ZBNsimUtlqkVLoQCuJsphRriyFjabWtEyf/Rv3iWcwwymTlgQuH851z5hu43wRGkEwmXwCIA4hiGAUAmUQikQbhEHwyGCWVSglVVUW73RYmyKnxjB56ncJ6NpsVxHGrI/ZLuniVb3DIqQmCHnrNkgcggNeSJPlisRgyJR2b737j/TcDsQUPwv6H5NR4BnroZcb6Z16N2PvyX6yna9Z8qp6JQ0Uf0ughmGHWBSAuyzJqrQ7eqKewY/dzE363C71e39LoWQq5wUwul4uzIBoIBHD01RgyrkZ8eDbvwUWnj623v2DHx4qB51IAzLIAXq8XP/7W0bUVVJtXWIk8wvlN364TA+/1IDMLwmWK/Hq3axmhaBdoGLeklm73ElaBYRgIzkyifHIOO4QQJKM3oJcZq6CgaVp0OTyHw9K/kQI4FiyHfdC0n2CWe5ApFosIPZ7C2tNpXpcDOehGyD/FIbd0euhlhllzFxRzC3fydbG4XRYbB9/tQ41n9m1U7l3lyp9LkfygiZeZCoecmtMqj/+Yxn7Od3v0j50qCO3zAAAAAElFTkSuQmCC";
const REMOVE_ICON = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABwklEQVR42qRTPU8CQRB9K2CCMRJ6NTQajOUaqfxIbLCRghhjQixosLAgFNBQ3l8wsabxLxBJbCyVUBiMCVQEQkOEKBbCnefM3p4eohWXzM3uvHlv52b2hG3bmOWZw4yPn1/XQkCQ9wFxcgZZ0QLKpifpN8Z1n1L13griBBjHhYK0nMT4b+wom53ClAAFQacZJ/m8rNfrSOZy0vxJjPP6IJ2WzWYTO6mUwiwtILiJJSHUKVSWkchkZK1WQzQaxU2pVGUglkjIbreLUCiEx0qlStlFCpfPiPstYDtVKJH9ZFI2Gw1FGA6H6LTbCAaDeGu1FJl6UuYjpwTGzucokZW1NfnS66kyfT4fXns9RaZmlgNcuhZQU+jowLzuOK/HgwEW3E5ZlhLXVWKk11P3wNYNWw+HZdA0sUgx1zjGmD05nckx0ilGjBJdUq3fr7K5e8bGf43RdL7fOPSQb4lI8SLbrUfkUIuY32VTI1bJn5BqDnh4Dodt9ryPUDzyD7aquWoKQohl2i9sAbubwPkTcHkP3FHsg+yT+7sN7G0AF3Xg6sHB3onbdgWWKBDQg/BcTuVt51dQA/JrnIcyIu6rmPV3/hJgACPc0BMEYTg+AAAAAElFTkSuQmCC";
/**
* Object to handle the creation of operations.
*/
class HTMLOperation {
/**
* HTMLOperation constructor.
*
* @param {string} name - The name of the operation.
* @param {Object} config - The configuration object for this operation.
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
constructor(name, config, app, manager) {
this.app = app;
this.manager = manager;
this.name = name;
this.description = config.description;
this.manualBake = config.manualBake || false;
this.config = config;
this.ingList = [];
for (let i = 0; i < config.args.length; i++) {
const ing = new HTMLIngredient(config.args[i], this.app, this.manager);
this.ingList.push(ing);
}
}
/**
* Renders the operation in HTML as a stub operation with no ingredients.
*
* @returns {string}
*/
toStubHtml(removeIcon) {
let html = "<li class='operation'";
if (this.description) {
html += " data-container='body' data-toggle='popover' data-placement='auto right'\
data-content=\"" + this.description + "\" data-html='true' data-trigger='hover'";
}
html += ">" + this.name;
if (removeIcon) {
html += "<img src='data:image/png;base64," + REMOVE_ICON +
"' class='op-icon remove-icon'>";
}
if (this.description) {
html += "<img src='data:image/png;base64," + INFO_ICON + "' class='op-icon'>";
}
html += "</li>";
return html;
}
/**
* Renders the operation in HTML as a full operation with ingredients.
*
* @returns {string}
*/
toFullHtml() {
let html = "<div class='arg-title'>" + this.name + "</div>";
for (let i = 0; i < this.ingList.length; i++) {
html += this.ingList[i].toHtml();
}
html += "<div class='recip-icons'>\
<div class='breakpoint' title='Set breakpoint' break='false'></div>\
<div class='disable-icon recip-icon' title='Disable operation'\
disabled='false'></div>";
html += "</div>\
<div class='clearfix'>&nbsp;</div>";
return html;
}
/**
* Highlights the searched string in the name and description of the operation.
*
* @param {string} searchStr
* @param {number} namePos - The position of the search string in the operation name
* @param {number} descPos - The position of the search string in the operation description
*/
highlightSearchString(searchStr, namePos, descPos) {
if (namePos >= 0) {
this.name = this.name.slice(0, namePos) + "<b><u>" +
this.name.slice(namePos, namePos + searchStr.length) + "</u></b>" +
this.name.slice(namePos + searchStr.length);
}
if (this.description && descPos >= 0) {
// Find HTML tag offsets
const re = /<[^>]+>/g;
let match;
while ((match = re.exec(this.description))) {
// If the search string occurs within an HTML tag, return without highlighting it.
if (descPos >= match.index && descPos <= (match.index + match[0].length))
return;
}
this.description = this.description.slice(0, descPos) + "<b><u>" +
this.description.slice(descPos, descPos + searchStr.length) + "</u></b>" +
this.description.slice(descPos + searchStr.length);
}
}
}
export default HTMLOperation;

View file

@ -1,461 +0,0 @@
/**
* Waiter to handle events related to highlighting in CyberChef.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const HighlighterWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
this.mouseButtonDown = false;
this.mouseTarget = null;
};
/**
* HighlighterWaiter data type enum for the input.
* @readonly
* @enum
*/
HighlighterWaiter.INPUT = 0;
/**
* HighlighterWaiter data type enum for the output.
* @readonly
* @enum
*/
HighlighterWaiter.OUTPUT = 1;
/**
* Determines if the current text selection is running backwards or forwards.
* StackOverflow answer id: 12652116
*
* @private
* @returns {boolean}
*/
HighlighterWaiter.prototype._isSelectionBackwards = function() {
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}
*/
HighlighterWaiter.prototype._getOutputHtmlOffset = function(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
*/
HighlighterWaiter.prototype._getOutputHtmlSelectionOffsets = function() {
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
*/
HighlighterWaiter.prototype.inputScroll = function(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
*/
HighlighterWaiter.prototype.outputScroll = function(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
*/
HighlighterWaiter.prototype.inputMousedown = function(e) {
this.mouseButtonDown = true;
this.mouseTarget = HighlighterWaiter.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
*/
HighlighterWaiter.prototype.outputMousedown = function(e) {
this.mouseButtonDown = true;
this.mouseTarget = HighlighterWaiter.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
*/
HighlighterWaiter.prototype.outputHtmlMousedown = function(e) {
this.mouseButtonDown = true;
this.mouseTarget = HighlighterWaiter.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
*/
HighlighterWaiter.prototype.inputMouseup = function(e) {
this.mouseButtonDown = false;
};
/**
* Handler for output mouseup events.
*
* @param {event} e
*/
HighlighterWaiter.prototype.outputMouseup = function(e) {
this.mouseButtonDown = false;
};
/**
* Handler for output HTML mouseup events.
*
* @param {event} e
*/
HighlighterWaiter.prototype.outputHtmlMouseup = function(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
*/
HighlighterWaiter.prototype.inputMousemove = function(e) {
// Check that the left mouse button is pressed
if (!this.mouseButtonDown ||
e.which !== 1 ||
this.mouseTarget !== HighlighterWaiter.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
*/
HighlighterWaiter.prototype.outputMousemove = function(e) {
// Check that the left mouse button is pressed
if (!this.mouseButtonDown ||
e.which !== 1 ||
this.mouseTarget !== HighlighterWaiter.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
*/
HighlighterWaiter.prototype.outputHtmlMousemove = function(e) {
// Check that the left mouse button is pressed
if (!this.mouseButtonDown ||
e.which !== 1 ||
this.mouseTarget !== HighlighterWaiter.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}
*/
HighlighterWaiter.prototype.selectionInfo = function(start, end) {
const len = end.toString().length;
const width = len < 2 ? 2 : len;
const startStr = start.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
const endStr = end.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
const lenStr = (end-start).toString().padStart(width, " ").replace(/ /g, "&nbsp;");
return "start: " + startStr + "<br>end: " + endStr + "<br>length: " + lenStr;
};
/**
* Removes highlighting and selection information.
*/
HighlighterWaiter.prototype.removeHighlights = function() {
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.
*/
HighlighterWaiter.prototype.highlightOutput = function(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.
*/
HighlighterWaiter.prototype.highlightInput = function(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
*/
HighlighterWaiter.prototype.displayHighlights = function(pos, direction) {
if (!pos) 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.
*/
HighlighterWaiter.prototype.highlight = async function(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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\n/g, "&#10;")
// Convert placeholders to tags
.replace(startPlaceholderRegex, "<span class=\""+cssClass+"\">")
.replace(endPlaceholderRegex, "</span>") + "&nbsp;";
// 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;

468
src/web/HighlighterWaiter.mjs Executable file
View file

@ -0,0 +1,468 @@
/**
* @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, "&nbsp;");
const endStr = end.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
const lenStr = (end-start).toString().padStart(width, " ").replace(/ /g, "&nbsp;");
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;
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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\n/g, "&#10;")
// Convert placeholders to tags
.replace(startPlaceholderRegex, "<span class=\""+cssClass+"\">")
.replace(endPlaceholderRegex, "</span>") + "&nbsp;";
// 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;

View file

@ -1,321 +0,0 @@
import LoaderWorker from "worker-loader?inline&fallback=false!./LoaderWorker.js";
import Utils from "../core/Utils";
/**
* Waiter to handle events related to the input.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const InputWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
// Define keys that don't change the input so we don't have to autobake when they are pressed
this.badKeys = [
16, //Shift
17, //Ctrl
18, //Alt
19, //Pause
20, //Caps
27, //Esc
33, 34, 35, 36, //PgUp, PgDn, End, Home
37, 38, 39, 40, //Directional
44, //PrntScrn
91, 92, //Win
93, //Context
112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, //F1-12
144, //Num
145, //Scroll
];
this.loaderWorker = null;
this.fileBuffer = null;
};
/**
* Gets the user's input from the input textarea.
*
* @returns {string}
*/
InputWaiter.prototype.get = function() {
return this.fileBuffer || document.getElementById("input-text").value;
};
/**
* Sets the input in the input area.
*
* @param {string|File} input
*
* @fires Manager#statechange
*/
InputWaiter.prototype.set = function(input) {
const inputText = document.getElementById("input-text");
if (input instanceof File) {
this.setFile(input);
inputText.value = "";
this.setInputInfo(input.size, null);
} else {
inputText.value = input;
this.closeFile();
window.dispatchEvent(this.manager.statechange);
const lines = input.length < (this.app.options.ioDisplayThreshold * 1024) ?
input.count("\n") + 1 : null;
this.setInputInfo(input.length, lines);
}
};
/**
* Shows file details.
*
* @param {File} file
*/
InputWaiter.prototype.setFile = function(file) {
// Display file overlay in input area with details
const fileOverlay = document.getElementById("input-file"),
fileName = document.getElementById("input-file-name"),
fileSize = document.getElementById("input-file-size"),
fileType = document.getElementById("input-file-type"),
fileLoaded = document.getElementById("input-file-loaded");
this.fileBuffer = new ArrayBuffer();
fileOverlay.style.display = "block";
fileName.textContent = file.name;
fileSize.textContent = file.size.toLocaleString() + " bytes";
fileType.textContent = file.type || "unknown";
fileLoaded.textContent = "0%";
};
/**
* Displays information about the input.
*
* @param {number} length - The length of the current input string
* @param {number} lines - The number of the lines in the current input string
*/
InputWaiter.prototype.setInputInfo = function(length, lines) {
let width = length.toString().length;
width = width < 2 ? 2 : width;
const lengthStr = length.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
let msg = "length: " + lengthStr;
if (typeof lines === "number") {
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>lines: " + linesStr;
}
document.getElementById("input-info").innerHTML = msg;
};
/**
* Handler for input change events.
*
* @param {event} e
*
* @fires Manager#statechange
*/
InputWaiter.prototype.inputChange = function(e) {
// Ignore this function if the input is a File
if (this.fileBuffer) return;
// Remove highlighting from input and output panes as the offsets might be different now
this.manager.highlighter.removeHighlights();
// Reset recipe progress as any previous processing will be redundant now
this.app.progress = 0;
// Update the input metadata info
const inputText = this.get();
const lines = inputText.length < (this.app.options.ioDisplayThreshold * 1024) ?
inputText.count("\n") + 1 : null;
this.setInputInfo(inputText.length, lines);
if (e && this.badKeys.indexOf(e.keyCode) < 0) {
// Fire the statechange event as the input has been modified
window.dispatchEvent(this.manager.statechange);
}
};
/**
* Handler for input paste events.
* Checks that the size of the input is below the display limit, otherwise treats it as a file/blob.
*
* @param {event} e
*/
InputWaiter.prototype.inputPaste = function(e) {
const pastedData = e.clipboardData.getData("Text");
if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024)) {
this.inputChange(e);
} else {
e.preventDefault();
e.stopPropagation();
const file = new File([pastedData], "PastedData", {
type: "text/plain",
lastModified: Date.now()
});
this.loaderWorker = new LoaderWorker();
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
this.loaderWorker.postMessage({"file": file});
this.set(file);
return false;
}
};
/**
* Handler for input dragover events.
* Gives the user a visual cue to show that items can be dropped here.
*
* @param {event} e
*/
InputWaiter.prototype.inputDragover = function(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("#input-text,#input-file").classList.add("dropping-file");
};
/**
* Handler for input dragleave events.
* Removes the visual cue.
*
* @param {event} e
*/
InputWaiter.prototype.inputDragleave = function(e) {
e.stopPropagation();
e.preventDefault();
document.getElementById("input-text").classList.remove("dropping-file");
document.getElementById("input-file").classList.remove("dropping-file");
};
/**
* Handler for input drop events.
* Loads the dragged data into the input textarea.
*
* @param {event} e
*/
InputWaiter.prototype.inputDrop = function(e) {
// This will be set if we're dragging an operation
if (e.dataTransfer.effectAllowed === "move")
return false;
e.stopPropagation();
e.preventDefault();
const file = e.dataTransfer.files[0];
const text = e.dataTransfer.getData("Text");
document.getElementById("input-text").classList.remove("dropping-file");
document.getElementById("input-file").classList.remove("dropping-file");
if (text) {
this.closeFile();
this.set(text);
return;
}
if (file) {
this.closeFile();
this.loaderWorker = new LoaderWorker();
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
this.loaderWorker.postMessage({"file": file});
this.set(file);
}
};
/**
* Handler for messages sent back by the LoaderWorker.
*
* @param {MessageEvent} e
*/
InputWaiter.prototype.handleLoaderMessage = function(e) {
const r = e.data;
if (r.hasOwnProperty("progress")) {
const fileLoaded = document.getElementById("input-file-loaded");
fileLoaded.textContent = r.progress + "%";
}
if (r.hasOwnProperty("error")) {
this.app.alert(r.error, "danger", 10000);
}
if (r.hasOwnProperty("fileBuffer")) {
log.debug("Input file loaded");
this.fileBuffer = r.fileBuffer;
this.displayFilePreview();
window.dispatchEvent(this.manager.statechange);
}
};
/**
* Shows a chunk of the file in the input behind the file overlay.
*/
InputWaiter.prototype.displayFilePreview = function() {
const inputText = document.getElementById("input-text"),
fileSlice = this.fileBuffer.slice(0, 4096);
inputText.style.overflow = "hidden";
inputText.classList.add("blur");
inputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
if (this.fileBuffer.byteLength > 4096) {
inputText.value += "[truncated]...";
}
};
/**
* Handler for file close events.
*/
InputWaiter.prototype.closeFile = function() {
if (this.loaderWorker) this.loaderWorker.terminate();
this.fileBuffer = null;
document.getElementById("input-file").style.display = "none";
const inputText = document.getElementById("input-text");
inputText.style.overflow = "auto";
inputText.classList.remove("blur");
};
/**
* Handler for clear IO events.
* Resets the input, output and info areas.
*
* @fires Manager#statechange
*/
InputWaiter.prototype.clearIoClick = function() {
this.closeFile();
this.manager.output.closeFile();
this.manager.highlighter.removeHighlights();
document.getElementById("input-text").value = "";
document.getElementById("output-text").value = "";
document.getElementById("input-info").innerHTML = "";
document.getElementById("output-info").innerHTML = "";
document.getElementById("input-selection-info").innerHTML = "";
document.getElementById("output-selection-info").innerHTML = "";
window.dispatchEvent(this.manager.statechange);
};
export default InputWaiter;

329
src/web/InputWaiter.mjs Executable file
View file

@ -0,0 +1,329 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import LoaderWorker from "worker-loader?inline&fallback=false!./LoaderWorker";
import Utils from "../core/Utils";
/**
* Waiter to handle events related to the input.
*/
class InputWaiter {
/**
* InputWaiter 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;
// Define keys that don't change the input so we don't have to autobake when they are pressed
this.badKeys = [
16, //Shift
17, //Ctrl
18, //Alt
19, //Pause
20, //Caps
27, //Esc
33, 34, 35, 36, //PgUp, PgDn, End, Home
37, 38, 39, 40, //Directional
44, //PrntScrn
91, 92, //Win
93, //Context
112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, //F1-12
144, //Num
145, //Scroll
];
this.loaderWorker = null;
this.fileBuffer = null;
}
/**
* Gets the user's input from the input textarea.
*
* @returns {string}
*/
get() {
return this.fileBuffer || document.getElementById("input-text").value;
}
/**
* Sets the input in the input area.
*
* @param {string|File} input
*
* @fires Manager#statechange
*/
set(input) {
const inputText = document.getElementById("input-text");
if (input instanceof File) {
this.setFile(input);
inputText.value = "";
this.setInputInfo(input.size, null);
} else {
inputText.value = input;
this.closeFile();
window.dispatchEvent(this.manager.statechange);
const lines = input.length < (this.app.options.ioDisplayThreshold * 1024) ?
input.count("\n") + 1 : null;
this.setInputInfo(input.length, lines);
}
}
/**
* Shows file details.
*
* @param {File} file
*/
setFile(file) {
// Display file overlay in input area with details
const fileOverlay = document.getElementById("input-file"),
fileName = document.getElementById("input-file-name"),
fileSize = document.getElementById("input-file-size"),
fileType = document.getElementById("input-file-type"),
fileLoaded = document.getElementById("input-file-loaded");
this.fileBuffer = new ArrayBuffer();
fileOverlay.style.display = "block";
fileName.textContent = file.name;
fileSize.textContent = file.size.toLocaleString() + " bytes";
fileType.textContent = file.type || "unknown";
fileLoaded.textContent = "0%";
}
/**
* Displays information about the input.
*
* @param {number} length - The length of the current input string
* @param {number} lines - The number of the lines in the current input string
*/
setInputInfo(length, lines) {
let width = length.toString().length;
width = width < 2 ? 2 : width;
const lengthStr = length.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
let msg = "length: " + lengthStr;
if (typeof lines === "number") {
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>lines: " + linesStr;
}
document.getElementById("input-info").innerHTML = msg;
}
/**
* Handler for input change events.
*
* @param {event} e
*
* @fires Manager#statechange
*/
inputChange(e) {
// Ignore this function if the input is a File
if (this.fileBuffer) return;
// Remove highlighting from input and output panes as the offsets might be different now
this.manager.highlighter.removeHighlights();
// Reset recipe progress as any previous processing will be redundant now
this.app.progress = 0;
// Update the input metadata info
const inputText = this.get();
const lines = inputText.length < (this.app.options.ioDisplayThreshold * 1024) ?
inputText.count("\n") + 1 : null;
this.setInputInfo(inputText.length, lines);
if (e && this.badKeys.indexOf(e.keyCode) < 0) {
// Fire the statechange event as the input has been modified
window.dispatchEvent(this.manager.statechange);
}
}
/**
* Handler for input paste events.
* Checks that the size of the input is below the display limit, otherwise treats it as a file/blob.
*
* @param {event} e
*/
inputPaste(e) {
const pastedData = e.clipboardData.getData("Text");
if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024)) {
this.inputChange(e);
} else {
e.preventDefault();
e.stopPropagation();
const file = new File([pastedData], "PastedData", {
type: "text/plain",
lastModified: Date.now()
});
this.loaderWorker = new LoaderWorker();
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
this.loaderWorker.postMessage({"file": file});
this.set(file);
return false;
}
}
/**
* Handler for input dragover events.
* Gives the user a visual cue to show that items can be dropped here.
*
* @param {event} e
*/
inputDragover(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("#input-text,#input-file").classList.add("dropping-file");
}
/**
* Handler for input dragleave events.
* Removes the visual cue.
*
* @param {event} e
*/
inputDragleave(e) {
e.stopPropagation();
e.preventDefault();
document.getElementById("input-text").classList.remove("dropping-file");
document.getElementById("input-file").classList.remove("dropping-file");
}
/**
* Handler for input drop events.
* Loads the dragged data into the input textarea.
*
* @param {event} e
*/
inputDrop(e) {
// This will be set if we're dragging an operation
if (e.dataTransfer.effectAllowed === "move")
return false;
e.stopPropagation();
e.preventDefault();
const file = e.dataTransfer.files[0];
const text = e.dataTransfer.getData("Text");
document.getElementById("input-text").classList.remove("dropping-file");
document.getElementById("input-file").classList.remove("dropping-file");
if (text) {
this.closeFile();
this.set(text);
return;
}
if (file) {
this.closeFile();
this.loaderWorker = new LoaderWorker();
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
this.loaderWorker.postMessage({"file": file});
this.set(file);
}
}
/**
* Handler for messages sent back by the LoaderWorker.
*
* @param {MessageEvent} e
*/
handleLoaderMessage(e) {
const r = e.data;
if (r.hasOwnProperty("progress")) {
const fileLoaded = document.getElementById("input-file-loaded");
fileLoaded.textContent = r.progress + "%";
}
if (r.hasOwnProperty("error")) {
this.app.alert(r.error, "danger", 10000);
}
if (r.hasOwnProperty("fileBuffer")) {
log.debug("Input file loaded");
this.fileBuffer = r.fileBuffer;
this.displayFilePreview();
window.dispatchEvent(this.manager.statechange);
}
}
/**
* Shows a chunk of the file in the input behind the file overlay.
*/
displayFilePreview() {
const inputText = document.getElementById("input-text"),
fileSlice = this.fileBuffer.slice(0, 4096);
inputText.style.overflow = "hidden";
inputText.classList.add("blur");
inputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
if (this.fileBuffer.byteLength > 4096) {
inputText.value += "[truncated]...";
}
}
/**
* Handler for file close events.
*/
closeFile() {
if (this.loaderWorker) this.loaderWorker.terminate();
this.fileBuffer = null;
document.getElementById("input-file").style.display = "none";
const inputText = document.getElementById("input-text");
inputText.style.overflow = "auto";
inputText.classList.remove("blur");
}
/**
* Handler for clear IO events.
* Resets the input, output and info areas.
*
* @fires Manager#statechange
*/
clearIoClick() {
this.closeFile();
this.manager.output.closeFile();
this.manager.highlighter.removeHighlights();
document.getElementById("input-text").value = "";
document.getElementById("output-text").value = "";
document.getElementById("input-info").innerHTML = "";
document.getElementById("output-info").innerHTML = "";
document.getElementById("input-selection-info").innerHTML = "";
document.getElementById("output-selection-info").innerHTML = "";
window.dispatchEvent(this.manager.statechange);
}
}
export default InputWaiter;

View file

@ -1,299 +0,0 @@
import WorkerWaiter from "./WorkerWaiter.js";
import WindowWaiter from "./WindowWaiter.js";
import ControlsWaiter from "./ControlsWaiter.js";
import RecipeWaiter from "./RecipeWaiter.js";
import OperationsWaiter from "./OperationsWaiter.js";
import InputWaiter from "./InputWaiter.js";
import OutputWaiter from "./OutputWaiter.js";
import OptionsWaiter from "./OptionsWaiter.js";
import HighlighterWaiter from "./HighlighterWaiter.js";
import SeasonalWaiter from "./SeasonalWaiter.js";
import BindingsWaiter from "./BindingsWaiter.js";
/**
* This object controls the Waiters responsible for handling events from all areas of the app.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
*/
const Manager = function(app) {
this.app = app;
// Define custom events
/**
* @event Manager#appstart
*/
this.appstart = new CustomEvent("appstart", {bubbles: true});
/**
* @event Manager#apploaded
*/
this.apploaded = new CustomEvent("apploaded", {bubbles: true});
/**
* @event Manager#operationadd
*/
this.operationadd = new CustomEvent("operationadd", {bubbles: true});
/**
* @event Manager#operationremove
*/
this.operationremove = new CustomEvent("operationremove", {bubbles: true});
/**
* @event Manager#oplistcreate
*/
this.oplistcreate = new CustomEvent("oplistcreate", {bubbles: true});
/**
* @event Manager#statechange
*/
this.statechange = new CustomEvent("statechange", {bubbles: true});
// Define Waiter objects to handle various areas
this.worker = new WorkerWaiter(this.app, this);
this.window = new WindowWaiter(this.app);
this.controls = new ControlsWaiter(this.app, this);
this.recipe = new RecipeWaiter(this.app, this);
this.ops = new OperationsWaiter(this.app, this);
this.input = new InputWaiter(this.app, this);
this.output = new OutputWaiter(this.app, this);
this.options = new OptionsWaiter(this.app, this);
this.highlighter = new HighlighterWaiter(this.app, this);
this.seasonal = new SeasonalWaiter(this.app, this);
this.bindings = new BindingsWaiter(this.app, this);
// Object to store dynamic handlers to fire on elements that may not exist yet
this.dynamicHandlers = {};
this.initialiseEventListeners();
};
/**
* Sets up the various components and listeners.
*/
Manager.prototype.setup = function() {
this.worker.registerChefWorker();
this.recipe.initialiseOperationDragNDrop();
this.controls.autoBakeChange();
this.bindings.updateKeybList();
this.seasonal.load();
};
/**
* Main function to handle the creation of the event listeners.
*/
Manager.prototype.initialiseEventListeners = function() {
// Global
window.addEventListener("resize", this.window.windowResize.bind(this.window));
window.addEventListener("blur", this.window.windowBlur.bind(this.window));
window.addEventListener("focus", this.window.windowFocus.bind(this.window));
window.addEventListener("statechange", this.app.stateChange.bind(this.app));
window.addEventListener("popstate", this.app.popState.bind(this.app));
// Controls
document.getElementById("bake").addEventListener("click", this.controls.bakeClick.bind(this.controls));
document.getElementById("auto-bake").addEventListener("change", this.controls.autoBakeChange.bind(this.controls));
document.getElementById("step").addEventListener("click", this.controls.stepClick.bind(this.controls));
document.getElementById("clr-recipe").addEventListener("click", this.controls.clearRecipeClick.bind(this.controls));
document.getElementById("clr-breaks").addEventListener("click", this.controls.clearBreaksClick.bind(this.controls));
document.getElementById("save").addEventListener("click", this.controls.saveClick.bind(this.controls));
document.getElementById("save-button").addEventListener("click", this.controls.saveButtonClick.bind(this.controls));
document.getElementById("save-link-recipe-checkbox").addEventListener("change", this.controls.slrCheckChange.bind(this.controls));
document.getElementById("save-link-input-checkbox").addEventListener("change", this.controls.sliCheckChange.bind(this.controls));
document.getElementById("load").addEventListener("click", this.controls.loadClick.bind(this.controls));
document.getElementById("load-delete-button").addEventListener("click", this.controls.loadDeleteClick.bind(this.controls));
document.getElementById("load-name").addEventListener("change", this.controls.loadNameChange.bind(this.controls));
document.getElementById("load-button").addEventListener("click", this.controls.loadButtonClick.bind(this.controls));
document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls));
this.addMultiEventListeners("#save-texts textarea", "keyup paste", this.controls.saveTextChange, this.controls);
// Operations
this.addMultiEventListener("#search", "keyup paste search", this.ops.searchOperations, this.ops);
this.addDynamicListener(".op-list li.operation", "dblclick", this.ops.operationDblclick, this.ops);
document.getElementById("edit-favourites").addEventListener("click", this.ops.editFavouritesClick.bind(this.ops));
document.getElementById("save-favourites").addEventListener("click", this.ops.saveFavouritesClick.bind(this.ops));
document.getElementById("reset-favourites").addEventListener("click", this.ops.resetFavouritesClick.bind(this.ops));
this.addDynamicListener(".op-list .op-icon", "mouseover", this.ops.opIconMouseover, this.ops);
this.addDynamicListener(".op-list .op-icon", "mouseleave", this.ops.opIconMouseleave, this.ops);
this.addDynamicListener(".op-list", "oplistcreate", this.ops.opListCreate, this.ops);
this.addDynamicListener("li.operation", "operationadd", this.recipe.opAdd, this.recipe);
// Recipe
this.addDynamicListener(".arg:not(select)", "input", this.recipe.ingChange, this.recipe);
this.addDynamicListener(".arg[type=checkbox], .arg[type=radio], select.arg", "change", this.recipe.ingChange, this.recipe);
this.addDynamicListener(".disable-icon", "click", this.recipe.disableClick, this.recipe);
this.addDynamicListener(".breakpoint", "click", this.recipe.breakpointClick, this.recipe);
this.addDynamicListener("#rec-list li.operation", "dblclick", this.recipe.operationDblclick, this.recipe);
this.addDynamicListener("#rec-list li.operation > div", "dblclick", this.recipe.operationChildDblclick, this.recipe);
this.addDynamicListener("#rec-list .input-group .dropdown-menu a", "click", this.recipe.dropdownToggleClick, this.recipe);
this.addDynamicListener("#rec-list", "operationremove", this.recipe.opRemove.bind(this.recipe));
// Input
this.addMultiEventListener("#input-text", "keyup", this.input.inputChange, this.input);
this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input);
document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app));
document.getElementById("clr-io").addEventListener("click", this.input.clearIoClick.bind(this.input));
this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input);
this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input);
this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input);
document.getElementById("input-text").addEventListener("scroll", this.highlighter.inputScroll.bind(this.highlighter));
document.getElementById("input-text").addEventListener("mouseup", this.highlighter.inputMouseup.bind(this.highlighter));
document.getElementById("input-text").addEventListener("mousemove", this.highlighter.inputMousemove.bind(this.highlighter));
this.addMultiEventListener("#input-text", "mousedown dblclick select", this.highlighter.inputMousedown, this.highlighter);
document.querySelector("#input-file .close").addEventListener("click", this.input.clearIoClick.bind(this.input));
// Output
document.getElementById("save-to-file").addEventListener("click", this.output.saveClick.bind(this.output));
document.getElementById("copy-output").addEventListener("click", this.output.copyClick.bind(this.output));
document.getElementById("switch").addEventListener("click", this.output.switchClick.bind(this.output));
document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output));
document.getElementById("maximise-output").addEventListener("click", this.output.maximiseOutputClick.bind(this.output));
document.getElementById("output-text").addEventListener("scroll", this.highlighter.outputScroll.bind(this.highlighter));
document.getElementById("output-text").addEventListener("mouseup", this.highlighter.outputMouseup.bind(this.highlighter));
document.getElementById("output-text").addEventListener("mousemove", this.highlighter.outputMousemove.bind(this.highlighter));
document.getElementById("output-html").addEventListener("mouseup", this.highlighter.outputHtmlMouseup.bind(this.highlighter));
document.getElementById("output-html").addEventListener("mousemove", this.highlighter.outputHtmlMousemove.bind(this.highlighter));
this.addMultiEventListener("#output-text", "mousedown dblclick select", this.highlighter.outputMousedown, this.highlighter);
this.addMultiEventListener("#output-html", "mousedown dblclick select", this.highlighter.outputHtmlMousedown, this.highlighter);
this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output);
this.addDynamicListener("#output-file-slice", "click", this.output.displayFileSlice, this.output);
document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
// Options
document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options));
document.getElementById("reset-options").addEventListener("click", this.options.resetOptionsClick.bind(this.options));
$(document).on("switchChange.bootstrapSwitch", ".option-item input:checkbox", this.options.switchChange.bind(this.options));
$(document).on("switchChange.bootstrapSwitch", ".option-item input:checkbox", this.options.setWordWrap.bind(this.options));
$(document).on("switchChange.bootstrapSwitch", ".option-item input:checkbox#useMetaKey", this.bindings.updateKeybList.bind(this.bindings));
this.addDynamicListener(".option-item input[type=number]", "keyup", this.options.numberChange, this.options);
this.addDynamicListener(".option-item input[type=number]", "change", this.options.numberChange, this.options);
this.addDynamicListener(".option-item select", "change", this.options.selectChange, this.options);
document.getElementById("theme").addEventListener("change", this.options.themeChange.bind(this.options));
document.getElementById("logLevel").addEventListener("change", this.options.logLevelChange.bind(this.options));
// Misc
window.addEventListener("keydown", this.bindings.parseInput.bind(this.bindings));
document.getElementById("alert-close").addEventListener("click", this.app.alertCloseClick.bind(this.app));
};
/**
* Adds an event listener to each element in the specified group.
*
* @param {string} selector - A selector string for the element group to add the event to, see
* this.getAll()
* @param {string} eventType - The event to listen for
* @param {function} callback - The function to execute when the event is triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Calls the clickable function whenever any element with the .clickable class is clicked
* this.addListeners(".clickable", "click", this.clickable, this);
*/
Manager.prototype.addListeners = function(selector, eventType, callback, scope) {
scope = scope || this;
[].forEach.call(document.querySelectorAll(selector), function(el) {
el.addEventListener(eventType, callback.bind(scope));
});
};
/**
* Adds multiple event listeners to the specified element.
*
* @param {string} selector - A selector string for the element to add the events to
* @param {string} eventTypes - A space-separated string of all the event types to listen for
* @param {function} callback - The function to execute when the events are triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Calls the search function whenever the the keyup, paste or search events are triggered on the
* // search element
* this.addMultiEventListener("search", "keyup paste search", this.search, this);
*/
Manager.prototype.addMultiEventListener = function(selector, eventTypes, callback, scope) {
const evs = eventTypes.split(" ");
for (let i = 0; i < evs.length; i++) {
document.querySelector(selector).addEventListener(evs[i], callback.bind(scope));
}
};
/**
* Adds multiple event listeners to each element in the specified group.
*
* @param {string} selector - A selector string for the element group to add the events to
* @param {string} eventTypes - A space-separated string of all the event types to listen for
* @param {function} callback - The function to execute when the events are triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Calls the save function whenever the the keyup or paste events are triggered on any element
* // with the .saveable class
* this.addMultiEventListener(".saveable", "keyup paste", this.save, this);
*/
Manager.prototype.addMultiEventListeners = function(selector, eventTypes, callback, scope) {
const evs = eventTypes.split(" ");
for (let i = 0; i < evs.length; i++) {
this.addListeners(selector, evs[i], callback, scope);
}
};
/**
* Adds an event listener to the global document object which will listen on dynamic elements which
* may not exist in the DOM yet.
*
* @param {string} selector - A selector string for the element(s) to add the event to
* @param {string} eventType - The event(s) to listen for
* @param {function} callback - The function to execute when the event(s) is/are triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Pops up an alert whenever any button is clicked, even if it is added to the DOM after this
* // listener is created
* this.addDynamicListener("button", "click", alert, this);
*/
Manager.prototype.addDynamicListener = function(selector, eventType, callback, scope) {
const eventConfig = {
selector: selector,
callback: callback.bind(scope || this)
};
if (this.dynamicHandlers.hasOwnProperty(eventType)) {
// Listener already exists, add new handler to the appropriate list
this.dynamicHandlers[eventType].push(eventConfig);
} else {
this.dynamicHandlers[eventType] = [eventConfig];
// Set up listener for this new type
document.addEventListener(eventType, this.dynamicListenerHandler.bind(this));
}
};
/**
* Handler for dynamic events. This function is called for any dynamic event and decides which
* callback(s) to execute based on the type and selector.
*
* @param {Event} e - The event to be handled
*/
Manager.prototype.dynamicListenerHandler = function(e) {
const { type, target } = e;
const handlers = this.dynamicHandlers[type];
const matches = target.matches ||
target.webkitMatchesSelector ||
target.mozMatchesSelector ||
target.msMatchesSelector ||
target.oMatchesSelector;
for (let i = 0; i < handlers.length; i++) {
if (matches && matches.call(target, handlers[i].selector)) {
handlers[i].callback(e);
}
}
};
export default Manager;

307
src/web/Manager.mjs Executable file
View file

@ -0,0 +1,307 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import WorkerWaiter from "./WorkerWaiter";
import WindowWaiter from "./WindowWaiter";
import ControlsWaiter from "./ControlsWaiter";
import RecipeWaiter from "./RecipeWaiter";
import OperationsWaiter from "./OperationsWaiter";
import InputWaiter from "./InputWaiter";
import OutputWaiter from "./OutputWaiter";
import OptionsWaiter from "./OptionsWaiter";
import HighlighterWaiter from "./HighlighterWaiter";
import SeasonalWaiter from "./SeasonalWaiter";
import BindingsWaiter from "./BindingsWaiter";
/**
* This object controls the Waiters responsible for handling events from all areas of the app.
*/
class Manager {
/**
* Manager constructor.
*
* @param {App} app - The main view object for CyberChef.
*/
constructor(app) {
this.app = app;
// Define custom events
/**
* @event Manager#appstart
*/
this.appstart = new CustomEvent("appstart", {bubbles: true});
/**
* @event Manager#apploaded
*/
this.apploaded = new CustomEvent("apploaded", {bubbles: true});
/**
* @event Manager#operationadd
*/
this.operationadd = new CustomEvent("operationadd", {bubbles: true});
/**
* @event Manager#operationremove
*/
this.operationremove = new CustomEvent("operationremove", {bubbles: true});
/**
* @event Manager#oplistcreate
*/
this.oplistcreate = new CustomEvent("oplistcreate", {bubbles: true});
/**
* @event Manager#statechange
*/
this.statechange = new CustomEvent("statechange", {bubbles: true});
// Define Waiter objects to handle various areas
this.worker = new WorkerWaiter(this.app, this);
this.window = new WindowWaiter(this.app);
this.controls = new ControlsWaiter(this.app, this);
this.recipe = new RecipeWaiter(this.app, this);
this.ops = new OperationsWaiter(this.app, this);
this.input = new InputWaiter(this.app, this);
this.output = new OutputWaiter(this.app, this);
this.options = new OptionsWaiter(this.app, this);
this.highlighter = new HighlighterWaiter(this.app, this);
this.seasonal = new SeasonalWaiter(this.app, this);
this.bindings = new BindingsWaiter(this.app, this);
// Object to store dynamic handlers to fire on elements that may not exist yet
this.dynamicHandlers = {};
this.initialiseEventListeners();
}
/**
* Sets up the various components and listeners.
*/
setup() {
this.worker.registerChefWorker();
this.recipe.initialiseOperationDragNDrop();
this.controls.autoBakeChange();
this.bindings.updateKeybList();
this.seasonal.load();
}
/**
* Main function to handle the creation of the event listeners.
*/
initialiseEventListeners() {
// Global
window.addEventListener("resize", this.window.windowResize.bind(this.window));
window.addEventListener("blur", this.window.windowBlur.bind(this.window));
window.addEventListener("focus", this.window.windowFocus.bind(this.window));
window.addEventListener("statechange", this.app.stateChange.bind(this.app));
window.addEventListener("popstate", this.app.popState.bind(this.app));
// Controls
document.getElementById("bake").addEventListener("click", this.controls.bakeClick.bind(this.controls));
document.getElementById("auto-bake").addEventListener("change", this.controls.autoBakeChange.bind(this.controls));
document.getElementById("step").addEventListener("click", this.controls.stepClick.bind(this.controls));
document.getElementById("clr-recipe").addEventListener("click", this.controls.clearRecipeClick.bind(this.controls));
document.getElementById("clr-breaks").addEventListener("click", this.controls.clearBreaksClick.bind(this.controls));
document.getElementById("save").addEventListener("click", this.controls.saveClick.bind(this.controls));
document.getElementById("save-button").addEventListener("click", this.controls.saveButtonClick.bind(this.controls));
document.getElementById("save-link-recipe-checkbox").addEventListener("change", this.controls.slrCheckChange.bind(this.controls));
document.getElementById("save-link-input-checkbox").addEventListener("change", this.controls.sliCheckChange.bind(this.controls));
document.getElementById("load").addEventListener("click", this.controls.loadClick.bind(this.controls));
document.getElementById("load-delete-button").addEventListener("click", this.controls.loadDeleteClick.bind(this.controls));
document.getElementById("load-name").addEventListener("change", this.controls.loadNameChange.bind(this.controls));
document.getElementById("load-button").addEventListener("click", this.controls.loadButtonClick.bind(this.controls));
document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls));
this.addMultiEventListeners("#save-texts textarea", "keyup paste", this.controls.saveTextChange, this.controls);
// Operations
this.addMultiEventListener("#search", "keyup paste search", this.ops.searchOperations, this.ops);
this.addDynamicListener(".op-list li.operation", "dblclick", this.ops.operationDblclick, this.ops);
document.getElementById("edit-favourites").addEventListener("click", this.ops.editFavouritesClick.bind(this.ops));
document.getElementById("save-favourites").addEventListener("click", this.ops.saveFavouritesClick.bind(this.ops));
document.getElementById("reset-favourites").addEventListener("click", this.ops.resetFavouritesClick.bind(this.ops));
this.addDynamicListener(".op-list .op-icon", "mouseover", this.ops.opIconMouseover, this.ops);
this.addDynamicListener(".op-list .op-icon", "mouseleave", this.ops.opIconMouseleave, this.ops);
this.addDynamicListener(".op-list", "oplistcreate", this.ops.opListCreate, this.ops);
this.addDynamicListener("li.operation", "operationadd", this.recipe.opAdd, this.recipe);
// Recipe
this.addDynamicListener(".arg:not(select)", "input", this.recipe.ingChange, this.recipe);
this.addDynamicListener(".arg[type=checkbox], .arg[type=radio], select.arg", "change", this.recipe.ingChange, this.recipe);
this.addDynamicListener(".disable-icon", "click", this.recipe.disableClick, this.recipe);
this.addDynamicListener(".breakpoint", "click", this.recipe.breakpointClick, this.recipe);
this.addDynamicListener("#rec-list li.operation", "dblclick", this.recipe.operationDblclick, this.recipe);
this.addDynamicListener("#rec-list li.operation > div", "dblclick", this.recipe.operationChildDblclick, this.recipe);
this.addDynamicListener("#rec-list .input-group .dropdown-menu a", "click", this.recipe.dropdownToggleClick, this.recipe);
this.addDynamicListener("#rec-list", "operationremove", this.recipe.opRemove.bind(this.recipe));
// Input
this.addMultiEventListener("#input-text", "keyup", this.input.inputChange, this.input);
this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input);
document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app));
document.getElementById("clr-io").addEventListener("click", this.input.clearIoClick.bind(this.input));
this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input);
this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input);
this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input);
document.getElementById("input-text").addEventListener("scroll", this.highlighter.inputScroll.bind(this.highlighter));
document.getElementById("input-text").addEventListener("mouseup", this.highlighter.inputMouseup.bind(this.highlighter));
document.getElementById("input-text").addEventListener("mousemove", this.highlighter.inputMousemove.bind(this.highlighter));
this.addMultiEventListener("#input-text", "mousedown dblclick select", this.highlighter.inputMousedown, this.highlighter);
document.querySelector("#input-file .close").addEventListener("click", this.input.clearIoClick.bind(this.input));
// Output
document.getElementById("save-to-file").addEventListener("click", this.output.saveClick.bind(this.output));
document.getElementById("copy-output").addEventListener("click", this.output.copyClick.bind(this.output));
document.getElementById("switch").addEventListener("click", this.output.switchClick.bind(this.output));
document.getElementById("undo-switch").addEventListener("click", this.output.undoSwitchClick.bind(this.output));
document.getElementById("maximise-output").addEventListener("click", this.output.maximiseOutputClick.bind(this.output));
document.getElementById("output-text").addEventListener("scroll", this.highlighter.outputScroll.bind(this.highlighter));
document.getElementById("output-text").addEventListener("mouseup", this.highlighter.outputMouseup.bind(this.highlighter));
document.getElementById("output-text").addEventListener("mousemove", this.highlighter.outputMousemove.bind(this.highlighter));
document.getElementById("output-html").addEventListener("mouseup", this.highlighter.outputHtmlMouseup.bind(this.highlighter));
document.getElementById("output-html").addEventListener("mousemove", this.highlighter.outputHtmlMousemove.bind(this.highlighter));
this.addMultiEventListener("#output-text", "mousedown dblclick select", this.highlighter.outputMousedown, this.highlighter);
this.addMultiEventListener("#output-html", "mousedown dblclick select", this.highlighter.outputHtmlMousedown, this.highlighter);
this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output);
this.addDynamicListener("#output-file-slice", "click", this.output.displayFileSlice, this.output);
document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
// Options
document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options));
document.getElementById("reset-options").addEventListener("click", this.options.resetOptionsClick.bind(this.options));
$(document).on("switchChange.bootstrapSwitch", ".option-item input:checkbox", this.options.switchChange.bind(this.options));
$(document).on("switchChange.bootstrapSwitch", ".option-item input:checkbox", this.options.setWordWrap.bind(this.options));
$(document).on("switchChange.bootstrapSwitch", ".option-item input:checkbox#useMetaKey", this.bindings.updateKeybList.bind(this.bindings));
this.addDynamicListener(".option-item input[type=number]", "keyup", this.options.numberChange, this.options);
this.addDynamicListener(".option-item input[type=number]", "change", this.options.numberChange, this.options);
this.addDynamicListener(".option-item select", "change", this.options.selectChange, this.options);
document.getElementById("theme").addEventListener("change", this.options.themeChange.bind(this.options));
document.getElementById("logLevel").addEventListener("change", this.options.logLevelChange.bind(this.options));
// Misc
window.addEventListener("keydown", this.bindings.parseInput.bind(this.bindings));
document.getElementById("alert-close").addEventListener("click", this.app.alertCloseClick.bind(this.app));
}
/**
* Adds an event listener to each element in the specified group.
*
* @param {string} selector - A selector string for the element group to add the event to, see
* this.getAll()
* @param {string} eventType - The event to listen for
* @param {function} callback - The function to execute when the event is triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Calls the clickable function whenever any element with the .clickable class is clicked
* this.addListeners(".clickable", "click", this.clickable, this);
*/
addListeners(selector, eventType, callback, scope) {
scope = scope || this;
[].forEach.call(document.querySelectorAll(selector), function(el) {
el.addEventListener(eventType, callback.bind(scope));
});
}
/**
* Adds multiple event listeners to the specified element.
*
* @param {string} selector - A selector string for the element to add the events to
* @param {string} eventTypes - A space-separated string of all the event types to listen for
* @param {function} callback - The function to execute when the events are triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Calls the search function whenever the the keyup, paste or search events are triggered on the
* // search element
* this.addMultiEventListener("search", "keyup paste search", this.search, this);
*/
addMultiEventListener(selector, eventTypes, callback, scope) {
const evs = eventTypes.split(" ");
for (let i = 0; i < evs.length; i++) {
document.querySelector(selector).addEventListener(evs[i], callback.bind(scope));
}
}
/**
* Adds multiple event listeners to each element in the specified group.
*
* @param {string} selector - A selector string for the element group to add the events to
* @param {string} eventTypes - A space-separated string of all the event types to listen for
* @param {function} callback - The function to execute when the events are triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Calls the save function whenever the the keyup or paste events are triggered on any element
* // with the .saveable class
* this.addMultiEventListener(".saveable", "keyup paste", this.save, this);
*/
addMultiEventListeners(selector, eventTypes, callback, scope) {
const evs = eventTypes.split(" ");
for (let i = 0; i < evs.length; i++) {
this.addListeners(selector, evs[i], callback, scope);
}
}
/**
* Adds an event listener to the global document object which will listen on dynamic elements which
* may not exist in the DOM yet.
*
* @param {string} selector - A selector string for the element(s) to add the event to
* @param {string} eventType - The event(s) to listen for
* @param {function} callback - The function to execute when the event(s) is/are triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Pops up an alert whenever any button is clicked, even if it is added to the DOM after this
* // listener is created
* this.addDynamicListener("button", "click", alert, this);
*/
addDynamicListener(selector, eventType, callback, scope) {
const eventConfig = {
selector: selector,
callback: callback.bind(scope || this)
};
if (this.dynamicHandlers.hasOwnProperty(eventType)) {
// Listener already exists, add new handler to the appropriate list
this.dynamicHandlers[eventType].push(eventConfig);
} else {
this.dynamicHandlers[eventType] = [eventConfig];
// Set up listener for this new type
document.addEventListener(eventType, this.dynamicListenerHandler.bind(this));
}
}
/**
* Handler for dynamic events. This function is called for any dynamic event and decides which
* callback(s) to execute based on the type and selector.
*
* @param {Event} e - The event to be handled
*/
dynamicListenerHandler(e) {
const { type, target } = e;
const handlers = this.dynamicHandlers[type];
const matches = target.matches ||
target.webkitMatchesSelector ||
target.mozMatchesSelector ||
target.msMatchesSelector ||
target.oMatchesSelector;
for (let i = 0; i < handlers.length; i++) {
if (matches && matches.call(target, handlers[i].selector)) {
handlers[i].callback(e);
}
}
}
}
export default Manager;

View file

@ -1,313 +0,0 @@
import HTMLOperation from "./HTMLOperation.js";
import Sortable from "sortablejs";
/**
* Waiter to handle events related to the operations.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const OperationsWaiter = function(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
*/
OperationsWaiter.prototype.searchOperations = function(e) {
let ops, selected;
if (e.type === "search") { // Search
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 === 13) { // Return
e.preventDefault();
} else 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("destroy");
} catch (err) {}
searchResultsEl.removeChild(searchResultsEl.firstChild);
}
$("#categories .in").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[]}
*/
OperationsWaiter.prototype.filterOperations = function(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}
*/
OperationsWaiter.prototype.getSelectedOp = function(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
*/
OperationsWaiter.prototype.opListCreate = function(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
*/
OperationsWaiter.prototype.enableOpsListPopovers = function(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
*/
OperationsWaiter.prototype.operationDblclick = function(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
*/
OperationsWaiter.prototype.editFavouritesClick = function(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("destroy");
el.parentNode.removeChild(el);
}
},
onEnd: function(evt) {
if (this.removeIntent) {
$(evt.item).popover("destroy");
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.
*/
OperationsWaiter.prototype.saveFavouritesClick = function() {
const favs = document.querySelectorAll("#edit-favourites-list li");
const favouritesList = Array.from(favs, e => e.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.
*/
OperationsWaiter.prototype.resetFavouritesClick = function() {
this.app.resetFavourites();
};
/**
* Handler for opIcon mouseover events.
* Hides any popovers already showing on the operation so that there aren't two at once.
*
* @param {event} e
*/
OperationsWaiter.prototype.opIconMouseover = function(e) {
const opEl = e.target.parentNode;
if (e.target.getAttribute("data-toggle") === "popover") {
$(opEl).popover("hide");
}
};
/**
* Handler for opIcon mouseleave events.
* If this icon created a popover and we're moving back to the operation element, display the
* operation popover again.
*
* @param {event} e
*/
OperationsWaiter.prototype.opIconMouseleave = function(e) {
const opEl = e.target.parentNode;
const toEl = e.toElement || e.relatedElement;
if (e.target.getAttribute("data-toggle") === "popover" && toEl === opEl) {
$(opEl).popover("show");
}
};
export default OperationsWaiter;

321
src/web/OperationsWaiter.mjs Executable file
View file

@ -0,0 +1,321 @@
/**
* @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") { // Search
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 === 13) { // Return
e.preventDefault();
} else 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("destroy");
} catch (err) {}
searchResultsEl.removeChild(searchResultsEl.firstChild);
}
$("#categories .in").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("destroy");
el.parentNode.removeChild(el);
}
},
onEnd: function(evt) {
if (this.removeIntent) {
$(evt.item).popover("destroy");
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.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();
}
/**
* Handler for opIcon mouseover events.
* Hides any popovers already showing on the operation so that there aren't two at once.
*
* @param {event} e
*/
opIconMouseover(e) {
const opEl = e.target.parentNode;
if (e.target.getAttribute("data-toggle") === "popover") {
$(opEl).popover("hide");
}
}
/**
* Handler for opIcon mouseleave events.
* If this icon created a popover and we're moving back to the operation element, display the
* operation popover again.
*
* @param {event} e
*/
opIconMouseleave(e) {
const opEl = e.target.parentNode;
const toEl = e.toElement || e.relatedElement;
if (e.target.getAttribute("data-toggle") === "popover" && toEl === opEl) {
$(opEl).popover("show");
}
}
}
export default OperationsWaiter;

View file

@ -1,441 +0,0 @@
import Utils from "../core/Utils";
import FileSaver from "file-saver";
/**
* Waiter to handle events related to the output.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const OutputWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
this.dishBuffer = null;
this.dishStr = null;
};
/**
* Gets the output string from the output textarea.
*
* @returns {string}
*/
OutputWaiter.prototype.get = function() {
return document.getElementById("output-text").value;
};
/**
* Sets the output in the output textarea.
*
* @param {string|ArrayBuffer} data - The output string/HTML/ArrayBuffer
* @param {string} type - The data type of the output
* @param {number} duration - The length of time (ms) it took to generate the output
* @param {boolean} [preserveBuffer=false] - Whether to preserve the dishBuffer
*/
OutputWaiter.prototype.set = async function(data, type, duration, preserveBuffer) {
log.debug("Output type: " + type);
const outputText = document.getElementById("output-text");
const outputHtml = document.getElementById("output-html");
const outputFile = document.getElementById("output-file");
const outputHighlighter = document.getElementById("output-highlighter");
const inputHighlighter = document.getElementById("input-highlighter");
let scriptElements, lines, length;
if (!preserveBuffer) {
this.closeFile();
this.dishStr = null;
document.getElementById("show-file-overlay").style.display = "none";
}
switch (type) {
case "html":
outputText.style.display = "none";
outputHtml.style.display = "block";
outputFile.style.display = "none";
outputHighlighter.display = "none";
inputHighlighter.display = "none";
outputText.value = "";
outputHtml.innerHTML = data;
// Execute script sections
scriptElements = outputHtml.querySelectorAll("script");
for (let i = 0; i < scriptElements.length; i++) {
try {
eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval
} catch (err) {
log.error(err);
}
}
await this.getDishStr();
length = this.dishStr.length;
lines = this.dishStr.count("\n") + 1;
break;
case "ArrayBuffer":
outputText.style.display = "block";
outputHtml.style.display = "none";
outputHighlighter.display = "none";
inputHighlighter.display = "none";
outputText.value = "";
outputHtml.innerHTML = "";
length = data.byteLength;
this.setFile(data);
break;
case "string":
default:
outputText.style.display = "block";
outputHtml.style.display = "none";
outputFile.style.display = "none";
outputHighlighter.display = "block";
inputHighlighter.display = "block";
outputText.value = Utils.printable(data, true);
outputHtml.innerHTML = "";
lines = data.count("\n") + 1;
length = data.length;
this.dishStr = data;
break;
}
this.manager.highlighter.removeHighlights();
this.setOutputInfo(length, lines, duration);
};
/**
* Shows file details.
*
* @param {ArrayBuffer} buf
*/
OutputWaiter.prototype.setFile = function(buf) {
this.dishBuffer = buf;
const file = new File([buf], "output.dat");
// Display file overlay in output area with details
const fileOverlay = document.getElementById("output-file"),
fileSize = document.getElementById("output-file-size");
fileOverlay.style.display = "block";
fileSize.textContent = file.size.toLocaleString() + " bytes";
// Display preview slice in the background
const outputText = document.getElementById("output-text"),
fileSlice = this.dishBuffer.slice(0, 4096);
outputText.classList.add("blur");
outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
};
/**
* Removes the output file and nulls its memory.
*/
OutputWaiter.prototype.closeFile = function() {
this.dishBuffer = null;
document.getElementById("output-file").style.display = "none";
document.getElementById("output-text").classList.remove("blur");
};
/**
* Handler for file download events.
*/
OutputWaiter.prototype.downloadFile = async function() {
this.filename = window.prompt("Please enter a filename:", this.filename || "download.dat");
await this.getDishBuffer();
const file = new File([this.dishBuffer], this.filename);
if (this.filename) FileSaver.saveAs(file, this.filename, false);
};
/**
* Handler for file slice display events.
*/
OutputWaiter.prototype.displayFileSlice = function() {
const startTime = new Date().getTime(),
showFileOverlay = document.getElementById("show-file-overlay"),
sliceFromEl = document.getElementById("output-file-slice-from"),
sliceToEl = document.getElementById("output-file-slice-to"),
sliceFrom = parseInt(sliceFromEl.value, 10),
sliceTo = parseInt(sliceToEl.value, 10),
str = Utils.arrayBufferToStr(this.dishBuffer.slice(sliceFrom, sliceTo));
document.getElementById("output-text").classList.remove("blur");
showFileOverlay.style.display = "block";
this.set(str, "string", new Date().getTime() - startTime, true);
};
/**
* Handler for show file overlay events.
*
* @param {Event} e
*/
OutputWaiter.prototype.showFileOverlayClick = function(e) {
const outputFile = document.getElementById("output-file"),
showFileOverlay = e.target;
document.getElementById("output-text").classList.add("blur");
outputFile.style.display = "block";
showFileOverlay.style.display = "none";
this.setOutputInfo(this.dishBuffer.byteLength, null, 0);
};
/**
* Displays information about the output.
*
* @param {number} length - The length of the current output string
* @param {number} lines - The number of the lines in the current output string
* @param {number} duration - The length of time (ms) it took to generate the output
*/
OutputWaiter.prototype.setOutputInfo = function(length, lines, duration) {
let width = length.toString().length;
width = width < 4 ? 4 : width;
const lengthStr = length.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, "&nbsp;");
let msg = "time: " + timeStr + "<br>length: " + lengthStr;
if (typeof lines === "number") {
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>lines: " + linesStr;
}
document.getElementById("output-info").innerHTML = msg;
document.getElementById("input-selection-info").innerHTML = "";
document.getElementById("output-selection-info").innerHTML = "";
};
/**
* Adjusts the display properties of the output buttons so that they fit within the current width
* without wrapping or overflowing.
*/
OutputWaiter.prototype.adjustWidth = function() {
const output = document.getElementById("output");
const saveToFile = document.getElementById("save-to-file");
const copyOutput = document.getElementById("copy-output");
const switchIO = document.getElementById("switch");
const undoSwitch = document.getElementById("undo-switch");
const maximiseOutput = document.getElementById("maximise-output");
if (output.clientWidth < 680) {
saveToFile.childNodes[1].nodeValue = "";
copyOutput.childNodes[1].nodeValue = "";
switchIO.childNodes[1].nodeValue = "";
undoSwitch.childNodes[1].nodeValue = "";
maximiseOutput.childNodes[1].nodeValue = "";
} else {
saveToFile.childNodes[1].nodeValue = " Save to file";
copyOutput.childNodes[1].nodeValue = " Copy output";
switchIO.childNodes[1].nodeValue = " Move output to input";
undoSwitch.childNodes[1].nodeValue = " Undo";
maximiseOutput.childNodes[1].nodeValue =
maximiseOutput.getAttribute("title") === "Maximise" ? " Max" : " Restore";
}
};
/**
* Handler for save click events.
* Saves the current output to a file.
*/
OutputWaiter.prototype.saveClick = function() {
this.downloadFile();
};
/**
* Handler for copy click events.
* Copies the output to the clipboard.
*/
OutputWaiter.prototype.copyClick = async function() {
await this.getDishStr();
// Create invisible textarea to populate with the raw dish string (not the printable version that
// contains dots instead of the actual bytes)
const textarea = document.createElement("textarea");
textarea.style.position = "fixed";
textarea.style.top = 0;
textarea.style.left = 0;
textarea.style.width = 0;
textarea.style.height = 0;
textarea.style.border = "none";
textarea.value = this.dishStr;
document.body.appendChild(textarea);
// Select and copy the contents of this textarea
let success = false;
try {
textarea.select();
success = this.dishStr && document.execCommand("copy");
} catch (err) {
success = false;
}
if (success) {
this.app.alert("Copied raw output successfully.", "success", 2000);
} else {
this.app.alert("Sorry, the output could not be copied.", "danger", 2000);
}
// Clean up
document.body.removeChild(textarea);
};
/**
* Handler for switch click events.
* Moves the current output into the input textarea.
*/
OutputWaiter.prototype.switchClick = async function() {
this.switchOrigData = this.manager.input.get();
document.getElementById("undo-switch").disabled = false;
if (this.dishBuffer) {
this.manager.input.setFile(new File([this.dishBuffer], "output.dat"));
this.manager.input.handleLoaderMessage({
data: {
progress: 100,
fileBuffer: this.dishBuffer
}
});
} else {
await this.getDishStr();
this.app.setInput(this.dishStr);
}
};
/**
* Handler for undo switch click events.
* Removes the output from the input and replaces the input that was removed.
*/
OutputWaiter.prototype.undoSwitchClick = function() {
this.app.setInput(this.switchOrigData);
document.getElementById("undo-switch").disabled = true;
};
/**
* Handler for maximise output click events.
* Resizes the output frame to be as large as possible, or restores it to its original size.
*/
OutputWaiter.prototype.maximiseOutputClick = function(e) {
const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode;
if (el.getAttribute("title") === "Maximise") {
this.app.columnSplitter.collapse(0);
this.app.columnSplitter.collapse(1);
this.app.ioSplitter.collapse(0);
el.setAttribute("title", "Restore");
el.innerHTML = "<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlUlEQVQ4y93RwQpBQRQG4C9ba1fxBteGPIj38BTejFJKLFnwCJIiCsW1mcV0k9yx82/OzGK+OXMGOpiiLTFjFNiilQI0sQ7IJiAjLKsgGVYB2YdaVO0kwy46/BVQi9ZDNPyQWen2ub/KufS8y7shfkq9tF9U7SC+/YluKvAI9YZeFeCECXJcA3JHP2WgMXJM/ZUcBwxeM+YuSWTgMtUAAAAASUVORK5CYII='> Restore";
this.adjustWidth();
} else {
el.setAttribute("title", "Maximise");
el.innerHTML = "<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAi0lEQVQ4y83TMQrCQBCF4S+5g4rJEdJ7KE+RQ1lrIQQCllroEULuoM0Ww3a7aXwwLAzMPzDvLcz4hnooUItT1rsoVNy+4lgLWNL7RlcCmDBij2eCfNCrUITc0dRCrhj8m5otw0O6SV8LuAV3uhrAAa8sJ2Np7KPFawhgscVLjH9bCDhjt8WNKft88w/HjCvuVqu53QAAAABJRU5ErkJggg=='> Max";
this.app.resetLayout();
}
};
/**
* Shows or hides the loading icon.
*
* @param {boolean} value
*/
OutputWaiter.prototype.toggleLoader = function(value) {
const outputLoader = document.getElementById("output-loader"),
outputElement = document.getElementById("output-text");
if (value) {
this.manager.controls.hideStaleIndicator();
this.bakingStatusTimeout = setTimeout(function() {
outputElement.disabled = true;
outputLoader.style.visibility = "visible";
outputLoader.style.opacity = 1;
this.manager.controls.toggleBakeButtonFunction(true);
}.bind(this), 200);
} else {
clearTimeout(this.bakingStatusTimeout);
outputElement.disabled = false;
outputLoader.style.opacity = 0;
outputLoader.style.visibility = "hidden";
this.manager.controls.toggleBakeButtonFunction(false);
this.setStatusMsg("");
}
};
/**
* Sets the baking status message value.
*
* @param {string} msg
*/
OutputWaiter.prototype.setStatusMsg = function(msg) {
const el = document.querySelector("#output-loader .loading-msg");
el.textContent = msg;
};
/**
* Returns true if the output contains carriage returns
*
* @returns {boolean}
*/
OutputWaiter.prototype.containsCR = async function() {
await this.getDishStr();
return this.dishStr.indexOf("\r") >= 0;
};
/**
* Retrieves the current dish as a string, returning the cached version if possible.
*
* @returns {string}
*/
OutputWaiter.prototype.getDishStr = async function() {
if (this.dishStr) return this.dishStr;
this.dishStr = await new Promise(resolve => {
this.manager.worker.getDishAs(this.app.dish, "string", r => {
resolve(r.value);
});
});
return this.dishStr;
};
/**
* Retrieves the current dish as an ArrayBuffer, returning the cached version if possible.
*
* @returns {ArrayBuffer}
*/
OutputWaiter.prototype.getDishBuffer = async function() {
if (this.dishBuffer) return this.dishBuffer;
this.dishBuffer = await new Promise(resolve => {
this.manager.worker.getDishAs(this.app.dish, "ArrayBuffer", r => {
resolve(r.value);
});
});
return this.dishBuffer;
};
export default OutputWaiter;

449
src/web/OutputWaiter.mjs Executable file
View file

@ -0,0 +1,449 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
import Utils from "../core/Utils";
import FileSaver from "file-saver";
/**
* Waiter to handle events related to the output.
*/
class OutputWaiter {
/**
* OutputWaiter 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.dishBuffer = null;
this.dishStr = null;
}
/**
* Gets the output string from the output textarea.
*
* @returns {string}
*/
get() {
return document.getElementById("output-text").value;
}
/**
* Sets the output in the output textarea.
*
* @param {string|ArrayBuffer} data - The output string/HTML/ArrayBuffer
* @param {string} type - The data type of the output
* @param {number} duration - The length of time (ms) it took to generate the output
* @param {boolean} [preserveBuffer=false] - Whether to preserve the dishBuffer
*/
async set(data, type, duration, preserveBuffer) {
log.debug("Output type: " + type);
const outputText = document.getElementById("output-text");
const outputHtml = document.getElementById("output-html");
const outputFile = document.getElementById("output-file");
const outputHighlighter = document.getElementById("output-highlighter");
const inputHighlighter = document.getElementById("input-highlighter");
let scriptElements, lines, length;
if (!preserveBuffer) {
this.closeFile();
this.dishStr = null;
document.getElementById("show-file-overlay").style.display = "none";
}
switch (type) {
case "html":
outputText.style.display = "none";
outputHtml.style.display = "block";
outputFile.style.display = "none";
outputHighlighter.display = "none";
inputHighlighter.display = "none";
outputText.value = "";
outputHtml.innerHTML = data;
// Execute script sections
scriptElements = outputHtml.querySelectorAll("script");
for (let i = 0; i < scriptElements.length; i++) {
try {
eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval
} catch (err) {
log.error(err);
}
}
await this.getDishStr();
length = this.dishStr.length;
lines = this.dishStr.count("\n") + 1;
break;
case "ArrayBuffer":
outputText.style.display = "block";
outputHtml.style.display = "none";
outputHighlighter.display = "none";
inputHighlighter.display = "none";
outputText.value = "";
outputHtml.innerHTML = "";
length = data.byteLength;
this.setFile(data);
break;
case "string":
default:
outputText.style.display = "block";
outputHtml.style.display = "none";
outputFile.style.display = "none";
outputHighlighter.display = "block";
inputHighlighter.display = "block";
outputText.value = Utils.printable(data, true);
outputHtml.innerHTML = "";
lines = data.count("\n") + 1;
length = data.length;
this.dishStr = data;
break;
}
this.manager.highlighter.removeHighlights();
this.setOutputInfo(length, lines, duration);
}
/**
* Shows file details.
*
* @param {ArrayBuffer} buf
*/
setFile(buf) {
this.dishBuffer = buf;
const file = new File([buf], "output.dat");
// Display file overlay in output area with details
const fileOverlay = document.getElementById("output-file"),
fileSize = document.getElementById("output-file-size");
fileOverlay.style.display = "block";
fileSize.textContent = file.size.toLocaleString() + " bytes";
// Display preview slice in the background
const outputText = document.getElementById("output-text"),
fileSlice = this.dishBuffer.slice(0, 4096);
outputText.classList.add("blur");
outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
}
/**
* Removes the output file and nulls its memory.
*/
closeFile() {
this.dishBuffer = null;
document.getElementById("output-file").style.display = "none";
document.getElementById("output-text").classList.remove("blur");
}
/**
* Handler for file download events.
*/
async downloadFile() {
this.filename = window.prompt("Please enter a filename:", this.filename || "download.dat");
await this.getDishBuffer();
const file = new File([this.dishBuffer], this.filename);
if (this.filename) FileSaver.saveAs(file, this.filename, false);
}
/**
* Handler for file slice display events.
*/
displayFileSlice() {
const startTime = new Date().getTime(),
showFileOverlay = document.getElementById("show-file-overlay"),
sliceFromEl = document.getElementById("output-file-slice-from"),
sliceToEl = document.getElementById("output-file-slice-to"),
sliceFrom = parseInt(sliceFromEl.value, 10),
sliceTo = parseInt(sliceToEl.value, 10),
str = Utils.arrayBufferToStr(this.dishBuffer.slice(sliceFrom, sliceTo));
document.getElementById("output-text").classList.remove("blur");
showFileOverlay.style.display = "block";
this.set(str, "string", new Date().getTime() - startTime, true);
}
/**
* Handler for show file overlay events.
*
* @param {Event} e
*/
showFileOverlayClick(e) {
const outputFile = document.getElementById("output-file"),
showFileOverlay = e.target;
document.getElementById("output-text").classList.add("blur");
outputFile.style.display = "block";
showFileOverlay.style.display = "none";
this.setOutputInfo(this.dishBuffer.byteLength, null, 0);
}
/**
* Displays information about the output.
*
* @param {number} length - The length of the current output string
* @param {number} lines - The number of the lines in the current output string
* @param {number} duration - The length of time (ms) it took to generate the output
*/
setOutputInfo(length, lines, duration) {
let width = length.toString().length;
width = width < 4 ? 4 : width;
const lengthStr = length.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, "&nbsp;");
let msg = "time: " + timeStr + "<br>length: " + lengthStr;
if (typeof lines === "number") {
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>lines: " + linesStr;
}
document.getElementById("output-info").innerHTML = msg;
document.getElementById("input-selection-info").innerHTML = "";
document.getElementById("output-selection-info").innerHTML = "";
}
/**
* Adjusts the display properties of the output buttons so that they fit within the current width
* without wrapping or overflowing.
*/
adjustWidth() {
const output = document.getElementById("output");
const saveToFile = document.getElementById("save-to-file");
const copyOutput = document.getElementById("copy-output");
const switchIO = document.getElementById("switch");
const undoSwitch = document.getElementById("undo-switch");
const maximiseOutput = document.getElementById("maximise-output");
if (output.clientWidth < 680) {
saveToFile.childNodes[1].nodeValue = "";
copyOutput.childNodes[1].nodeValue = "";
switchIO.childNodes[1].nodeValue = "";
undoSwitch.childNodes[1].nodeValue = "";
maximiseOutput.childNodes[1].nodeValue = "";
} else {
saveToFile.childNodes[1].nodeValue = " Save to file";
copyOutput.childNodes[1].nodeValue = " Copy output";
switchIO.childNodes[1].nodeValue = " Move output to input";
undoSwitch.childNodes[1].nodeValue = " Undo";
maximiseOutput.childNodes[1].nodeValue =
maximiseOutput.getAttribute("title") === "Maximise" ? " Max" : " Restore";
}
}
/**
* Handler for save click events.
* Saves the current output to a file.
*/
saveClick() {
this.downloadFile();
}
/**
* Handler for copy click events.
* Copies the output to the clipboard.
*/
async copyClick() {
await this.getDishStr();
// Create invisible textarea to populate with the raw dish string (not the printable version that
// contains dots instead of the actual bytes)
const textarea = document.createElement("textarea");
textarea.style.position = "fixed";
textarea.style.top = 0;
textarea.style.left = 0;
textarea.style.width = 0;
textarea.style.height = 0;
textarea.style.border = "none";
textarea.value = this.dishStr;
document.body.appendChild(textarea);
// Select and copy the contents of this textarea
let success = false;
try {
textarea.select();
success = this.dishStr && document.execCommand("copy");
} catch (err) {
success = false;
}
if (success) {
this.app.alert("Copied raw output successfully.", "success", 2000);
} else {
this.app.alert("Sorry, the output could not be copied.", "danger", 2000);
}
// Clean up
document.body.removeChild(textarea);
}
/**
* Handler for switch click events.
* Moves the current output into the input textarea.
*/
async switchClick() {
this.switchOrigData = this.manager.input.get();
document.getElementById("undo-switch").disabled = false;
if (this.dishBuffer) {
this.manager.input.setFile(new File([this.dishBuffer], "output.dat"));
this.manager.input.handleLoaderMessage({
data: {
progress: 100,
fileBuffer: this.dishBuffer
}
});
} else {
await this.getDishStr();
this.app.setInput(this.dishStr);
}
}
/**
* Handler for undo switch click events.
* Removes the output from the input and replaces the input that was removed.
*/
undoSwitchClick() {
this.app.setInput(this.switchOrigData);
document.getElementById("undo-switch").disabled = true;
}
/**
* Handler for maximise output click events.
* Resizes the output frame to be as large as possible, or restores it to its original size.
*/
maximiseOutputClick(e) {
const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode;
if (el.getAttribute("title") === "Maximise") {
this.app.columnSplitter.collapse(0);
this.app.columnSplitter.collapse(1);
this.app.ioSplitter.collapse(0);
el.setAttribute("title", "Restore");
el.innerHTML = "<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAlUlEQVQ4y93RwQpBQRQG4C9ba1fxBteGPIj38BTejFJKLFnwCJIiCsW1mcV0k9yx82/OzGK+OXMGOpiiLTFjFNiilQI0sQ7IJiAjLKsgGVYB2YdaVO0kwy46/BVQi9ZDNPyQWen2ub/KufS8y7shfkq9tF9U7SC+/YluKvAI9YZeFeCECXJcA3JHP2WgMXJM/ZUcBwxeM+YuSWTgMtUAAAAASUVORK5CYII='> Restore";
this.adjustWidth();
} else {
el.setAttribute("title", "Maximise");
el.innerHTML = "<img src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAi0lEQVQ4y83TMQrCQBCF4S+5g4rJEdJ7KE+RQ1lrIQQCllroEULuoM0Ww3a7aXwwLAzMPzDvLcz4hnooUItT1rsoVNy+4lgLWNL7RlcCmDBij2eCfNCrUITc0dRCrhj8m5otw0O6SV8LuAV3uhrAAa8sJ2Np7KPFawhgscVLjH9bCDhjt8WNKft88w/HjCvuVqu53QAAAABJRU5ErkJggg=='> Max";
this.app.resetLayout();
}
}
/**
* Shows or hides the loading icon.
*
* @param {boolean} value
*/
toggleLoader(value) {
const outputLoader = document.getElementById("output-loader"),
outputElement = document.getElementById("output-text");
if (value) {
this.manager.controls.hideStaleIndicator();
this.bakingStatusTimeout = setTimeout(function() {
outputElement.disabled = true;
outputLoader.style.visibility = "visible";
outputLoader.style.opacity = 1;
this.manager.controls.toggleBakeButtonFunction(true);
}.bind(this), 200);
} else {
clearTimeout(this.bakingStatusTimeout);
outputElement.disabled = false;
outputLoader.style.opacity = 0;
outputLoader.style.visibility = "hidden";
this.manager.controls.toggleBakeButtonFunction(false);
this.setStatusMsg("");
}
}
/**
* Sets the baking status message value.
*
* @param {string} msg
*/
setStatusMsg(msg) {
const el = document.querySelector("#output-loader .loading-msg");
el.textContent = msg;
}
/**
* Returns true if the output contains carriage returns
*
* @returns {boolean}
*/
async containsCR() {
await this.getDishStr();
return this.dishStr.indexOf("\r") >= 0;
}
/**
* Retrieves the current dish as a string, returning the cached version if possible.
*
* @returns {string}
*/
async getDishStr() {
if (this.dishStr) return this.dishStr;
this.dishStr = await new Promise(resolve => {
this.manager.worker.getDishAs(this.app.dish, "string", r => {
resolve(r.value);
});
});
return this.dishStr;
}
/**
* Retrieves the current dish as an ArrayBuffer, returning the cached version if possible.
*
* @returns {ArrayBuffer}
*/
async getDishBuffer() {
if (this.dishBuffer) return this.dishBuffer;
this.dishBuffer = await new Promise(resolve => {
this.manager.worker.getDishAs(this.app.dish, "ArrayBuffer", r => {
resolve(r.value);
});
});
return this.dishBuffer;
}
}
export default OutputWaiter;

View file

@ -1,467 +0,0 @@
import HTMLOperation from "./HTMLOperation.js";
import Sortable from "sortablejs";
import Utils from "../core/Utils";
/**
* Waiter to handle events related to the recipe.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const RecipeWaiter = function(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.
*/
RecipeWaiter.prototype.initialiseOperationDragNDrop = function() {
const recList = document.getElementById("rec-list");
// Recipe list
Sortable.create(recList, {
group: "recipe",
sort: true,
animation: 0,
delay: 0,
filter: ".arg-input,.arg",
preventOnFilter: false,
setData: function(dataTransfer, dragEl) {
dataTransfer.setData("Text", dragEl.querySelector(".arg-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
*/
RecipeWaiter.prototype.createSortableSeedList = function(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("destroy").removeData("bs.popover").off("mouseenter").off("mouseleave");
$(evt.clone).off(".popover").removeData("bs.popover");
evt.item.setAttribute("data-toggle", "popover-disabled");
},
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
*/
RecipeWaiter.prototype.opSortEnd = function(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
*/
RecipeWaiter.prototype.favDragover = function(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
*/
RecipeWaiter.prototype.favDragleave = function(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
*/
RecipeWaiter.prototype.favDrop = function(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
*/
RecipeWaiter.prototype.ingChange = function(e) {
window.dispatchEvent(this.manager.statechange);
};
/**
* Handler for disable click events.
* Updates the icon status.
*
* @fires Manager#statechange
* @param {event} e
*/
RecipeWaiter.prototype.disableClick = function(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
*/
RecipeWaiter.prototype.breakpointClick = function(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
*/
RecipeWaiter.prototype.operationDblclick = function(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
*/
RecipeWaiter.prototype.operationChildDblclick = function(e) {
e.target.parentNode.remove();
this.opRemove(e);
};
/**
* Generates a configuration object to represent the current recipe.
*
* @returns {recipeConfig}
*/
RecipeWaiter.prototype.getConfig = function() {
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].previousSibling.children[0].textContent.slice(0, -1),
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(".arg-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} position
*/
RecipeWaiter.prototype.updateBreakpointIndicator = function(position) {
const operations = document.querySelectorAll("#rec-list li.operation");
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
*/
RecipeWaiter.prototype.buildRecipeOperation = function(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.", "info", 5000);
}
};
/**
* Adds the specified operation to the recipe.
*
* @fires Manager#operationadd
* @param {string} name - The name of the operation to add
* @returns {element}
*/
RecipeWaiter.prototype.addOperation = function(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
*/
RecipeWaiter.prototype.clearRecipe = function() {
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
*/
RecipeWaiter.prototype.dropdownToggleClick = function(e) {
const el = e.target;
const button = el.parentNode.parentNode.previousSibling;
button.innerHTML = el.textContent + " <span class='caret'></span>";
this.ingChange();
};
/**
* Handler for operationadd events.
*
* @listens Manager#operationadd
* @fires Manager#statechange
* @param {event} e
*/
RecipeWaiter.prototype.opAdd = function(e) {
log.debug(`'${e.target.querySelector(".arg-title").textContent}' added to recipe`);
window.dispatchEvent(this.manager.statechange);
};
/**
* Handler for operationremove events.
*
* @listens Manager#operationremove
* @fires Manager#statechange
* @param {event} e
*/
RecipeWaiter.prototype.opRemove = function(e) {
log.debug("Operation removed from recipe");
window.dispatchEvent(this.manager.statechange);
};
/**
* Sets register values.
*
* @param {number} opIndex
* @param {number} numPrevRegisters
* @param {string[]} registers
*/
RecipeWaiter.prototype.setRegisters = function(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);
};
export default RecipeWaiter;

475
src/web/RecipeWaiter.mjs Executable file
View file

@ -0,0 +1,475 @@
/**
* @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-input,.arg",
preventOnFilter: false,
setData: function(dataTransfer, dragEl) {
dataTransfer.setData("Text", dragEl.querySelector(".arg-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("destroy").removeData("bs.popover").off("mouseenter").off("mouseleave");
$(evt.clone).off(".popover").removeData("bs.popover");
evt.item.setAttribute("data-toggle", "popover-disabled");
},
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) {
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].previousSibling.children[0].textContent.slice(0, -1),
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(".arg-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} position
*/
updateBreakpointIndicator(position) {
const operations = document.querySelectorAll("#rec-list li.operation");
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.", "info", 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) {
const el = e.target;
const button = el.parentNode.parentNode.previousSibling;
button.innerHTML = el.textContent + " <span class='caret'></span>";
this.ingChange();
}
/**
* Handler for operationadd events.
*
* @listens Manager#operationadd
* @fires Manager#statechange
* @param {event} e
*/
opAdd(e) {
log.debug(`'${e.target.querySelector(".arg-title").textContent}' added to recipe`);
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);
}
/**
* 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);
}
}
export default RecipeWaiter;

View file

@ -1,48 +0,0 @@
/**
* Waiter to handle seasonal events and easter eggs.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const SeasonalWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
};
/**
* Loads all relevant items depending on the current date.
*/
SeasonalWaiter.prototype.load = function() {
// Konami code
this.kkeys = [];
window.addEventListener("keydown", this.konamiCodeListener.bind(this));
};
/**
* Listen for the Konami code sequence of keys. Turn the page upside down if they are all heard in
* sequence.
* #konamicode
*/
SeasonalWaiter.prototype.konamiCodeListener = function(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 = [];
}
}
};
export default SeasonalWaiter;

56
src/web/SeasonalWaiter.mjs Executable file
View file

@ -0,0 +1,56 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
/**
* 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;
}
/**
* Loads all relevant items depending on the current date.
*/
load() {
// Konami code
this.kkeys = [];
window.addEventListener("keydown", this.konamiCodeListener.bind(this));
}
/**
* 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 = [];
}
}
}
}
export default SeasonalWaiter;

View file

@ -1,54 +0,0 @@
/**
* Waiter to handle events related to the window object.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
*/
const WindowWaiter = function(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).
*/
WindowWaiter.prototype.windowResize = function() {
clearTimeout(this.resetLayoutTimeout);
this.resetLayoutTimeout = setTimeout(this.app.resetLayout.bind(this.app), 200);
};
/**
* 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.
*/
WindowWaiter.prototype.windowBlur = function() {
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.
*/
WindowWaiter.prototype.windowFocus = function() {
const unfocusedTime = new Date().getTime() - this.windowBlurTime;
if (unfocusedTime > 60000) {
this.app.silentBake();
}
};
export default WindowWaiter;

62
src/web/WindowWaiter.mjs Executable file
View file

@ -0,0 +1,62 @@
/**
* @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() {
clearTimeout(this.resetLayoutTimeout);
this.resetLayoutTimeout = setTimeout(this.app.resetLayout.bind(this.app), 200);
}
/**
* 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;

View file

@ -1,231 +0,0 @@
import ChefWorker from "worker-loader?inline&fallback=false!../core/ChefWorker.js";
/**
* Waiter to handle conversations with the ChefWorker.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2017
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
const WorkerWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
this.callbacks = {};
this.callbackID = 0;
};
/**
* Sets up the ChefWorker and associated listeners.
*/
WorkerWaiter.prototype.registerChefWorker = function() {
log.debug("Registering new ChefWorker");
this.chefWorker = new ChefWorker();
this.chefWorker.addEventListener("message", this.handleChefMessage.bind(this));
this.setLogLevel();
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
*/
WorkerWaiter.prototype.handleChefMessage = function(e) {
const r = e.data;
log.debug("Receiving '" + r.action + "' from ChefWorker");
switch (r.action) {
case "bakeComplete":
this.bakingComplete(r.data);
break;
case "bakeError":
this.app.handleError(r.data);
this.setBakingStatus(false);
break;
case "dishReturned":
this.callbacks[r.data.id](r.data);
break;
case "silentBakeComplete":
break;
case "workerLoaded":
this.app.workerLoaded = true;
log.debug("ChefWorker loaded");
this.app.loaded();
break;
case "statusMessage":
this.manager.output.setStatusMsg(r.data);
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;
}
};
/**
* Updates the UI to show if baking is in process or not.
*
* @param {bakingStatus}
*/
WorkerWaiter.prototype.setBakingStatus = function(bakingStatus) {
this.app.baking = bakingStatus;
this.manager.output.toggleLoader(bakingStatus);
};
/**
* Cancels the current bake by terminating the ChefWorker and creating a new one.
*/
WorkerWaiter.prototype.cancelBake = function() {
this.chefWorker.terminate();
this.registerChefWorker();
this.setBakingStatus(false);
this.manager.controls.showStaleIndicator();
};
/**
* Handler for completed bakes.
*
* @param {Object} response
*/
WorkerWaiter.prototype.bakingComplete = function(response) {
this.setBakingStatus(false);
if (!response) return;
if (response.error) {
this.app.handleError(response.error);
}
this.app.progress = response.progress;
this.app.dish = response.dish;
this.manager.recipe.updateBreakpointIndicator(response.progress);
this.manager.output.set(response.result, response.type, response.duration);
log.debug("--- Bake complete ---");
};
/**
* Asks the ChefWorker to bake the current input using the current recipe.
*
* @param {string} input
* @param {Object[]} recipeConfig
* @param {Object} options
* @param {number} progress
* @param {boolean} step
*/
WorkerWaiter.prototype.bake = function(input, recipeConfig, options, progress, step) {
this.setBakingStatus(true);
this.chefWorker.postMessage({
action: "bake",
data: {
input: input,
recipeConfig: recipeConfig,
options: options,
progress: progress,
step: step
}
});
};
/**
* 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]
*/
WorkerWaiter.prototype.silentBake = function(recipeConfig) {
this.chefWorker.postMessage({
action: "silentBake",
data: {
recipeConfig: recipeConfig
}
});
};
/**
* 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.
*/
WorkerWaiter.prototype.highlight = function(recipeConfig, direction, pos) {
this.chefWorker.postMessage({
action: "highlight",
data: {
recipeConfig: recipeConfig,
direction: direction,
pos: pos
}
});
};
/**
* Asks the ChefWorker to return the dish as the specified type
*
* @param {Dish} dish
* @param {string} type
* @param {Function} callback
*/
WorkerWaiter.prototype.getDishAs = function(dish, type, callback) {
const id = this.callbackID++;
this.callbacks[id] = callback;
this.chefWorker.postMessage({
action: "getDishAs",
data: {
dish: dish,
type: type,
id: id
}
});
};
/**
* Sets the console log level in the worker.
*
* @param {string} level
*/
WorkerWaiter.prototype.setLogLevel = function(level) {
if (!this.chefWorker) return;
this.chefWorker.postMessage({
action: "setLogLevel",
data: log.getLevel()
});
};
export default WorkerWaiter;

239
src/web/WorkerWaiter.mjs Executable file
View file

@ -0,0 +1,239 @@
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2017
* @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.callbacks = {};
this.callbackID = 0;
}
/**
* Sets up the ChefWorker and associated listeners.
*/
registerChefWorker() {
log.debug("Registering new ChefWorker");
this.chefWorker = new ChefWorker();
this.chefWorker.addEventListener("message", this.handleChefMessage.bind(this));
this.setLogLevel();
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");
switch (r.action) {
case "bakeComplete":
this.bakingComplete(r.data);
break;
case "bakeError":
this.app.handleError(r.data);
this.setBakingStatus(false);
break;
case "dishReturned":
this.callbacks[r.data.id](r.data);
break;
case "silentBakeComplete":
break;
case "workerLoaded":
this.app.workerLoaded = true;
log.debug("ChefWorker loaded");
this.app.loaded();
break;
case "statusMessage":
this.manager.output.setStatusMsg(r.data);
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;
}
}
/**
* Updates the UI to show if baking is in process or not.
*
* @param {bakingStatus}
*/
setBakingStatus(bakingStatus) {
this.app.baking = bakingStatus;
this.manager.output.toggleLoader(bakingStatus);
}
/**
* Cancels the current bake by terminating the ChefWorker and creating a new one.
*/
cancelBake() {
this.chefWorker.terminate();
this.registerChefWorker();
this.setBakingStatus(false);
this.manager.controls.showStaleIndicator();
}
/**
* Handler for completed bakes.
*
* @param {Object} response
*/
bakingComplete(response) {
this.setBakingStatus(false);
if (!response) return;
if (response.error) {
this.app.handleError(response.error);
}
this.app.progress = response.progress;
this.app.dish = response.dish;
this.manager.recipe.updateBreakpointIndicator(response.progress);
this.manager.output.set(response.result, response.type, response.duration);
log.debug("--- Bake complete ---");
}
/**
* Asks the ChefWorker to bake the current input using the current recipe.
*
* @param {string} input
* @param {Object[]} recipeConfig
* @param {Object} options
* @param {number} progress
* @param {boolean} step
*/
bake(input, recipeConfig, options, progress, step) {
this.setBakingStatus(true);
this.chefWorker.postMessage({
action: "bake",
data: {
input: input,
recipeConfig: recipeConfig,
options: options,
progress: progress,
step: step
}
});
}
/**
* 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) {
this.chefWorker.postMessage({
action: "silentBake",
data: {
recipeConfig: recipeConfig
}
});
}
/**
* 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) {
this.chefWorker.postMessage({
action: "highlight",
data: {
recipeConfig: recipeConfig,
direction: direction,
pos: pos
}
});
}
/**
* 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;
this.chefWorker.postMessage({
action: "getDishAs",
data: {
dish: dish,
type: type,
id: id
}
});
}
/**
* Sets the console log level in the worker.
*
* @param {string} level
*/
setLogLevel(level) {
if (!this.chefWorker) return;
this.chefWorker.postMessage({
action: "setLogLevel",
data: log.getLevel()
});
}
}
export default WorkerWaiter;

View file

@ -16,7 +16,7 @@ import moment from "moment-timezone";
import CanvasComponents from "../core/vendor/canvascomponents.js"; import CanvasComponents from "../core/vendor/canvascomponents.js";
// CyberChef // CyberChef
import App from "./App.js"; import App from "./App";
import Categories from "../core/config/Categories.json"; import Categories from "../core/config/Categories.json";
import OperationConfig from "../core/config/OperationConfig.json"; import OperationConfig from "../core/config/OperationConfig.json";

View file

@ -67,6 +67,7 @@ module.exports = {
{ {
test: /\.m?js$/, test: /\.m?js$/,
exclude: /node_modules\/(?!jsesc)/, exclude: /node_modules\/(?!jsesc)/,
type: "javascript/auto",
loader: "babel-loader?compact=false" loader: "babel-loader?compact=false"
}, },
{ {