Add carriage return detection for pasted and switched inputs.

Fix switching the output to input not working properly.
Add nicer confirmation boxes for zipping outputs.
This commit is contained in:
j433866 2019-08-22 11:53:41 +01:00
parent 9f2d1453ed
commit f43a868607
6 changed files with 399 additions and 246 deletions

View file

@ -222,8 +222,6 @@ class InputWaiter {
if (Object.prototype.hasOwnProperty.call(r, "progress") &&
Object.prototype.hasOwnProperty.call(r, "inputNum")) {
this.manager.tabs.updateInputTabProgress(r.inputNum, r.progress, 100);
} else if (Object.prototype.hasOwnProperty.call(r, "fileBuffer")) {
this.manager.tabs.updateInputTabProgress(r.inputNum, 100, 100);
}
const transferable = Object.prototype.hasOwnProperty.call(r, "fileBuffer") ? [r.fileBuffer] : undefined;
@ -305,6 +303,9 @@ class InputWaiter {
case "removeChefWorker":
this.removeChefWorker();
break;
case "fileLoaded":
this.fileLoaded(r.data.inputNum);
break;
default:
log.error(`Unknown action ${r.action}.`);
}
@ -331,7 +332,7 @@ class InputWaiter {
* @param {number} inputData.size - The size in bytes of the input file
* @param {string} inputData.type - The MIME type of the input file
* @param {number} inputData.progress - The load progress of the input file
* @param {boolean} [silent=false] - If true, fires the manager statechange event
* @param {boolean} [silent=false] - If false, fires the manager statechange event
*/
async set(inputData, silent=false) {
return new Promise(function(resolve, reject) {
@ -373,7 +374,7 @@ class InputWaiter {
if (!silent) window.dispatchEvent(this.manager.statechange);
} else {
this.setFile(inputData);
this.setFile(inputData, silent);
}
}.bind(this));
@ -389,8 +390,9 @@ class InputWaiter {
* @param {number} inputData.size - The size in bytes of the input file
* @param {string} inputData.type - The MIME type of the input file
* @param {number} inputData.progress - The load progress of the input file
* @param {boolean} [silent=true] - If false, fires the manager statechange event
*/
setFile(inputData) {
setFile(inputData, silent=true) {
const activeTab = this.manager.tabs.getActiveInputTab();
if (inputData.inputNum !== activeTab) return;
@ -414,6 +416,30 @@ class InputWaiter {
this.setInputInfo(inputData.size, null);
this.displayFilePreview(inputData);
if (!silent) window.dispatchEvent(this.manager.statechange);
}
/**
* Update file details when a file completes loading
*
* @param {number} inputNum - The inputNum of the input which has finished loading
*/
fileLoaded(inputNum) {
this.manager.tabs.updateInputTabProgress(inputNum, 100, 100);
const activeTab = this.manager.tabs.getActiveInputTab();
if (activeTab !== inputNum) return;
this.inputWorker.postMessage({
action: "setInput",
data: {
inputNum: inputNum,
silent: false
}
});
this.updateFileProgress(inputNum, 100);
}
/**
@ -495,19 +521,6 @@ class InputWaiter {
fileLoaded.textContent = progress + "%";
fileLoaded.style.color = "";
}
if (progress === 100 && progress !== oldProgress) {
// Don't set the input if the progress hasn't changed
this.inputWorker.postMessage({
action: "setInput",
data: {
inputNum: inputNum,
silent: false
}
});
window.dispatchEvent(this.manager.statechange);
}
}
/**
@ -711,13 +724,17 @@ class InputWaiter {
*
* @param {event} e
*/
inputPaste(e) {
async inputPaste(e) {
e.preventDefault();
e.stopPropagation();
const pastedData = e.clipboardData.getData("Text");
if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024)) {
const preserve = await this.preserveCarriageReturns(pastedData);
if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024) && !preserve) {
// Pasting normally fires the inputChange() event before
// changing the value, so instead change it here ourselves
// and manually fire inputChange()
e.preventDefault();
const inputText = document.getElementById("input-text");
const selStart = inputText.selectionStart;
const selEnd = inputText.selectionEnd;
@ -728,9 +745,6 @@ class InputWaiter {
inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length);
this.debounceInputChange(e);
} else {
e.preventDefault();
e.stopPropagation();
const file = new File([pastedData], "PastedData", {
type: "text/plain",
lastModified: Date.now()
@ -815,6 +829,45 @@ class InputWaiter {
}
}
/**
* Checks if an input contains carriage returns.
* If a CR is detected, checks if the preserve CR option has been set,
* and if not, asks the user for their preference.
*
* @param {string} input - The input to be checked
* @returns {boolean} - If true, the input contains a CR which should be
* preserved, so display an overlay so it can't be edited
*/
async preserveCarriageReturns(input) {
if (input.indexOf("\r") >= 0) {
const optionsStr = "This behaviour can be changed in the <a href='#' onclick='document.getElementById(\"options\").click()'>options</a>";
if (!this.app.options.userSetCR) {
let preserve = await new Promise(function(resolve, reject) {
this.app.confirm(
"Carriage Return Detected",
"A carriage return was detected in your input. As HTML textareas can't display carriage returns, editing must be turned off to preserve them. <br>Alternatively, you can enable editing but your carriage returns will not be preserved.<br><br>This preference will be saved, and can be toggled in the options.",
"Preserve Carriage Returns",
"Enable Editing", resolve, this);
}.bind(this));
if (preserve === undefined) {
this.app.alert(`Not preserving carriage returns. ${optionsStr}`, 4000);
preserve = false;
}
this.manager.options.updateOption("preserveCR", preserve);
this.manager.options.updateOption("userSetCR", true);
} else {
if (this.app.options.preserveCR) {
this.app.alert(`Carriage return(s) detected in input, so editing has been disabled to preserve them. ${optionsStr}`, 6000);
} else {
this.app.alert(`Carriage return(s) detected in input. Editing is remaining enabled, but any carriage returns will be removed. ${optionsStr}`, 6000);
}
}
return this.app.options.preserveCR;
} else {
return false;
}
}
/**
* Load files from the UI into the inputWorker
*

View file

@ -1,174 +1,180 @@
/**
* Waiter to handle events related to the CyberChef options.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {App} app - The main view object for CyberChef.
*/
const OptionsWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
};
/**
* Loads options and sets values of switches and inputs to match them.
*
* @param {Object} options
* Waiter to handle events related to the CyberChef options.
*/
OptionsWaiter.prototype.load = function(options) {
for (const option in options) {
this.app.options[option] = options[option];
class OptionsWaiter {
/**
* OptionsWaiter 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;
}
// Set options to match object
const cboxes = document.querySelectorAll("#options-body input[type=checkbox]");
let i;
for (i = 0; i < cboxes.length; i++) {
cboxes[i].checked = this.app.options[cboxes[i].getAttribute("option")];
}
/**
* Loads options and sets values of switches and inputs to match them.
*
* @param {Object} options
*/
load(options) {
for (const option in options) {
this.app.options[option] = options[option];
}
const nboxes = document.querySelectorAll("#options-body input[type=number]");
for (i = 0; i < nboxes.length; i++) {
nboxes[i].value = this.app.options[nboxes[i].getAttribute("option")];
nboxes[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
// Set options to match object
const cboxes = document.querySelectorAll("#options-body input[type=checkbox]");
let i;
for (i = 0; i < cboxes.length; i++) {
cboxes[i].checked = this.app.options[cboxes[i].getAttribute("option")];
}
const selects = document.querySelectorAll("#options-body select");
for (i = 0; i < selects.length; i++) {
const val = this.app.options[selects[i].getAttribute("option")];
if (val) {
selects[i].value = val;
selects[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
} else {
selects[i].selectedIndex = 0;
const nboxes = document.querySelectorAll("#options-body input[type=number]");
for (i = 0; i < nboxes.length; i++) {
nboxes[i].value = this.app.options[nboxes[i].getAttribute("option")];
nboxes[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
const selects = document.querySelectorAll("#options-body select");
for (i = 0; i < selects.length; i++) {
const val = this.app.options[selects[i].getAttribute("option")];
if (val) {
selects[i].value = val;
selects[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
} else {
selects[i].selectedIndex = 0;
}
}
}
};
/**
* Handler for options click events.
* Dispays the options pane.
*
* @param {event} e
*/
OptionsWaiter.prototype.optionsClick = function(e) {
e.preventDefault();
$("#options-modal").modal();
};
/**
* Handler for reset options click events.
* Resets options back to their default values.
*/
OptionsWaiter.prototype.resetOptionsClick = function() {
this.load(this.app.doptions);
};
/**
* Handler for switch change events.
* Modifies the option state and saves it to local storage.
*
* @param {event} e
*/
OptionsWaiter.prototype.switchChange = function(e) {
const el = e.target;
const option = el.getAttribute("option");
const state = el.checked;
log.debug(`Setting ${option} to ${state}`);
this.app.options[option] = state;
if (this.app.isLocalStorageAvailable())
localStorage.setItem("options", JSON.stringify(this.app.options));
};
/**
* Handler for number change events.
* Modifies the option value and saves it to local storage.
*
* @param {event} e
*/
OptionsWaiter.prototype.numberChange = function(e) {
const el = e.target;
const option = el.getAttribute("option");
const val = parseInt(el.value, 10);
log.debug(`Setting ${option} to ${val}`);
this.app.options[option] = val;
if (this.app.isLocalStorageAvailable())
localStorage.setItem("options", JSON.stringify(this.app.options));
};
/**
* Handler for select change events.
* Modifies the option value and saves it to local storage.
*
* @param {event} e
*/
OptionsWaiter.prototype.selectChange = function(e) {
const el = e.target;
const option = el.getAttribute("option");
log.debug(`Setting ${option} to ${el.value}`);
this.app.options[option] = el.value;
if (this.app.isLocalStorageAvailable())
localStorage.setItem("options", JSON.stringify(this.app.options));
};
/**
* Sets or unsets word wrap on the input and output depending on the wordWrap option value.
*/
OptionsWaiter.prototype.setWordWrap = function() {
document.getElementById("input-text").classList.remove("word-wrap");
document.getElementById("output-text").classList.remove("word-wrap");
document.getElementById("output-html").classList.remove("word-wrap");
document.getElementById("input-highlighter").classList.remove("word-wrap");
document.getElementById("output-highlighter").classList.remove("word-wrap");
if (!this.app.options.wordWrap) {
document.getElementById("input-text").classList.add("word-wrap");
document.getElementById("output-text").classList.add("word-wrap");
document.getElementById("output-html").classList.add("word-wrap");
document.getElementById("input-highlighter").classList.add("word-wrap");
document.getElementById("output-highlighter").classList.add("word-wrap");
/**
* Handler for options click events.
* Dispays the options pane.
*
* @param {event} e
*/
optionsClick(e) {
e.preventDefault();
$("#options-modal").modal();
}
};
/**
* Changes the theme by setting the class of the <html> element.
*
* @param {Event} e
*/
OptionsWaiter.prototype.themeChange = function (e) {
const themeClass = e.target.value;
document.querySelector(":root").className = themeClass;
};
/**
* Handler for reset options click events.
* Resets options back to their default values.
*/
resetOptionsClick() {
this.load(this.app.doptions);
}
/**
* Changes the console logging level.
*
* @param {Event} e
*/
OptionsWaiter.prototype.logLevelChange = function (e) {
const level = e.target.value;
log.setLevel(level, false);
this.manager.worker.setLogLevel();
this.manager.input.setLogLevel();
};
/**
* Handler for switch change events.
*
* @param {event} e
*/
switchChange(e) {
const el = e.target;
const option = el.getAttribute("option");
const state = el.checked;
this.updateOption(option, state);
}
/**
* Handler for number change events.
*
* @param {event} e
*/
numberChange(e) {
const el = e.target;
const option = el.getAttribute("option");
const val = parseInt(el.value, 10);
this.updateOption(option, val);
}
/**
* Handler for select change events.
*
* @param {event} e
*/
selectChange(e) {
const el = e.target;
const option = el.getAttribute("option");
this.updateOption(option, el.value);
}
/**
* Modifies an option value and saves it to local storage.
*
* @param {string} option - The option to be updated
* @param {string|number|boolean} value - The new value of the option
*/
updateOption(option, value) {
log.debug(`Setting ${option} to ${value}`);
this.app.options[option] = value;
if (this.app.isLocalStorageAvailable())
localStorage.setItem("options", JSON.stringify(this.app.options));
}
/**
* Sets or unsets word wrap on the input and output depending on the wordWrap option value.
*/
setWordWrap() {
document.getElementById("input-text").classList.remove("word-wrap");
document.getElementById("output-text").classList.remove("word-wrap");
document.getElementById("output-html").classList.remove("word-wrap");
document.getElementById("input-highlighter").classList.remove("word-wrap");
document.getElementById("output-highlighter").classList.remove("word-wrap");
if (!this.app.options.wordWrap) {
document.getElementById("input-text").classList.add("word-wrap");
document.getElementById("output-text").classList.add("word-wrap");
document.getElementById("output-html").classList.add("word-wrap");
document.getElementById("input-highlighter").classList.add("word-wrap");
document.getElementById("output-highlighter").classList.add("word-wrap");
}
}
/**
* Changes the theme by setting the class of the <html> element.
*
* @param {Event} e
*/
themeChange(e) {
const themeClass = e.target.value;
document.querySelector(":root").className = themeClass;
}
/**
* Changes the console logging level.
*
* @param {Event} e
*/
logLevelChange(e) {
const level = e.target.value;
log.setLevel(level, false);
this.manager.worker.setLogLevel();
this.manager.input.setLogLevel();
}
}
export default OptionsWaiter;

View file

@ -217,6 +217,9 @@ class OutputWaiter {
*/
removeAllOutputs() {
this.outputs = {};
this.resetSwitch();
const tabsList = document.getElementById("output-tabs");
const tabsListChildren = tabsList.children;
@ -516,9 +519,10 @@ class OutputWaiter {
this.app.alert("Could not find any output data to download. Has this output been baked?", 3000);
return;
}
let fileName = window.prompt("Please enter a filename: ", "download.dat");
const fileName = window.prompt("Please enter a filename: ", "download.dat");
if (fileName === null) fileName = "download.dat";
// Assume if the user clicks cancel they don't want to download
if (fileName === null) return;
const data = await dish.get(Dish.ARRAY_BUFFER),
file = new File([data], fileName);
@ -529,12 +533,22 @@ class OutputWaiter {
* Handler for save all click event
* Saves all outputs to a single archvie file
*/
saveAllClick() {
async saveAllClick() {
const downloadButton = document.getElementById("save-all-to-file");
if (downloadButton.firstElementChild.innerHTML === "archive") {
this.downloadAllFiles();
} else if (window.confirm("Cancel zipping of outputs?")) {
this.terminateZipWorker();
} else {
const cancel = await new Promise(function(resolve, reject) {
this.app.confirm(
"Cancel zipping?",
"The outputs are currently being zipped for download.<br>Cancel zipping?",
"Continue zipping",
"Cancel zipping",
resolve, this);
}.bind(this));
if (!cancel && cancel !== undefined) {
this.terminateZipWorker();
}
}
}
@ -544,57 +558,61 @@ class OutputWaiter {
* be zipped for download
*/
async downloadAllFiles() {
return new Promise(resolve => {
const inputNums = Object.keys(this.outputs);
for (let i = 0; i < inputNums.length; i++) {
const iNum = inputNums[i];
if (this.outputs[iNum].status !== "baked" ||
this.outputs[iNum].bakeId !== this.manager.worker.bakeId) {
if (window.confirm("Not all outputs have been baked yet. Continue downloading outputs?")) {
break;
} else {
return;
}
const inputNums = Object.keys(this.outputs);
for (let i = 0; i < inputNums.length; i++) {
const iNum = inputNums[i];
if (this.outputs[iNum].status !== "baked" ||
this.outputs[iNum].bakeId !== this.manager.worker.bakeId) {
const continueDownloading = await new Promise(function(resolve, reject) {
this.app.confirm(
"Incomplete outputs",
"Not all outputs have been baked yet. Continue downloading outputs?",
"Download", "Cancel", resolve, this);
}.bind(this));
if (continueDownloading) {
break;
} else {
return;
}
}
}
let fileName = window.prompt("Please enter a filename: ", "download.zip");
let fileName = window.prompt("Please enter a filename: ", "download.zip");
if (fileName === null || fileName === "") {
// Don't zip the files if there isn't a filename
this.app.alert("No filename was specified.", 3000);
return;
}
if (fileName === null || fileName === "") {
// Don't zip the files if there isn't a filename
this.app.alert("No filename was specified.", 3000);
return;
}
if (!fileName.match(/.zip$/)) {
fileName += ".zip";
}
if (!fileName.match(/.zip$/)) {
fileName += ".zip";
}
let fileExt = window.prompt("Please enter a file extension for the files, or leave blank to detect automatically.", "");
let fileExt = window.prompt("Please enter a file extension for the files, or leave blank to detect automatically.", "");
if (fileExt === null) fileExt = "";
if (fileExt === null) fileExt = "";
if (this.zipWorker !== null) {
this.terminateZipWorker();
}
if (this.zipWorker !== null) {
this.terminateZipWorker();
}
const downloadButton = document.getElementById("save-all-to-file");
const downloadButton = document.getElementById("save-all-to-file");
downloadButton.classList.add("spin");
downloadButton.title = `Zipping ${inputNums.length} files...`;
downloadButton.setAttribute("data-original-title", `Zipping ${inputNums.length} files...`);
downloadButton.classList.add("spin");
downloadButton.title = `Zipping ${inputNums.length} files...`;
downloadButton.setAttribute("data-original-title", `Zipping ${inputNums.length} files...`);
downloadButton.firstElementChild.innerHTML = "autorenew";
downloadButton.firstElementChild.innerHTML = "autorenew";
log.debug("Creating ZipWorker");
this.zipWorker = new ZipWorker();
this.zipWorker.postMessage({
outputs: this.outputs,
filename: fileName,
fileExtension: fileExt
});
this.zipWorker.addEventListener("message", this.handleZipWorkerMessage.bind(this));
log.debug("Creating ZipWorker");
this.zipWorker = new ZipWorker();
this.zipWorker.postMessage({
outputs: this.outputs,
filename: fileName,
fileExtension: fileExt
});
this.zipWorker.addEventListener("message", this.handleZipWorkerMessage.bind(this));
}
/**
@ -1213,14 +1231,39 @@ class OutputWaiter {
* Moves the current output into the input textarea.
*/
async switchClick() {
const active = await this.getDishBuffer(this.getOutputDish(this.manager.tabs.getActiveOutputTab()));
const activeTab = this.manager.tabs.getActiveOutputTab();
const transferable = [];
const switchButton = document.getElementById("switch");
switchButton.classList.add("spin");
switchButton.disabled = true;
switchButton.firstElementChild.innerHTML = "autorenew";
$(switchButton).tooltip("hide");
let active = await this.getDishBuffer(this.getOutputDish(activeTab));
if (!this.outputExists(activeTab)) {
this.resetSwitchButton();
return;
}
if (this.outputs[activeTab].data.type === "string" &&
active.byteLength <= this.app.options.ioDisplayThreshold * 1024) {
const dishString = await this.getDishStr(this.getOutputDish(activeTab));
if (!await this.manager.input.preserveCarriageReturns(dishString)) {
active = dishString;
}
} else {
transferable.push(active);
}
this.manager.input.inputWorker.postMessage({
action: "inputSwitch",
data: {
inputNum: this.manager.tabs.getActiveInputTab(),
inputNum: activeTab,
outputData: active
}
}, [active]);
}, transferable);
}
/**
@ -1238,6 +1281,9 @@ class OutputWaiter {
inputSwitch(switchData) {
this.switchOrigData = switchData;
document.getElementById("undo-switch").disabled = false;
this.resetSwitchButton();
}
/**
@ -1246,17 +1292,35 @@ class OutputWaiter {
*/
undoSwitchClick() {
this.manager.input.updateInputObj(this.switchOrigData.inputNum, this.switchOrigData.data);
this.manager.input.fileLoaded(this.switchOrigData.inputNum);
this.resetSwitch();
}
/**
* Removes the switch data and resets the switch buttons
*/
resetSwitch() {
if (this.switchOrigData !== undefined) {
delete this.switchOrigData;
}
const undoSwitch = document.getElementById("undo-switch");
undoSwitch.disabled = true;
$(undoSwitch).tooltip("hide");
this.manager.input.inputWorker.postMessage({
action: "setInput",
data: {
inputNum: this.switchOrigData.inputNum,
silent: false
}
});
this.resetSwitchButton();
}
/**
* Resets the switch button to its usual state
*/
resetSwitchButton() {
const switchButton = document.getElementById("switch");
switchButton.classList.remove("spin");
switchButton.disabled = false;
switchButton.firstElementChild.innerHTML = "open_in_browser";
}
/**