mirror of
https://github.com/gchq/CyberChef
synced 2024-11-14 16:47: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
|
* @returns {Object[]} pos
|
||||||
*/
|
*/
|
||||||
highlight(pos, args) {
|
highlight(pos, args) {
|
||||||
|
if (args[0] === "Auto") return false;
|
||||||
const delim = Utils.charRep(args[0] || "Space"),
|
const delim = Utils.charRep(args[0] || "Space"),
|
||||||
len = delim === "\r\n" ? 1 : delim.length,
|
len = delim === "\r\n" ? 1 : delim.length,
|
||||||
width = len + 2;
|
width = len + 2;
|
||||||
|
|
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";
|
import CanvasComponents from "../core/vendor/canvascomponents.js";
|
||||||
|
|
||||||
// CyberChef
|
// CyberChef
|
||||||
import App from "./App.js";
|
import App from "./App";
|
||||||
import Categories from "../core/config/Categories.json";
|
import Categories from "../core/config/Categories.json";
|
||||||
import OperationConfig from "../core/config/OperationConfig.json";
|
import OperationConfig from "../core/config/OperationConfig.json";
|
||||||
|
|
||||||
|
|
|
@ -67,6 +67,7 @@ module.exports = {
|
||||||
{
|
{
|
||||||
test: /\.m?js$/,
|
test: /\.m?js$/,
|
||||||
exclude: /node_modules\/(?!jsesc)/,
|
exclude: /node_modules\/(?!jsesc)/,
|
||||||
|
type: "javascript/auto",
|
||||||
loader: "babel-loader?compact=false"
|
loader: "babel-loader?compact=false"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue