mirror of
https://github.com/gchq/CyberChef
synced 2024-11-14 08:37:07 +00:00
ESM: Rewritten src/web/ in ESM format.
This commit is contained in:
parent
c90acd24f5
commit
07715bd167
34 changed files with 5207 additions and 4341 deletions
|
@ -53,6 +53,7 @@ class FromHex extends Operation {
|
|||
* @returns {Object[]} pos
|
||||
*/
|
||||
highlight(pos, args) {
|
||||
if (args[0] === "Auto") return false;
|
||||
const delim = Utils.charRep(args[0] || "Space"),
|
||||
len = delim === "\r\n" ? 1 : delim.length,
|
||||
width = len + 2;
|
||||
|
|
1312
src/web/App.js
Executable file → Normal file
1312
src/web/App.js
Executable file → Normal file
File diff suppressed because it is too large
Load diff
753
src/web/App.mjs
Executable file
753
src/web/App.mjs
Executable 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;
|
|
@ -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
224
src/web/BindingsWaiter.mjs
Executable 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;
|
|
@ -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
449
src/web/ControlsWaiter.mjs
Executable 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;
|
|
@ -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
60
src/web/HTMLCategory.mjs
Executable 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;
|
|
@ -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'> </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
223
src/web/HTMLIngredient.mjs
Executable 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'> </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;
|
|
@ -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'> </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
129
src/web/HTMLOperation.mjs
Executable 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'> </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;
|
|
@ -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, " ");
|
||||
const endStr = end.toString().padStart(width, " ").replace(/ /g, " ");
|
||||
const lenStr = (end-start).toString().padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
return "start: " + startStr + "<br>end: " + endStr + "<br>length: " + lenStr;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Removes highlighting and selection information.
|
||||
*/
|
||||
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, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\n/g, " ")
|
||||
// Convert placeholders to tags
|
||||
.replace(startPlaceholderRegex, "<span class=\""+cssClass+"\">")
|
||||
.replace(endPlaceholderRegex, "</span>") + " ";
|
||||
|
||||
// Adjust width to allow for scrollbars
|
||||
highlighter.style.width = textarea.clientWidth + "px";
|
||||
highlighter.innerHTML = text;
|
||||
highlighter.scrollTop = textarea.scrollTop;
|
||||
highlighter.scrollLeft = textarea.scrollLeft;
|
||||
};
|
||||
|
||||
export default HighlighterWaiter;
|
468
src/web/HighlighterWaiter.mjs
Executable file
468
src/web/HighlighterWaiter.mjs
Executable 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, " ");
|
||||
const endStr = end.toString().padStart(width, " ").replace(/ /g, " ");
|
||||
const lenStr = (end-start).toString().padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
return "start: " + startStr + "<br>end: " + endStr + "<br>length: " + lenStr;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Removes highlighting and selection information.
|
||||
*/
|
||||
removeHighlights() {
|
||||
document.getElementById("input-highlighter").innerHTML = "";
|
||||
document.getElementById("output-highlighter").innerHTML = "";
|
||||
document.getElementById("input-selection-info").innerHTML = "";
|
||||
document.getElementById("output-selection-info").innerHTML = "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Highlights the given offsets in the output.
|
||||
* We will only highlight if:
|
||||
* - input hasn't changed since last bake
|
||||
* - last bake was a full bake
|
||||
* - all operations in the recipe support highlighting
|
||||
*
|
||||
* @param {Object} pos - The position object for the highlight.
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
*/
|
||||
highlightOutput(pos) {
|
||||
if (!this.app.autoBake_ || this.app.baking) return false;
|
||||
this.manager.worker.highlight(this.app.getRecipeConfig(), "forward", pos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Highlights the given offsets in the input.
|
||||
* We will only highlight if:
|
||||
* - input hasn't changed since last bake
|
||||
* - last bake was a full bake
|
||||
* - all operations in the recipe support highlighting
|
||||
*
|
||||
* @param {Object} pos - The position object for the highlight.
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
*/
|
||||
highlightInput(pos) {
|
||||
if (!this.app.autoBake_ || this.app.baking) return false;
|
||||
this.manager.worker.highlight(this.app.getRecipeConfig(), "reverse", pos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays highlight offsets sent back from the Chef.
|
||||
*
|
||||
* @param {Object} pos - The position object for the highlight.
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
* @param {string} direction
|
||||
*/
|
||||
displayHighlights(pos, direction) {
|
||||
if (!pos) return;
|
||||
|
||||
const io = direction === "forward" ? "output" : "input";
|
||||
|
||||
document.getElementById(io + "-selection-info").innerHTML = this.selectionInfo(pos[0].start, pos[0].end);
|
||||
this.highlight(
|
||||
document.getElementById(io + "-text"),
|
||||
document.getElementById(io + "-highlighter"),
|
||||
pos);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds the relevant HTML to the specified highlight element such that highlighting appears
|
||||
* underneath the correct offset.
|
||||
*
|
||||
* @param {element} textarea - The input or output textarea.
|
||||
* @param {element} highlighter - The input or output highlighter element.
|
||||
* @param {Object} pos - The position object for the highlight.
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
*/
|
||||
async highlight(textarea, highlighter, pos) {
|
||||
if (!this.app.options.showHighlighter) return false;
|
||||
if (!this.app.options.attemptHighlight) return false;
|
||||
|
||||
// Check if there is a carriage return in the output dish as this will not
|
||||
// be displayed by the HTML textarea and will mess up highlighting offsets.
|
||||
if (await this.manager.output.containsCR()) return false;
|
||||
|
||||
const startPlaceholder = "[startHighlight]";
|
||||
const startPlaceholderRegex = /\[startHighlight\]/g;
|
||||
const endPlaceholder = "[endHighlight]";
|
||||
const endPlaceholderRegex = /\[endHighlight\]/g;
|
||||
let text = textarea.value;
|
||||
|
||||
// Put placeholders in position
|
||||
// If there's only one value, select that
|
||||
// If there are multiple, ignore the first one and select all others
|
||||
if (pos.length === 1) {
|
||||
if (pos[0].end < pos[0].start) return;
|
||||
text = text.slice(0, pos[0].start) +
|
||||
startPlaceholder + text.slice(pos[0].start, pos[0].end) + endPlaceholder +
|
||||
text.slice(pos[0].end, text.length);
|
||||
} else {
|
||||
// O(n^2) - Can anyone improve this without overwriting placeholders?
|
||||
let result = "",
|
||||
endPlaced = true;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
for (let j = 1; j < pos.length; j++) {
|
||||
if (pos[j].end < pos[j].start) continue;
|
||||
if (pos[j].start === i) {
|
||||
result += startPlaceholder;
|
||||
endPlaced = false;
|
||||
}
|
||||
if (pos[j].end === i) {
|
||||
result += endPlaceholder;
|
||||
endPlaced = true;
|
||||
}
|
||||
}
|
||||
result += text[i];
|
||||
}
|
||||
if (!endPlaced) result += endPlaceholder;
|
||||
text = result;
|
||||
}
|
||||
|
||||
const cssClass = "hl1";
|
||||
//if (colour) cssClass += "-"+colour;
|
||||
|
||||
// Remove HTML tags
|
||||
text = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\n/g, " ")
|
||||
// Convert placeholders to tags
|
||||
.replace(startPlaceholderRegex, "<span class=\""+cssClass+"\">")
|
||||
.replace(endPlaceholderRegex, "</span>") + " ";
|
||||
|
||||
// Adjust width to allow for scrollbars
|
||||
highlighter.style.width = textarea.clientWidth + "px";
|
||||
highlighter.innerHTML = text;
|
||||
highlighter.scrollTop = textarea.scrollTop;
|
||||
highlighter.scrollLeft = textarea.scrollLeft;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default HighlighterWaiter;
|
|
@ -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, " ");
|
||||
let msg = "length: " + lengthStr;
|
||||
|
||||
if (typeof lines === "number") {
|
||||
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
|
||||
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
329
src/web/InputWaiter.mjs
Executable 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, " ");
|
||||
let msg = "length: " + lengthStr;
|
||||
|
||||
if (typeof lines === "number") {
|
||||
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
|
||||
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;
|
|
@ -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
307
src/web/Manager.mjs
Executable 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;
|
|
@ -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
321
src/web/OperationsWaiter.mjs
Executable 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;
|
|
@ -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, " ");
|
||||
const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
let msg = "time: " + timeStr + "<br>length: " + lengthStr;
|
||||
|
||||
if (typeof lines === "number") {
|
||||
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
|
||||
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
449
src/web/OutputWaiter.mjs
Executable 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, " ");
|
||||
const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
let msg = "time: " + timeStr + "<br>length: " + lengthStr;
|
||||
|
||||
if (typeof lines === "number") {
|
||||
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
|
||||
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;
|
|
@ -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
475
src/web/RecipeWaiter.mjs
Executable 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;
|
|
@ -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
56
src/web/SeasonalWaiter.mjs
Executable 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;
|
|
@ -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
62
src/web/WindowWaiter.mjs
Executable 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;
|
|
@ -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
239
src/web/WorkerWaiter.mjs
Executable 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;
|
|
@ -16,7 +16,7 @@ import moment from "moment-timezone";
|
|||
import CanvasComponents from "../core/vendor/canvascomponents.js";
|
||||
|
||||
// CyberChef
|
||||
import App from "./App.js";
|
||||
import App from "./App";
|
||||
import Categories from "../core/config/Categories.json";
|
||||
import OperationConfig from "../core/config/OperationConfig.json";
|
||||
|
||||
|
|
|
@ -67,6 +67,7 @@ module.exports = {
|
|||
{
|
||||
test: /\.m?js$/,
|
||||
exclude: /node_modules\/(?!jsesc)/,
|
||||
type: "javascript/auto",
|
||||
loader: "babel-loader?compact=false"
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue