mirror of
https://github.com/gchq/CyberChef.git
synced 2025-04-21 07:16:17 -04:00
Merged ESM into feature-bootstrap4. Started work on text inputs.
This commit is contained in:
commit
1ce6e32086
447 changed files with 39118 additions and 16687 deletions
736
src/web/App.js
736
src/web/App.js
|
@ -1,736 +0,0 @@
|
|||
import Utils from "../core/Utils.js";
|
||||
import Manager from "./Manager.js";
|
||||
import HTMLCategory from "./HTMLCategory.js";
|
||||
import HTMLOperation from "./HTMLOperation.js";
|
||||
import Split from "split.js";
|
||||
|
||||
|
||||
/**
|
||||
* HTML view for CyberChef responsible for building the web page and dealing with all user
|
||||
* interactions.
|
||||
*
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2016
|
||||
* @license Apache-2.0
|
||||
*
|
||||
* @constructor
|
||||
* @param {CatConf[]} categories - The list of categories and operations to be populated.
|
||||
* @param {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.
|
||||
*/
|
||||
const App = function(categories, operations, defaultFavourites, defaultOptions) {
|
||||
this.categories = categories;
|
||||
this.operations = operations;
|
||||
this.dfavourites = defaultFavourites;
|
||||
this.doptions = defaultOptions;
|
||||
this.options = Utils.extend({}, 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
|
||||
*/
|
||||
App.prototype.setup = function() {
|
||||
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
|
||||
*/
|
||||
App.prototype.loaded = function() {
|
||||
// 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);
|
||||
|
||||
document.dispatchEvent(this.manager.apploaded);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An error handler for displaying the error to the user.
|
||||
*
|
||||
* @param {Error} err
|
||||
* @param {boolean} [logToConsole=false]
|
||||
*/
|
||||
App.prototype.handleError = function(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.
|
||||
*/
|
||||
App.prototype.bake = function(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.
|
||||
*/
|
||||
App.prototype.autoBake = function() {
|
||||
// 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.
|
||||
*/
|
||||
App.prototype.silentBake = function() {
|
||||
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}
|
||||
*/
|
||||
App.prototype.getInput = function() {
|
||||
return this.manager.input.get();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the user's input data.
|
||||
*
|
||||
* @param {string} input - The string to set the input to
|
||||
*/
|
||||
App.prototype.setInput = function(input) {
|
||||
this.manager.input.set(input);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Populates the operations accordion list with the categories and operations specified in the
|
||||
* view constructor.
|
||||
*
|
||||
* @fires Manager#oplistcreate
|
||||
*/
|
||||
App.prototype.populateOperationsList = function() {
|
||||
// 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++) {
|
||||
let catConf = this.categories[i],
|
||||
selected = i === 0,
|
||||
cat = new HTMLCategory(catConf.name, selected);
|
||||
|
||||
for (let j = 0; j < catConf.ops.length; j++) {
|
||||
let opName = catConf.ops[j],
|
||||
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.
|
||||
*/
|
||||
App.prototype.initialiseSplitter = function() {
|
||||
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.
|
||||
*/
|
||||
App.prototype.loadLocalStorage = function() {
|
||||
// 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.
|
||||
*/
|
||||
App.prototype.loadFavourites = function() {
|
||||
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
|
||||
*/
|
||||
App.prototype.validFavourites = function(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
|
||||
*/
|
||||
App.prototype.saveFavourites = function(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.
|
||||
*/
|
||||
App.prototype.resetFavourites = function() {
|
||||
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
|
||||
*/
|
||||
App.prototype.addFavourite = function(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.
|
||||
*/
|
||||
App.prototype.loadURIParams = function() {
|
||||
// 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 = Utils.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}
|
||||
*/
|
||||
App.prototype.nextIngId = function() {
|
||||
return this.ingId++;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the current recipe configuration.
|
||||
*
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
App.prototype.getRecipeConfig = function() {
|
||||
return this.manager.recipe.getConfig();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Given a recipe configuration, sets the recipe to that configuration.
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
* @param {Object[]} recipeConfig - The recipe configuration
|
||||
*/
|
||||
App.prototype.setRecipeConfig = function(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.
|
||||
*/
|
||||
App.prototype.resetLayout = function() {
|
||||
this.columnSplitter.setSizes([20, 30, 50]);
|
||||
this.ioSplitter.setSizes([50, 50]);
|
||||
|
||||
this.manager.controls.adjustWidth();
|
||||
this.manager.output.adjustWidth();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the compile message.
|
||||
*/
|
||||
App.prototype.setCompileMessage = function() {
|
||||
// Display time since last build and compile message
|
||||
let now = new Date(),
|
||||
timeSinceCompile = Utils.fuzzyTime(now.getTime() - window.compileTime);
|
||||
|
||||
// Calculate previous version to compare to
|
||||
let 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}
|
||||
*/
|
||||
App.prototype.isLocalStorageAvailable = function() {
|
||||
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);
|
||||
*/
|
||||
App.prototype.alert = function(str, style, timeout, silent) {
|
||||
const time = new Date();
|
||||
|
||||
log.info("[" + time.toLocaleString() + "] " + str);
|
||||
if (silent) return;
|
||||
|
||||
style = style || "danger";
|
||||
timeout = timeout || 0;
|
||||
|
||||
let 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);});
|
||||
*/
|
||||
App.prototype.confirm = function(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.
|
||||
*/
|
||||
App.prototype.alertCloseClick = function() {
|
||||
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
|
||||
*/
|
||||
App.prototype.stateChange = function(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
|
||||
*/
|
||||
App.prototype.popState = function(e) {
|
||||
this.loadURIParams();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Function to call an external API from this view.
|
||||
*/
|
||||
App.prototype.callApi = function(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;
|
746
src/web/App.mjs
Executable file
746
src/web/App.mjs
Executable file
|
@ -0,0 +1,746 @@
|
|||
/**
|
||||
* @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
|
||||
});
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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,440 +0,0 @@
|
|||
import Utils from "../core/Utils.js";
|
||||
|
||||
|
||||
/**
|
||||
* 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-secondary");
|
||||
} else {
|
||||
autoBakeLabel.classList.add("btn-secondary");
|
||||
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 = Utils.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;
|
||||
}
|
||||
|
||||
let savedRecipes = localStorage.savedRecipes ?
|
||||
JSON.parse(localStorage.savedRecipes) : [],
|
||||
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;
|
396
src/web/ControlsWaiter.mjs
Executable file
396
src/web/ControlsWaiter.mjs
Executable file
|
@ -0,0 +1,396 @@
|
|||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initialise Bootstrap componenets
|
||||
*/
|
||||
initComponents() {
|
||||
$("body").bootstrapMaterialDesign();
|
||||
$("[data-toggle=tooltip]").tooltip({
|
||||
animation: false,
|
||||
container: "body",
|
||||
boundary: "viewport",
|
||||
trigger: "hover"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks or unchecks the Auto Bake checkbox based on the given value.
|
||||
*
|
||||
* @param {boolean} value - The new value for Auto Bake.
|
||||
*/
|
||||
setAutoBake(value) {
|
||||
const autoBakeCheckbox = document.getElementById("auto-bake");
|
||||
|
||||
if (autoBakeCheckbox.checked !== value) {
|
||||
autoBakeCheckbox.click();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler to trigger baking.
|
||||
*/
|
||||
bakeClick() {
|
||||
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() {
|
||||
this.app.autoBake_ = document.getElementById("auto-bake").checked;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for the 'Clear recipe' command. Removes all operations from the recipe.
|
||||
*/
|
||||
clearRecipeClick() {
|
||||
this.manager.recipe.clearRecipe();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Populates the save 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,51 +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 ? " show" : "")}">
|
||||
<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;
|
59
src/web/HTMLCategory.mjs
Executable file
59
src/web/HTMLCategory.mjs
Executable file
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* @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 ? " show" : "")}">
|
||||
<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,213 +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() {
|
||||
let inline = (this.type === "boolean" ||
|
||||
this.type === "number" ||
|
||||
this.type === "option" ||
|
||||
this.type === "shortString" ||
|
||||
this.type === "binaryShortString"),
|
||||
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-secondary 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) {
|
||||
let select = e.target,
|
||||
input = select.nextSibling;
|
||||
|
||||
input.value = select.childNodes[select.selectedIndex].value;
|
||||
|
||||
this.manager.recipe.ingChange();
|
||||
};
|
||||
|
||||
export default HTMLIngredient;
|
239
src/web/HTMLIngredient.mjs
Executable file
239
src/web/HTMLIngredient.mjs
Executable file
|
@ -0,0 +1,239 @@
|
|||
/**
|
||||
* @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.hint = config.hint || 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 += `<div class="form-group">
|
||||
<label for="${this.id}" class="bmd-label-floating">${this.name}</label>
|
||||
<input type="text"
|
||||
class="form-control arg"
|
||||
id="${this.id}"
|
||||
arg-name="${this.name}"
|
||||
value="${this.value}"
|
||||
${this.disabled ? "disabled" : ""}>
|
||||
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
|
||||
</div>`;
|
||||
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-secondary 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 += `<label class="checkbox-inline">
|
||||
<input type="checkbox"
|
||||
id="${this.id}"
|
||||
class="arg"
|
||||
arg-name="${this.name}"
|
||||
${this.value ? " checked='checked' " : ""}
|
||||
${this.disabled ? " disabled='disabled'" : ""}
|
||||
value="${this.name}"> ${this.name}
|
||||
</label>`;
|
||||
|
||||
// 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,129 +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='right'\
|
||||
data-content=\"" + this.description + "\" data-html='true' data-trigger='hover'\
|
||||
data-boundary='viewport'";
|
||||
}
|
||||
|
||||
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;
|
127
src/web/HTMLOperation.mjs
Executable file
127
src/web/HTMLOperation.mjs
Executable file
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* @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='right'
|
||||
data-content="${this.description}" data-html='true' data-trigger='hover'
|
||||
data-boundary='viewport'`;
|
||||
}
|
||||
|
||||
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="op-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>
|
||||
</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,
|
||||
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 = 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 (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.js";
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
2
src/web/LoaderWorker.js
Normal file → Executable file
2
src/web/LoaderWorker.js
Normal file → Executable file
|
@ -25,7 +25,7 @@ self.addEventListener("message", function(e) {
|
|||
*/
|
||||
self.loadFile = function(file) {
|
||||
const reader = new FileReader();
|
||||
let data = new Uint8Array(file.size);
|
||||
const data = new Uint8Array(file.size);
|
||||
let offset = 0;
|
||||
const CHUNK_SIZE = 10485760; // 10MiB
|
||||
|
||||
|
|
|
@ -1,300 +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(".file-switch", "click", this.output.fileSwitch, this.output);
|
||||
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.initComponents();
|
||||
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("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("dispose");
|
||||
} catch (err) {}
|
||||
searchResultsEl.removeChild(searchResultsEl.firstChild);
|
||||
}
|
||||
|
||||
$("#categories .show").collapse("hide");
|
||||
if (str) {
|
||||
const matchedOps = this.filterOperations(str, true);
|
||||
const matchedOpsHtml = matchedOps
|
||||
.map(v => v.toStubHtml())
|
||||
.join("");
|
||||
|
||||
searchResultsEl.innerHTML = matchedOpsHtml;
|
||||
searchResultsEl.dispatchEvent(this.manager.oplistcreate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Filters operations based on the search string and returns the matching ones.
|
||||
*
|
||||
* @param {string} searchStr
|
||||
* @param {boolean} highlight - Whether or not to highlight the matching string in the operation
|
||||
* name and description
|
||||
* @returns {string[]}
|
||||
*/
|
||||
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("dispose");
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
},
|
||||
onEnd: function(evt) {
|
||||
if (this.removeIntent) {
|
||||
$(evt.item).popover("dispose");
|
||||
evt.item.remove();
|
||||
}
|
||||
}.bind(this),
|
||||
});
|
||||
|
||||
Sortable.utils.on(editFavouritesList, "dragleave", function() {
|
||||
this.removeIntent = true;
|
||||
}.bind(this));
|
||||
|
||||
Sortable.utils.on(editFavouritesList, "dragover", function() {
|
||||
this.removeIntent = false;
|
||||
}.bind(this));
|
||||
|
||||
$("#edit-favourites-list [data-toggle=popover]").popover();
|
||||
$("#favourites-modal").modal();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for save favourites click events.
|
||||
* Saves the selected favourites and reloads them.
|
||||
*/
|
||||
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("dispose");
|
||||
} catch (err) {}
|
||||
searchResultsEl.removeChild(searchResultsEl.firstChild);
|
||||
}
|
||||
|
||||
$("#categories .show").collapse("hide");
|
||||
if (str) {
|
||||
const matchedOps = this.filterOperations(str, true);
|
||||
const matchedOpsHtml = matchedOps
|
||||
.map(v => v.toStubHtml())
|
||||
.join("");
|
||||
|
||||
searchResultsEl.innerHTML = matchedOpsHtml;
|
||||
searchResultsEl.dispatchEvent(this.manager.oplistcreate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Filters operations based on the search string and returns the matching ones.
|
||||
*
|
||||
* @param {string} searchStr
|
||||
* @param {boolean} highlight - Whether or not to highlight the matching string in the operation
|
||||
* name and description
|
||||
* @returns {string[]}
|
||||
*/
|
||||
filterOperations(inStr, highlight) {
|
||||
const matchedOps = [];
|
||||
const matchedDescs = [];
|
||||
|
||||
const searchStr = inStr.toLowerCase();
|
||||
|
||||
for (const opName in this.app.operations) {
|
||||
const op = this.app.operations[opName];
|
||||
const namePos = opName.toLowerCase().indexOf(searchStr);
|
||||
const descPos = op.description.toLowerCase().indexOf(searchStr);
|
||||
|
||||
if (namePos >= 0 || descPos >= 0) {
|
||||
const operation = new HTMLOperation(opName, this.app.operations[opName], this.app, this.manager);
|
||||
if (highlight) {
|
||||
operation.highlightSearchString(searchStr, namePos, descPos);
|
||||
}
|
||||
|
||||
if (namePos < 0) {
|
||||
matchedOps.push(operation);
|
||||
} else {
|
||||
matchedDescs.push(operation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matchedDescs.concat(matchedOps);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds the operation which has been selected using keyboard shortcuts. This will have the class
|
||||
* 'selected-op' set. Returns the index of the operation within the given list.
|
||||
*
|
||||
* @param {element[]} ops
|
||||
* @returns {number}
|
||||
*/
|
||||
getSelectedOp(ops) {
|
||||
for (let i = 0; i < ops.length; i++) {
|
||||
if (ops[i].classList.contains("selected-op")) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for oplistcreate events.
|
||||
*
|
||||
* @listens Manager#oplistcreate
|
||||
* @param {event} e
|
||||
*/
|
||||
opListCreate(e) {
|
||||
this.manager.recipe.createSortableSeedList(e.target);
|
||||
this.enableOpsListPopovers(e.target);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets up popovers, allowing the popover itself to gain focus which enables scrolling
|
||||
* and other interactions.
|
||||
*
|
||||
* @param {Element} el - The element to start selecting from
|
||||
*/
|
||||
enableOpsListPopovers(el) {
|
||||
$(el).find("[data-toggle=popover]").addBack("[data-toggle=popover]")
|
||||
.popover({trigger: "manual"})
|
||||
.on("mouseenter", function(e) {
|
||||
if (e.buttons > 0) return; // Mouse button held down - likely dragging an opertion
|
||||
const _this = this;
|
||||
$(this).popover("show");
|
||||
$(".popover").on("mouseleave", function () {
|
||||
$(_this).popover("hide");
|
||||
});
|
||||
}).on("mouseleave", function () {
|
||||
const _this = this;
|
||||
setTimeout(function() {
|
||||
// Determine if the popover associated with this element is being hovered over
|
||||
if ($(_this).data("bs.popover") &&
|
||||
($(_this).data("bs.popover").tip && !$($(_this).data("bs.popover").tip).is(":hover"))) {
|
||||
$(_this).popover("hide");
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for operation doubleclick events.
|
||||
* Adds the operation to the recipe and auto bakes.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
operationDblclick(e) {
|
||||
const li = e.target;
|
||||
|
||||
this.manager.recipe.addOperation(li.textContent);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for edit favourites click events.
|
||||
* Sets up the 'Edit favourites' pane and displays it.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
editFavouritesClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Add favourites to modal
|
||||
const favCat = this.app.categories.filter(function(c) {
|
||||
return c.name === "Favourites";
|
||||
})[0];
|
||||
|
||||
let html = "";
|
||||
for (let i = 0; i < favCat.ops.length; i++) {
|
||||
const opName = favCat.ops[i];
|
||||
const operation = new HTMLOperation(opName, this.app.operations[opName], this.app, this.manager);
|
||||
html += operation.toStubHtml(true);
|
||||
}
|
||||
|
||||
const editFavouritesList = document.getElementById("edit-favourites-list");
|
||||
editFavouritesList.innerHTML = html;
|
||||
this.removeIntent = false;
|
||||
|
||||
const editableList = Sortable.create(editFavouritesList, {
|
||||
filter: ".remove-icon",
|
||||
onFilter: function (evt) {
|
||||
const el = editableList.closest(evt.item);
|
||||
if (el && el.parentNode) {
|
||||
$(el).popover("dispose");
|
||||
el.parentNode.removeChild(el);
|
||||
}
|
||||
},
|
||||
onEnd: function(evt) {
|
||||
if (this.removeIntent) {
|
||||
$(evt.item).popover("dispose");
|
||||
evt.item.remove();
|
||||
}
|
||||
}.bind(this),
|
||||
});
|
||||
|
||||
Sortable.utils.on(editFavouritesList, "dragleave", function() {
|
||||
this.removeIntent = true;
|
||||
}.bind(this));
|
||||
|
||||
Sortable.utils.on(editFavouritesList, "dragover", function() {
|
||||
this.removeIntent = false;
|
||||
}.bind(this));
|
||||
|
||||
$("#edit-favourites-list [data-toggle=popover]").popover();
|
||||
$("#favourites-modal").modal();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for save favourites click events.
|
||||
* Saves the selected favourites and reloads them.
|
||||
*/
|
||||
saveFavouritesClick() {
|
||||
const favs = document.querySelectorAll("#edit-favourites-list li");
|
||||
const favouritesList = Array.from(favs, e => e.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,416 +0,0 @@
|
|||
import Utils from "../core/Utils.js";
|
||||
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 = 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();
|
||||
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;
|
||||
this.dishStr = Utils.unescapeHtml(Utils.stripHtmlTags(data, true));
|
||||
length = data.length;
|
||||
lines = this.dishStr.count("\n") + 1;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "ArrayBuffer":
|
||||
outputText.style.display = "block";
|
||||
outputHtml.style.display = "none";
|
||||
outputHighlighter.display = "none";
|
||||
inputHighlighter.display = "none";
|
||||
|
||||
outputText.value = "";
|
||||
outputHtml.innerHTML = "";
|
||||
this.dishStr = "";
|
||||
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 = function() {
|
||||
this.filename = window.prompt("Please enter a filename:", this.filename || "download.dat");
|
||||
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() {
|
||||
if (!this.dishBuffer) {
|
||||
this.dishBuffer = new Uint8Array(Utils.strToCharcode(this.dishStr)).buffer;
|
||||
}
|
||||
this.downloadFile();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for copy click events.
|
||||
* Copies the output to the clipboard.
|
||||
*/
|
||||
OutputWaiter.prototype.copyClick = function() {
|
||||
// Create invisible textarea to populate with the raw dishStr (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 = 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 {
|
||||
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 file switch click events.
|
||||
* Moves a file's data for items created via Utils.displayFilesAsHTML to the input.
|
||||
*/
|
||||
OutputWaiter.prototype.fileSwitch = function(e) {
|
||||
e.preventDefault();
|
||||
this.switchOrigData = this.manager.input.get();
|
||||
this.app.setInput(e.target.getAttribute("fileValue"));
|
||||
document.getElementById("undo-switch").disabled = false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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 = function() {
|
||||
return this.dishStr.indexOf("\r") >= 0;
|
||||
};
|
||||
|
||||
export default OutputWaiter;
|
421
src/web/OutputWaiter.mjs
Executable file
421
src/web/OutputWaiter.mjs
Executable file
|
@ -0,0 +1,421 @@
|
|||
/**
|
||||
* @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 = "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const undoSwitch = document.getElementById("undo-switch");
|
||||
undoSwitch.disabled = true;
|
||||
$(undoSwitch).tooltip("hide");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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("data-original-title").indexOf("Maximise") === 0) {
|
||||
this.app.columnSplitter.collapse(0);
|
||||
this.app.columnSplitter.collapse(1);
|
||||
this.app.ioSplitter.collapse(0);
|
||||
|
||||
$(el).attr("data-original-title", "Restore output pane");
|
||||
el.querySelector("i").innerHTML = "fullscreen_exit";
|
||||
} else {
|
||||
$(el).attr("data-original-title", "Maximise output pane");
|
||||
el.querySelector("i").innerHTML = "fullscreen";
|
||||
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,473 +0,0 @@
|
|||
import HTMLOperation from "./HTMLOperation.js";
|
||||
import Sortable from "sortablejs";
|
||||
import Utils from "../core/Utils.js";
|
||||
|
||||
|
||||
/**
|
||||
* 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("dispose")
|
||||
.removeData("bs.popover")
|
||||
.off("mouseenter")
|
||||
.off("mouseleave")
|
||||
.attr("data-toggle", "popover-disabled");
|
||||
$(evt.clone)
|
||||
.off(".popover")
|
||||
.removeData("bs.popover");
|
||||
},
|
||||
onEnd: this.opSortEnd.bind(this)
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for operation sort end events.
|
||||
* Removes the operation from the list if it has been dropped outside. If not, adds it to the list
|
||||
* at the appropriate place and initialises it.
|
||||
*
|
||||
* @fires Manager#operationadd
|
||||
* @param {event} evt
|
||||
*/
|
||||
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();
|
||||
|
||||
let 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;
|
481
src/web/RecipeWaiter.mjs
Executable file
481
src/web/RecipeWaiter.mjs
Executable file
|
@ -0,0 +1,481 @@
|
|||
/**
|
||||
* @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(".op-title").textContent);
|
||||
},
|
||||
onEnd: function(evt) {
|
||||
if (this.removeIntent) {
|
||||
evt.item.remove();
|
||||
evt.target.dispatchEvent(this.manager.operationremove);
|
||||
}
|
||||
}.bind(this),
|
||||
onSort: function(evt) {
|
||||
if (evt.from.id === "rec-list") {
|
||||
document.dispatchEvent(this.manager.statechange);
|
||||
}
|
||||
}.bind(this)
|
||||
});
|
||||
|
||||
Sortable.utils.on(recList, "dragover", function() {
|
||||
this.removeIntent = false;
|
||||
}.bind(this));
|
||||
|
||||
Sortable.utils.on(recList, "dragleave", function() {
|
||||
this.removeIntent = true;
|
||||
this.app.progress = 0;
|
||||
}.bind(this));
|
||||
|
||||
Sortable.utils.on(recList, "touchend", function(e) {
|
||||
const loc = e.changedTouches[0];
|
||||
const target = document.elementFromPoint(loc.clientX, loc.clientY);
|
||||
|
||||
this.removeIntent = !recList.contains(target);
|
||||
}.bind(this));
|
||||
|
||||
// Favourites category
|
||||
document.querySelector("#categories a").addEventListener("dragover", this.favDragover.bind(this));
|
||||
document.querySelector("#categories a").addEventListener("dragleave", this.favDragleave.bind(this));
|
||||
document.querySelector("#categories a").addEventListener("drop", this.favDrop.bind(this));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a drag-n-droppable seed list of operations.
|
||||
*
|
||||
* @param {element} listEl - The list to initialise
|
||||
*/
|
||||
createSortableSeedList(listEl) {
|
||||
Sortable.create(listEl, {
|
||||
group: {
|
||||
name: "recipe",
|
||||
pull: "clone",
|
||||
put: false,
|
||||
},
|
||||
sort: false,
|
||||
setData: function(dataTransfer, dragEl) {
|
||||
dataTransfer.setData("Text", dragEl.textContent);
|
||||
},
|
||||
onStart: function(evt) {
|
||||
// Removes popover element and event bindings from the dragged operation but not the
|
||||
// event bindings from the one left in the operations list. Without manually removing
|
||||
// these bindings, we cannot re-initialise the popover on the stub operation.
|
||||
$(evt.item)
|
||||
.popover("dispose")
|
||||
.removeData("bs.popover")
|
||||
.off("mouseenter")
|
||||
.off("mouseleave")
|
||||
.attr("data-toggle", "popover-disabled");
|
||||
$(evt.clone)
|
||||
.off(".popover")
|
||||
.removeData("bs.popover");
|
||||
},
|
||||
onEnd: this.opSortEnd.bind(this)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for operation sort end events.
|
||||
* Removes the operation from the list if it has been dropped outside. If not, adds it to the list
|
||||
* at the appropriate place and initialises it.
|
||||
*
|
||||
* @fires Manager#operationadd
|
||||
* @param {event} evt
|
||||
*/
|
||||
opSortEnd(evt) {
|
||||
if (this.removeIntent) {
|
||||
if (evt.item.parentNode.id === "rec-list") {
|
||||
evt.item.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Reinitialise the popover on the original element in the ops list because for some reason it
|
||||
// gets destroyed and recreated.
|
||||
this.manager.ops.enableOpsListPopovers(evt.clone);
|
||||
|
||||
if (evt.item.parentNode.id !== "rec-list") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildRecipeOperation(evt.item);
|
||||
evt.item.dispatchEvent(this.manager.operationadd);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for favourite dragover events.
|
||||
* If the element being dragged is an operation, displays a visual cue so that the user knows it can
|
||||
* be dropped here.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
favDragover(e) {
|
||||
if (e.dataTransfer.effectAllowed !== "move")
|
||||
return false;
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (e.target.className && e.target.className.indexOf("category-title") > -1) {
|
||||
// Hovering over the a
|
||||
e.target.classList.add("favourites-hover");
|
||||
} else if (e.target.parentNode.className && e.target.parentNode.className.indexOf("category-title") > -1) {
|
||||
// Hovering over the Edit button
|
||||
e.target.parentNode.classList.add("favourites-hover");
|
||||
} else if (e.target.parentNode.parentNode.className && e.target.parentNode.parentNode.className.indexOf("category-title") > -1) {
|
||||
// Hovering over the image on the Edit button
|
||||
e.target.parentNode.parentNode.classList.add("favourites-hover");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for favourite dragleave events.
|
||||
* Removes the visual cue.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
favDragleave(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
document.querySelector("#categories a").classList.remove("favourites-hover");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for favourite drop events.
|
||||
* Adds the dragged operation to the favourites list.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
favDrop(e) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
e.target.classList.remove("favourites-hover");
|
||||
|
||||
const opName = e.dataTransfer.getData("Text");
|
||||
this.app.addFavourite(opName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for ingredient change events.
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
*/
|
||||
ingChange(e) {
|
||||
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(".op-title").textContent,
|
||||
args: ingredients
|
||||
};
|
||||
|
||||
if (disabled && disabled.getAttribute("disabled") === "true") {
|
||||
item.disabled = true;
|
||||
}
|
||||
|
||||
if (bp && bp.getAttribute("break") === "true") {
|
||||
item.breakpoint = true;
|
||||
}
|
||||
|
||||
config.push(item);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Moves or removes the breakpoint indicator in the recipe based on the position.
|
||||
*
|
||||
* @param {number} 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(".op-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,203 +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;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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 "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.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
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
|
@ -42,7 +42,7 @@
|
|||
}
|
||||
|
||||
// Define loading messages
|
||||
const loadingMsgs = [
|
||||
var loadingMsgs = [
|
||||
"Proving P = NP...",
|
||||
"Computing 6 x 9...",
|
||||
"Mining bitcoin...",
|
||||
|
@ -66,18 +66,18 @@
|
|||
|
||||
// Shuffle array using Durstenfeld algorithm
|
||||
for (let i = loadingMsgs.length - 1; i > 0; --i) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const temp = loadingMsgs[i];
|
||||
var j = Math.floor(Math.random() * (i + 1));
|
||||
var temp = loadingMsgs[i];
|
||||
loadingMsgs[i] = loadingMsgs[j];
|
||||
loadingMsgs[j] = temp;
|
||||
}
|
||||
|
||||
// Show next loading message and move it to the end of the array
|
||||
function changeLoadingMsg() {
|
||||
const msg = loadingMsgs.shift();
|
||||
var msg = loadingMsgs.shift();
|
||||
loadingMsgs.push(msg);
|
||||
try {
|
||||
const el = document.getElementById("preloader-msg");
|
||||
var el = document.getElementById("preloader-msg");
|
||||
if (!el.classList.contains("loading"))
|
||||
el.classList.add("loading"); // Causes CSS transition on first message
|
||||
el.innerHTML = msg;
|
||||
|
@ -86,6 +86,46 @@
|
|||
|
||||
changeLoadingMsg();
|
||||
window.loadingMsgsInt = setInterval(changeLoadingMsg, (Math.random() * 2000) + 1500);
|
||||
|
||||
// If any errors are thrown during loading, handle them here
|
||||
function loadingErrorHandler(e) {
|
||||
function escapeHtml(str) {
|
||||
var HTML_CHARS = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'", // ' not recommended because it's not in the HTML spec
|
||||
"/": "/", // forward slash is included as it helps end an HTML entity
|
||||
"`": "`"
|
||||
};
|
||||
|
||||
return str.replace(/[&<>"'/`]/g, function (match) {
|
||||
return HTML_CHARS[match];
|
||||
});
|
||||
}
|
||||
|
||||
var msg = e.message +
|
||||
(e.filename ? "\nFilename: " + e.filename : "") +
|
||||
(e.lineno ? "\nLine: " + e.lineno : "") +
|
||||
(e.colno ? "\nColumn: " + e.colno : "") +
|
||||
(e.error ? "\nError: " + e.error : "") +
|
||||
"\nUser-Agent: " + navigator.userAgent +
|
||||
"\nCyberChef version: <%= htmlWebpackPlugin.options.version %>";
|
||||
|
||||
clearInterval(window.loadingMsgsInt);
|
||||
document.getElementById("preloader").remove();
|
||||
document.getElementById("preloader-msg").remove();
|
||||
document.getElementById("preloader-error").innerHTML =
|
||||
"CyberChef encountered an error while loading.<br><br>" +
|
||||
"The following browser versions are supported:" +
|
||||
"<ul><li>Google Chrome 40+</li><li>Mozilla Firefox 35+</li><li>Microsoft Edge 14+</li></ul>" +
|
||||
"Your user agent is:<br>" + escapeHtml(navigator.userAgent) + "<br><br>" +
|
||||
"If your browser is supported, please <a href='https://github.com/gchq/CyberChef/issues/new'>" +
|
||||
"raise an issue</a> including the following details:<br><br>" +
|
||||
"<pre>" + escapeHtml(msg) + "</pre>";
|
||||
};
|
||||
window.addEventListener("error", loadingErrorHandler);
|
||||
</script>
|
||||
<% if (htmlWebpackPlugin.options.inline) { %>
|
||||
<meta name="robots" content="noindex" />
|
||||
|
@ -100,9 +140,12 @@
|
|||
<div id="loader-wrapper">
|
||||
<div id="preloader" class="loader"></div>
|
||||
<div id="preloader-msg" class="loading-msg"></div>
|
||||
<div id="preloader-error" class="loading-error"></div>
|
||||
</div>
|
||||
<!-- End preloader overlay -->
|
||||
<span id="edit-favourites" class="btn btn-secondary btn-sm"><img aria-hidden="true" src="<%- require('../static/images/favourite-16x16.png') %>" alt="Star Icon"/> Edit</span>
|
||||
<button type="button" class="btn btn-warning bmd-btn-icon" id="edit-favourites" data-toggle="tooltip" title="Edit favourites">
|
||||
<i class="material-icons">star</i>
|
||||
</button>
|
||||
<div id="alert" class="alert alert-danger">
|
||||
<button type="button" class="close" id="alert-close">×</button>
|
||||
<span id="alert-content"></span>
|
||||
|
@ -142,32 +185,38 @@
|
|||
</div>
|
||||
|
||||
<div id="recipe" class="split split-horizontal no-select">
|
||||
<div class="title no-select">Recipe</div>
|
||||
<div class="title no-select">
|
||||
Recipe
|
||||
<span class="float-right">
|
||||
<button type="button" class="btn btn-danger bmd-btn-icon" id="save" data-toggle="tooltip" title="Save recipe">
|
||||
<i class="material-icons">save</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger bmd-btn-icon" id="load" data-toggle="tooltip" title="Load recipe">
|
||||
<i class="material-icons">folder</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger bmd-btn-icon" id="clr-recipe" data-toggle="tooltip" title="Clear recipe">
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<ul id="rec-list" class="list-area no-select"></ul>
|
||||
|
||||
<div id="controls" class="no-select">
|
||||
<div id="operational-controls">
|
||||
<div id="bake-group">
|
||||
<button type="button" class="btn btn-success btn-lg" id="bake">
|
||||
<img aria-hidden="true" src="<%- require('../static/images/cook_male-32x32.png') %>" alt="Chef Icon"/>
|
||||
<span>Bake!</span>
|
||||
</button>
|
||||
<label class="btn btn-success btn-lg" id="auto-bake-label" for="auto-bake">
|
||||
<input type="checkbox" checked="checked" id="auto-bake">
|
||||
<div>Auto Bake</div>
|
||||
<div class="d-flex justify-content-end align-items-center">
|
||||
<div class="mx-1 checkbox">
|
||||
<label>
|
||||
<input type="checkbox" checked="checked" id="auto-bake"> Auto Bake
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-group" style="padding-top: 10px;">
|
||||
<button type="button" class="btn btn-secondary" id="step"><img aria-hidden="true" src="<%- require('../static/images/step-16x16.png') %>" alt="Footstep Icon"/> Step through</button>
|
||||
<button type="button" class="btn btn-secondary" id="clr-breaks"><img aria-hidden="true" src="<%- require('../static/images/erase-16x16.png') %>" alt="Eraser Icon"/> Clear breakpoints</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group-vertical" id="extra-controls">
|
||||
<button type="button" class="btn btn-secondary" id="save"><img aria-hidden="true" src="<%- require('../static/images/save-16x16.png') %>" alt="Save Icon"/> Save recipe</button>
|
||||
<button type="button" class="btn btn-secondary" id="load"><img aria-hidden="true" src="<%- require('../static/images/open_yellow-16x16.png') %>" alt="Open Icon"/> Load recipe</button>
|
||||
<button type="button" class="btn btn-secondary" id="clr-recipe"><img aria-hidden="true" src="<%- require('../static/images/clean-16x16.png') %>" alt="Broom Icon"/> Clear recipe</button>
|
||||
<button type="button" class="mx-1 btn btn-outline-secondary" id="step" data-toggle="tooltip" title="Step through the recipe">
|
||||
<!-- <i class="material-icons">directions_walk</i> -->
|
||||
Step
|
||||
</button>
|
||||
<button type="button" class="mx-1 btn btn-success btn-raised" id="bake">
|
||||
<!-- <img aria-hidden="true" src="<%- require('../static/images/cook_male-32x32.png') %>" alt="Chef Icon"/> -->
|
||||
<!-- <i class="material-icons">restaurant</i> -->
|
||||
<span>Bake!</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -176,10 +225,14 @@
|
|||
<div id="input" class="split no-select">
|
||||
<div class="title no-select">
|
||||
<label for="input-text">Input</label>
|
||||
<div class="btn-group io-btn-group">
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="clr-io"><img aria-hidden="true" src="<%- require('../static/images/recycle-16x16.png') %>" alt="Recycle Icon"/> Clear I/O</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="reset-layout"><img aria-hidden="true" src="<%- require('../static/images/layout-16x16.png') %>" alt="Grid Icon"/> Reset layout</button>
|
||||
</div>
|
||||
<span class="float-right">
|
||||
<button type="button" class="btn btn-danger bmd-btn-icon" id="clr-io" data-toggle="tooltip" title="Clear input and output">
|
||||
<i class="material-icons">delete</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-info bmd-btn-icon" id="reset-layout" data-toggle="tooltip" title="Reset pane layout">
|
||||
<i class="material-icons">view_compact</i>
|
||||
</button>
|
||||
</span>
|
||||
<div class="io-info" id="input-info"></div>
|
||||
<div class="io-info" id="input-selection-info"></div>
|
||||
</div>
|
||||
|
@ -207,13 +260,23 @@
|
|||
<div id="output" class="split">
|
||||
<div class="title no-select">
|
||||
<label for="output-text">Output</label>
|
||||
<div class="btn-group io-btn-group">
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="save-to-file" title="Save to file"><img aria-hidden="true" src="<%- require('../static/images/save_as-16x16.png') %>" alt="Save Icon"/> Save to file</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="copy-output" title="Copy output"><img aria-hidden="true" src="<%- require('../static/images/copy-16x16.png') %>" alt="Copy Icon"/> Copy raw output</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="switch" title="Move output to input"><img aria-hidden="true" src="<%- require('../static/images/switch-16x16.png') %>" alt="Switch Icon"/> Move output to input</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="undo-switch" title="Undo move" disabled="disabled"><img aria-hidden="true" src="<%- require('../static/images/undo-16x16.png') %>" alt="Undo Icon"/> Undo</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" id="maximise-output" title="Maximise"><img aria-hidden="true" src="<%- require('../static/images/maximise-16x16.png') %>" alt="Maximise Icon"/> Max</button>
|
||||
</div>
|
||||
<span class="float-right">
|
||||
<button type="button" class="btn btn-danger bmd-btn-icon" id="save-to-file" data-toggle="tooltip" title="Save output to file">
|
||||
<i class="material-icons">save</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger bmd-btn-icon" id="copy-output" data-toggle="tooltip" title="Copy raw output to the clipboard">
|
||||
<i class="material-icons">content_copy</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger bmd-btn-icon" id="switch" data-toggle="tooltip" title="Move output to input">
|
||||
<i class="material-icons">loop</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning bmd-btn-icon" id="undo-switch" data-toggle="tooltip" title="Undo" disabled="disabled">
|
||||
<i class="material-icons">undo</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger bmd-btn-icon" id="maximise-output" data-toggle="tooltip" title="Maximise output pane">
|
||||
<i class="material-icons">fullscreen</i>
|
||||
</button>
|
||||
</span>
|
||||
<div class="io-info" id="output-info"></div>
|
||||
<div class="io-info" id="output-selection-info"></div>
|
||||
<span id="stale-indicator" title="The output is stale. The input or recipe has changed since this output was generated. Bake again to get the new value.">🕑</span>
|
||||
|
|
|
@ -9,15 +9,17 @@ import "./stylesheets/index.js";
|
|||
|
||||
// Libs
|
||||
import "babel-polyfill";
|
||||
import "bootstrap";
|
||||
import "arrive";
|
||||
import "bootstrap-material-design";
|
||||
import "bootstrap-switch";
|
||||
import "bootstrap-colorpicker";
|
||||
import CanvasComponents from "../core/lib/canvascomponents.js";
|
||||
import moment from "moment-timezone";
|
||||
import * as CanvasComponents from "../core/lib/CanvasComponents";
|
||||
|
||||
// CyberChef
|
||||
import App from "./App.js";
|
||||
import Categories from "../core/config/Categories.js";
|
||||
import OperationConfig from "../core/config/MetaConfig.js";
|
||||
import App from "./App";
|
||||
import Categories from "../core/config/Categories.json";
|
||||
import OperationConfig from "../core/config/OperationConfig.json";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -34,7 +36,8 @@ function main() {
|
|||
"URL Decode",
|
||||
"Regular expression",
|
||||
"Entropy",
|
||||
"Fork"
|
||||
"Fork",
|
||||
"Magic"
|
||||
];
|
||||
|
||||
const defaultOptions = {
|
||||
|
@ -63,3 +66,4 @@ window.compileMessage = COMPILE_MSG;
|
|||
window.CanvasComponents = CanvasComponents;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", main, false);
|
||||
|
||||
|
|
0
src/web/static/ga.html
Normal file → Executable file
0
src/web/static/ga.html
Normal file → Executable file
0
src/web/static/images/file-128x128.png
Normal file → Executable file
0
src/web/static/images/file-128x128.png
Normal file → Executable file
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
0
src/web/static/images/file-32x32.png
Normal file → Executable file
0
src/web/static/images/file-32x32.png
Normal file → Executable file
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
@ -1,5 +1,5 @@
|
|||
import sm from "sitemap";
|
||||
import OperationConfig from "../../core/config/MetaConfig.js";
|
||||
import OperationConfig from "../../core/config/OperationConfig.json";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -20,7 +20,7 @@ sitemap.add({
|
|||
priority: 1.0
|
||||
});
|
||||
|
||||
for (let op in OperationConfig) {
|
||||
for (const op in OperationConfig) {
|
||||
sitemap.add({
|
||||
url: `/?op=${encodeURIComponent(op)}`,
|
||||
changeFreq: "yearly",
|
||||
|
|
0
src/web/static/structuredData.json
Normal file → Executable file
0
src/web/static/structuredData.json
Normal file → Executable file
0
src/web/stylesheets/components/_alert.css
Normal file → Executable file
0
src/web/stylesheets/components/_alert.css
Normal file → Executable file
0
src/web/stylesheets/components/_button.css
Normal file → Executable file
0
src/web/stylesheets/components/_button.css
Normal file → Executable file
0
src/web/stylesheets/components/_list.css
Normal file → Executable file
0
src/web/stylesheets/components/_list.css
Normal file → Executable file
30
src/web/stylesheets/components/_operation.css
Normal file → Executable file
30
src/web/stylesheets/components/_operation.css
Normal file → Executable file
|
@ -18,13 +18,13 @@
|
|||
border-right: none;
|
||||
}
|
||||
|
||||
.arg-group {
|
||||
/* .arg-group {
|
||||
display: table;
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
} */
|
||||
|
||||
.arg-group-text {
|
||||
/* .arg-group-text {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
@ -33,21 +33,21 @@
|
|||
width: auto;
|
||||
margin-right: 30px;
|
||||
height: 34px;
|
||||
}
|
||||
} */
|
||||
|
||||
.inline-args input[type="checkbox"] {
|
||||
/* .inline-args input[type="checkbox"] {
|
||||
margin-top: 10px;
|
||||
}
|
||||
} */
|
||||
|
||||
.inline-args input[type="number"] {
|
||||
/* .inline-args input[type="number"] {
|
||||
width: 100px;
|
||||
} */
|
||||
|
||||
.op-title {
|
||||
font-weight: var(--op-title-font-weight);
|
||||
}
|
||||
|
||||
.arg-title {
|
||||
font-weight: var(--arg-title-font-weight);
|
||||
}
|
||||
|
||||
.arg-input {
|
||||
/* .arg-input {
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
padding: 6px 12px;
|
||||
|
@ -60,9 +60,9 @@
|
|||
border: 1px solid var(--arg-border-colour);
|
||||
font-family: var(--fixed-width-font-family);
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
} */
|
||||
|
||||
.short-string {
|
||||
/* .short-string {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ textarea.arg {
|
|||
|
||||
button.dropdown-toggle {
|
||||
background-color: var(--secondary-background-colour);
|
||||
}
|
||||
} */
|
||||
|
||||
.register-list {
|
||||
background-color: var(--fc-operation-border-colour);
|
||||
|
|
8
src/web/stylesheets/components/_pane.css
Normal file → Executable file
8
src/web/stylesheets/components/_pane.css
Normal file → Executable file
|
@ -7,16 +7,20 @@
|
|||
*/
|
||||
|
||||
:root {
|
||||
--title-height: 43px;
|
||||
--title-height: 48px;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 10px;
|
||||
padding: 8px;
|
||||
padding-left: 20px;
|
||||
padding-right: 12px;
|
||||
height: var(--title-height);
|
||||
border-bottom: 1px solid var(--primary-border-colour);
|
||||
font-weight: var(--title-weight);
|
||||
font-size: var(--title-size);
|
||||
color: var(--title-colour);
|
||||
background-color: var(--title-background-colour);
|
||||
line-height: calc(var(--title-height) - 20px);
|
||||
}
|
||||
|
||||
.list-area {
|
||||
|
|
0
src/web/stylesheets/index.css
Normal file → Executable file
0
src/web/stylesheets/index.css
Normal file → Executable file
2
src/web/stylesheets/index.js
Normal file → Executable file
2
src/web/stylesheets/index.js
Normal file → Executable file
|
@ -10,7 +10,7 @@
|
|||
import "highlight.js/styles/vs.css";
|
||||
|
||||
/* Frameworks */
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import "./vendors/bootstrap.scss";
|
||||
import "bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.css";
|
||||
import "bootstrap-colorpicker/dist/css/bootstrap-colorpicker.css";
|
||||
|
||||
|
|
0
src/web/stylesheets/layout/_banner.css
Normal file → Executable file
0
src/web/stylesheets/layout/_banner.css
Normal file → Executable file
48
src/web/stylesheets/layout/_controls.css
Normal file → Executable file
48
src/web/stylesheets/layout/_controls.css
Normal file → Executable file
|
@ -7,8 +7,7 @@
|
|||
*/
|
||||
|
||||
:root {
|
||||
--controls-height: 130px;
|
||||
--controls-division: 65%;
|
||||
--controls-height: 80px;
|
||||
}
|
||||
|
||||
#controls {
|
||||
|
@ -18,48 +17,5 @@
|
|||
bottom: 0;
|
||||
padding: 10px;
|
||||
border-top: 1px solid var(--primary-border-colour);
|
||||
background-color: var(--secondary-background-colour);
|
||||
}
|
||||
|
||||
#operational-controls {
|
||||
width: var(--controls-division);
|
||||
float: left;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#bake-group {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#bake {
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
#auto-bake-label {
|
||||
display: table-cell;
|
||||
padding: 1px;
|
||||
line-height: 1.35;
|
||||
width: 60px;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: 1px solid transparent;
|
||||
}
|
||||
|
||||
#auto-bake-label:hover {
|
||||
border-left-color: var(--btn-success-hover-border-colour);
|
||||
}
|
||||
|
||||
#auto-bake-label div {
|
||||
font-size: 10px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#extra-controls {
|
||||
float: right;
|
||||
width: calc(100% - var(--controls-division));
|
||||
padding-left: 10px;
|
||||
background-color: var(--primary-background-colour);
|
||||
}
|
||||
|
|
13
src/web/stylesheets/layout/_io.css
Normal file → Executable file
13
src/web/stylesheets/layout/_io.css
Normal file → Executable file
|
@ -32,7 +32,7 @@
|
|||
|
||||
.textarea-wrapper {
|
||||
position: absolute;
|
||||
top: 43px;
|
||||
top: var(--title-height);
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
@ -103,18 +103,13 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.io-btn-group {
|
||||
float: right;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.io-info {
|
||||
margin-right: 20px;
|
||||
margin-top: -4px;
|
||||
margin-top: 1px;
|
||||
float: right;
|
||||
height: 30px;
|
||||
text-align: right;
|
||||
line-height: 10px;
|
||||
line-height: 12px;
|
||||
font-family: var(--fixed-width-font-family);
|
||||
font-weight: normal;
|
||||
font-size: 8pt;
|
||||
|
@ -145,6 +140,6 @@
|
|||
line-height: var(--primary-line-height);
|
||||
color: var(--primary-font-colour);
|
||||
top: 50%;
|
||||
|
||||
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
|
0
src/web/stylesheets/layout/_modals.css
Normal file → Executable file
0
src/web/stylesheets/layout/_modals.css
Normal file → Executable file
2
src/web/stylesheets/layout/_operations.css
Normal file → Executable file
2
src/web/stylesheets/layout/_operations.css
Normal file → Executable file
|
@ -21,7 +21,7 @@
|
|||
|
||||
#edit-favourites {
|
||||
float: right;
|
||||
margin-top: -5px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.favourites-hover {
|
||||
|
|
0
src/web/stylesheets/layout/_recipe.css
Normal file → Executable file
0
src/web/stylesheets/layout/_recipe.css
Normal file → Executable file
0
src/web/stylesheets/layout/_structure.css
Normal file → Executable file
0
src/web/stylesheets/layout/_structure.css
Normal file → Executable file
8
src/web/stylesheets/preloader.css
Normal file → Executable file
8
src/web/stylesheets/preloader.css
Normal file → Executable file
|
@ -74,6 +74,14 @@
|
|||
transition: all 0.1s ease-in;
|
||||
}
|
||||
|
||||
.loading-error {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 600px;
|
||||
left: calc(50% - 300px);
|
||||
top: 10%;
|
||||
}
|
||||
|
||||
|
||||
/* Loaded */
|
||||
.loaded .loading-msg {
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
|
||||
--title-colour: #424242;
|
||||
--title-weight: bold;
|
||||
--title-size: 16px;
|
||||
--title-background-colour: #fafafa;
|
||||
|
||||
--banner-font-colour: #468847;
|
||||
|
@ -67,7 +68,7 @@
|
|||
|
||||
|
||||
/* Operation arguments */
|
||||
--arg-title-font-weight: bold;
|
||||
--op-title-font-weight: bold;
|
||||
--arg-input-height: 34px;
|
||||
--arg-input-line-height: 20px;
|
||||
--arg-input-font-size: 15px;
|
||||
|
|
2
src/web/stylesheets/themes/_dark.css
Normal file → Executable file
2
src/web/stylesheets/themes/_dark.css
Normal file → Executable file
|
@ -64,7 +64,7 @@
|
|||
|
||||
|
||||
/* Operation arguments */
|
||||
--arg-title-font-weight: bold;
|
||||
--op-title-font-weight: bold;
|
||||
--arg-input-height: 34px;
|
||||
--arg-input-line-height: 20px;
|
||||
--arg-input-font-size: 15px;
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
|
||||
|
||||
/* Operation arguments */
|
||||
--arg-title-font-weight: bold;
|
||||
--op-title-font-weight: bold;
|
||||
--arg-input-height: 34px;
|
||||
--arg-input-line-height: 20px;
|
||||
--arg-input-font-size: 15px;
|
||||
|
|
|
@ -8,6 +8,31 @@
|
|||
|
||||
/* Bootstrap */
|
||||
|
||||
/* fallback */
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'), local('MaterialIcons-Regular'), url(https://fonts.gstatic.com/s/materialicons/v17/2fcrYFNaTjcS6g4U3t-Y5ZjZjT5FdEJ140U2DJYC3mY.woff2) format('woff2');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/*
|
||||
button,
|
||||
a:focus {
|
||||
outline: none;
|
||||
|
@ -54,20 +79,21 @@ a:focus {
|
|||
color: var(--btn-success-hover-font-colour);
|
||||
background-color: var(--btn-success-hover-bg-colour);
|
||||
border-color: var(--btn-success-hover-border-colour);
|
||||
}
|
||||
}*/
|
||||
|
||||
.btn,
|
||||
/*.btn,
|
||||
.btn-lg,
|
||||
.nav-tabs>li>a,
|
||||
.form-control,
|
||||
.popover,
|
||||
.alert,
|
||||
.panel,
|
||||
.modal-content,
|
||||
.tooltip-inner,
|
||||
.dropdown-menu,
|
||||
.input-group-addon {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}*/
|
||||
|
||||
.btn.dropdown-toggle {
|
||||
height: 34px;
|
||||
|
|
58
src/web/stylesheets/vendors/bootstrap.less
vendored
58
src/web/stylesheets/vendors/bootstrap.less
vendored
|
@ -1,58 +0,0 @@
|
|||
/**
|
||||
* Bootstrap imports
|
||||
*
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2017
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
// Core variables and mixins
|
||||
@import "~bootstrap/less/variables.less";
|
||||
@import "~bootstrap/less/mixins.less";
|
||||
|
||||
// Reset and dependencies
|
||||
@import "~bootstrap/less/normalize.less";
|
||||
@import "~bootstrap/less/print.less";
|
||||
// @import "~bootstrap/less/glyphicons.less";
|
||||
|
||||
// Core CSS
|
||||
@import "~bootstrap/less/scaffolding.less";
|
||||
@import "~bootstrap/less/type.less";
|
||||
@import "~bootstrap/less/code.less";
|
||||
@import "~bootstrap/less/grid.less";
|
||||
@import "~bootstrap/less/tables.less";
|
||||
@import "~bootstrap/less/forms.less";
|
||||
@import "~bootstrap/less/buttons.less";
|
||||
|
||||
// Components
|
||||
@import "~bootstrap/less/component-animations.less";
|
||||
@import "~bootstrap/less/dropdowns.less";
|
||||
@import "~bootstrap/less/button-groups.less";
|
||||
@import "~bootstrap/less/input-groups.less";
|
||||
@import "~bootstrap/less/navs.less";
|
||||
// @import "~bootstrap/less/navbar.less";
|
||||
// @import "~bootstrap/less/breadcrumbs.less";
|
||||
// @import "~bootstrap/less/pagination.less";
|
||||
// @import "~bootstrap/less/pager.less";
|
||||
@import "~bootstrap/less/labels.less";
|
||||
// @import "~bootstrap/less/badges.less";
|
||||
// @import "~bootstrap/less/jumbotron.less";
|
||||
// @import "~bootstrap/less/thumbnails.less";
|
||||
@import "~bootstrap/less/alerts.less";
|
||||
@import "~bootstrap/less/progress-bars.less";
|
||||
// @import "~bootstrap/less/media.less";
|
||||
@import "~bootstrap/less/list-group.less";
|
||||
@import "~bootstrap/less/panels.less";
|
||||
// @import "~bootstrap/less/responsive-embed.less";
|
||||
// @import "~bootstrap/less/wells.less";
|
||||
@import "~bootstrap/less/close.less";
|
||||
|
||||
// Components w/ JavaScript
|
||||
@import "~bootstrap/less/modals.less";
|
||||
@import "~bootstrap/less/tooltip.less";
|
||||
@import "~bootstrap/less/popovers.less";
|
||||
// @import "~bootstrap/less/carousel.less";
|
||||
|
||||
// Utility classes
|
||||
@import "~bootstrap/less/utilities.less";
|
||||
// @import "~bootstrap/less/responsive-utilities.less";
|
24
src/web/stylesheets/vendors/bootstrap.scss
vendored
Normal file
24
src/web/stylesheets/vendors/bootstrap.scss
vendored
Normal file
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Bootstrap Material Design with overrides
|
||||
*
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
@import "~bootstrap-material-design/scss/variables/colors";
|
||||
|
||||
$theme-colors: (
|
||||
primary: $blue-700,
|
||||
success: $green,
|
||||
info: $light-blue,
|
||||
warning: $deep-orange,
|
||||
danger: $red,
|
||||
light: $grey-100,
|
||||
dark: $grey-800
|
||||
);
|
||||
|
||||
$bmd-label-color: $green-700;
|
||||
$bmd-label-color-inner-focus: $green-500;
|
||||
|
||||
@import "~bootstrap-material-design/scss/core";
|
Loading…
Add table
Add a link
Reference in a new issue