mirror of
https://github.com/gchq/CyberChef.git
synced 2025-04-23 00:06:17 -04:00
Merge branch 'multiple-input-files' of https://github.com/j433866/CyberChef into j433866-multiple-input-files
This commit is contained in:
commit
e49974beaa
45 changed files with 6643 additions and 1342 deletions
|
@ -28,8 +28,6 @@ class Chef {
|
|||
* @param {Object[]} recipeConfig - The recipe configuration object
|
||||
* @param {Object} options - The options object storing various user choices
|
||||
* @param {boolean} options.attempHighlight - Whether or not to attempt highlighting
|
||||
* @param {number} progress - The position in the recipe to start from
|
||||
* @param {number} [step] - Whether to only execute one operation in the recipe
|
||||
*
|
||||
* @returns {Object} response
|
||||
* @returns {string} response.result - The output of the recipe
|
||||
|
@ -38,46 +36,20 @@ class Chef {
|
|||
* @returns {number} response.duration - The number of ms it took to execute the recipe
|
||||
* @returns {number} response.error - The error object thrown by a failed operation (false if no error)
|
||||
*/
|
||||
async bake(input, recipeConfig, options, progress, step) {
|
||||
async bake(input, recipeConfig, options) {
|
||||
log.debug("Chef baking");
|
||||
const startTime = new Date().getTime(),
|
||||
recipe = new Recipe(recipeConfig),
|
||||
containsFc = recipe.containsFlowControl(),
|
||||
notUTF8 = options && options.hasOwnProperty("treatAsUtf8") && !options.treatAsUtf8;
|
||||
let error = false;
|
||||
let error = false,
|
||||
progress = 0;
|
||||
|
||||
if (containsFc && ENVIRONMENT_IS_WORKER()) self.setOption("attemptHighlight", false);
|
||||
|
||||
// Clean up progress
|
||||
if (progress >= recipeConfig.length) {
|
||||
progress = 0;
|
||||
}
|
||||
|
||||
if (step) {
|
||||
// Unset breakpoint on this step
|
||||
recipe.setBreakpoint(progress, false);
|
||||
// Set breakpoint on next step
|
||||
recipe.setBreakpoint(progress + 1, true);
|
||||
}
|
||||
|
||||
// If the previously run operation presented a different value to its
|
||||
// normal output, we need to recalculate it.
|
||||
if (recipe.lastOpPresented(progress)) {
|
||||
progress = 0;
|
||||
}
|
||||
|
||||
// If stepping with flow control, we have to start from the beginning
|
||||
// but still want to skip all previous breakpoints
|
||||
if (progress > 0 && containsFc) {
|
||||
recipe.removeBreaksUpTo(progress);
|
||||
progress = 0;
|
||||
}
|
||||
|
||||
// If starting from scratch, load data
|
||||
if (progress === 0) {
|
||||
const type = input instanceof ArrayBuffer ? Dish.ARRAY_BUFFER : Dish.STRING;
|
||||
this.dish.set(input, type);
|
||||
}
|
||||
// Load data
|
||||
const type = input instanceof ArrayBuffer ? Dish.ARRAY_BUFFER : Dish.STRING;
|
||||
this.dish.set(input, type);
|
||||
|
||||
try {
|
||||
progress = await recipe.execute(this.dish, progress);
|
||||
|
@ -196,6 +168,18 @@ class Chef {
|
|||
return await newDish.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the title of a dish and returns it
|
||||
*
|
||||
* @param {Dish} dish
|
||||
* @param {number} [maxLength=100]
|
||||
* @returns {string}
|
||||
*/
|
||||
async getDishTitle(dish, maxLength=100) {
|
||||
const newDish = new Dish(dish);
|
||||
return await newDish.getTitle(maxLength);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Chef;
|
||||
|
|
|
@ -25,6 +25,8 @@ self.chef = new Chef();
|
|||
|
||||
self.OpModules = OpModules;
|
||||
self.OperationConfig = OperationConfig;
|
||||
self.inputNum = -1;
|
||||
|
||||
|
||||
// Tell the app that the worker has loaded and is ready to operate
|
||||
self.postMessage({
|
||||
|
@ -35,6 +37,9 @@ self.postMessage({
|
|||
/**
|
||||
* Respond to message from parent thread.
|
||||
*
|
||||
* inputNum is optional and only used for baking multiple inputs.
|
||||
* Defaults to -1 when one isn't sent with the bake message.
|
||||
*
|
||||
* Messages should have the following format:
|
||||
* {
|
||||
* action: "bake" | "silentBake",
|
||||
|
@ -43,8 +48,9 @@ self.postMessage({
|
|||
* recipeConfig: {[Object]},
|
||||
* options: {Object},
|
||||
* progress: {number},
|
||||
* step: {boolean}
|
||||
* } | undefined
|
||||
* step: {boolean},
|
||||
* [inputNum=-1]: {number}
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
self.addEventListener("message", function(e) {
|
||||
|
@ -62,6 +68,9 @@ self.addEventListener("message", function(e) {
|
|||
case "getDishAs":
|
||||
getDishAs(r.data);
|
||||
break;
|
||||
case "getDishTitle":
|
||||
getDishTitle(r.data);
|
||||
break;
|
||||
case "docURL":
|
||||
// Used to set the URL of the current document so that scripts can be
|
||||
// imported into an inline worker.
|
||||
|
@ -91,30 +100,35 @@ self.addEventListener("message", function(e) {
|
|||
async function bake(data) {
|
||||
// Ensure the relevant modules are loaded
|
||||
self.loadRequiredModules(data.recipeConfig);
|
||||
|
||||
try {
|
||||
self.inputNum = (data.inputNum !== undefined) ? data.inputNum : -1;
|
||||
const response = await self.chef.bake(
|
||||
data.input, // The user's input
|
||||
data.recipeConfig, // The configuration of the recipe
|
||||
data.options, // Options set by the user
|
||||
data.progress, // The current position in the recipe
|
||||
data.step // Whether or not to take one step or execute the whole recipe
|
||||
data.options // Options set by the user
|
||||
);
|
||||
|
||||
const transferable = (data.input instanceof ArrayBuffer) ? [data.input] : undefined;
|
||||
self.postMessage({
|
||||
action: "bakeComplete",
|
||||
data: Object.assign(response, {
|
||||
id: data.id
|
||||
id: data.id,
|
||||
inputNum: data.inputNum,
|
||||
bakeId: data.bakeId
|
||||
})
|
||||
});
|
||||
}, transferable);
|
||||
|
||||
} catch (err) {
|
||||
self.postMessage({
|
||||
action: "bakeError",
|
||||
data: Object.assign(err, {
|
||||
id: data.id
|
||||
})
|
||||
data: {
|
||||
error: err.message || err,
|
||||
id: data.id,
|
||||
inputNum: data.inputNum
|
||||
}
|
||||
});
|
||||
}
|
||||
self.inputNum = -1;
|
||||
}
|
||||
|
||||
|
||||
|
@ -136,13 +150,33 @@ function silentBake(data) {
|
|||
*/
|
||||
async function getDishAs(data) {
|
||||
const value = await self.chef.getDishAs(data.dish, data.type);
|
||||
|
||||
const transferable = (data.type === "ArrayBuffer") ? [value] : undefined;
|
||||
self.postMessage({
|
||||
action: "dishReturned",
|
||||
data: {
|
||||
value: value,
|
||||
id: data.id
|
||||
}
|
||||
}, transferable);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the dish title
|
||||
*
|
||||
* @param {object} data
|
||||
* @param {Dish} data.dish
|
||||
* @param {number} data.maxLength
|
||||
* @param {number} data.id
|
||||
*/
|
||||
async function getDishTitle(data) {
|
||||
const title = await self.chef.getDishTitle(data.dish, data.maxLength);
|
||||
self.postMessage({
|
||||
action: "dishReturned",
|
||||
data: {
|
||||
value: title,
|
||||
id: data.id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -193,7 +227,28 @@ self.loadRequiredModules = function(recipeConfig) {
|
|||
self.sendStatusMessage = function(msg) {
|
||||
self.postMessage({
|
||||
action: "statusMessage",
|
||||
data: msg
|
||||
data: {
|
||||
message: msg,
|
||||
inputNum: self.inputNum
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Send progress update to the app.
|
||||
*
|
||||
* @param {number} progress
|
||||
* @param {number} total
|
||||
*/
|
||||
self.sendProgressMessage = function(progress, total) {
|
||||
self.postMessage({
|
||||
action: "progressMessage",
|
||||
data: {
|
||||
progress: progress,
|
||||
total: total,
|
||||
inputNum: self.inputNum
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import Utils from "./Utils";
|
||||
import DishError from "./errors/DishError";
|
||||
import BigNumber from "bignumber.js";
|
||||
import {detectFileType} from "./lib/FileType";
|
||||
import log from "loglevel";
|
||||
|
||||
/**
|
||||
|
@ -141,6 +142,56 @@ class Dish {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detects the MIME type of the current dish
|
||||
* @returns {string}
|
||||
*/
|
||||
async detectDishType() {
|
||||
const data = new Uint8Array(this.value.slice(0, 2048)),
|
||||
types = detectFileType(data);
|
||||
|
||||
if (!types.length || !types[0].mime || !types[0].mime === "text/plain") {
|
||||
return null;
|
||||
} else {
|
||||
return types[0].mime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the title of the data up to the specified length
|
||||
*
|
||||
* @param {number} maxLength - The maximum title length
|
||||
* @returns {string}
|
||||
*/
|
||||
async getTitle(maxLength) {
|
||||
let title = "";
|
||||
const cloned = this.clone();
|
||||
|
||||
switch (this.type) {
|
||||
case Dish.FILE:
|
||||
title = cloned.value.name;
|
||||
break;
|
||||
case Dish.LIST_FILE:
|
||||
title = `${cloned.value.length} file(s)`;
|
||||
break;
|
||||
case Dish.ARRAY_BUFFER:
|
||||
case Dish.BYTE_ARRAY:
|
||||
title = await cloned.detectDishType();
|
||||
if (title === null) {
|
||||
cloned.value = cloned.value.slice(0, 2048);
|
||||
title = await cloned.get(Dish.STRING);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
title = await cloned.get(Dish.STRING);
|
||||
}
|
||||
|
||||
title = title.slice(0, maxLength);
|
||||
return title;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Translates the data to the given type format.
|
||||
*
|
||||
|
|
|
@ -200,7 +200,12 @@ class Recipe {
|
|||
|
||||
try {
|
||||
input = await dish.get(op.inputType);
|
||||
log.debug("Executing operation");
|
||||
log.debug(`Executing operation '${op.name}'`);
|
||||
|
||||
if (ENVIRONMENT_IS_WORKER()) {
|
||||
self.sendStatusMessage(`Baking... (${i+1}/${this.opList.length})`);
|
||||
self.sendProgressMessage(i + 1, this.opList.length);
|
||||
}
|
||||
|
||||
if (op.flowControl) {
|
||||
// Package up the current state
|
||||
|
|
141
src/web/App.mjs
141
src/web/App.mjs
|
@ -41,6 +41,7 @@ class App {
|
|||
this.autoBakePause = false;
|
||||
this.progress = 0;
|
||||
this.ingId = 0;
|
||||
this.timeouts = {};
|
||||
}
|
||||
|
||||
|
||||
|
@ -87,7 +88,10 @@ class App {
|
|||
setTimeout(function() {
|
||||
document.getElementById("loader-wrapper").remove();
|
||||
document.body.classList.remove("loaded");
|
||||
}, 1000);
|
||||
|
||||
// Bake initial input
|
||||
this.manager.input.bakeAll();
|
||||
}.bind(this), 1000);
|
||||
|
||||
// Clear the loading message interval
|
||||
clearInterval(window.loadingMsgsInt);
|
||||
|
@ -96,6 +100,9 @@ class App {
|
|||
window.removeEventListener("error", window.loadingErrorHandler);
|
||||
|
||||
document.dispatchEvent(this.manager.apploaded);
|
||||
|
||||
this.manager.input.calcMaxTabs();
|
||||
this.manager.output.calcMaxTabs();
|
||||
}
|
||||
|
||||
|
||||
|
@ -128,7 +135,6 @@ class App {
|
|||
this.manager.recipe.updateBreakpointIndicator(false);
|
||||
|
||||
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
|
||||
|
@ -148,13 +154,46 @@ class App {
|
|||
|
||||
if (this.autoBake_ && !this.baking) {
|
||||
log.debug("Auto-baking");
|
||||
this.bake();
|
||||
this.manager.input.inputWorker.postMessage({
|
||||
action: "autobake",
|
||||
data: {
|
||||
activeTab: this.manager.tabs.getActiveInputTab()
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.manager.controls.showStaleIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes the next step of the recipe.
|
||||
*/
|
||||
step() {
|
||||
if (this.baking) return;
|
||||
|
||||
// Reset status using cancelBake
|
||||
this.manager.worker.cancelBake(true, false);
|
||||
|
||||
const activeTab = this.manager.tabs.getActiveInputTab();
|
||||
if (activeTab === -1) return;
|
||||
|
||||
let progress = 0;
|
||||
if (this.manager.output.outputs[activeTab].progress !== false) {
|
||||
log.error(this.manager.output.outputs[activeTab]);
|
||||
progress = this.manager.output.outputs[activeTab].progress;
|
||||
}
|
||||
|
||||
this.manager.input.inputWorker.postMessage({
|
||||
action: "step",
|
||||
data: {
|
||||
activeTab: activeTab,
|
||||
progress: progress + 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Runs a silent bake, forcing the browser to load and cache all the relevant JavaScript code needed
|
||||
* to do a real bake.
|
||||
|
@ -175,24 +214,25 @@ class App {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @param {boolean} [silent=false] - Suppress statechange event
|
||||
*/
|
||||
setInput(input, silent=false) {
|
||||
this.manager.input.set(input, silent);
|
||||
setInput(input) {
|
||||
// Get the currently active tab.
|
||||
// If there isn't one, assume there are no inputs so use inputNum of 1
|
||||
let inputNum = this.manager.tabs.getActiveInputTab();
|
||||
if (inputNum === -1) inputNum = 1;
|
||||
this.manager.input.updateInputValue(inputNum, input);
|
||||
|
||||
this.manager.input.inputWorker.postMessage({
|
||||
action: "setInput",
|
||||
data: {
|
||||
inputNum: inputNum,
|
||||
silent: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -255,9 +295,11 @@ class App {
|
|||
minSize: minimise ? [0, 0, 0] : [240, 310, 450],
|
||||
gutterSize: 4,
|
||||
expandToMin: true,
|
||||
onDrag: function() {
|
||||
onDrag: this.debounce(function() {
|
||||
this.manager.recipe.adjustWidth();
|
||||
}.bind(this)
|
||||
this.manager.input.calcMaxTabs();
|
||||
this.manager.output.calcMaxTabs();
|
||||
}, 50, "dragSplitter", this, [])
|
||||
});
|
||||
|
||||
this.ioSplitter = Split(["#input", "#output"], {
|
||||
|
@ -391,11 +433,12 @@ class App {
|
|||
this.manager.recipe.initialiseOperationDragNDrop();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks for input and recipe in the URI parameters and loads them if present.
|
||||
* Gets the URI params from the window and parses them to extract the actual values.
|
||||
*
|
||||
* @returns {object}
|
||||
*/
|
||||
loadURIParams() {
|
||||
getURIParams() {
|
||||
// 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,
|
||||
|
@ -403,8 +446,20 @@ class App {
|
|||
const params = window.location.search ||
|
||||
window.location.href.split("#")[1] ||
|
||||
window.location.hash;
|
||||
this.uriParams = Utils.parseURIParams(params);
|
||||
const parsedParams = Utils.parseURIParams(params);
|
||||
return parsedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches the URI parameters for recipe and input parameters.
|
||||
* If recipe is present, replaces the current recipe with the recipe provided in the URI.
|
||||
* If input is present, decodes and sets the input to the one provided in the URI.
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
*/
|
||||
loadURIParams() {
|
||||
this.autoBakePause = true;
|
||||
this.uriParams = this.getURIParams();
|
||||
|
||||
// Read in recipe from URI params
|
||||
if (this.uriParams.recipe) {
|
||||
|
@ -433,7 +488,7 @@ class App {
|
|||
if (this.uriParams.input) {
|
||||
try {
|
||||
const inputData = fromBase64(this.uriParams.input);
|
||||
this.setInput(inputData, true);
|
||||
this.setInput(inputData);
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
|
@ -522,6 +577,8 @@ class App {
|
|||
this.columnSplitter.setSizes([20, 30, 50]);
|
||||
this.ioSplitter.setSizes([50, 50]);
|
||||
this.manager.recipe.adjustWidth();
|
||||
this.manager.input.calcMaxTabs();
|
||||
this.manager.output.calcMaxTabs();
|
||||
}
|
||||
|
||||
|
||||
|
@ -656,6 +713,17 @@ class App {
|
|||
this.progress = 0;
|
||||
this.autoBake();
|
||||
|
||||
this.updateTitle(false, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the page title to contain the new recipe
|
||||
*
|
||||
* @param {boolean} includeInput
|
||||
* @param {string} input
|
||||
* @param {boolean} [changeUrl=true]
|
||||
*/
|
||||
updateTitle(includeInput, input, changeUrl=true) {
|
||||
// Set title
|
||||
const recipeConfig = this.getRecipeConfig();
|
||||
let title = "CyberChef";
|
||||
|
@ -674,8 +742,8 @@ class App {
|
|||
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);
|
||||
if (this.options.updateUrl && changeUrl) {
|
||||
this.lastStateUrl = this.manager.controls.generateStateUrl(true, includeInput, input, recipeConfig);
|
||||
window.history.replaceState({}, title, this.lastStateUrl);
|
||||
}
|
||||
}
|
||||
|
@ -691,6 +759,29 @@ class App {
|
|||
this.loadURIParams();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Debouncer to stop functions from being executed multiple times in a
|
||||
* short space of time
|
||||
* https://davidwalsh.name/javascript-debounce-function
|
||||
*
|
||||
* @param {function} func - The function to be executed after the debounce time
|
||||
* @param {number} wait - The time (ms) to wait before executing the function
|
||||
* @param {string} id - Unique ID to reference the timeout for the function
|
||||
* @param {object} scope - The object to bind to the debounced function
|
||||
* @param {array} args - Array of arguments to be passed to func
|
||||
* @returns {function}
|
||||
*/
|
||||
debounce(func, wait, id, scope, args) {
|
||||
return function() {
|
||||
const later = function() {
|
||||
func.apply(scope, args);
|
||||
};
|
||||
clearTimeout(this.timeouts[id]);
|
||||
this.timeouts[id] = setTimeout(later, wait);
|
||||
}.bind(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -1,354 +0,0 @@
|
|||
/**
|
||||
* @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
|
||||
* @param {boolean} [silent=false] - Suppress statechange event
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
*/
|
||||
set(input, silent=false) {
|
||||
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();
|
||||
if (!silent) 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.loadFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for open input button events
|
||||
* Loads the opened data into the input textarea
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
inputOpen(e) {
|
||||
e.preventDefault();
|
||||
const file = e.srcElement.files[0];
|
||||
this.loadFile(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, 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");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads a file into the input.
|
||||
*
|
||||
* @param {File} file
|
||||
*/
|
||||
loadFile(file) {
|
||||
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 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;
|
|
@ -4,18 +4,19 @@
|
|||
* @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";
|
||||
import BackgroundWorkerWaiter from "./BackgroundWorkerWaiter";
|
||||
import WorkerWaiter from "./waiters/WorkerWaiter";
|
||||
import WindowWaiter from "./waiters/WindowWaiter";
|
||||
import ControlsWaiter from "./waiters/ControlsWaiter";
|
||||
import RecipeWaiter from "./waiters/RecipeWaiter";
|
||||
import OperationsWaiter from "./waiters/OperationsWaiter";
|
||||
import InputWaiter from "./waiters/InputWaiter";
|
||||
import OutputWaiter from "./waiters/OutputWaiter";
|
||||
import OptionsWaiter from "./waiters/OptionsWaiter";
|
||||
import HighlighterWaiter from "./waiters/HighlighterWaiter";
|
||||
import SeasonalWaiter from "./waiters/SeasonalWaiter";
|
||||
import BindingsWaiter from "./waiters/BindingsWaiter";
|
||||
import BackgroundWorkerWaiter from "./waiters/BackgroundWorkerWaiter";
|
||||
import TabWaiter from "./waiters/TabWaiter";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -63,6 +64,7 @@ class Manager {
|
|||
this.controls = new ControlsWaiter(this.app, this);
|
||||
this.recipe = new RecipeWaiter(this.app, this);
|
||||
this.ops = new OperationsWaiter(this.app, this);
|
||||
this.tabs = new TabWaiter(this.app, this);
|
||||
this.input = new InputWaiter(this.app, this);
|
||||
this.output = new OutputWaiter(this.app, this);
|
||||
this.options = new OptionsWaiter(this.app, this);
|
||||
|
@ -82,7 +84,9 @@ class Manager {
|
|||
* Sets up the various components and listeners.
|
||||
*/
|
||||
setup() {
|
||||
this.worker.registerChefWorker();
|
||||
this.input.setupInputWorker();
|
||||
this.input.addInput(true);
|
||||
this.worker.setupChefWorker();
|
||||
this.recipe.initialiseOperationDragNDrop();
|
||||
this.controls.initComponents();
|
||||
this.controls.autoBakeChange();
|
||||
|
@ -142,11 +146,11 @@ class Manager {
|
|||
this.addDynamicListener("textarea.arg", "drop", this.recipe.textArgDrop, this.recipe);
|
||||
|
||||
// Input
|
||||
this.addMultiEventListener("#input-text", "keyup", this.input.inputChange, this.input);
|
||||
this.addMultiEventListener("#input-text", "keyup", this.input.debounceInputChange, 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("#open-file", "change", this.input.inputOpen, this.input);
|
||||
this.addListeners("#clr-io,#btn-close-all-tabs", "click", this.input.clearAllIoClick, this.input);
|
||||
this.addListeners("#open-file,#open-folder", "change", this.input.inputOpen, 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);
|
||||
|
@ -155,9 +159,31 @@ class Manager {
|
|||
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));
|
||||
document.getElementById("btn-new-tab").addEventListener("click", this.input.addInputClick.bind(this.input));
|
||||
document.getElementById("btn-previous-input-tab").addEventListener("mousedown", this.input.previousTabClick.bind(this.input));
|
||||
document.getElementById("btn-next-input-tab").addEventListener("mousedown", this.input.nextTabClick.bind(this.input));
|
||||
this.addListeners("#btn-next-input-tab,#btn-previous-input-tab", "mouseup", this.input.tabMouseUp, this.input);
|
||||
this.addListeners("#btn-next-input-tab,#btn-previous-input-tab", "mouseout", this.input.tabMouseUp, this.input);
|
||||
document.getElementById("btn-go-to-input-tab").addEventListener("click", this.input.goToTab.bind(this.input));
|
||||
document.getElementById("btn-find-input-tab").addEventListener("click", this.input.findTab.bind(this.input));
|
||||
this.addDynamicListener("#input-tabs li .input-tab-content", "click", this.input.changeTabClick, this.input);
|
||||
document.getElementById("input-show-pending").addEventListener("change", this.input.filterTabSearch.bind(this.input));
|
||||
document.getElementById("input-show-loading").addEventListener("change", this.input.filterTabSearch.bind(this.input));
|
||||
document.getElementById("input-show-loaded").addEventListener("change", this.input.filterTabSearch.bind(this.input));
|
||||
this.addListeners("#input-filter-content,#input-filter-filename", "click", this.input.filterOptionClick, this.input);
|
||||
document.getElementById("input-filter").addEventListener("change", this.input.filterTabSearch.bind(this.input));
|
||||
document.getElementById("input-filter").addEventListener("keyup", this.input.filterTabSearch.bind(this.input));
|
||||
document.getElementById("input-num-results").addEventListener("change", this.input.filterTabSearch.bind(this.input));
|
||||
document.getElementById("input-num-results").addEventListener("keyup", this.input.filterTabSearch.bind(this.input));
|
||||
document.getElementById("input-filter-refresh").addEventListener("click", this.input.filterTabSearch.bind(this.input));
|
||||
this.addDynamicListener(".input-filter-result", "click", this.input.filterItemClick, this.input);
|
||||
document.getElementById("btn-open-file").addEventListener("click", this.input.inputOpenClick.bind(this.input));
|
||||
document.getElementById("btn-open-folder").addEventListener("click", this.input.folderOpenClick.bind(this.input));
|
||||
|
||||
|
||||
// Output
|
||||
document.getElementById("save-to-file").addEventListener("click", this.output.saveClick.bind(this.output));
|
||||
document.getElementById("save-all-to-file").addEventListener("click", this.output.saveAllClick.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));
|
||||
|
@ -174,6 +200,25 @@ class Manager {
|
|||
this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
|
||||
document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
|
||||
this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output);
|
||||
this.addDynamicListener("#output-tabs-wrapper #output-tabs li .output-tab-content", "click", this.output.changeTabClick, this.output);
|
||||
document.getElementById("btn-previous-output-tab").addEventListener("mousedown", this.output.previousTabClick.bind(this.output));
|
||||
document.getElementById("btn-next-output-tab").addEventListener("mousedown", this.output.nextTabClick.bind(this.output));
|
||||
this.addListeners("#btn-next-output-tab,#btn-previous-output-tab", "mouseup", this.output.tabMouseUp, this.output);
|
||||
this.addListeners("#btn-next-output-tab,#btn-previous-output-tab", "mouseout", this.output.tabMouseUp, this.output);
|
||||
document.getElementById("btn-go-to-output-tab").addEventListener("click", this.output.goToTab.bind(this.output));
|
||||
document.getElementById("btn-find-output-tab").addEventListener("click", this.output.findTab.bind(this.output));
|
||||
document.getElementById("output-show-pending").addEventListener("change", this.output.filterTabSearch.bind(this.output));
|
||||
document.getElementById("output-show-baking").addEventListener("change", this.output.filterTabSearch.bind(this.output));
|
||||
document.getElementById("output-show-baked").addEventListener("change", this.output.filterTabSearch.bind(this.output));
|
||||
document.getElementById("output-show-stale").addEventListener("change", this.output.filterTabSearch.bind(this.output));
|
||||
document.getElementById("output-show-errored").addEventListener("change", this.output.filterTabSearch.bind(this.output));
|
||||
document.getElementById("output-content-filter").addEventListener("change", this.output.filterTabSearch.bind(this.output));
|
||||
document.getElementById("output-content-filter").addEventListener("keyup", this.output.filterTabSearch.bind(this.output));
|
||||
document.getElementById("output-num-results").addEventListener("change", this.output.filterTabSearch.bind(this.output));
|
||||
document.getElementById("output-num-results").addEventListener("keyup", this.output.filterTabSearch.bind(this.output));
|
||||
document.getElementById("output-filter-refresh").addEventListener("click", this.output.filterTabSearch.bind(this.output));
|
||||
this.addDynamicListener(".output-filter-result", "click", this.output.filterItemClick, this.output);
|
||||
|
||||
|
||||
// Options
|
||||
document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options));
|
||||
|
@ -186,6 +231,7 @@ class Manager {
|
|||
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));
|
||||
document.getElementById("imagePreview").addEventListener("change", this.input.renderFileThumb.bind(this.input));
|
||||
|
||||
// Misc
|
||||
window.addEventListener("keydown", this.bindings.parseInput.bind(this.bindings));
|
||||
|
@ -307,7 +353,6 @@ class Manager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Manager;
|
||||
|
|
|
@ -1,547 +0,0 @@
|
|||
/**
|
||||
* @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);
|
||||
this.backgroundMagic();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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.", 2000);
|
||||
} else {
|
||||
this.app.alert("Sorry, the output could not be copied.", 3000);
|
||||
}
|
||||
|
||||
// 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.initialiseSplitter(true);
|
||||
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.initialiseSplitter(false);
|
||||
this.app.resetLayout();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Save bombe object then remove it from the DOM so that it does not cause performance issues.
|
||||
*/
|
||||
saveBombe() {
|
||||
this.bombeEl = document.getElementById("bombe");
|
||||
this.bombeEl.parentNode.removeChild(this.bombeEl);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows or hides the output loading screen.
|
||||
* The animated Bombe SVG, whilst quite aesthetically pleasing, is reasonably CPU
|
||||
* intensive, so we remove it from the DOM when not in use. We only show it if the
|
||||
* recipe is taking longer than 200ms. We add it to the DOM just before that so that
|
||||
* it is ready to fade in without stuttering.
|
||||
*
|
||||
* @param {boolean} value - true == show loader
|
||||
*/
|
||||
toggleLoader(value) {
|
||||
clearTimeout(this.appendBombeTimeout);
|
||||
clearTimeout(this.outputLoaderTimeout);
|
||||
|
||||
const outputLoader = document.getElementById("output-loader"),
|
||||
outputElement = document.getElementById("output-text"),
|
||||
animation = document.getElementById("output-loader-animation");
|
||||
|
||||
if (value) {
|
||||
this.manager.controls.hideStaleIndicator();
|
||||
|
||||
// Start a timer to add the Bombe to the DOM just before we make it
|
||||
// visible so that there is no stuttering
|
||||
this.appendBombeTimeout = setTimeout(function() {
|
||||
animation.appendChild(this.bombeEl);
|
||||
}.bind(this), 150);
|
||||
|
||||
// Show the loading screen
|
||||
this.outputLoaderTimeout = setTimeout(function() {
|
||||
outputElement.disabled = true;
|
||||
outputLoader.style.visibility = "visible";
|
||||
outputLoader.style.opacity = 1;
|
||||
this.manager.controls.toggleBakeButtonFunction(true);
|
||||
}.bind(this), 200);
|
||||
} else {
|
||||
// Remove the Bombe from the DOM to save resources
|
||||
this.outputLoaderTimeout = setTimeout(function () {
|
||||
try {
|
||||
animation.removeChild(this.bombeEl);
|
||||
} catch (err) {}
|
||||
}.bind(this), 500);
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Triggers the BackgroundWorker to attempt Magic on the current output.
|
||||
*/
|
||||
backgroundMagic() {
|
||||
this.hideMagicButton();
|
||||
if (!this.app.options.autoMagic) return;
|
||||
|
||||
const sample = this.dishStr ? this.dishStr.slice(0, 1000) :
|
||||
this.dishBuffer ? this.dishBuffer.slice(0, 1000) : "";
|
||||
|
||||
if (sample.length) {
|
||||
this.manager.background.magic(sample);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles the results of a background Magic call.
|
||||
*
|
||||
* @param {Object[]} options
|
||||
*/
|
||||
backgroundMagicResult(options) {
|
||||
if (!options.length ||
|
||||
!options[0].recipe.length)
|
||||
return;
|
||||
|
||||
const currentRecipeConfig = this.app.getRecipeConfig();
|
||||
const newRecipeConfig = currentRecipeConfig.concat(options[0].recipe);
|
||||
const opSequence = options[0].recipe.map(o => o.op).join(", ");
|
||||
|
||||
this.showMagicButton(opSequence, options[0].data, newRecipeConfig);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for Magic click events.
|
||||
*
|
||||
* Loads the Magic recipe.
|
||||
*
|
||||
* @fires Manager#statechange
|
||||
*/
|
||||
magicClick() {
|
||||
const magicButton = document.getElementById("magic");
|
||||
this.app.setRecipeConfig(JSON.parse(magicButton.getAttribute("data-recipe")));
|
||||
window.dispatchEvent(this.manager.statechange);
|
||||
this.hideMagicButton();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays the Magic button with a title and adds a link to a complete recipe.
|
||||
*
|
||||
* @param {string} opSequence
|
||||
* @param {string} result
|
||||
* @param {Object[]} recipeConfig
|
||||
*/
|
||||
showMagicButton(opSequence, result, recipeConfig) {
|
||||
const magicButton = document.getElementById("magic");
|
||||
magicButton.setAttribute("data-original-title", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.escapeHtml(Utils.truncate(result), 30)}"</span>`);
|
||||
magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, "");
|
||||
magicButton.classList.remove("hidden");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hides the Magic button and resets its values.
|
||||
*/
|
||||
hideMagicButton() {
|
||||
const magicButton = document.getElementById("magic");
|
||||
magicButton.classList.add("hidden");
|
||||
magicButton.setAttribute("data-recipe", "");
|
||||
magicButton.setAttribute("data-original-title", "Magic!");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for extract file events.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
async extractFileClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const el = e.target.nodeName === "I" ? e.target.parentNode : e.target;
|
||||
const blobURL = el.getAttribute("blob-url");
|
||||
const fileName = el.getAttribute("file-name");
|
||||
|
||||
const blob = await fetch(blobURL).then(r => r.blob());
|
||||
this.manager.input.loadFile(new File([blob], fileName, {type: blob.type}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default OutputWaiter;
|
|
@ -1,239 +0,0 @@
|
|||
/**
|
||||
* @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;
|
|
@ -218,9 +218,16 @@
|
|||
<div class="title no-select">
|
||||
<label for="input-text">Input</label>
|
||||
<span class="float-right">
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-open-file" data-toggle="tooltip" title="Open file as input" onclick="document.getElementById('open-file').click();">
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-new-tab" data-toggle="tooltip" title="Add a new input tab">
|
||||
<i class="material-icons">add</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-open-folder" data-toggle="tooltip" title="Open folder as input">
|
||||
<i class="material-icons">folder_open</i>
|
||||
<input type="file" id="open-folder" style="display: none" multiple directory webkitdirectory>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="btn-open-file" data-toggle="tooltip" title="Open file as input">
|
||||
<i class="material-icons">input</i>
|
||||
<input type="file" id="open-file" style="display: none">
|
||||
<input type="file" id="open-file" style="display: none" multiple>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="clr-io" data-toggle="tooltip" title="Clear input and output">
|
||||
<i class="material-icons">delete</i>
|
||||
|
@ -229,17 +236,42 @@
|
|||
<i class="material-icons">view_compact</i>
|
||||
</button>
|
||||
</span>
|
||||
<div class="io-info" id="input-files-info"></div>
|
||||
<div class="io-info" id="input-info"></div>
|
||||
<div class="io-info" id="input-selection-info"></div>
|
||||
</div>
|
||||
<div class="textarea-wrapper no-select">
|
||||
<div id="input-tabs-wrapper" style="display: none;" class="no-select">
|
||||
<span id="btn-previous-input-tab" class="input-tab-buttons">
|
||||
<
|
||||
</span>
|
||||
<span id="btn-input-tab-dropdown" class="input-tab-buttons" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
···
|
||||
</span>
|
||||
<div class="dropdown-menu" aria-labelledby="btn-input-tab-dropdown">
|
||||
<a id="btn-go-to-input-tab" class="dropdown-item">
|
||||
Go to tab
|
||||
</a>
|
||||
<a id="btn-find-input-tab" class="dropdown-item">
|
||||
Find tab
|
||||
</a>
|
||||
<a id="btn-close-all-tabs" class="dropdown-item">
|
||||
Close all tabs
|
||||
</a>
|
||||
</div>
|
||||
<span id="btn-next-input-tab" class="input-tab-buttons">
|
||||
>
|
||||
</span>
|
||||
<ul id="input-tabs">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="textarea-wrapper no-select input-wrapper" id="input-wrapper">
|
||||
<div id="input-highlighter" class="no-select"></div>
|
||||
<textarea id="input-text" spellcheck="false"></textarea>
|
||||
<div id="input-file">
|
||||
<div class="file-overlay"></div>
|
||||
<textarea id="input-text" class="input-text" spellcheck="false"></textarea>
|
||||
<div class="input-file" id="input-file">
|
||||
<div class="file-overlay" id="file-overlay"></div>
|
||||
<div style="position: relative; height: 100%;">
|
||||
<div class="io-card card">
|
||||
<img aria-hidden="true" src="<%- require('../static/images/file-128x128.png') %>" alt="File icon"/>
|
||||
<img aria-hidden="true" src="<%- require('../static/images/file-128x128.png') %>" alt="File icon" id="input-file-thumbnail"/>
|
||||
<div class="card-body">
|
||||
<button type="button" class="close" id="input-file-close">×</button>
|
||||
Name: <span id="input-file-name"></span><br>
|
||||
|
@ -257,13 +289,16 @@
|
|||
<div class="title no-select">
|
||||
<label for="output-text">Output</label>
|
||||
<span class="float-right">
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="save-all-to-file" data-toggle="tooltip" title="Save all outputs to a zip file" style="display: none">
|
||||
<i class="material-icons">archive</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary 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-primary 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-primary bmd-btn-icon" id="switch" data-toggle="tooltip" title="Move output to input">
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="switch" data-toggle="tooltip" title="Replace input with output">
|
||||
<i class="material-icons">open_in_browser</i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon" id="undo-switch" data-toggle="tooltip" title="Undo" disabled="disabled">
|
||||
|
@ -273,6 +308,7 @@
|
|||
<i class="material-icons">fullscreen</i>
|
||||
</button>
|
||||
</span>
|
||||
<div class="io-info" id="bake-info"></div>
|
||||
<div class="io-info" id="output-info"></div>
|
||||
<div class="io-info" id="output-selection-info"></div>
|
||||
<button type="button" class="btn btn-primary bmd-btn-icon hidden" id="magic" data-toggle="tooltip" title="Magic!" data-html="true">
|
||||
|
@ -284,38 +320,61 @@
|
|||
<i class="material-icons">access_time</i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="textarea-wrapper">
|
||||
<div id="output-highlighter" class="no-select"></div>
|
||||
<div id="output-html"></div>
|
||||
<textarea id="output-text" readonly="readonly" spellcheck="false"></textarea>
|
||||
<img id="show-file-overlay" aria-hidden="true" src="<%- require('../static/images/file-32x32.png') %>" alt="Show file overlay" title="Show file overlay"/>
|
||||
<div id="output-file">
|
||||
<div class="file-overlay"></div>
|
||||
<div style="position: relative; height: 100%;">
|
||||
<div class="io-card card">
|
||||
<img aria-hidden="true" src="<%- require('../static/images/file-128x128.png') %>" alt="File icon"/>
|
||||
<div class="card-body">
|
||||
Size: <span id="output-file-size"></span><br>
|
||||
<button id="output-file-download" type="button" class="btn btn-primary btn-outline">Download</button>
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button id="output-file-slice" type="button" class="btn btn-secondary bmd-btn-icon" title="View slice">
|
||||
<i class="material-icons">search</i>
|
||||
</button>
|
||||
</span>
|
||||
<input type="number" class="form-control" id="output-file-slice-from" placeholder="From" value="0" step="1024" min="0">
|
||||
<div class="input-group-addon">to</div>
|
||||
<input type="number" class="form-control" id="output-file-slice-to" placeholder="To" value="2048" step="1024" min="0">
|
||||
<div id="output-wrapper">
|
||||
<div id="output-tabs-wrapper" style="display: none" class="no-select">
|
||||
<span id="btn-previous-output-tab" class="output-tab-buttons">
|
||||
<
|
||||
</span>
|
||||
<span id="btn-output-tab-dropdown" class="output-tab-buttons" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
···
|
||||
</span>
|
||||
<div class="dropdown-menu" aria-labelledby="btn-input-tab-dropdown">
|
||||
<a id="btn-go-to-output-tab" class="dropdown-item">
|
||||
Go to tab
|
||||
</a>
|
||||
<a id="btn-find-output-tab" class="dropdown-item">
|
||||
Find tab
|
||||
</a>
|
||||
</div>
|
||||
<span id="btn-next-output-tab" class="output-tab-buttons">
|
||||
>
|
||||
</span>
|
||||
<ul id="output-tabs">
|
||||
</ul>
|
||||
</div>
|
||||
<div class="textarea-wrapper">
|
||||
<div id="output-highlighter" class="no-select"></div>
|
||||
<div id="output-html"></div>
|
||||
<textarea id="output-text" readonly="readonly" spellcheck="false"></textarea>
|
||||
<img id="show-file-overlay" aria-hidden="true" src="<%- require('../static/images/file-32x32.png') %>" alt="Show file overlay" title="Show file overlay"/>
|
||||
<div id="output-file">
|
||||
<div class="file-overlay"></div>
|
||||
<div style="position: relative; height: 100%;">
|
||||
<div class="io-card card">
|
||||
<img aria-hidden="true" src="<%- require('../static/images/file-128x128.png') %>" alt="File icon"/>
|
||||
<div class="card-body">
|
||||
Size: <span id="output-file-size"></span><br>
|
||||
<button id="output-file-download" type="button" class="btn btn-primary btn-outline">Download</button>
|
||||
<div class="input-group">
|
||||
<span class="input-group-btn">
|
||||
<button id="output-file-slice" type="button" class="btn btn-secondary bmd-btn-icon" title="View slice">
|
||||
<i class="material-icons">search</i>
|
||||
</button>
|
||||
</span>
|
||||
<input type="number" class="form-control" id="output-file-slice-from" placeholder="From" value="0" step="1024" min="0">
|
||||
<div class="input-group-addon">to</div>
|
||||
<input type="number" class="form-control" id="output-file-slice-to" placeholder="To" value="2048" step="1024" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="output-loader">
|
||||
<div id="output-loader-animation">
|
||||
<object id="bombe" data="<%- require('../static/images/bombe.svg') %>" width="100%" height="100%"></object>
|
||||
<div id="output-loader">
|
||||
<div id="output-loader-animation">
|
||||
<object id="bombe" data="<%- require('../static/images/bombe.svg') %>" width="100%" height="100%"></object>
|
||||
</div>
|
||||
<div class="loading-msg"></div>
|
||||
</div>
|
||||
<div class="loading-msg"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -425,6 +484,8 @@
|
|||
<option value="classic">Classic</option>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="geocities">GeoCities</option>
|
||||
<option value="solarizedDark">Solarized Dark</option>
|
||||
<option value="solarizedLight">Solarized Light</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
@ -498,6 +559,20 @@
|
|||
Attempt to detect encoded data automagically
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox option-item">
|
||||
<label for="imagePreview">
|
||||
<input type="checkbox" option="imagePreview" id="imagePreview">
|
||||
Render a preview of the input if it's detected to be an image.
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox option-item">
|
||||
<label for="syncTabs">
|
||||
<input type="checkbox" option="syncTabs" id="syncTabs">
|
||||
Keep the current tab in sync between the input and output.
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button>
|
||||
|
@ -599,7 +674,7 @@
|
|||
</a>
|
||||
<div class="collapse" id="faq-load-files">
|
||||
<p>Yes! Just drag your file over the input box and drop it.</p>
|
||||
<p>CyberChef can handle files up to around 500MB (depending on your browser), however some of the operations may take a very long time to run over this much data.</p>
|
||||
<p>CyberChef can handle files up to around 2GB (depending on your browser), however some of the operations may take a very long time to run over this much data.</p>
|
||||
<p>If the output is larger than a certain threshold (default 1MiB), it will be presented to you as a file available for download. Slices of the file can be viewed in the output if you need to inspect them.</p>
|
||||
</div>
|
||||
<br>
|
||||
|
@ -688,5 +763,125 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="input-tab-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Find Input Tab</h5>
|
||||
</div>
|
||||
<div class="modal-body" id="input-tab-body">
|
||||
<h6>Load Status</h6>
|
||||
<div id="input-find-options">
|
||||
<ul id="input-find-options-checkboxes">
|
||||
<li class="checkbox input-find-option">
|
||||
<label for="input-show-pending">
|
||||
<input type="checkbox" id="input-show-pending" checked="">
|
||||
Pending
|
||||
</label>
|
||||
</li>
|
||||
<li class="checkbox input-find-option">
|
||||
<label for="input-show-loading">
|
||||
<input type="checkbox" id="input-show-loading" checked="">
|
||||
Loading
|
||||
</label>
|
||||
</li>
|
||||
<li class="checkbox input-find-option">
|
||||
<label for="input-show-loaded">
|
||||
<input type="checkbox" id="input-show-loaded" checked="">
|
||||
Loaded
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group input-group">
|
||||
<div class="toggle-string">
|
||||
<label for="input-filter" class="bmd-label-floating toggle-string">Filter (regex)</label>
|
||||
<input type="text" class="form-control toggle-string" id="input-filter">
|
||||
</div>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-secondary dropdown-toggle" id="input-filter-button" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">CONTENT</button>
|
||||
<div class="dropdown-menu toggle-dropdown">
|
||||
<a class="dropdown-item" id="input-filter-content">Content</a>
|
||||
<a class="dropdown-item" id="input-filter-filename">Filename</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group input-find-option" id="input-num-results-container">
|
||||
<label for="input-num-results" class="bmd-label-floating">Number of results</label>
|
||||
<input type="number" class="form-control" id="input-num-results" value="20" min="1">
|
||||
</div>
|
||||
<div style="clear:both"></div>
|
||||
<h6>Results</h6>
|
||||
<ul id="input-search-results"></ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" id="input-filter-refresh">Refresh</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" id="output-tab-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Find Output Tab</h5>
|
||||
</div>
|
||||
<div class="modal-body" id="output-tab-body">
|
||||
<h6>Bake Status</h6>
|
||||
<div id="output-find-options">
|
||||
<ul id="output-find-options-checkboxes">
|
||||
<li class="checkbox output-find-option">
|
||||
<label for="output-show-pending">
|
||||
<input type="checkbox" id="output-show-pending" checked="">
|
||||
Pending
|
||||
</label>
|
||||
</li>
|
||||
<li class="checkbox output-find-option">
|
||||
<label for="output-show-baking">
|
||||
<input type="checkbox" id="output-show-baking" checked="">
|
||||
Baking
|
||||
</label>
|
||||
</li>
|
||||
<li class="checkbox output-find-option">
|
||||
<label for="output-show-baked">
|
||||
<input type="checkbox" id="output-show-baked" checked="">
|
||||
Baked
|
||||
</label>
|
||||
</li>
|
||||
<li class="checkbox output-find-option">
|
||||
<label for="output-show-stale">
|
||||
<input type="checkbox" id="output-show-stale" checked="">
|
||||
Stale
|
||||
</label>
|
||||
</li>
|
||||
<li class="checkbox output-find-option">
|
||||
<label for="output-show-errored">
|
||||
<input type="checkbox" id="output-show-errored" checked="">
|
||||
Errored
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="form-group output-find-option">
|
||||
<label for="output-content-filter" class="bmd-label-floating">Content filter (regex)</label>
|
||||
<input type="text" class="form-control" id="output-content-filter">
|
||||
</div>
|
||||
<div class="form-group output-find-option" id="output-num-results-container">
|
||||
<label for="output-num-results" class="bmd-label-floating">Number of results</label>
|
||||
<input type="number" class="form-control" id="output-num-results" value="20">
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<h6>Results</h6>
|
||||
<ul id="output-search-results"></ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" id="output-filter-refresh">Refresh</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -52,6 +52,8 @@ function main() {
|
|||
ioDisplayThreshold: 512,
|
||||
logLevel: "info",
|
||||
autoMagic: true,
|
||||
imagePreview: true,
|
||||
syncTabs: true
|
||||
};
|
||||
|
||||
document.removeEventListener("DOMContentLoaded", main, false);
|
||||
|
|
|
@ -82,7 +82,43 @@ div.toggle-string {
|
|||
.operation .is-focused [class*=' bmd-label'],
|
||||
.operation .is-focused label,
|
||||
.operation .checkbox label:hover {
|
||||
color: #1976d2;
|
||||
color: var(--input-highlight-colour);
|
||||
}
|
||||
|
||||
.ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check,
|
||||
.ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check::before {
|
||||
border-color: var(--input-border-colour);
|
||||
color: var(--input-highlight-colour);
|
||||
}
|
||||
|
||||
.ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check,
|
||||
.ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check::before {
|
||||
border-color: var(--input-highlight-colour);
|
||||
color: var(--input-highlight-colour);
|
||||
}
|
||||
|
||||
.disabled .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check,
|
||||
.disabled .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check::before,
|
||||
.disabled .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check,
|
||||
.disabled .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check::before {
|
||||
border-color: var(--disabled-font-colour);
|
||||
color: var(--disabled-font-colour);
|
||||
}
|
||||
|
||||
.break .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check,
|
||||
.break .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check::before,
|
||||
.break .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check,
|
||||
.break .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check::before {
|
||||
border-color: var(--breakpoint-font-colour);
|
||||
color: var(--breakpoint-font-colour);
|
||||
}
|
||||
|
||||
.flow-control-op.break .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check,
|
||||
.flow-control-op.break .ingredients .checkbox label input[type=checkbox]+.checkbox-decorator .check::before,
|
||||
.flow-control-op.break .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check,
|
||||
.flow-control-op.break .ingredients .checkbox label input[type=checkbox]:checked+.checkbox-decorator .check::before {
|
||||
border-color: var(--fc-breakpoint-operation-font-colour);
|
||||
color: var(--fc-breakpoint-operation-font-colour);
|
||||
}
|
||||
|
||||
.operation .form-control {
|
||||
|
@ -97,7 +133,7 @@ div.toggle-string {
|
|||
|
||||
.operation .form-control:hover {
|
||||
background-image:
|
||||
linear-gradient(to top, #1976d2 2px, rgba(25, 118, 210, 0) 2px),
|
||||
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(25, 118, 210, 0) 2px),
|
||||
linear-gradient(to top, rgba(0, 0, 0, 0.26) 1px, rgba(0, 0, 0, 0) 1px);
|
||||
filter: brightness(97%);
|
||||
}
|
||||
|
@ -105,7 +141,7 @@ div.toggle-string {
|
|||
.operation .form-control:focus {
|
||||
background-color: var(--arg-background);
|
||||
background-image:
|
||||
linear-gradient(to top, #1976d2 2px, rgba(25, 118, 210, 0) 2px),
|
||||
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(25, 118, 210, 0) 2px),
|
||||
linear-gradient(to top, rgba(0, 0, 0, 0.26) 1px, rgba(0, 0, 0, 0) 1px);
|
||||
filter: brightness(100%);
|
||||
}
|
||||
|
@ -205,19 +241,19 @@ div.toggle-string {
|
|||
}
|
||||
|
||||
.disable-icon {
|
||||
color: #9e9e9e;
|
||||
color: var(--disable-icon-colour);
|
||||
}
|
||||
|
||||
.disable-icon-selected {
|
||||
color: #f44336;
|
||||
color: var(--disable-icon-selected-colour);
|
||||
}
|
||||
|
||||
.breakpoint {
|
||||
color: #9e9e9e;
|
||||
color: var(--breakpoint-icon-colour);
|
||||
}
|
||||
|
||||
.breakpoint-selected {
|
||||
color: #f44336;
|
||||
color: var(--breakpoint-icon-selected-colour);
|
||||
}
|
||||
|
||||
.break {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
:root {
|
||||
--title-height: 48px;
|
||||
--tab-height: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
@ -52,6 +53,7 @@
|
|||
line-height: 30px;
|
||||
background-color: var(--primary-background-colour);
|
||||
flex-direction: row;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.io-card.card:hover {
|
||||
|
@ -60,10 +62,16 @@
|
|||
|
||||
.io-card.card>img {
|
||||
float: left;
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
margin-left: 10px;
|
||||
margin-top: 11px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 128px;
|
||||
max-height: 128px;
|
||||
margin-left: auto;
|
||||
margin-top: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: auto;
|
||||
padding: 0px;
|
||||
|
||||
}
|
||||
|
||||
.io-card.card .card-body .close {
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
@import "./themes/_classic.css";
|
||||
@import "./themes/_dark.css";
|
||||
@import "./themes/_geocities.css";
|
||||
@import "./themes/_solarizedDark.css";
|
||||
@import "./themes/_solarizedLight.css";
|
||||
|
||||
/* Utilities */
|
||||
@import "./utils/_overrides.css";
|
||||
|
|
|
@ -22,6 +22,10 @@
|
|||
padding-right: 10px;
|
||||
}
|
||||
|
||||
#banner a {
|
||||
color: var(--banner-url-colour);
|
||||
}
|
||||
|
||||
#notice-wrapper {
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
|
|
@ -40,6 +40,12 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
#auto-bake-label .check,
|
||||
#auto-bake-label .check::before {
|
||||
border-color: var(--input-highlight-colour);
|
||||
color: var(--input-highlight-colour);
|
||||
}
|
||||
|
||||
#auto-bake-label .checkbox-decorator {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -51,3 +57,15 @@
|
|||
#controls .btn {
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation-name: spin;
|
||||
animation-duration: 3s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {transform: rotate(0deg);}
|
||||
100% {transform: rotate(360deg);}
|
||||
}
|
||||
|
|
|
@ -24,18 +24,172 @@
|
|||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#output-wrapper{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#output-wrapper .textarea-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
|
||||
#output-html {
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
-moz-padding-start: 1px; /* Fixes bug in Firefox */
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
position: absolute;
|
||||
top: var(--title-height);
|
||||
bottom: 0;
|
||||
#input-tabs-wrapper #input-tabs,
|
||||
#output-tabs-wrapper #output-tabs {
|
||||
list-style: none;
|
||||
background-color: var(--title-background-colour);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid var(--primary-border-colour);
|
||||
border-left: 1px solid var(--primary-border-colour);
|
||||
height: var(--tab-height);
|
||||
clear: none;
|
||||
}
|
||||
|
||||
#input-tabs li,
|
||||
#output-tabs li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
min-width: 120px;
|
||||
float: left;
|
||||
padding: 0px;
|
||||
text-align: center;
|
||||
border-right: 1px solid var(--primary-border-colour);
|
||||
height: var(--tab-height);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#input-tabs li:hover,
|
||||
#output-tabs li:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--primary-background-colour);
|
||||
}
|
||||
|
||||
.active-input-tab,
|
||||
.active-output-tab {
|
||||
font-weight: bold;
|
||||
background-color: var(--primary-background-colour);
|
||||
}
|
||||
|
||||
.input-tab-content+.btn-close-tab {
|
||||
display: block;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.input-tab-content+.btn-close-tab i {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.input-tab-buttons,
|
||||
.output-tab-buttons {
|
||||
width: 25px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
height: var(--tab-height);
|
||||
line-height: var(--tab-height);
|
||||
font-weight: bold;
|
||||
background-color: var(--title-background-colour);
|
||||
border-bottom: 1px solid var(--primary-border-colour);
|
||||
}
|
||||
|
||||
.input-tab-buttons:hover,
|
||||
.output-tab-buttons:hover {
|
||||
cursor: pointer;
|
||||
background-color: var(--primary-background-colour);
|
||||
}
|
||||
|
||||
|
||||
#btn-next-input-tab,
|
||||
#btn-input-tab-dropdown,
|
||||
#btn-next-output-tab,
|
||||
#btn-output-tab-dropdown {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#btn-previous-input-tab,
|
||||
#btn-previous-output-tab {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#btn-close-all-tabs {
|
||||
color: var(--breakpoint-font-colour) !important;
|
||||
}
|
||||
|
||||
.input-tab-content,
|
||||
.output-tab-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
height: var(--tab-height);
|
||||
vertical-align: middle;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.btn-close-tab {
|
||||
height: var(--tab-height);
|
||||
vertical-align: middle;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.tabs-left > li:first-child {
|
||||
box-shadow: 15px 0px 15px -15px var(--primary-border-colour) inset;
|
||||
}
|
||||
|
||||
.tabs-right > li:last-child {
|
||||
box-shadow: -15px 0px 15px -15px var(--primary-border-colour) inset;
|
||||
}
|
||||
|
||||
#input-wrapper,
|
||||
#output-wrapper,
|
||||
#input-wrapper > * ,
|
||||
#output-wrapper > .textarea-wrapper > div,
|
||||
#output-wrapper > .textarea-wrapper > textarea {
|
||||
height: calc(100% - var(--title-height));
|
||||
}
|
||||
|
||||
#input-wrapper.show-tabs,
|
||||
#input-wrapper.show-tabs > *,
|
||||
#output-wrapper.show-tabs,
|
||||
#output-wrapper.show-tabs > .textarea-wrapper > div,
|
||||
#output-wrapper.show-tabs > .textarea-wrapper > textarea {
|
||||
height: calc(100% - var(--tab-height) - var(--title-height));
|
||||
}
|
||||
|
||||
#output-wrapper > .textarea-wrapper > #output-html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#show-file-overlay {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.input-wrapper.textarea-wrapper {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.textarea-wrapper textarea,
|
||||
|
@ -49,9 +203,8 @@
|
|||
#output-highlighter {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 3px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
|
@ -61,14 +214,14 @@
|
|||
color: #fff;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#output-loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background-color: var(--primary-background-colour);
|
||||
visibility: hidden;
|
||||
|
@ -105,9 +258,8 @@
|
|||
#output-file {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -122,7 +274,7 @@
|
|||
#show-file-overlay {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
top: calc(var(--title-height) + 10px);
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
}
|
||||
|
@ -147,7 +299,6 @@
|
|||
|
||||
.dropping-file {
|
||||
border: 5px dashed var(--drop-file-border-colour) !important;
|
||||
margin: -5px;
|
||||
}
|
||||
|
||||
#stale-indicator {
|
||||
|
@ -185,3 +336,103 @@
|
|||
#magic svg path {
|
||||
fill: var(--primary-font-colour);
|
||||
}
|
||||
|
||||
#input-find-options,
|
||||
#output-find-options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#input-tab-body .form-group.input-group,
|
||||
#output-tab-body .form-group.input-group {
|
||||
width: 70%;
|
||||
float: left;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.input-find-option .toggle-string {
|
||||
width: 70%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.input-find-option-append button {
|
||||
border-top-right-radius: 4px;
|
||||
background-color: var(--arg-background) !important;
|
||||
margin: unset;
|
||||
}
|
||||
|
||||
.input-find-option-append button:hover {
|
||||
filter: brightness(97%);
|
||||
}
|
||||
|
||||
.form-group.output-find-option {
|
||||
width: 70%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#input-num-results-container,
|
||||
#output-num-results-container {
|
||||
width: 20%;
|
||||
float: right;
|
||||
margin: 0;
|
||||
margin-left: 10%;
|
||||
}
|
||||
|
||||
#input-find-options-checkboxes,
|
||||
#output-find-options-checkboxes {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
text-align: center;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
#input-find-options-checkboxes li,
|
||||
#output-find-options-checkboxes li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
float: left;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
#input-search-results,
|
||||
#output-search-results {
|
||||
list-style: none;
|
||||
width: 75%;
|
||||
min-width: 200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
#input-search-results li,
|
||||
#output-search-results li {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
color: var(--op-list-operation-font-colour);
|
||||
background-color: var(--op-list-operation-bg-colour);
|
||||
border-bottom: 2px solid var(--op-list-operation-border-colour);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
#input-search-results li:first-of-type,
|
||||
#output-search-results li:first-of-type {
|
||||
border-top: 2px solid var(--op-list-operation-border-colour);
|
||||
}
|
||||
|
||||
#input-search-results li:hover,
|
||||
#output-search-results li:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(98%);
|
||||
}
|
||||
|
|
|
@ -77,3 +77,34 @@
|
|||
padding: 20px;
|
||||
border-left: 2px solid var(--primary-border-colour);
|
||||
}
|
||||
|
||||
.checkbox label input[type=checkbox]+.checkbox-decorator .check,
|
||||
.checkbox label input[type=checkbox]+.checkbox-decorator .check::before {
|
||||
border-color: var(--input-border-colour);
|
||||
color: var(--input-highlight-colour);
|
||||
}
|
||||
|
||||
.checkbox label input[type=checkbox]:checked+.checkbox-decorator .check,
|
||||
.checkbox label input[type=checkbox]:checked+.checkbox-decorator .check::before {
|
||||
border-color: var(--input-highlight-colour);
|
||||
color: var(--input-highlight-colour);
|
||||
}
|
||||
|
||||
.bmd-form-group.is-focused .option-item label {
|
||||
color: var(--input-highlight-colour);
|
||||
}
|
||||
|
||||
.bmd-form-group.is-focused [class^='bmd-label'],
|
||||
.bmd-form-group.is-focused [class*=' bmd-label'],
|
||||
.bmd-form-group.is-focused [class^='bmd-label'],
|
||||
.bmd-form-group.is-focused [class*=' bmd-label'],
|
||||
.bmd-form-group.is-focused label,
|
||||
.checkbox label:hover {
|
||||
color: var(--input-highlight-colour);
|
||||
}
|
||||
|
||||
.bmd-form-group.option-item label+.form-control{
|
||||
background-image:
|
||||
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(0, 0, 0, 0) 2px),
|
||||
linear-gradient(to top, var(--primary-border-colour) 1px, rgba(0, 0, 0, 0) 1px);
|
||||
}
|
|
@ -16,7 +16,7 @@
|
|||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
background-image:
|
||||
linear-gradient(to top, #1976d2 2px, rgba(25, 118, 210, 0) 2px),
|
||||
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(0, 0, 0, 0) 2px),
|
||||
linear-gradient(to top, var(--primary-border-colour) 1px, rgba(0, 0, 0, 0) 1px);
|
||||
}
|
||||
|
||||
|
@ -33,7 +33,7 @@
|
|||
}
|
||||
|
||||
#categories a {
|
||||
color: #1976d2;
|
||||
color: var(--category-list-font-colour);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
#loader-wrapper {
|
||||
#loader-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1000;
|
||||
background-color: var(--secondary-border-colour);
|
||||
background-color: var(--loader-background-colour);
|
||||
}
|
||||
|
||||
.loader {
|
||||
|
@ -26,7 +26,7 @@
|
|||
margin: -75px 0 0 -75px;
|
||||
|
||||
border: 3px solid transparent;
|
||||
border-top-color: #3498db;
|
||||
border-top-color: var(--loader-outer-colour);
|
||||
border-radius: 50%;
|
||||
|
||||
animation: spin 2s linear infinite;
|
||||
|
@ -45,7 +45,7 @@
|
|||
left: 5px;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
border-top-color: #e74c3c;
|
||||
border-top-color: var(--loader-middle-colour);
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
|
||||
|
@ -54,7 +54,7 @@
|
|||
left: 13px;
|
||||
right: 13px;
|
||||
bottom: 13px;
|
||||
border-top-color: #f9c922;
|
||||
border-top-color: var(--loader-inner-colour);
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
|
|
|
@ -35,6 +35,14 @@
|
|||
|
||||
--banner-font-colour: #468847;
|
||||
--banner-bg-colour: #dff0d8;
|
||||
--banner-url-colour: #1976d2;
|
||||
|
||||
--category-list-font-colour: #1976d2;
|
||||
|
||||
--loader-background-colour: var(--secondary-border-colour);
|
||||
--loader-outer-colour: #3498db;
|
||||
--loader-middle-colour: #e74c3c;
|
||||
--loader-inner-colour: #f9c922;
|
||||
|
||||
|
||||
/* Operation colours */
|
||||
|
@ -76,6 +84,13 @@
|
|||
--arg-label-colour: #388e3c;
|
||||
|
||||
|
||||
/* Operation buttons */
|
||||
--disable-icon-colour: #9e9e9e;
|
||||
--disable-icon-selected-colour: #f44336;
|
||||
--breakpoint-icon-colour: #9e9e9e;
|
||||
--breakpoint-icon-selected-colour: #f44336;
|
||||
|
||||
|
||||
/* Buttons */
|
||||
--btn-default-font-colour: #333;
|
||||
--btn-default-bg-colour: #fff;
|
||||
|
@ -114,4 +129,6 @@
|
|||
--popover-border-colour: #ccc;
|
||||
--code-background: #f9f2f4;
|
||||
--code-font-colour: #c7254e;
|
||||
--input-highlight-colour: #1976d2;
|
||||
--input-border-colour: #424242;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,14 @@
|
|||
|
||||
--banner-font-colour: #c5c5c5;
|
||||
--banner-bg-colour: #252525;
|
||||
--banner-url-colour: #1976d2;
|
||||
|
||||
--category-list-font-colour: #1976d2;
|
||||
|
||||
--loader-background-colour: var(--secondary-border-colour);
|
||||
--loader-outer-colour: #3498db;
|
||||
--loader-middle-colour: #e74c3c;
|
||||
--loader-inner-colour: #f9c922;
|
||||
|
||||
|
||||
/* Operation colours */
|
||||
|
@ -72,6 +80,13 @@
|
|||
--arg-label-colour: rgb(25, 118, 210);
|
||||
|
||||
|
||||
/* Operation buttons */
|
||||
--disable-icon-colour: #9e9e9e;
|
||||
--disable-icon-selected-colour: #f44336;
|
||||
--breakpoint-icon-colour: #9e9e9e;
|
||||
--breakpoint-icon-selected-colour: #f44336;
|
||||
|
||||
|
||||
/* Buttons */
|
||||
--btn-default-font-colour: #c5c5c5;
|
||||
--btn-default-bg-colour: #2d2d2d;
|
||||
|
@ -110,4 +125,6 @@
|
|||
--popover-border-colour: #555;
|
||||
--code-background: #0e639c;
|
||||
--code-font-colour: #fff;
|
||||
--input-highlight-colour: #1976d2;
|
||||
--input-border-colour: #424242;
|
||||
}
|
||||
|
|
|
@ -31,6 +31,14 @@
|
|||
|
||||
--banner-font-colour: white;
|
||||
--banner-bg-colour: maroon;
|
||||
--banner-url-colour: yellow;
|
||||
|
||||
--category-list-font-colour: yellow;
|
||||
|
||||
--loader-background-colour: #00f;
|
||||
--loader-outer-colour: #0f0;
|
||||
--loader-middle-colour: red;
|
||||
--loader-inner-colour: yellow;
|
||||
|
||||
|
||||
/* Operation colours */
|
||||
|
@ -72,6 +80,13 @@
|
|||
--arg-label-colour: red;
|
||||
|
||||
|
||||
/* Operation buttons */
|
||||
--disable-icon-colour: #0f0;
|
||||
--disable-icon-selected-colour: yellow;
|
||||
--breakpoint-icon-colour: #0f0;
|
||||
--breakpoint-icon-selected-colour: yellow;
|
||||
|
||||
|
||||
/* Buttons */
|
||||
--btn-default-font-colour: black;
|
||||
--btn-default-bg-colour: white;
|
||||
|
@ -110,4 +125,6 @@
|
|||
--popover-border-colour: violet;
|
||||
--code-background: black;
|
||||
--code-font-colour: limegreen;
|
||||
--input-highlight-colour: limegreen;
|
||||
--input-border-colour: limegreen;
|
||||
}
|
||||
|
|
147
src/web/stylesheets/themes/_solarizedDark.css
Executable file
147
src/web/stylesheets/themes/_solarizedDark.css
Executable file
|
@ -0,0 +1,147 @@
|
|||
/**
|
||||
* Solarized dark theme definitions
|
||||
*
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
:root.solarizedDark {
|
||||
--base03: #002b36;
|
||||
--base02: #073642;
|
||||
--base01: #586e75;
|
||||
--base00: #657b83;
|
||||
--base0: #839496;
|
||||
--base1: #93a1a1;
|
||||
--base2: #eee8d5;
|
||||
--base3: #fdf6e3;
|
||||
--sol-yellow: #b58900;
|
||||
--sol-orange: #cb4b16;
|
||||
--sol-red: #dc322f;
|
||||
--sol-magenta: #d33682;
|
||||
--sol-violet: #6c71c4;
|
||||
--sol-blue: #268bd2;
|
||||
--sol-cyan: #2aa198;
|
||||
--sol-green: #859900;
|
||||
|
||||
--primary-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--primary-font-colour: var(--base0);
|
||||
--primary-font-size: 14px;
|
||||
--primary-line-height: 20px;
|
||||
|
||||
--fixed-width-font-family: SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New", monospace;
|
||||
--fixed-width-font-colour: inherit;
|
||||
--fixed-width-font-size: inherit;
|
||||
|
||||
--subtext-font-colour: var(--base01);
|
||||
--subtext-font-size: 13px;
|
||||
|
||||
--primary-background-colour: var(--base03);
|
||||
--secondary-background-colour: var(--base02);
|
||||
|
||||
--primary-border-colour: var(--base00);
|
||||
--secondary-border-colour: var(--base01);
|
||||
|
||||
--title-colour: var(--base1);
|
||||
--title-weight: bold;
|
||||
--title-background-colour: var(--base02);
|
||||
|
||||
--banner-font-colour: var(--base0);
|
||||
--banner-bg-colour: var(--base03);
|
||||
--banner-url-colour: var(--base1);
|
||||
|
||||
--category-list-font-colour: var(--base1);
|
||||
|
||||
--loader-background-colour: var(--base03);
|
||||
--loader-outer-colour: var(--base1);
|
||||
--loader-middle-colour: var(--base0);
|
||||
--loader-inner-colour: var(--base00);
|
||||
|
||||
|
||||
/* Operation colours */
|
||||
--op-list-operation-font-colour: var(--base0);
|
||||
--op-list-operation-bg-colour: var(--base03);
|
||||
--op-list-operation-border-colour: var(--base02);
|
||||
|
||||
--rec-list-operation-font-colour: var(--base0);
|
||||
--rec-list-operation-bg-colour: var(--base02);
|
||||
--rec-list-operation-border-colour: var(--base01);
|
||||
|
||||
--selected-operation-font-color: var(--base1);
|
||||
--selected-operation-bg-colour: var(--base02);
|
||||
--selected-operation-border-colour: var(--base01);
|
||||
|
||||
--breakpoint-font-colour: var(--sol-red);
|
||||
--breakpoint-bg-colour: var(--base02);
|
||||
--breakpoint-border-colour: var(--base00);
|
||||
|
||||
--disabled-font-colour: var(--base01);
|
||||
--disabled-bg-colour: var(--base03);
|
||||
--disabled-border-colour: var(--base02);
|
||||
|
||||
--fc-operation-font-colour: var(--base1);
|
||||
--fc-operation-bg-colour: var(--base02);
|
||||
--fc-operation-border-colour: var(--base01);
|
||||
|
||||
--fc-breakpoint-operation-font-colour: var(--sol-orange);
|
||||
--fc-breakpoint-operation-bg-colour: var(--base02);
|
||||
--fc-breakpoint-operation-border-colour: var(--base00);
|
||||
|
||||
|
||||
/* Operation arguments */
|
||||
--op-title-font-weight: bold;
|
||||
--arg-font-colour: var(--base0);
|
||||
--arg-background: var(--base03);
|
||||
--arg-border-colour: var(--base00);
|
||||
--arg-disabled-background: var(--base03);
|
||||
--arg-label-colour: var(--base1);
|
||||
|
||||
|
||||
/* Operation buttons */
|
||||
--disable-icon-colour: var(--base00);
|
||||
--disable-icon-selected-colour: var(--sol-red);
|
||||
--breakpoint-icon-colour: var(--base00);
|
||||
--breakpoint-icon-selected-colour: var(--sol-red);
|
||||
|
||||
/* Buttons */
|
||||
--btn-default-font-colour: var(--base0);
|
||||
--btn-default-bg-colour: var(--base02);
|
||||
--btn-default-border-colour: var(--base01);
|
||||
|
||||
--btn-default-hover-font-colour: var(--base1);
|
||||
--btn-default-hover-bg-colour: var(--base01);
|
||||
--btn-default-hover-border-colour: var(--base00);
|
||||
|
||||
--btn-success-font-colour: var(--base0);
|
||||
--btn-success-bg-colour: var(--base03);
|
||||
--btn-success-border-colour: var(--base00);
|
||||
|
||||
--btn-success-hover-font-colour: var(--base1);
|
||||
--btn-success-hover-bg-colour: var(--base01);
|
||||
--btn-success-hover-border-colour: var(--base00);
|
||||
|
||||
/* Highlighter colours */
|
||||
--hl1: var(--base01);
|
||||
--hl2: var(--sol-blue);
|
||||
--hl3: var(--sol-magenta);
|
||||
--hl4: var(--sol-yellow);
|
||||
--hl5: var(--sol-green);
|
||||
|
||||
|
||||
/* Scrollbar */
|
||||
--scrollbar-track: var(--base03);
|
||||
--scrollbar-thumb: var(--base00);
|
||||
--scrollbar-hover: var(--base01);
|
||||
|
||||
|
||||
/* Misc. */
|
||||
--drop-file-border-colour: var(--base01);
|
||||
--popover-background: var(--base02);
|
||||
--popover-border-colour: var(--base01);
|
||||
--code-background: var(--base03);
|
||||
--code-font-colour: var(--base1);
|
||||
--input-highlight-colour: var(--base1);
|
||||
--input-border-colour: var(--base0);
|
||||
}
|
149
src/web/stylesheets/themes/_solarizedLight.css
Executable file
149
src/web/stylesheets/themes/_solarizedLight.css
Executable file
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* Solarized light theme definitions
|
||||
*
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
:root.solarizedLight {
|
||||
--base03: #002b36;
|
||||
--base02: #073642;
|
||||
--base01: #586e75;
|
||||
--base00: #657b83;
|
||||
--base0: #839496;
|
||||
--base1: #93a1a1;
|
||||
--base2: #eee8d5;
|
||||
--base3: #fdf6e3;
|
||||
--sol-yellow: #b58900;
|
||||
--sol-orange: #cb4b16;
|
||||
--sol-red: #dc322f;
|
||||
--sol-magenta: #d33682;
|
||||
--sol-violet: #6c71c4;
|
||||
--sol-blue: #268bd2;
|
||||
--sol-cyan: #2aa198;
|
||||
--sol-green: #859900;
|
||||
|
||||
--primary-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--primary-font-colour: var(--base00);
|
||||
--primary-font-size: 14px;
|
||||
--primary-line-height: 20px;
|
||||
|
||||
--fixed-width-font-family: SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
"Liberation Mono", "Courier New", monospace;
|
||||
--fixed-width-font-colour: inherit;
|
||||
--fixed-width-font-size: inherit;
|
||||
|
||||
--subtext-font-colour: var(--base1);
|
||||
--subtext-font-size: 13px;
|
||||
|
||||
--primary-background-colour: var(--base3);
|
||||
--secondary-background-colour: var(--base2);
|
||||
|
||||
--primary-border-colour: var(--base0);
|
||||
--secondary-border-colour: var(--base1);
|
||||
|
||||
--title-colour: var(--base01);
|
||||
--title-weight: bold;
|
||||
--title-background-colour: var(--base2);
|
||||
|
||||
--banner-font-colour: var(--base00);
|
||||
--banner-bg-colour: var(--base3);
|
||||
--banner-url-colour: var(--base01);
|
||||
|
||||
--category-list-font-colour: var(--base01);
|
||||
|
||||
--loader-background-colour: var(--base3);
|
||||
--loader-outer-colour: var(--base01);
|
||||
--loader-middle-colour: var(--base00);
|
||||
--loader-inner-colour: var(--base0);
|
||||
|
||||
|
||||
/* Operation colours */
|
||||
--op-list-operation-font-colour: var(--base00);
|
||||
--op-list-operation-bg-colour: var(--base3);
|
||||
--op-list-operation-border-colour: var(--base2);
|
||||
|
||||
--rec-list-operation-font-colour: var(--base00);
|
||||
--rec-list-operation-bg-colour: var(--base2);
|
||||
--rec-list-operation-border-colour: var(--base1);
|
||||
|
||||
--selected-operation-font-color: var(--base01);
|
||||
--selected-operation-bg-colour: var(--base2);
|
||||
--selected-operation-border-colour: var(--base1);
|
||||
|
||||
--breakpoint-font-colour: var(--sol-red);
|
||||
--breakpoint-bg-colour: var(--base2);
|
||||
--breakpoint-border-colour: var(--base0);
|
||||
|
||||
--disabled-font-colour: var(--base1);
|
||||
--disabled-bg-colour: var(--base3);
|
||||
--disabled-border-colour: var(--base2);
|
||||
|
||||
--fc-operation-font-colour: var(--base01);
|
||||
--fc-operation-bg-colour: var(--base2);
|
||||
--fc-operation-border-colour: var(--base1);
|
||||
|
||||
--fc-breakpoint-operation-font-colour: var(--base02);
|
||||
--fc-breakpoint-operation-bg-colour: var(--base1);
|
||||
--fc-breakpoint-operation-border-colour: var(--base0);
|
||||
|
||||
|
||||
/* Operation arguments */
|
||||
--op-title-font-weight: bold;
|
||||
--arg-font-colour: var(--base00);
|
||||
--arg-background: var(--base3);
|
||||
--arg-border-colour: var(--base0);
|
||||
--arg-disabled-background: var(--base3);
|
||||
--arg-label-colour: var(--base01);
|
||||
|
||||
|
||||
/* Operation buttons */
|
||||
--disable-icon-colour: #9e9e9e;
|
||||
--disable-icon-selected-colour: #f44336;
|
||||
--breakpoint-icon-colour: #9e9e9e;
|
||||
--breakpoint-icon-selected-colour: #f44336;
|
||||
|
||||
|
||||
/* Buttons */
|
||||
--btn-default-font-colour: var(--base00);
|
||||
--btn-default-bg-colour: var(--base2);
|
||||
--btn-default-border-colour: var(--base1);
|
||||
|
||||
--btn-default-hover-font-colour: var(--base01);
|
||||
--btn-default-hover-bg-colour: var(--base1);
|
||||
--btn-default-hover-border-colour: var(--base0);
|
||||
|
||||
--btn-success-font-colour: var(--base00);
|
||||
--btn-success-bg-colour: var(--base3);
|
||||
--btn-success-border-colour: var(--base0);
|
||||
|
||||
--btn-success-hover-font-colour: var(--base01);
|
||||
--btn-success-hover-bg-colour: var(--base1);
|
||||
--btn-success-hover-border-colour: var(--base0);
|
||||
|
||||
|
||||
/* Highlighter colours */
|
||||
--hl1: var(--base1);
|
||||
--hl2: var(--sol-blue);
|
||||
--hl3: var(--sol-magenta);
|
||||
--hl4: var(--sol-yellow);
|
||||
--hl5: var(--sol-green);
|
||||
|
||||
|
||||
/* Scrollbar */
|
||||
--scrollbar-track: var(--base3);
|
||||
--scrollbar-thumb: var(--base1);
|
||||
--scrollbar-hover: var(--base0);
|
||||
|
||||
|
||||
/* Misc. */
|
||||
--drop-file-border-colour: var(--base1);
|
||||
--popover-background: var(--base2);
|
||||
--popover-border-colour: var(--base1);
|
||||
--code-background: var(--base3);
|
||||
--code-font-colour: var(--base01);
|
||||
--input-highlight-colour: var(--base01);
|
||||
--input-border-colour: var(--base00);
|
||||
}
|
|
@ -104,8 +104,11 @@ select.form-control:not([size]):not([multiple]), select.custom-file-control:not(
|
|||
color: var(--primary-font-colour);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background-image: linear-gradient(to top, rgb(25, 118, 210) 2px, rgba(25, 118, 210, 0) 2px), linear-gradient(to top, var(--primary-border-colour) 1px, rgba(0, 0, 0, 0) 1px);
|
||||
.form-control,
|
||||
.is-focused .form-control {
|
||||
background-image:
|
||||
linear-gradient(to top, var(--input-highlight-colour) 2px, rgba(0, 0, 0, 0) 2px),
|
||||
linear-gradient(to top, var(--primary-border-colour) 1px, rgba(0, 0, 0, 0) 1px);
|
||||
}
|
||||
|
||||
code {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import ChefWorker from "worker-loader?inline&fallback=false!../core/ChefWorker";
|
||||
import ChefWorker from "worker-loader?inline&fallback=false!../../core/ChefWorker";
|
||||
|
||||
/**
|
||||
* Waiter to handle conversations with a ChefWorker in the background.
|
||||
|
@ -68,6 +68,7 @@ class BackgroundWorkerWaiter {
|
|||
break;
|
||||
case "optionUpdate":
|
||||
case "statusMessage":
|
||||
case "progressMessage":
|
||||
// Ignore these messages
|
||||
break;
|
||||
default:
|
|
@ -98,11 +98,11 @@ class BindingsWaiter {
|
|||
break;
|
||||
case "Space": // Bake
|
||||
e.preventDefault();
|
||||
this.app.bake();
|
||||
this.manager.controls.bakeClick();
|
||||
break;
|
||||
case "Quote": // Step through
|
||||
e.preventDefault();
|
||||
this.app.bake(true);
|
||||
this.manager.controls.stepClick();
|
||||
break;
|
||||
case "KeyC": // Clear recipe
|
||||
e.preventDefault();
|
||||
|
@ -120,6 +120,22 @@ class BindingsWaiter {
|
|||
e.preventDefault();
|
||||
this.manager.output.switchClick();
|
||||
break;
|
||||
case "KeyT": // New tab
|
||||
e.preventDefault();
|
||||
this.manager.input.addInputClick();
|
||||
break;
|
||||
case "KeyW": // Close tab
|
||||
e.preventDefault();
|
||||
this.manager.input.removeInput(this.manager.tabs.getActiveInputTab());
|
||||
break;
|
||||
case "ArrowLeft": // Go to previous tab
|
||||
e.preventDefault();
|
||||
this.manager.input.changeTabLeft();
|
||||
break;
|
||||
case "ArrowRight": // Go to next tab
|
||||
e.preventDefault();
|
||||
this.manager.input.changeTabRight();
|
||||
break;
|
||||
default:
|
||||
if (e.code.match(/Digit[0-9]/g)) { // Select nth operation
|
||||
e.preventDefault();
|
||||
|
@ -216,6 +232,26 @@ class BindingsWaiter {
|
|||
<td>Ctrl+${modWinLin}+m</td>
|
||||
<td>Ctrl+${modMac}+m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Create a new tab</td>
|
||||
<td>Ctrl+${modWinLin}+t</td>
|
||||
<td>Ctrl+${modMac}+t</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Close the current tab</td>
|
||||
<td>Ctrl+${modWinLin}+w</td>
|
||||
<td>Ctrl+${modMac}+w</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Go to next tab</td>
|
||||
<td>Ctrl+${modWinLin}+RightArrow</td>
|
||||
<td>Ctrl+${modMac}+RightArrow</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Go to previous tab</td>
|
||||
<td>Ctrl+${modWinLin}+LeftArrow</td>
|
||||
<td>Ctrl+${modMac}+LeftArrow</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
|
@ -4,8 +4,7 @@
|
|||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Utils from "../core/Utils";
|
||||
import {toBase64} from "../core/lib/Base64";
|
||||
import Utils from "../../core/Utils";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -57,10 +56,11 @@ class ControlsWaiter {
|
|||
* Handler to trigger baking.
|
||||
*/
|
||||
bakeClick() {
|
||||
if (document.getElementById("bake").textContent.indexOf("Bake") > 0) {
|
||||
this.app.bake();
|
||||
} else {
|
||||
this.manager.worker.cancelBake();
|
||||
const btnBake = document.getElementById("bake");
|
||||
if (btnBake.textContent.indexOf("Bake") > 0) {
|
||||
this.app.manager.input.bakeAll();
|
||||
} else if (btnBake.textContent.indexOf("Cancel") > 0) {
|
||||
this.manager.worker.cancelBake(false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,7 @@ class ControlsWaiter {
|
|||
* Handler for the 'Step through' command. Executes the next step of the recipe.
|
||||
*/
|
||||
stepClick() {
|
||||
this.app.bake(true);
|
||||
this.app.step();
|
||||
}
|
||||
|
||||
|
||||
|
@ -90,7 +90,7 @@ class ControlsWaiter {
|
|||
|
||||
|
||||
/**
|
||||
* Populates the save disalog box with a URL incorporating the recipe and input.
|
||||
* Populates the save dialog box with a URL incorporating the recipe and input.
|
||||
*
|
||||
* @param {Object[]} [recipeConfig] - The recipe configuration object array.
|
||||
*/
|
||||
|
@ -112,26 +112,33 @@ class ControlsWaiter {
|
|||
*
|
||||
* @param {boolean} includeRecipe - Whether to include the recipe in the URL.
|
||||
* @param {boolean} includeInput - Whether to include the input in the URL.
|
||||
* @param {string} input
|
||||
* @param {Object[]} [recipeConfig] - The recipe configuration object array.
|
||||
* @param {string} [baseURL] - The CyberChef URL, set to the current URL if not included
|
||||
* @returns {string}
|
||||
*/
|
||||
generateStateUrl(includeRecipe, includeInput, recipeConfig, baseURL) {
|
||||
generateStateUrl(includeRecipe, includeInput, input, recipeConfig, baseURL) {
|
||||
recipeConfig = recipeConfig || this.app.getRecipeConfig();
|
||||
|
||||
const link = baseURL || window.location.protocol + "//" +
|
||||
window.location.host +
|
||||
window.location.pathname;
|
||||
const recipeStr = Utils.generatePrettyRecipe(recipeConfig);
|
||||
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);
|
||||
|
||||
// If we don't get passed an input, get it from the current URI
|
||||
if (input === null) {
|
||||
const params = this.app.getURIParams();
|
||||
if (params.input) {
|
||||
includeInput = true;
|
||||
input = params.input;
|
||||
}
|
||||
}
|
||||
|
||||
const params = [
|
||||
includeRecipe ? ["recipe", recipeStr] : undefined,
|
||||
includeInput ? ["input", inputStr] : undefined,
|
||||
includeInput ? ["input", input] : undefined,
|
||||
];
|
||||
|
||||
const hash = params
|
||||
|
@ -335,7 +342,7 @@ class ControlsWaiter {
|
|||
e.preventDefault();
|
||||
|
||||
const reportBugInfo = document.getElementById("report-bug-info");
|
||||
const saveLink = this.generateStateUrl(true, true, null, "https://gchq.github.io/CyberChef/");
|
||||
const saveLink = this.generateStateUrl(true, true, null, null, "https://gchq.github.io/CyberChef/");
|
||||
|
||||
if (reportBugInfo) {
|
||||
reportBugInfo.innerHTML = `* Version: ${PKG_VERSION}
|
||||
|
@ -370,22 +377,34 @@ ${navigator.userAgent}
|
|||
|
||||
|
||||
/**
|
||||
* Switches the Bake button between 'Bake' and 'Cancel' functions.
|
||||
* Switches the Bake button between 'Bake', 'Cancel' and 'Loading' functions.
|
||||
*
|
||||
* @param {boolean} cancel - Whether to change to cancel or not
|
||||
* @param {string} func - The function to change to. Either "cancel", "loading" or "bake"
|
||||
*/
|
||||
toggleBakeButtonFunction(cancel) {
|
||||
toggleBakeButtonFunction(func) {
|
||||
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");
|
||||
switch (func) {
|
||||
case "cancel":
|
||||
btnText.innerText = "Cancel";
|
||||
bakeButton.classList.remove("btn-success");
|
||||
bakeButton.classList.remove("btn-warning");
|
||||
bakeButton.classList.add("btn-danger");
|
||||
break;
|
||||
case "loading":
|
||||
bakeButton.style.background = "";
|
||||
btnText.innerText = "Loading...";
|
||||
bakeButton.classList.remove("btn-success");
|
||||
bakeButton.classList.remove("btn-danger");
|
||||
bakeButton.classList.add("btn-warning");
|
||||
break;
|
||||
default:
|
||||
bakeButton.style.background = "";
|
||||
btnText.innerText = "Bake!";
|
||||
bakeButton.classList.remove("btn-danger");
|
||||
bakeButton.classList.remove("btn-warning");
|
||||
bakeButton.classList.add("btn-success");
|
||||
}
|
||||
}
|
||||
|
|
@ -378,6 +378,8 @@ class HighlighterWaiter {
|
|||
displayHighlights(pos, direction) {
|
||||
if (!pos) return;
|
||||
|
||||
if (this.manager.tabs.getActiveInputTab() !== this.manager.tabs.getActiveOutputTab()) return;
|
||||
|
||||
const io = direction === "forward" ? "output" : "input";
|
||||
|
||||
document.getElementById(io + "-selection-info").innerHTML = this.selectionInfo(pos[0].start, pos[0].end);
|
1371
src/web/waiters/InputWaiter.mjs
Normal file
1371
src/web/waiters/InputWaiter.mjs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@
|
|||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import HTMLOperation from "./HTMLOperation";
|
||||
import HTMLOperation from "../HTMLOperation";
|
||||
import Sortable from "sortablejs";
|
||||
|
||||
|
|
@ -168,6 +168,7 @@ OptionsWaiter.prototype.logLevelChange = function (e) {
|
|||
const level = e.target.value;
|
||||
log.setLevel(level, false);
|
||||
this.manager.worker.setLogLevel();
|
||||
this.manager.input.setLogLevel();
|
||||
};
|
||||
|
||||
export default OptionsWaiter;
|
1417
src/web/waiters/OutputWaiter.mjs
Executable file
1417
src/web/waiters/OutputWaiter.mjs
Executable file
File diff suppressed because it is too large
Load diff
|
@ -4,9 +4,9 @@
|
|||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import HTMLOperation from "./HTMLOperation";
|
||||
import HTMLOperation from "../HTMLOperation";
|
||||
import Sortable from "sortablejs";
|
||||
import Utils from "../core/Utils";
|
||||
import Utils from "../../core/Utils";
|
||||
|
||||
|
||||
/**
|
|
@ -5,8 +5,8 @@
|
|||
*/
|
||||
|
||||
import clippy from "clippyjs";
|
||||
import "./static/clippy_assets/agents/Clippy/agent.js";
|
||||
import clippyMap from "./static/clippy_assets/agents/Clippy/map.png";
|
||||
import "../static/clippy_assets/agents/Clippy/agent.js";
|
||||
import clippyMap from "../static/clippy_assets/agents/Clippy/map.png";
|
||||
|
||||
/**
|
||||
* Waiter to handle seasonal events and easter eggs.
|
428
src/web/waiters/TabWaiter.mjs
Normal file
428
src/web/waiters/TabWaiter.mjs
Normal file
|
@ -0,0 +1,428 @@
|
|||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Waiter to handle events related to the input and output tabs
|
||||
*/
|
||||
class TabWaiter {
|
||||
|
||||
/**
|
||||
* TabWaiter 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the maximum number of tabs to display
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
calcMaxTabs() {
|
||||
let numTabs = Math.floor((document.getElementById("IO").offsetWidth - 75) / 120);
|
||||
numTabs = (numTabs > 1) ? numTabs : 2;
|
||||
|
||||
return numTabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently active input or active tab number
|
||||
*
|
||||
* @param {string} io - Either "input" or "output"
|
||||
* @returns {number} - The currently active tab or -1
|
||||
*/
|
||||
getActiveTab(io) {
|
||||
const activeTabs = document.getElementsByClassName(`active-${io}-tab`);
|
||||
if (activeTabs.length > 0) {
|
||||
if (!activeTabs.item(0).hasAttribute("inputNum")) return -1;
|
||||
const tabNum = activeTabs.item(0).getAttribute("inputNum");
|
||||
return parseInt(tabNum, 10);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently active input tab number
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
getActiveInputTab() {
|
||||
return this.getActiveTab("input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currently active output tab number
|
||||
*
|
||||
* @returns {number}
|
||||
*/
|
||||
getActiveOutputTab() {
|
||||
return this.getActiveTab("output");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the li element for the tab of a given input number
|
||||
*
|
||||
* @param {number} inputNum - The inputNum of the tab we're trying to get
|
||||
* @param {string} io - Either "input" or "output"
|
||||
* @returns {Element}
|
||||
*/
|
||||
getTabItem(inputNum, io) {
|
||||
const tabs = document.getElementById(`${io}-tabs`).children;
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
if (parseInt(tabs.item(i).getAttribute("inputNum"), 10) === inputNum) {
|
||||
return tabs.item(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the li element for an input tab of the given input number
|
||||
*
|
||||
* @param {inputNum} - The inputNum of the tab we're trying to get
|
||||
* @returns {Element}
|
||||
*/
|
||||
getInputTabItem(inputNum) {
|
||||
return this.getTabItem(inputNum, "input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the li element for an output tab of the given input number
|
||||
*
|
||||
* @param {number} inputNum
|
||||
* @returns {Element}
|
||||
*/
|
||||
getOutputTabItem(inputNum) {
|
||||
return this.getTabItem(inputNum, "output");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of tab numbers for the currently displayed tabs
|
||||
*
|
||||
* @param {string} io - Either "input" or "output"
|
||||
* @returns {number[]}
|
||||
*/
|
||||
getTabList(io) {
|
||||
const nums = [],
|
||||
tabs = document.getElementById(`${io}-tabs`).children;
|
||||
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
nums.push(parseInt(tabs.item(i).getAttribute("inputNum"), 10));
|
||||
}
|
||||
|
||||
return nums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of tab numbers for the currently displayed input tabs
|
||||
*
|
||||
* @returns {number[]}
|
||||
*/
|
||||
getInputTabList() {
|
||||
return this.getTabList("input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of tab numbers for the currently displayed output tabs
|
||||
*
|
||||
* @returns {number[]}
|
||||
*/
|
||||
getOutputTabList() {
|
||||
return this.getTabList("output");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tab element for the tab bar
|
||||
*
|
||||
* @param {number} inputNum - The inputNum of the new tab
|
||||
* @param {boolean} active - If true, sets the tab to active
|
||||
* @param {string} io - Either "input" or "output"
|
||||
* @returns {Element}
|
||||
*/
|
||||
createTabElement(inputNum, active, io) {
|
||||
const newTab = document.createElement("li");
|
||||
newTab.setAttribute("inputNum", inputNum.toString());
|
||||
|
||||
if (active) newTab.classList.add(`active-${io}-tab`);
|
||||
|
||||
const newTabContent = document.createElement("div");
|
||||
newTabContent.classList.add(`${io}-tab-content`);
|
||||
|
||||
newTabContent.innerText = `Tab ${inputNum.toString()}`;
|
||||
|
||||
newTabContent.addEventListener("wheel", this.manager[io].scrollTab.bind(this.manager[io]), {passive: false});
|
||||
|
||||
newTab.appendChild(newTabContent);
|
||||
|
||||
if (io === "input") {
|
||||
const newTabButton = document.createElement("button"),
|
||||
newTabButtonIcon = document.createElement("i");
|
||||
newTabButton.type = "button";
|
||||
newTabButton.className = "btn btn-primary bmd-btn-icon btn-close-tab";
|
||||
|
||||
newTabButtonIcon.classList.add("material-icons");
|
||||
newTabButtonIcon.innerText = "clear";
|
||||
|
||||
newTabButton.appendChild(newTabButtonIcon);
|
||||
|
||||
newTabButton.addEventListener("click", this.manager.input.removeTabClick.bind(this.manager.input));
|
||||
|
||||
newTab.appendChild(newTabButton);
|
||||
}
|
||||
|
||||
return newTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tab element for the input tab bar
|
||||
*
|
||||
* @param {number} inputNum - The inputNum of the new input tab
|
||||
* @param {boolean} [active=false] - If true, sets the tab to active
|
||||
* @returns {Element}
|
||||
*/
|
||||
createInputTabElement(inputNum, active=false) {
|
||||
return this.createTabElement(inputNum, active, "input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tab element for the output tab bar
|
||||
*
|
||||
* @param {number} inputNum - The inputNum of the new output tab
|
||||
* @param {boolean} [active=false] - If true, sets the tab to active
|
||||
* @returns {Element}
|
||||
*/
|
||||
createOutputTabElement(inputNum, active=false) {
|
||||
return this.createTabElement(inputNum, active, "output");
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the tab bar for both the input and output
|
||||
*/
|
||||
showTabBar() {
|
||||
document.getElementById("input-tabs-wrapper").style.display = "block";
|
||||
document.getElementById("output-tabs-wrapper").style.display = "block";
|
||||
|
||||
document.getElementById("input-wrapper").classList.add("show-tabs");
|
||||
document.getElementById("output-wrapper").classList.add("show-tabs");
|
||||
|
||||
document.getElementById("save-all-to-file").style.display = "inline-block";
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the tab bar for both the input and output
|
||||
*/
|
||||
hideTabBar() {
|
||||
document.getElementById("input-tabs-wrapper").style.display = "none";
|
||||
document.getElementById("output-tabs-wrapper").style.display = "none";
|
||||
|
||||
document.getElementById("input-wrapper").classList.remove("show-tabs");
|
||||
document.getElementById("output-wrapper").classList.remove("show-tabs");
|
||||
|
||||
document.getElementById("save-all-to-file").style.display = "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Redraws the tab bar with an updated list of tabs, then changes to activeTab
|
||||
*
|
||||
* @param {number[]} nums - The inputNums of the tab bar to be drawn
|
||||
* @param {number} activeTab - The inputNum of the activeTab
|
||||
* @param {boolean} tabsLeft - True if there are tabs to the left of the displayed tabs
|
||||
* @param {boolean} tabsRight - True if there are tabs to the right of the displayed tabs
|
||||
* @param {string} io - Either "input" or "output"
|
||||
*/
|
||||
refreshTabs(nums, activeTab, tabsLeft, tabsRight, io) {
|
||||
const tabsList = document.getElementById(`${io}-tabs`);
|
||||
|
||||
// Remove existing tab elements
|
||||
for (let i = tabsList.children.length - 1; i >= 0; i--) {
|
||||
tabsList.children.item(i).remove();
|
||||
}
|
||||
|
||||
// Create and add new tab elements
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
const active = (nums[i] === activeTab);
|
||||
tabsList.appendChild(this.createTabElement(nums[i], active, io));
|
||||
}
|
||||
|
||||
// Display shadows if there are tabs left / right of the displayed tabs
|
||||
if (tabsLeft) {
|
||||
tabsList.classList.add("tabs-left");
|
||||
} else {
|
||||
tabsList.classList.remove("tabs-left");
|
||||
}
|
||||
if (tabsRight) {
|
||||
tabsList.classList.add("tabs-right");
|
||||
} else {
|
||||
tabsList.classList.remove("tabs-right");
|
||||
}
|
||||
|
||||
// Show or hide the tab bar depending on how many tabs we have
|
||||
if (nums.length > 1) {
|
||||
this.showTabBar();
|
||||
} else {
|
||||
this.hideTabBar();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the input tabs, and changes to activeTab
|
||||
*
|
||||
* @param {number[]} nums - The inputNums to be displayed as tabs
|
||||
* @param {number} activeTab - The tab to change to
|
||||
* @param {boolean} tabsLeft - True if there are input tabs to the left of the displayed tabs
|
||||
* @param {boolean} tabsRight - True if there are input tabs to the right of the displayed tabs
|
||||
*/
|
||||
refreshInputTabs(nums, activeTab, tabsLeft, tabsRight) {
|
||||
this.refreshTabs(nums, activeTab, tabsLeft, tabsRight, "input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the output tabs, and changes to activeTab
|
||||
*
|
||||
* @param {number[]} nums - The inputNums to be displayed as tabs
|
||||
* @param {number} activeTab - The tab to change to
|
||||
* @param {boolean} tabsLeft - True if there are output tabs to the left of the displayed tabs
|
||||
* @param {boolean} tabsRight - True if there are output tabs to the right of the displayed tabs
|
||||
*/
|
||||
refreshOutputTabs(nums, activeTab, tabsLeft, tabsRight) {
|
||||
this.refreshTabs(nums, activeTab, tabsLeft, tabsRight, "output");
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the active tab to a different tab
|
||||
*
|
||||
* @param {number} inputNum - The inputNum of the tab to change to
|
||||
* @param {string} io - Either "input" or "output"
|
||||
* @return {boolean} - False if the tab is not currently being displayed
|
||||
*/
|
||||
changeTab(inputNum, io) {
|
||||
const tabsList = document.getElementById(`${io}-tabs`);
|
||||
|
||||
this.manager.highlighter.removeHighlights();
|
||||
getSelection().removeAllRanges();
|
||||
|
||||
let found = false;
|
||||
for (let i = 0; i < tabsList.children.length; i++) {
|
||||
const tabNum = parseInt(tabsList.children.item(i).getAttribute("inputNum"), 10);
|
||||
if (tabNum === inputNum) {
|
||||
tabsList.children.item(i).classList.add(`active-${io}-tab`);
|
||||
found = true;
|
||||
} else {
|
||||
tabsList.children.item(i).classList.remove(`active-${io}-tab`);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the active input tab to a different tab
|
||||
*
|
||||
* @param {number} inputNum
|
||||
* @returns {boolean} - False if the tab is not currently being displayed
|
||||
*/
|
||||
changeInputTab(inputNum) {
|
||||
return this.changeTab(inputNum, "input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the active output tab to a different tab
|
||||
*
|
||||
* @param {number} inputNum
|
||||
* @returns {boolean} - False if the tab is not currently being displayed
|
||||
*/
|
||||
changeOutputTab(inputNum) {
|
||||
return this.changeTab(inputNum, "output");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tab header to display a preview of the tab contents
|
||||
*
|
||||
* @param {number} inputNum - The inputNum of the tab to update the header of
|
||||
* @param {string} data - The data to display in the tab header
|
||||
* @param {string} io - Either "input" or "output"
|
||||
*/
|
||||
updateTabHeader(inputNum, data, io) {
|
||||
const tab = this.getTabItem(inputNum, io);
|
||||
if (tab === null) return;
|
||||
|
||||
let headerData = `Tab ${inputNum}`;
|
||||
if (data.length > 0) {
|
||||
headerData = data.slice(0, 100);
|
||||
headerData = `${inputNum}: ${headerData}`;
|
||||
}
|
||||
tab.firstElementChild.innerText = headerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the input tab header to display a preview of the tab contents
|
||||
*
|
||||
* @param {number} inputNum - The inputNum of the tab to update the header of
|
||||
* @param {string} data - The data to display in the tab header
|
||||
*/
|
||||
updateInputTabHeader(inputNum, data) {
|
||||
this.updateTabHeader(inputNum, data, "input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the output tab header to display a preview of the tab contents
|
||||
*
|
||||
* @param {number} inputNum - The inputNum of the tab to update the header of
|
||||
* @param {string} data - The data to display in the tab header
|
||||
*/
|
||||
updateOutputTabHeader(inputNum, data) {
|
||||
this.updateTabHeader(inputNum, data, "output");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tab background to display the progress of the current tab
|
||||
*
|
||||
* @param {number} inputNum - The inputNum of the tab
|
||||
* @param {number} progress - The current progress
|
||||
* @param {number} total - The total which the progress is a percent of
|
||||
* @param {string} io - Either "input" or "output"
|
||||
*/
|
||||
updateTabProgress(inputNum, progress, total, io) {
|
||||
const tabItem = this.getTabItem(inputNum, io);
|
||||
if (tabItem === null) return;
|
||||
|
||||
const percentComplete = (progress / total) * 100;
|
||||
if (percentComplete >= 100 || progress === false) {
|
||||
tabItem.style.background = "";
|
||||
} else {
|
||||
tabItem.style.background = `linear-gradient(to right, var(--title-background-colour) ${percentComplete}%, var(--primary-background-colour) ${percentComplete}%)`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the input tab background to display its progress
|
||||
*
|
||||
* @param {number} inputNum
|
||||
* @param {number} progress
|
||||
* @param {number} total
|
||||
*/
|
||||
updateInputTabProgress(inputNum, progress, total) {
|
||||
this.updateTabProgress(inputNum, progress, total, "input");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the output tab background to display its progress
|
||||
*
|
||||
* @param {number} inputNum
|
||||
* @param {number} progress
|
||||
* @param {number} total
|
||||
*/
|
||||
updateOutputTabProgress(inputNum, progress, total) {
|
||||
this.updateTabProgress(inputNum, progress, total, "output");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default TabWaiter;
|
|
@ -25,8 +25,7 @@ class WindowWaiter {
|
|||
* continuous resetting).
|
||||
*/
|
||||
windowResize() {
|
||||
clearTimeout(this.resetLayoutTimeout);
|
||||
this.resetLayoutTimeout = setTimeout(this.app.resetLayout.bind(this.app), 200);
|
||||
this.app.debounce(this.app.resetLayout, 200, "windowResize", this.app, [])();
|
||||
}
|
||||
|
||||
|
817
src/web/waiters/WorkerWaiter.mjs
Normal file
817
src/web/waiters/WorkerWaiter.mjs
Normal file
|
@ -0,0 +1,817 @@
|
|||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import ChefWorker from "worker-loader?inline&fallback=false!../../core/ChefWorker";
|
||||
import DishWorker from "worker-loader?inline&fallback=false!../workers/DishWorker";
|
||||
|
||||
/**
|
||||
* Waiter to handle conversations with the ChefWorker
|
||||
*/
|
||||
class WorkerWaiter {
|
||||
|
||||
/**
|
||||
* WorkerWaiter constructor
|
||||
*
|
||||
* @param {App} app - The main view object for CyberChef
|
||||
* @param {Manager} manager - The CyberChef event manager
|
||||
*/
|
||||
constructor(app, manager) {
|
||||
this.app = app;
|
||||
this.manager = manager;
|
||||
|
||||
this.loaded = false;
|
||||
this.chefWorkers = [];
|
||||
this.inputs = [];
|
||||
this.inputNums = [];
|
||||
this.totalOutputs = 0;
|
||||
this.loadingOutputs = 0;
|
||||
this.bakeId = 0;
|
||||
this.callbacks = {};
|
||||
this.callbackID = 0;
|
||||
|
||||
this.maxWorkers = 1;
|
||||
if (navigator.hardwareConcurrency !== undefined &&
|
||||
navigator.hardwareConcurrency > 1) {
|
||||
this.maxWorkers = navigator.hardwareConcurrency - 1;
|
||||
}
|
||||
|
||||
// Store dishWorker action (getDishAs or getDishTitle)
|
||||
this.dishWorker = {
|
||||
worker: null,
|
||||
currentAction: ""
|
||||
};
|
||||
this.dishWorkerQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminates any existing ChefWorkers and sets up a new worker
|
||||
*/
|
||||
setupChefWorker() {
|
||||
for (let i = this.chefWorkers.length - 1; i >= 0; i--) {
|
||||
this.removeChefWorker(this.chefWorkers[i]);
|
||||
}
|
||||
|
||||
this.addChefWorker();
|
||||
this.setupDishWorker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a DishWorker to be used for performing Dish operations
|
||||
*/
|
||||
setupDishWorker() {
|
||||
if (this.dishWorker.worker !== null) {
|
||||
this.dishWorker.worker.terminate();
|
||||
this.dishWorker.currentAction = "";
|
||||
}
|
||||
log.debug("Adding new DishWorker");
|
||||
|
||||
this.dishWorker.worker = new DishWorker();
|
||||
this.dishWorker.worker.addEventListener("message", this.handleDishMessage.bind(this));
|
||||
|
||||
if (this.dishWorkerQueue.length > 0) {
|
||||
this.postDishMessage(this.dishWorkerQueue.splice(0, 1)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new ChefWorker
|
||||
*
|
||||
* @returns {number} The index of the created worker
|
||||
*/
|
||||
addChefWorker() {
|
||||
if (this.chefWorkers.length === this.maxWorkers) {
|
||||
// Can't create any more workers
|
||||
return -1;
|
||||
}
|
||||
|
||||
log.debug("Adding new ChefWorker");
|
||||
|
||||
// Create a new ChefWorker and send it the docURL
|
||||
const newWorker = new ChefWorker();
|
||||
newWorker.addEventListener("message", this.handleChefMessage.bind(this));
|
||||
let docURL = document.location.href.split(/[#?]/)[0];
|
||||
const index = docURL.lastIndexOf("/");
|
||||
if (index > 0) {
|
||||
docURL = docURL.substring(0, index);
|
||||
}
|
||||
|
||||
newWorker.postMessage({"action": "docURL", "data": docURL});
|
||||
newWorker.postMessage({
|
||||
action: "setLogLevel",
|
||||
data: log.getLevel()
|
||||
});
|
||||
|
||||
// Store the worker, whether or not it's active, and the inputNum as an object
|
||||
const newWorkerObj = {
|
||||
worker: newWorker,
|
||||
active: false,
|
||||
inputNum: -1
|
||||
};
|
||||
|
||||
this.chefWorkers.push(newWorkerObj);
|
||||
return this.chefWorkers.indexOf(newWorkerObj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an inactive ChefWorker to be used for baking
|
||||
*
|
||||
* @param {boolean} [setActive=true] - If true, set the worker status to active
|
||||
* @returns {number} - The index of the ChefWorker
|
||||
*/
|
||||
getInactiveChefWorker(setActive=true) {
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
if (!this.chefWorkers[i].active) {
|
||||
this.chefWorkers[i].active = setActive;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a ChefWorker
|
||||
*
|
||||
* @param {Object} workerObj
|
||||
*/
|
||||
removeChefWorker(workerObj) {
|
||||
const index = this.chefWorkers.indexOf(workerObj);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.chefWorkers.length > 1 || this.chefWorkers[index].active) {
|
||||
log.debug(`Removing ChefWorker at index ${index}`);
|
||||
this.chefWorkers[index].worker.terminate();
|
||||
this.chefWorkers.splice(index, 1);
|
||||
}
|
||||
|
||||
// There should always be a ChefWorker loaded
|
||||
if (this.chefWorkers.length === 0) {
|
||||
this.addChefWorker();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the object for the ChefWorker of a given inputNum
|
||||
*
|
||||
* @param {number} inputNum
|
||||
*/
|
||||
getChefWorker(inputNum) {
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
if (this.chefWorkers[i].inputNum === inputNum) {
|
||||
return this.chefWorkers[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for messages sent back by the ChefWorkers
|
||||
*
|
||||
* @param {MessageEvent} e
|
||||
*/
|
||||
handleChefMessage(e) {
|
||||
const r = e.data;
|
||||
let inputNum = 0;
|
||||
log.debug(`Receiving ${r.action} from ChefWorker.`);
|
||||
|
||||
if (r.data.hasOwnProperty("inputNum")) {
|
||||
inputNum = r.data.inputNum;
|
||||
}
|
||||
|
||||
const currentWorker = this.getChefWorker(inputNum);
|
||||
|
||||
switch (r.action) {
|
||||
case "bakeComplete":
|
||||
log.debug(`Bake ${inputNum} complete.`);
|
||||
|
||||
if (r.data.error) {
|
||||
this.app.handleError(r.data.error);
|
||||
this.manager.output.updateOutputError(r.data.error, inputNum, r.data.progress);
|
||||
} else {
|
||||
this.updateOutput(r.data, r.data.inputNum, r.data.bakeId, r.data.progress);
|
||||
}
|
||||
|
||||
this.app.progress = r.data.progress;
|
||||
|
||||
if (r.data.progress === this.recipeConfig.length) {
|
||||
this.step = false;
|
||||
}
|
||||
|
||||
this.workerFinished(currentWorker);
|
||||
break;
|
||||
case "bakeError":
|
||||
this.app.handleError(r.data.error);
|
||||
this.manager.output.updateOutputError(r.data.error, inputNum, r.data.progress);
|
||||
this.app.progress = r.data.progress;
|
||||
this.workerFinished(currentWorker);
|
||||
break;
|
||||
case "dishReturned":
|
||||
this.callbacks[r.data.id](r.data);
|
||||
break;
|
||||
case "silentBakeComplete":
|
||||
break;
|
||||
case "workerLoaded":
|
||||
this.app.workerLoaded = true;
|
||||
log.debug("ChefWorker loaded.");
|
||||
if (!this.loaded) {
|
||||
this.app.loaded();
|
||||
this.loaded = true;
|
||||
} else {
|
||||
this.bakeNextInput(this.getInactiveChefWorker(false));
|
||||
}
|
||||
break;
|
||||
case "statusMessage":
|
||||
this.manager.output.updateOutputMessage(r.data.message, r.data.inputNum, true);
|
||||
break;
|
||||
case "progressMessage":
|
||||
this.manager.output.updateOutputProgress(r.data.progress, r.data.total, r.data.inputNum);
|
||||
break;
|
||||
case "optionUpdate":
|
||||
log.debug(`Setting ${r.data.option} to ${r.data.value}`);
|
||||
this.app.options[r.data.option] = r.data.value;
|
||||
break;
|
||||
case "setRegisters":
|
||||
this.manager.recipe.setRegisters(r.data.opIndex, r.data.numPrevRegisters, r.data.registers);
|
||||
break;
|
||||
case "highlightsCalculated":
|
||||
this.manager.highlighter.displayHighlights(r.data.pos, r.data.direction);
|
||||
break;
|
||||
default:
|
||||
log.error("Unrecognised message from ChefWorker", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the value of an output
|
||||
*
|
||||
* @param {Object} data
|
||||
* @param {number} inputNum
|
||||
* @param {number} bakeId
|
||||
* @param {number} progress
|
||||
*/
|
||||
updateOutput(data, inputNum, bakeId, progress) {
|
||||
this.manager.output.updateOutputBakeId(bakeId, inputNum);
|
||||
if (progress === this.recipeConfig.length) {
|
||||
progress = false;
|
||||
}
|
||||
this.manager.output.updateOutputProgress(progress, this.recipeConfig.length, inputNum);
|
||||
this.manager.output.updateOutputValue(data, inputNum, false);
|
||||
|
||||
if (progress !== false) {
|
||||
this.manager.output.updateOutputStatus("error", inputNum);
|
||||
|
||||
if (inputNum === this.manager.tabs.getActiveInputTab()) {
|
||||
this.manager.recipe.updateBreakpointIndicator(progress);
|
||||
}
|
||||
|
||||
} else {
|
||||
this.manager.output.updateOutputStatus("baked", inputNum);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI to show if baking is in progress or not.
|
||||
*
|
||||
* @param {boolean} bakingStatus
|
||||
*/
|
||||
setBakingStatus(bakingStatus) {
|
||||
this.app.baking = bakingStatus;
|
||||
this.app.debounce(this.manager.controls.toggleBakeButtonFunction, 20, "toggleBakeButton", this, [bakingStatus ? "cancel" : "bake"])();
|
||||
|
||||
if (bakingStatus) this.manager.output.hideMagicButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the progress of the ChefWorkers
|
||||
*/
|
||||
getBakeProgress() {
|
||||
const pendingInputs = this.inputNums.length + this.loadingOutputs + this.inputs.length;
|
||||
let bakingInputs = 0;
|
||||
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
if (this.chefWorkers[i].active) {
|
||||
bakingInputs++;
|
||||
}
|
||||
}
|
||||
|
||||
const total = this.totalOutputs;
|
||||
const bakedInputs = total - pendingInputs - bakingInputs;
|
||||
|
||||
return {
|
||||
total: total,
|
||||
pending: pendingInputs,
|
||||
baking: bakingInputs,
|
||||
baked: bakedInputs
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the current bake by terminating and removing all ChefWorkers
|
||||
*
|
||||
* @param {boolean} [silent=false] - If true, don't set the output
|
||||
* @param {boolean} killAll - If true, kills all chefWorkers regardless of status
|
||||
*/
|
||||
cancelBake(silent, killAll) {
|
||||
for (let i = this.chefWorkers.length - 1; i >= 0; i--) {
|
||||
if (this.chefWorkers[i].active || killAll) {
|
||||
const inputNum = this.chefWorkers[i].inputNum;
|
||||
this.removeChefWorker(this.chefWorkers[i]);
|
||||
this.manager.output.updateOutputStatus("inactive", inputNum);
|
||||
}
|
||||
}
|
||||
this.setBakingStatus(false);
|
||||
|
||||
for (let i = 0; i < this.inputs.length; i++) {
|
||||
this.manager.output.updateOutputStatus("inactive", this.inputs[i].inputNum);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.inputNums.length; i++) {
|
||||
this.manager.output.updateOutputStatus("inactive", this.inputNums[i]);
|
||||
}
|
||||
|
||||
const tabList = this.manager.tabs.getOutputTabList();
|
||||
for (let i = 0; i < tabList.length; i++) {
|
||||
this.manager.tabs.getOutputTabItem(tabList[i]).style.background = "";
|
||||
}
|
||||
|
||||
this.inputs = [];
|
||||
this.inputNums = [];
|
||||
this.totalOutputs = 0;
|
||||
this.loadingOutputs = 0;
|
||||
if (!silent) this.manager.output.set(this.manager.tabs.getActiveOutputTab());
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a worker completing baking
|
||||
*
|
||||
* @param {object} workerObj - Object containing the worker information
|
||||
* @param {ChefWorker} workerObj.worker - The actual worker object
|
||||
* @param {number} workerObj.inputNum - The inputNum of the input being baked by the worker
|
||||
* @param {boolean} workerObj.active - If true, the worker is currrently baking an input
|
||||
*/
|
||||
workerFinished(workerObj) {
|
||||
const workerIdx = this.chefWorkers.indexOf(workerObj);
|
||||
this.chefWorkers[workerIdx].active = false;
|
||||
if (this.inputs.length > 0) {
|
||||
this.bakeNextInput(workerIdx);
|
||||
} else if (this.inputNums.length === 0 && this.loadingOutputs === 0) {
|
||||
// The ChefWorker is no longer needed
|
||||
log.debug("No more inputs to bake.");
|
||||
const progress = this.getBakeProgress();
|
||||
if (progress.total === progress.baked) {
|
||||
this.bakingComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for completed bakes
|
||||
*/
|
||||
bakingComplete() {
|
||||
this.setBakingStatus(false);
|
||||
let duration = new Date().getTime() - this.bakeStartTime;
|
||||
duration = duration.toLocaleString() + "ms";
|
||||
const progress = this.getBakeProgress();
|
||||
|
||||
if (progress.total > 1) {
|
||||
let width = progress.total.toLocaleString().length;
|
||||
if (duration.length > width) {
|
||||
width = duration.length;
|
||||
}
|
||||
width = width < 2 ? 2 : width;
|
||||
|
||||
const totalStr = progress.total.toLocaleString().padStart(width, " ").replace(/ /g, " ");
|
||||
const durationStr = duration.padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
const inputNums = Object.keys(this.manager.output.outputs);
|
||||
let avgTime = 0,
|
||||
numOutputs = 0;
|
||||
for (let i = 0; i < inputNums.length; i++) {
|
||||
const output = this.manager.output.outputs[inputNums[i]];
|
||||
if (output.status === "baked") {
|
||||
numOutputs++;
|
||||
avgTime += output.data.duration;
|
||||
}
|
||||
}
|
||||
avgTime = Math.round(avgTime / numOutputs).toLocaleString() + "ms";
|
||||
avgTime = avgTime.padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
const msg = `total: ${totalStr}<br>time: ${durationStr}<br>average: ${avgTime}`;
|
||||
|
||||
const bakeInfo = document.getElementById("bake-info");
|
||||
bakeInfo.innerHTML = msg;
|
||||
bakeInfo.style.display = "";
|
||||
} else {
|
||||
document.getElementById("bake-info").style.display = "none";
|
||||
}
|
||||
|
||||
document.getElementById("bake").style.background = "";
|
||||
this.totalOutputs = 0; // Reset for next time
|
||||
log.debug("--- Bake complete ---");
|
||||
}
|
||||
|
||||
/**
|
||||
* Bakes the next input and tells the inputWorker to load the next input
|
||||
*
|
||||
* @param {number} workerIdx - The index of the worker to bake with
|
||||
*/
|
||||
bakeNextInput(workerIdx) {
|
||||
if (this.inputs.length === 0) return;
|
||||
if (workerIdx === -1) return;
|
||||
if (!this.chefWorkers[workerIdx]) return;
|
||||
this.chefWorkers[workerIdx].active = true;
|
||||
const nextInput = this.inputs.splice(0, 1)[0];
|
||||
if (typeof nextInput.inputNum === "string") nextInput.inputNum = parseInt(nextInput.inputNum, 10);
|
||||
|
||||
log.debug(`Baking input ${nextInput.inputNum}.`);
|
||||
this.manager.output.updateOutputMessage(`Baking input ${nextInput.inputNum}...`, nextInput.inputNum, false);
|
||||
this.manager.output.updateOutputStatus("baking", nextInput.inputNum);
|
||||
|
||||
this.chefWorkers[workerIdx].inputNum = nextInput.inputNum;
|
||||
const input = nextInput.input,
|
||||
recipeConfig = this.recipeConfig;
|
||||
|
||||
if (this.step) {
|
||||
// Remove all breakpoints from the recipe up to progress
|
||||
if (nextInput.progress !== false) {
|
||||
for (let i = 0; i < nextInput.progress; i++) {
|
||||
if (recipeConfig[i].hasOwnProperty("breakpoint")) {
|
||||
delete recipeConfig[i].breakpoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set a breakpoint at the next operation so we stop baking there
|
||||
if (recipeConfig[this.app.progress]) recipeConfig[this.app.progress].breakpoint = true;
|
||||
}
|
||||
|
||||
let transferable;
|
||||
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
|
||||
transferable = [input];
|
||||
}
|
||||
this.chefWorkers[workerIdx].worker.postMessage({
|
||||
action: "bake",
|
||||
data: {
|
||||
input: input,
|
||||
recipeConfig: recipeConfig,
|
||||
options: this.options,
|
||||
inputNum: nextInput.inputNum,
|
||||
bakeId: this.bakeId
|
||||
}
|
||||
}, transferable);
|
||||
|
||||
if (this.inputNums.length > 0) {
|
||||
this.manager.input.inputWorker.postMessage({
|
||||
action: "bakeNext",
|
||||
data: {
|
||||
inputNum: this.inputNums.splice(0, 1)[0],
|
||||
bakeId: this.bakeId
|
||||
}
|
||||
});
|
||||
this.loadingOutputs++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bakes the current input using the current recipe.
|
||||
*
|
||||
* @param {Object[]} recipeConfig
|
||||
* @param {Object} options
|
||||
* @param {number} progress
|
||||
* @param {boolean} step
|
||||
*/
|
||||
bake(recipeConfig, options, progress, step) {
|
||||
this.setBakingStatus(true);
|
||||
this.manager.recipe.updateBreakpointIndicator(false);
|
||||
this.bakeStartTime = new Date().getTime();
|
||||
this.bakeId++;
|
||||
this.recipeConfig = recipeConfig;
|
||||
this.options = options;
|
||||
this.progress = progress;
|
||||
this.step = step;
|
||||
|
||||
this.displayProgress();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues an input ready to be baked
|
||||
*
|
||||
* @param {object} inputData
|
||||
* @param {string | ArrayBuffer} inputData.input
|
||||
* @param {number} inputData.inputNum
|
||||
* @param {number} inputData.bakeId
|
||||
*/
|
||||
queueInput(inputData) {
|
||||
this.loadingOutputs--;
|
||||
if (this.app.baking && inputData.bakeId === this.bakeId) {
|
||||
this.inputs.push(inputData);
|
||||
this.bakeNextInput(this.getInactiveChefWorker(true));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles if an error is thrown by QueueInput
|
||||
*
|
||||
* @param {object} inputData
|
||||
* @param {number} inputData.inputNum
|
||||
* @param {number} inputData.bakeId
|
||||
*/
|
||||
queueInputError(inputData) {
|
||||
this.loadingOutputs--;
|
||||
if (this.app.baking && inputData.bakeId === this.bakeId) {
|
||||
this.manager.output.updateOutputError("Error queueing the input for a bake.", inputData.inputNum, 0);
|
||||
|
||||
if (this.inputNums.length === 0) return;
|
||||
|
||||
// Load the next input
|
||||
this.manager.input.inputWorker.postMessage({
|
||||
action: "bakeNext",
|
||||
data: {
|
||||
inputNum: this.inputNums.splice(0, 1)[0],
|
||||
bakeId: this.bakeId
|
||||
}
|
||||
});
|
||||
this.loadingOutputs++;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a list of inputNums to be baked by ChefWorkers, and begins baking
|
||||
*
|
||||
* @param {object} inputData
|
||||
* @param {number[]} inputData.nums - The inputNums to be queued for baking
|
||||
* @param {boolean} inputData.step - If true, only execute the next operation in the recipe
|
||||
* @param {number} inputData.progress - The current progress through the recipe. Used when stepping
|
||||
*/
|
||||
async bakeAllInputs(inputData) {
|
||||
return await new Promise(resolve => {
|
||||
if (this.app.baking) return;
|
||||
const inputNums = inputData.nums;
|
||||
const step = inputData.step;
|
||||
|
||||
// Use cancelBake to clear out the inputs
|
||||
this.cancelBake(true, false);
|
||||
|
||||
this.inputNums = inputNums;
|
||||
this.totalOutputs = inputNums.length;
|
||||
this.app.progress = inputData.progress;
|
||||
|
||||
let inactiveWorkers = 0;
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
if (!this.chefWorkers[i].active) {
|
||||
inactiveWorkers++;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < inputNums.length - inactiveWorkers; i++) {
|
||||
if (this.addChefWorker() === -1) break;
|
||||
}
|
||||
|
||||
this.app.bake(step);
|
||||
|
||||
for (let i = 0; i < this.inputNums.length; i++) {
|
||||
this.manager.output.updateOutputMessage(`Input ${inputNums[i]} has not been baked yet.`, inputNums[i], false);
|
||||
this.manager.output.updateOutputStatus("pending", inputNums[i]);
|
||||
}
|
||||
|
||||
let numBakes = this.chefWorkers.length;
|
||||
if (this.inputNums.length < numBakes) {
|
||||
numBakes = this.inputNums.length;
|
||||
}
|
||||
for (let i = 0; i < numBakes; i++) {
|
||||
this.manager.input.inputWorker.postMessage({
|
||||
action: "bakeNext",
|
||||
data: {
|
||||
inputNum: this.inputNums.splice(0, 1)[0],
|
||||
bakeId: this.bakeId
|
||||
}
|
||||
});
|
||||
this.loadingOutputs++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the ChefWorker to run a silent bake, forcing the browser to load and cache all the relevant
|
||||
* JavaScript code needed to do a real bake.
|
||||
*
|
||||
* @param {Object[]} [recipeConfig]
|
||||
*/
|
||||
silentBake(recipeConfig) {
|
||||
// If there aren't any active ChefWorkers, try to add one
|
||||
let workerId = this.getInactiveChefWorker();
|
||||
if (workerId === -1) {
|
||||
workerId = this.addChefWorker();
|
||||
}
|
||||
if (workerId === -1) return;
|
||||
this.chefWorkers[workerId].worker.postMessage({
|
||||
action: "silentBake",
|
||||
data: {
|
||||
recipeConfig: recipeConfig
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for messages sent back from DishWorker
|
||||
*
|
||||
* @param {MessageEvent} e
|
||||
*/
|
||||
handleDishMessage(e) {
|
||||
const r = e.data;
|
||||
log.debug(`Receiving ${r.action} from DishWorker`);
|
||||
|
||||
switch (r.action) {
|
||||
case "dishReturned":
|
||||
this.dishWorker.currentAction = "";
|
||||
this.callbacks[r.data.id](r.data);
|
||||
|
||||
if (this.dishWorkerQueue.length > 0) {
|
||||
this.postDishMessage(this.dishWorkerQueue.splice(0, 1)[0]);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
log.error("Unrecognised message from DishWorker", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the ChefWorker to return the dish as the specified type
|
||||
*
|
||||
* @param {Dish} dish
|
||||
* @param {string} type
|
||||
* @param {Function} callback
|
||||
*/
|
||||
getDishAs(dish, type, callback) {
|
||||
const id = this.callbackID++;
|
||||
|
||||
this.callbacks[id] = callback;
|
||||
|
||||
if (this.dishWorker.worker === null) this.setupDishWorker();
|
||||
this.postDishMessage({
|
||||
action: "getDishAs",
|
||||
data: {
|
||||
dish: dish,
|
||||
type: type,
|
||||
id: id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the ChefWorker to get the title of the dish
|
||||
*
|
||||
* @param {Dish} dish
|
||||
* @param {number} maxLength
|
||||
* @param {Function} callback
|
||||
* @returns {string}
|
||||
*/
|
||||
getDishTitle(dish, maxLength, callback) {
|
||||
const id = this.callbackID++;
|
||||
|
||||
this.callbacks[id] = callback;
|
||||
|
||||
if (this.dishWorker.worker === null) this.setupDishWorker();
|
||||
|
||||
this.postDishMessage({
|
||||
action: "getDishTitle",
|
||||
data: {
|
||||
dish: dish,
|
||||
maxLength: maxLength,
|
||||
id: id
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a message to be sent to the dishWorker
|
||||
*
|
||||
* @param {object} message
|
||||
* @param {string} message.action
|
||||
* @param {object} message.data
|
||||
* @param {Dish} message.data.dish
|
||||
* @param {number} message.data.id
|
||||
*/
|
||||
queueDishMessage(message) {
|
||||
if (message.action === "getDishAs") {
|
||||
this.dishWorkerQueue = [message].concat(this.dishWorkerQueue);
|
||||
} else {
|
||||
this.dishWorkerQueue.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a message to the DishWorker
|
||||
*
|
||||
* @param {object} message
|
||||
* @param {string} message.action
|
||||
* @param {object} message.data
|
||||
*/
|
||||
postDishMessage(message) {
|
||||
if (this.dishWorker.currentAction !== "") {
|
||||
this.queueDishMessage(message);
|
||||
} else {
|
||||
this.dishWorker.currentAction = message.action;
|
||||
this.dishWorker.worker.postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the console log level in the workers.
|
||||
*/
|
||||
setLogLevel() {
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
this.chefWorkers[i].worker.postMessage({
|
||||
action: "setLogLevel",
|
||||
data: log.getLevel()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the bake progress in the output bar and bake button
|
||||
*/
|
||||
displayProgress() {
|
||||
const progress = this.getBakeProgress();
|
||||
if (progress.total === progress.baked) return;
|
||||
|
||||
const percentComplete = ((progress.pending + progress.baking) / progress.total) * 100;
|
||||
const bakeButton = document.getElementById("bake");
|
||||
if (this.app.baking) {
|
||||
if (percentComplete < 100) {
|
||||
bakeButton.style.background = `linear-gradient(to left, #fea79a ${percentComplete}%, #f44336 ${percentComplete}%)`;
|
||||
} else {
|
||||
bakeButton.style.background = "";
|
||||
}
|
||||
} else {
|
||||
// not baking
|
||||
bakeButton.style.background = "";
|
||||
}
|
||||
|
||||
const bakeInfo = document.getElementById("bake-info");
|
||||
if (progress.total > 1) {
|
||||
let width = progress.total.toLocaleString().length;
|
||||
width = width < 2 ? 2 : width;
|
||||
|
||||
const totalStr = progress.total.toLocaleString().padStart(width, " ").replace(/ /g, " ");
|
||||
const bakedStr = progress.baked.toLocaleString().padStart(width, " ").replace(/ /g, " ");
|
||||
const pendingStr = progress.pending.toLocaleString().padStart(width, " ").replace(/ /g, " ");
|
||||
const bakingStr = progress.baking.toLocaleString().padStart(width, " ").replace(/ /g, " ");
|
||||
|
||||
let msg = "total: " + totalStr;
|
||||
msg += "<br>baked: " + bakedStr;
|
||||
|
||||
if (progress.pending > 0) {
|
||||
msg += "<br>pending: " + pendingStr;
|
||||
} else if (progress.baking > 0) {
|
||||
msg += "<br>baking: " + bakingStr;
|
||||
}
|
||||
bakeInfo.innerHTML = msg;
|
||||
bakeInfo.style.display = "";
|
||||
} else {
|
||||
bakeInfo.style.display = "none";
|
||||
}
|
||||
|
||||
if (progress.total !== progress.baked) {
|
||||
setTimeout(function() {
|
||||
this.displayProgress();
|
||||
}.bind(this), 100);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the ChefWorker to calculate highlight offsets if possible.
|
||||
*
|
||||
* @param {Object[]} recipeConfig
|
||||
* @param {string} direction
|
||||
* @param {Object} pos - The position object for the highlight.
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
*/
|
||||
highlight(recipeConfig, direction, pos) {
|
||||
let workerIdx = this.getInactiveChefWorker(false);
|
||||
if (workerIdx === -1) {
|
||||
workerIdx = this.addChefWorker();
|
||||
}
|
||||
if (workerIdx === -1) return;
|
||||
this.chefWorkers[workerIdx].worker.postMessage({
|
||||
action: "highlight",
|
||||
data: {
|
||||
recipeConfig: recipeConfig,
|
||||
direction: direction,
|
||||
pos: pos
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default WorkerWaiter;
|
69
src/web/workers/DishWorker.mjs
Normal file
69
src/web/workers/DishWorker.mjs
Normal file
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Web worker to handle dish conversion operations.
|
||||
*
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Dish from "../../core/Dish";
|
||||
|
||||
self.addEventListener("message", function(e) {
|
||||
// Handle message from the main thread
|
||||
const r = e.data;
|
||||
log.debug(`DishWorker receiving command '${r.action}'`);
|
||||
|
||||
switch (r.action) {
|
||||
case "getDishAs":
|
||||
getDishAs(r.data);
|
||||
break;
|
||||
case "getDishTitle":
|
||||
getDishTitle(r.data);
|
||||
break;
|
||||
default:
|
||||
log.error(`DishWorker sent invalid action: '${r.action}'`);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Translates the dish to a given type
|
||||
*
|
||||
* @param {object} data
|
||||
* @param {Dish} data.dish
|
||||
* @param {string} data.type
|
||||
* @param {number} data.id
|
||||
*/
|
||||
async function getDishAs(data) {
|
||||
const newDish = new Dish(data.dish),
|
||||
value = await newDish.get(data.type),
|
||||
transferable = (data.type === "ArrayBuffer") ? [value] : undefined;
|
||||
|
||||
self.postMessage({
|
||||
action: "dishReturned",
|
||||
data: {
|
||||
value: value,
|
||||
id: data.id
|
||||
}
|
||||
}, transferable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the title of the given dish
|
||||
*
|
||||
* @param {object} data
|
||||
* @param {Dish} data.dish
|
||||
* @param {number} data.id
|
||||
* @param {number} data.maxLength
|
||||
*/
|
||||
async function getDishTitle(data) {
|
||||
const newDish = new Dish(data.dish),
|
||||
title = await newDish.getTitle(data.maxLength);
|
||||
|
||||
self.postMessage({
|
||||
action: "dishReturned",
|
||||
data: {
|
||||
value: title,
|
||||
id: data.id
|
||||
}
|
||||
});
|
||||
}
|
1057
src/web/workers/InputWorker.mjs
Normal file
1057
src/web/workers/InputWorker.mjs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -6,14 +6,32 @@
|
|||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
self.id = null;
|
||||
|
||||
|
||||
self.handleMessage = function(e) {
|
||||
const r = e.data;
|
||||
log.debug(`LoaderWorker receiving command '${r.action}'`);
|
||||
|
||||
switch (r.action) {
|
||||
case "loadInput":
|
||||
self.loadFile(r.data.file, r.data.inputNum);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Respond to message from parent thread.
|
||||
*/
|
||||
self.addEventListener("message", function(e) {
|
||||
const r = e.data;
|
||||
if (r.hasOwnProperty("file")) {
|
||||
self.loadFile(r.file);
|
||||
if (r.hasOwnProperty("file") && (r.hasOwnProperty("inputNum"))) {
|
||||
self.loadFile(r.file, r.inputNum);
|
||||
} else if (r.hasOwnProperty("file")) {
|
||||
self.loadFile(r.file, "");
|
||||
} else if (r.hasOwnProperty("id")) {
|
||||
self.id = r.id;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -22,20 +40,24 @@ self.addEventListener("message", function(e) {
|
|||
* Loads a file object into an ArrayBuffer, then transfers it back to the parent thread.
|
||||
*
|
||||
* @param {File} file
|
||||
* @param {string} inputNum
|
||||
*/
|
||||
self.loadFile = function(file) {
|
||||
self.loadFile = function(file, inputNum) {
|
||||
const reader = new FileReader();
|
||||
if (file.size >= 256*256*256*128) {
|
||||
self.postMessage({"error": "File size too large.", "inputNum": inputNum, "id": self.id});
|
||||
return;
|
||||
}
|
||||
const data = new Uint8Array(file.size);
|
||||
let offset = 0;
|
||||
const CHUNK_SIZE = 10485760; // 10MiB
|
||||
|
||||
const seek = function() {
|
||||
if (offset >= file.size) {
|
||||
self.postMessage({"progress": 100});
|
||||
self.postMessage({"fileBuffer": data.buffer}, [data.buffer]);
|
||||
self.postMessage({"fileBuffer": data.buffer, "inputNum": inputNum, "id": self.id}, [data.buffer]);
|
||||
return;
|
||||
}
|
||||
self.postMessage({"progress": Math.round(offset / file.size * 100)});
|
||||
self.postMessage({"progress": Math.round(offset / file.size * 100), "inputNum": inputNum});
|
||||
const slice = file.slice(offset, offset + CHUNK_SIZE);
|
||||
reader.readAsArrayBuffer(slice);
|
||||
};
|
||||
|
@ -47,7 +69,7 @@ self.loadFile = function(file) {
|
|||
};
|
||||
|
||||
reader.onerror = function(e) {
|
||||
self.postMessage({"error": reader.error.message});
|
||||
self.postMessage({"error": reader.error.message, "inputNum": inputNum, "id": self.id});
|
||||
};
|
||||
|
||||
seek();
|
73
src/web/workers/ZipWorker.mjs
Normal file
73
src/web/workers/ZipWorker.mjs
Normal file
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Web Worker to handle zipping the outputs for download.
|
||||
*
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import zip from "zlibjs/bin/zip.min";
|
||||
import Utils from "../../core/Utils";
|
||||
import Dish from "../../core/Dish";
|
||||
import {detectFileType} from "../../core/lib/FileType";
|
||||
|
||||
const Zlib = zip.Zlib;
|
||||
|
||||
/**
|
||||
* Respond to message from parent thread.
|
||||
*/
|
||||
self.addEventListener("message", function(e) {
|
||||
const r = e.data;
|
||||
if (!r.hasOwnProperty("outputs")) {
|
||||
log.error("No files were passed to the ZipWorker.");
|
||||
return;
|
||||
}
|
||||
if (!r.hasOwnProperty("filename")) {
|
||||
log.error("No filename was passed to the ZipWorker");
|
||||
return;
|
||||
}
|
||||
|
||||
self.zipFiles(r.outputs, r.filename, r.fileExtension);
|
||||
});
|
||||
|
||||
self.setOption = function(...args) {};
|
||||
|
||||
/**
|
||||
* Compress the files into a zip file and send the zip back
|
||||
* to the OutputWaiter.
|
||||
*
|
||||
* @param {object} outputs
|
||||
* @param {string} filename
|
||||
* @param {string} fileExtension
|
||||
*/
|
||||
self.zipFiles = async function(outputs, filename, fileExtension) {
|
||||
const zip = new Zlib.Zip();
|
||||
const inputNums = Object.keys(outputs);
|
||||
|
||||
for (let i = 0; i < inputNums.length; i++) {
|
||||
const iNum = inputNums[i];
|
||||
let ext = fileExtension;
|
||||
|
||||
const cloned = new Dish(outputs[iNum].data.dish);
|
||||
const output = new Uint8Array(await cloned.get(Dish.ARRAY_BUFFER));
|
||||
|
||||
if (fileExtension === undefined || fileExtension === "") {
|
||||
// Detect automatically
|
||||
const types = detectFileType(output);
|
||||
if (!types.length) {
|
||||
ext = ".dat";
|
||||
} else {
|
||||
ext = `.${types[0].extension.split(",", 1)[0]}`;
|
||||
}
|
||||
}
|
||||
const name = Utils.strToByteArray(iNum + ext);
|
||||
|
||||
zip.addFile(output, {filename: name});
|
||||
}
|
||||
|
||||
const zippedFile = zip.compress();
|
||||
self.postMessage({
|
||||
zippedFile: zippedFile.buffer,
|
||||
filename: filename
|
||||
}, [zippedFile.buffer]);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue