Initial commit

This commit is contained in:
n1474335 2016-11-28 10:42:58 +00:00
commit b1d73a725d
238 changed files with 105357 additions and 0 deletions

View file

@ -0,0 +1,340 @@
/**
* Waiter to handle events related to the CyberChef controls (i.e. Bake, Step, Save, Load etc.)
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {HTMLApp} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
var ControlsWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
};
/**
* Adjusts the display properties of the control buttons so that they fit within the current width
* without wrapping or overflowing.
*/
ControlsWaiter.prototype.adjust_width = function() {
var controls = document.getElementById("controls"),
step = document.getElementById("step"),
clr_breaks = document.getElementById("clr-breaks"),
save_img = document.querySelector("#save img"),
load_img = document.querySelector("#load img"),
step_img = document.querySelector("#step img"),
clr_recip_img = document.querySelector("#clr-recipe img"),
clr_breaks_img = document.querySelector("#clr-breaks img");
if (controls.clientWidth < 470) {
step.childNodes[1].nodeValue = " Step";
} else {
step.childNodes[1].nodeValue = " Step through";
}
if (controls.clientWidth < 400) {
save_img.style.display = "none";
load_img.style.display = "none";
step_img.style.display = "none";
clr_recip_img.style.display = "none";
clr_breaks_img.style.display = "none";
} else {
save_img.style.display = "inline";
load_img.style.display = "inline";
step_img.style.display = "inline";
clr_recip_img.style.display = "inline";
clr_breaks_img.style.display = "inline";
}
if (controls.clientWidth < 330) {
clr_breaks.childNodes[1].nodeValue = " Clear breaks";
} else {
clr_breaks.childNodes[1].nodeValue = " Clear breakpoints";
}
};
/**
* Checks or unchecks the Auto Bake checkbox based on the given value.
*
* @param {boolean} value - The new value for Auto Bake.
*/
ControlsWaiter.prototype.set_auto_bake = function(value) {
var auto_bake_checkbox = document.getElementById("auto-bake");
if (auto_bake_checkbox.checked != value) {
auto_bake_checkbox.click();
}
};
/**
* Handler to trigger baking.
*/
ControlsWaiter.prototype.bake_click = function() {
this.app.bake();
$("#output-text").selectRange(0);
};
/**
* Handler for the 'Step through' command. Executes the next step of the recipe.
*/
ControlsWaiter.prototype.step_click = function() {
this.app.bake(true);
$("#output-text").selectRange(0);
};
/**
* Handler for changes made to the Auto Bake checkbox.
*/
ControlsWaiter.prototype.auto_bake_change = function() {
var auto_bake_label = document.getElementById("auto-bake-label"),
auto_bake_checkbox = document.getElementById("auto-bake");
this.app.auto_bake_ = auto_bake_checkbox.checked;
if (auto_bake_checkbox.checked) {
auto_bake_label.classList.remove("btn-default");
auto_bake_label.classList.add("btn-success");
} else {
auto_bake_label.classList.remove("btn-success");
auto_bake_label.classList.add("btn-default");
}
};
/**
* Handler for the 'Clear recipe' command. Removes all operations from the recipe.
*/
ControlsWaiter.prototype.clear_recipe_click = function() {
this.manager.recipe.clear_recipe();
};
/**
* Handler for the 'Clear breakpoints' command. Removes all breakpoints from operations in the
* recipe.
*/
ControlsWaiter.prototype.clear_breaks_click = function() {
var bps = document.querySelectorAll("#rec_list li.operation .breakpoint");
for (var i = 0; i < bps.length; i++) {
bps[i].setAttribute("break", "false");
bps[i].classList.remove("breakpoint-selected");
}
};
/**
* Populates the save disalog box with a URL incorporating the recipe and input.
*
* @param {Object[]} [recipe_config] - The recipe configuration object array.
*/
ControlsWaiter.prototype.initialise_save_link = function(recipe_config) {
recipe_config = recipe_config || this.app.get_recipe_config();
var recipe_str = JSON.stringify(recipe_config),
input_str = Utils.to_base64(this.app.get_input()),
include_recipe = document.getElementById("save-link-recipe-checkbox").checked,
include_input = document.getElementById("save-link-input-checkbox").checked,
save_link_el = document.getElementById("save-link"),
save_link = this.generate_state_url(include_recipe, include_input, recipe_config);
save_link_el.innerHTML = Utils.truncate(save_link, 120);
save_link_el.setAttribute("href", save_link);
};
/**
* Generates a URL containing the current recipe and input state.
*
* @param {boolean} include_recipe - Whether to include the recipe in the URL.
* @param {boolean} include_input - Whether to include the input in the URL.
* @param {Object[]} [recipe_config] - The recipe configuration object array.
* @returns {string}
*/
ControlsWaiter.prototype.generate_state_url = function(include_recipe, include_input, recipe_config) {
recipe_config = recipe_config || this.app.get_recipe_config();
var link = window.location.protocol + "//" +
window.location.host +
window.location.pathname,
recipe_str = JSON.stringify(recipe_config),
input_str = Utils.to_base64(this.app.get_input(), "A-Za-z0-9+/"); // B64 alphabet with no padding
include_recipe = include_recipe && (recipe_config.length > 0);
include_input = include_input && (input_str.length > 0) && (input_str.length < 8000);
if (include_recipe) {
link += "?recipe=" + encodeURIComponent(recipe_str);
}
if (include_recipe && include_input) {
link += "&input=" + encodeURIComponent(input_str);
} else if (include_input) {
link += "?input=" + encodeURIComponent(input_str);
}
return link;
};
/**
* Handler for changes made to the save dialog text area. Re-initialises the save link.
*/
ControlsWaiter.prototype.save_text_change = function() {
try {
var recipe_config = JSON.parse(document.getElementById("save-text").value);
this.initialise_save_link(recipe_config);
} catch(err) {}
};
/**
* Handler for the 'Save' command. Pops up the save dialog box.
*/
ControlsWaiter.prototype.save_click = function() {
var recipe_config = this.app.get_recipe_config();
var recipe_str = JSON.stringify(recipe_config).replace(/},{/g, '},\n{');
document.getElementById("save-text").value = recipe_str;
this.initialise_save_link(recipe_config);
$("#save-modal").modal();
};
/**
* Handler for the save link recipe checkbox change event.
*/
ControlsWaiter.prototype.slr_check_change = function() {
this.initialise_save_link();
};
/**
* Handler for the save link input checkbox change event.
*/
ControlsWaiter.prototype.sli_check_change = function() {
this.initialise_save_link();
};
/**
* Handler for the 'Load' command. Pops up the load dialog box.
*/
ControlsWaiter.prototype.load_click = function() {
this.populate_load_recipes_list();
$("#load-modal").modal();
};
/**
* Saves the recipe specified in the save textarea to local storage.
*/
ControlsWaiter.prototype.save_button_click = function() {
var recipe_name = document.getElementById("save-name").value,
recipe_str = document.getElementById("save-text").value;
if (!recipe_name) {
this.app.alert("Please enter a recipe name", "danger", 2000);
return;
}
var saved_recipes = localStorage.saved_recipes ?
JSON.parse(localStorage.saved_recipes) : [],
recipe_id = localStorage.recipe_id || 0;
saved_recipes.push({
id: ++recipe_id,
name: recipe_name,
recipe: recipe_str
});
localStorage.saved_recipes = JSON.stringify(saved_recipes);
localStorage.recipe_id = recipe_id;
this.app.alert("Recipe saved as \"" + recipe_name + "\".", "success", 2000);
};
/**
* Populates the list of saved recipes in the load dialog box from local storage.
*/
ControlsWaiter.prototype.populate_load_recipes_list = function() {
var load_name_el = document.getElementById("load-name");
// Remove current recipes from select
var i = load_name_el.options.length;
while (i--) {
load_name_el.remove(i);
}
// Add recipes to select
var saved_recipes = localStorage.saved_recipes ?
JSON.parse(localStorage.saved_recipes) : [];
for (i = 0; i < saved_recipes.length; i++) {
var opt = document.createElement("option");
opt.value = saved_recipes[i].id;
opt.innerHTML = saved_recipes[i].name;
load_name_el.appendChild(opt);
}
// Populate textarea with first recipe
document.getElementById("load-text").value = saved_recipes.length ? saved_recipes[0].recipe : "";
};
/**
* Removes the currently selected recipe from local storage.
*/
ControlsWaiter.prototype.load_delete_click = function() {
var id = document.getElementById("load-name").value,
saved_recipes = localStorage.saved_recipes ?
JSON.parse(localStorage.saved_recipes) : [];
saved_recipes = saved_recipes.filter(function(r) {
return r.id != id;
});
localStorage.saved_recipes = JSON.stringify(saved_recipes);
this.populate_load_recipes_list();
};
/**
* Displays the selected recipe in the load text box.
*/
ControlsWaiter.prototype.load_name_change = function(e) {
var el = e.target,
saved_recipes = localStorage.saved_recipes ?
JSON.parse(localStorage.saved_recipes) : [],
id = parseInt(el.value, 10);
var recipe = saved_recipes.filter(function(r) {
return r.id == id;
})[0];
document.getElementById("load-text").value = recipe.recipe;
};
/**
* Loads the selected recipe and populates the Recipe with its operations.
*/
ControlsWaiter.prototype.load_button_click = function() {
try {
var recipe_config = JSON.parse(document.getElementById("load-text").value);
this.app.set_recipe_config(recipe_config);
$("#rec_list [data-toggle=popover]").popover();
} catch(e) {
this.app.alert("Invalid recipe", "danger", 2000);
}
};

667
src/js/views/html/HTMLApp.js Executable file
View file

@ -0,0 +1,667 @@
/* globals Split */
/**
* HTML view for CyberChef responsible for building the web page and dealing with all user
* interactions.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {CatConf[]} categories - The list of categories and operations to be populated.
* @param {Object.<string, OpConf>} operations - The list of operation configuration objects.
* @param {String[]} default_favourites - A list of default favourite operations.
* @param {Object} options - Default setting for app options.
*/
var HTMLApp = function(categories, operations, default_favourites, default_options) {
this.categories = categories;
this.operations = operations;
this.dfavourites = default_favourites;
this.doptions = default_options;
this.options = Utils.extend({}, default_options);
this.chef = new Chef();
this.manager = new Manager(this);
this.auto_bake_ = false;
this.progress = 0;
this.ing_id = 0;
window.chef = this.chef;
};
/**
* This function sets up the stage and creates listeners for all events.
*
* @fires Manager#appstart
*/
HTMLApp.prototype.setup = function() {
document.dispatchEvent(this.manager.appstart);
this.initialise_splitter();
this.load_local_storage();
this.populate_operations_list();
this.manager.setup();
this.reset_layout();
this.set_compile_message();
this.load_URI_params();
};
/**
* An error handler for displaying the error to the user.
*
* @param {Error} err
*/
HTMLApp.prototype.handle_error = function(err) {
console.error(err);
var msg = err.display_str || err.toString();
this.alert(msg, "danger", this.options.error_timeout, !this.options.show_errors);
};
/**
* Calls the Chef to bake the current input using the current recipe.
*
* @param {boolean} [step] - Set to true if we should only execute one operation instead of the
* whole recipe.
*/
HTMLApp.prototype.bake = function(step) {
var response;
try {
response = this.chef.bake(
this.get_input(), // The user's input
this.get_recipe_config(), // The configuration of the recipe
this.options, // Options set by the user
this.progress, // The current position in the recipe
step // Whether or not to take one step or execute the whole recipe
);
} catch (err) {
this.handle_error(err);
} finally {
if (!response) return;
if (response.error) {
this.handle_error(response.error);
}
this.options = response.options;
this.dish_str = response.type == "html" ? Utils.strip_html_tags(response.result, true) : response.result;
this.progress = response.progress;
this.manager.recipe.update_breakpoint_indicator(response.progress);
this.manager.output.set(response.result, response.type, response.duration);
// If baking took too long, disable auto-bake
if (response.duration > this.options.auto_bake_threshold && this.auto_bake_) {
this.manager.controls.set_auto_bake(false);
this.alert("Baking took longer than " + this.options.auto_bake_threshold +
"ms, Auto Bake has been disabled.", "warning", 5000);
}
}
};
/**
* Runs Auto Bake if it is set.
*/
HTMLApp.prototype.auto_bake = function() {
if (this.auto_bake_) {
this.bake();
}
};
/**
* Runs a silent bake forcing the browser to load and cache all the relevant JavaScript code needed
* to do a real bake.
*
* The output will not be modified (hence "silent" bake). This will only actually execute the
* recipe if auto-bake is enabled, otherwise it will just load the recipe, ingredients and dish.
*
* @returns {number} - The number of miliseconds it took to run the silent bake.
*/
HTMLApp.prototype.silent_bake = function() {
var start_time = new Date().getTime(),
recipe_config = this.get_recipe_config();
if (this.auto_bake_) {
this.chef.silent_bake(recipe_config);
}
return new Date().getTime() - start_time;
};
/**
* Gets the user's input data.
*
* @returns {string}
*/
HTMLApp.prototype.get_input = function() {
var input = this.manager.input.get();
// Save to session storage in case we need to restore it later
sessionStorage.setItem("input_length", input.length);
sessionStorage.setItem("input", input);
return input;
};
/**
* Sets the user's input data.
*
* @param {string} input - The string to set the input to
*/
HTMLApp.prototype.set_input = function(input) {
sessionStorage.setItem("input_length", input.length);
sessionStorage.setItem("input", input);
this.manager.input.set(input);
};
/**
* Populates the operations accordion list with the categories and operations specified in the
* view constructor.
*
* @fires Manager#oplistcreate
*/
HTMLApp.prototype.populate_operations_list = function() {
// Move edit button away before we overwrite it
document.body.appendChild(document.getElementById("edit-favourites"));
var html = "";
for (var i = 0; i < this.categories.length; i++) {
var cat_conf = this.categories[i],
selected = i === 0,
cat = new HTMLCategory(cat_conf.name, selected);
for (var j = 0; j < cat_conf.ops.length; j++) {
var op_name = cat_conf.ops[j],
op = new HTMLOperation(op_name, this.operations[op_name], this, this.manager);
cat.add_operation(op);
}
html += cat.to_html();
}
document.getElementById("categories").innerHTML = html;
var op_lists = document.querySelectorAll("#categories .op_list");
for (i = 0; i < op_lists.length; i++) {
op_lists[i].dispatchEvent(this.manager.oplistcreate);
}
// Add edit button to first category (Favourites)
document.querySelector("#categories a").appendChild(document.getElementById("edit-favourites"));
};
/**
* Sets up the adjustable splitter to allow the user to resize areas of the page.
*/
HTMLApp.prototype.initialise_splitter = function() {
Split(["#operations", "#recipe", "#IO"], {
sizes: [20, 30, 50],
minSize: [240, 325, 500],
gutterSize: 4,
onDrag: this.manager.controls.adjust_width.bind(this.manager.controls)
});
Split(["#input", "#output"], {
direction: "vertical",
gutterSize: 4,
});
this.reset_layout();
};
/**
* Loads the information previously saved to the HTML5 local storage object so that user options
* and favourites can be restored.
*/
HTMLApp.prototype.load_local_storage = function() {
// Load options
var l_options;
if (localStorage.options !== undefined) {
l_options = JSON.parse(localStorage.options);
}
this.manager.options.load(l_options);
// Load favourites
this.load_favourites();
};
/**
* Loads the user's favourite operations from the HTML5 local storage object and populates the
* Favourites category with them.
* If the user currently has no saved favourites, the defaults from the view constructor are used.
*/
HTMLApp.prototype.load_favourites = function() {
var favourites = localStorage.favourites &&
localStorage.favourites.length > 2 ?
JSON.parse(localStorage.favourites) :
this.dfavourites;
favourites = this.valid_favourites(favourites);
this.save_favourites(favourites);
var fav_cat = this.categories.filter(function(c) {
return c.name == "Favourites";
})[0];
if (fav_cat) {
fav_cat.ops = favourites;
} else {
this.categories.unshift({
name: "Favourites",
ops: favourites
});
}
};
/**
* Filters the list of favourite operations that the user had stored and removes any that are no
* longer available. The user is notified if this is the case.
* @param {string[]} favourites - A list of the user's favourite operations
* @returns {string[]} A list of the valid favourites
*/
HTMLApp.prototype.valid_favourites = function(favourites) {
var valid_favs = [];
for (var i = 0; i < favourites.length; i++) {
if (this.operations.hasOwnProperty(favourites[i])) {
valid_favs.push(favourites[i]);
} else {
this.alert("The operation \"" + favourites[i] + "\" is no longer " +
"available. It has been removed from your favourites.", "info");
}
}
return valid_favs;
};
/**
* Saves a list of favourite operations to the HTML5 local storage object.
*
* @param {string[]} favourites - A list of the user's favourite operations
*/
HTMLApp.prototype.save_favourites = function(favourites) {
localStorage.setItem("favourites", JSON.stringify(this.valid_favourites(favourites)));
};
/**
* Resets favourite operations back to the default as specified in the view constructor and
* refreshes the operation list.
*/
HTMLApp.prototype.reset_favourites = function() {
this.save_favourites(this.dfavourites);
this.load_favourites();
this.populate_operations_list();
this.manager.recipe.initialise_operation_drag_n_drop();
};
/**
* Adds an operation to the user's favourites.
*
* @param {string} name - The name of the operation
*/
HTMLApp.prototype.add_favourite = function(name) {
var favourites = JSON.parse(localStorage.favourites);
if (favourites.indexOf(name) >= 0) {
this.alert("'" + name + "' is already in your favourites", "info", 2000);
return;
}
favourites.push(name);
this.save_favourites(favourites);
this.load_favourites();
this.populate_operations_list();
this.manager.recipe.initialise_operation_drag_n_drop();
};
/**
* Checks for input and recipe in the URI parameters and loads them if present.
*/
HTMLApp.prototype.load_URI_params = function() {
// Load query string from URI
this.query_string = (function(a) {
if (a === "") return {};
var b = {};
for (var i = 0; i < a.length; i++) {
var p = a[i].split('=');
if (p.length != 2) {
b[a[i]] = true;
} else {
b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
}
}
return b;
})(window.location.search.substr(1).split('&'));
// Turn off auto-bake while loading
var auto_bake_val = this.auto_bake_;
this.auto_bake_ = false;
// Read in recipe from query string
if (this.query_string.recipe) {
try {
var recipe_config = JSON.parse(this.query_string.recipe);
this.set_recipe_config(recipe_config);
} catch(err) {}
} else if (this.query_string.op) {
// If there's no recipe, look for single operations
this.manager.recipe.clear_recipe();
try {
this.manager.recipe.add_operation(this.query_string.op);
} catch(err) {
// If no exact match, search for nearest match and add that
var matched_ops = this.manager.ops.filter_operations(this.query_string.op, false);
if (matched_ops.length) {
this.manager.recipe.add_operation(matched_ops[0].name);
}
// Populate search with the string
var search = document.getElementById("search");
search.value = this.query_string.op;
search.dispatchEvent(new Event("search"));
}
}
// Read in input data from query string
if (this.query_string.input) {
try {
var input_data = Utils.from_base64(this.query_string.input);
this.set_input(input_data);
} catch(err) {}
}
// Restore auto-bake state
this.auto_bake_ = auto_bake_val;
this.auto_bake();
};
/**
* Returns the next ingredient ID and increments it for next time.
*
* @returns {number}
*/
HTMLApp.prototype.next_ing_id = function() {
return this.ing_id++;
};
/**
* Gets the current recipe configuration.
*
* @returns {Object[]}
*/
HTMLApp.prototype.get_recipe_config = function() {
var recipe_config = this.manager.recipe.get_config();
sessionStorage.setItem("recipe_config", JSON.stringify(recipe_config));
return recipe_config;
};
/**
* Given a recipe configuration, sets the recipe to that configuration.
*
* @param {Object[]} recipe_config - The recipe configuration
*/
HTMLApp.prototype.set_recipe_config = function(recipe_config) {
sessionStorage.setItem("recipe_config", JSON.stringify(recipe_config));
document.getElementById("rec_list").innerHTML = null;
for (var i = 0; i < recipe_config.length; i++) {
var item = this.manager.recipe.add_operation(recipe_config[i].op);
// Populate arguments
var args = item.querySelectorAll(".arg");
for (var j = 0; j < args.length; j++) {
if (args[j].getAttribute("type") == "checkbox") {
// checkbox
args[j].checked = recipe_config[i].args[j];
} else if (args[j].classList.contains("toggle-string")) {
// toggle_string
args[j].value = recipe_config[i].args[j].string;
args[j].previousSibling.children[0].innerHTML = recipe_config[i].args[j].option +
" <span class='caret'></span>";
} else {
// all others
args[j].value = recipe_config[i].args[j];
}
}
// Set disabled and breakpoint
if (recipe_config[i].disabled) {
item.querySelector(".disable-icon").click();
}
if (recipe_config[i].breakpoint) {
item.querySelector(".breakpoint").click();
}
this.progress = 0;
}
};
/**
* Resets the splitter positions to default.
*/
HTMLApp.prototype.reset_layout = function() {
document.getElementById("operations").style.width = "calc(20% - 2px)";
document.getElementById("recipe").style.width = "calc(30% - 4px)";
document.getElementById("IO").style.width = "calc(50% - 2px)";
document.getElementById("input").style.height = "calc(50% - 2px)";
document.getElementById("output").style.height = "calc(50% - 2px)";
this.manager.controls.adjust_width();
};
/**
* Sets the compile message.
*/
HTMLApp.prototype.set_compile_message = function() {
// Display time since last build and compile message
var now = new Date(),
time_since_compile = Utils.fuzzy_time(now.getTime() - window.compile_time),
compile_info = "<span style=\"font-weight: normal\">Last build: " +
time_since_compile.substr(0,1).toUpperCase() + time_since_compile.substr(1) + " ago";
if (window.compile_message !== "") {
compile_info += " - " + window.compile_message;
}
compile_info += "</span>";
document.getElementById("notice").innerHTML = compile_info;
};
/**
* Pops up a message to the user and writes it to the console log.
*
* @param {string} str - The message to display (HTML supported)
* @param {string} style - The colour of the popup
* "danger" = red
* "warning" = amber
* "info" = blue
* "success" = green
* @param {number} timeout - The number of milliseconds before the popup closes automatically
* 0 for never (until the user closes it)
* @param {boolean} [silent=false] - Don't show the message in the popup, only print it to the
* console
*
* @example
* // Pops up a red box with the message "[current time] Error: Something has gone wrong!"
* // that will need to be dismissed by the user.
* this.alert("Error: Something has gone wrong!", "danger", 0);
*
* // Pops up a blue information box with the message "[current time] Happy Christmas!"
* // that will disappear after 5 seconds.
* this.alert("Happy Christmas!", "info", 5000);
*/
HTMLApp.prototype.alert = function(str, style, timeout, silent) {
var time = new Date();
console.log("[" + time.toLocaleString() + "] " + str);
if (silent) return;
style = style || "danger";
timeout = timeout || 0;
var alert_el = document.getElementById("alert"),
alert_content = document.getElementById("alert-content");
alert_el.classList.remove("alert-danger");
alert_el.classList.remove("alert-warning");
alert_el.classList.remove("alert-info");
alert_el.classList.remove("alert-success");
alert_el.classList.add("alert-" + style);
// If the box hasn't been closed, append to it rather than replacing
if (alert_el.style.display == "block") {
alert_content.innerHTML +=
"<br><br>[" + time.toLocaleTimeString() + "] " + str;
} else {
alert_content.innerHTML =
"[" + time.toLocaleTimeString() + "] " + str;
}
// Stop the animation if it is in progress
$("#alert").stop();
alert_el.style.display = "block";
alert_el.style.opacity = 1;
if (timeout > 0) {
clearTimeout(this.alert_timeout);
this.alert_timeout = setTimeout(function(){
$("#alert").slideUp(100);
}, timeout);
}
};
/**
* Pops up a box asking the user a question and sending the answer to a specified callback function.
*
* @param {string} title - The title of the box
* @param {string} body - The question (HTML supported)
* @param {function} callback - A function accepting one boolean argument which handles the
* response e.g. function(answer) {...}
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Pops up a box asking if the user would like a cookie. Prints the answer to the console.
* this.confirm("Question", "Would you like a cookie?", function(answer) {console.log(answer);});
*/
HTMLApp.prototype.confirm = function(title, body, callback, scope) {
scope = scope || this;
document.getElementById("confirm-title").innerHTML = title;
document.getElementById("confirm-body").innerHTML = body;
document.getElementById("confirm-modal").style.display = "block";
this.confirm_closed = false;
$("#confirm-modal").modal()
.one("show.bs.modal", function(e) {
this.confirm_closed = false;
}.bind(this))
.one("click", "#confirm-yes", function() {
this.confirm_closed = true;
callback.bind(scope)(true);
$("#confirm-modal").modal("hide");
}.bind(this))
.one("hide.bs.modal", function(e) {
if (!this.confirm_closed)
callback.bind(scope)(false);
this.confirm_closed = true;
}.bind(this));
};
/**
* Handler for the alert close button click event.
* Closes the alert box.
*/
HTMLApp.prototype.alert_close_click = function() {
document.getElementById("alert").style.display = "none";
};
/**
* Handler for CyerChef statechange events.
* Fires whenever the input or recipe changes in any way.
*
* @listens Manager#statechange
* @param {event} e
*/
HTMLApp.prototype.state_change = function(e) {
this.auto_bake();
// Update the current history state (not creating a new one)
if (this.options.update_url) {
this.last_state_url = this.manager.controls.generate_state_url(true, true);
window.history.replaceState({}, "CyberChef", this.last_state_url);
}
};
/**
* Handler for the history popstate event.
* Reloads parameters from the URL.
*
* @param {event} e
*/
HTMLApp.prototype.pop_state = function(e) {
if (window.location.href.split("#")[0] !== this.last_state_url) {
this.load_URI_params();
}
};
/**
* Function to call an external API from this view.
*/
HTMLApp.prototype.call_api = function(url, type, data, data_type, content_type) {
type = type || "POST";
data = data || {};
data_type = data_type || undefined;
content_type = content_type || "application/json";
var response = null,
success = false;
$.ajax({
url: url,
async: false,
type: type,
data: data,
dataType: data_type,
contentType: content_type,
success: function(data) {
success = true;
response = data;
},
error: function(data) {
success = false;
response = data;
},
});
return {
success: success,
response: response
};
};

View file

@ -0,0 +1,50 @@
/**
* Object to handle the creation of operation categories.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {string} name - The name of the category.
* @param {boolean} selected - Whether this category is pre-selected or not.
*/
var HTMLCategory = function(name, selected) {
this.name = name;
this.selected = selected;
this.op_list = [];
};
/**
* Adds an operation to this category.
*
* @param {HTMLOperation} operation - The operation to add.
*/
HTMLCategory.prototype.add_operation = function(operation) {
this.op_list.push(operation);
};
/**
* Renders the category and all operations within it in HTML.
*
* @returns {string}
*/
HTMLCategory.prototype.to_html = function() {
var cat_name = "cat" + this.name.replace(/[\s/-:_]/g, "");
var html = "<div class='panel category'>\
<a class='category-title' data-toggle='collapse'\
data-parent='#categories' href='#" + cat_name + "'>\
" + this.name + "\
</a>\
<div id='" + cat_name + "' class='panel-collapse collapse\
" + (this.selected ? " in" : "") + "'><ul class='op_list'>";
for (var i = 0; i < this.op_list.length; i++) {
html += this.op_list[i].to_stub_html();
}
html += "</ul></div></div>";
return html;
};

View file

@ -0,0 +1,212 @@
/**
* Object to handle the creation of operation ingredients.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {Object} config - The configuration object for this ingredient.
* @param {HTMLApp} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
var HTMLIngredient = function(config, app, manager) {
this.app = app;
this.manager = manager;
this.name = config.name;
this.type = config.type;
this.value = config.value;
this.disabled = config.disabled || false;
this.disable_args = config.disable_args || false;
this.placeholder = config.placeholder || false;
this.target = config.target;
this.toggle_values = config.toggle_values;
this.id = "ing-" + this.app.next_ing_id();
};
/**
* Renders the ingredient in HTML.
*
* @returns {string}
*/
HTMLIngredient.prototype.to_html = function() {
var inline = (this.type == "boolean" ||
this.type == "number" ||
this.type == "option" ||
this.type == "short_string" ||
this.type == "binary_short_string"),
html = inline ? "" : "<div class='clearfix'>&nbsp;</div>",
i, m;
html += "<div class='arg-group" + (inline ? " inline-args" : "") +
(this.type == "text" ? " arg-group-text" : "") + "'><label class='arg-label' for='" +
this.id + "'>" + this.name + "</label>";
switch (this.type) {
case "string":
case "binary_string":
case "byte_array":
html += "<input type='text' id='" + this.id + "' class='arg arg-input' arg_name='" +
this.name + "' value='" + this.value + "'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
break;
case "short_string":
case "binary_short_string":
html += "<input type='text' id='" + this.id +
"'class='arg arg-input short-string' arg_name='" + this.name + "'value='" +
this.value + "'" + (this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
break;
case "toggle_string":
html += "<div class='input-group'><div class='input-group-btn'>\
<button type='button' class='btn btn-default dropdown-toggle' data-toggle='dropdown'\
aria-haspopup='true' aria-expanded='false'" +
(this.disabled ? " disabled='disabled'" : "") + ">" + this.toggle_values[0] +
" <span class='caret'></span></button><ul class='dropdown-menu'>";
for (i = 0; i < this.toggle_values.length; i++) {
html += "<li><a href='#'>" + this.toggle_values[i] + "</a></li>";
}
html += "</ul></div><input type='text' class='arg arg-input toggle-string'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + "></div>";
break;
case "number":
html += "<input type='number' id='" + this.id + "'class='arg arg-input' arg_name='" +
this.name + "'value='" + this.value + "'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
break;
case "boolean":
html += "<input type='checkbox' id='" + this.id + "'class='arg' arg_name='" +
this.name + "'" + (this.value ? " checked='checked' " : "") +
(this.disabled ? " disabled='disabled'" : "") + ">";
if (this.disable_args) {
this.manager.add_dynamic_listener("#" + this.id, "click", this.toggle_disable_args, this);
}
break;
case "option":
html += "<select class='arg' id='" + this.id + "'arg_name='" + this.name + "'" +
(this.disabled ? " disabled='disabled'" : "") + ">";
for (i = 0; i < this.value.length; i++) {
if (!!(m = this.value[i].match(/\[([a-z0-9 -()^]+)\]/i))) {
html += "<optgroup label='" + m[1] + "'>";
} else if (!!(m = this.value[i].match(/\[\/([a-z0-9 -()^]+)\]/i))) {
html += "</optgroup>";
} else {
html += "<option>" + this.value[i] + "</option>";
}
}
html += "</select>";
break;
case "populate_option":
html += "<select class='arg' id='" + this.id + "'arg_name='" + this.name + "'" +
(this.disabled ? " disabled='disabled'" : "") + ">";
for (i = 0; i < this.value.length; i++) {
if (!!(m = this.value[i].name.match(/\[([a-z0-9 -()^]+)\]/i))) {
html += "<optgroup label='" + m[1] + "'>";
} else if (!!(m = this.value[i].name.match(/\[\/([a-z0-9 -()^]+)\]/i))) {
html += "</optgroup>";
} else {
html += "<option populate-value='" + this.value[i].value + "'>" +
this.value[i].name + "</option>";
}
}
html += "</select>";
this.manager.add_dynamic_listener("#" + this.id, "change", this.populate_option_change, this);
break;
case "editable_option":
html += "<div class='editable-option'>";
html += "<select class='editable-option-select' id='sel-" + this.id + "'" +
(this.disabled ? " disabled='disabled'" : "") + ">";
for (i = 0; i < this.value.length; i++) {
html += "<option value='" + this.value[i].value + "'>" + this.value[i].name + "</option>";
}
html += "</select>";
html += "<input class='arg arg-input editable-option-input' id='" + this.id +
"'arg_name='" + this.name + "'" + " value='" + this.value[0].value + "'" +
(this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">";
html += "</div>";
this.manager.add_dynamic_listener("#sel-" + this.id, "change", this.editable_option_change, this);
break;
case "text":
html += "<textarea id='" + this.id + "' class='arg' arg_name='" +
this.name + "'" + (this.disabled ? " disabled='disabled'" : "") +
(this.placeholder ? " placeholder='" + this.placeholder + "'" : "") + ">" +
this.value + "</textarea>";
break;
default:
break;
}
html += "</div>";
return html;
};
/**
* Handler for argument disable toggle.
* Toggles disabled state for all arguments in the disable_args list for this ingredient.
*
* @param {event} e
*/
HTMLIngredient.prototype.toggle_disable_args = function(e) {
var el = e.target,
op = el.parentNode.parentNode,
args = op.querySelectorAll(".arg-group"),
els;
for (var i = 0; i < this.disable_args.length; i++) {
els = args[this.disable_args[i]].querySelectorAll("input, select, button");
for (var j = 0; j < els.length; j++) {
if (els[j].getAttribute("disabled")) {
els[j].removeAttribute("disabled");
} else {
els[j].setAttribute("disabled", "disabled");
}
}
}
this.manager.recipe.ing_change();
};
/**
* Handler for populate option changes.
* Populates the relevant argument with the specified value.
*
* @param {event} e
*/
HTMLIngredient.prototype.populate_option_change = function(e) {
var el = e.target,
op = el.parentNode.parentNode,
target = op.querySelectorAll(".arg-group")[this.target].querySelector("input, select, textarea");
target.value = el.childNodes[el.selectedIndex].getAttribute("populate-value");
this.manager.recipe.ing_change();
};
/**
* Handler for editable option changes.
* Populates the input box with the selected value.
*
* @param {event} e
*/
HTMLIngredient.prototype.editable_option_change = function(e) {
var select = e.target,
input = select.nextSibling;
input.value = select.childNodes[select.selectedIndex].value;
this.manager.recipe.ing_change();
};

View file

@ -0,0 +1,114 @@
/**
* Object to handle the creation of operations.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {string} name - The name of the operation.
* @param {Object} config - The configuration object for this operation.
* @param {HTMLApp} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
var HTMLOperation = function(name, config, app, manager) {
this.app = app;
this.manager = manager;
this.name = name;
this.description = config.description;
this.manual_bake = config.manual_bake || false;
this.config = config;
this.ing_list = [];
for (var i = 0; i < config.args.length; i++) {
var ing = new HTMLIngredient(config.args[i], this.app, this.manager);
this.ing_list.push(ing);
}
};
/**
* @constant
*/
HTMLOperation.INFO_ICON = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAByElEQVR4XqVTzWoaYRQ9KZJmoVaS1J1QiYTIuOgqi9lEugguQhYhdGs3hTyAi0CWJTvJIks30ZBNsimUtlqkVLoQCuJsphRriyFjabWtEyf/Rv3iWcwwymTlgQuH851z5hu43wRGkEwmXwCIA4hiGAUAmUQikQbhEHwyGCWVSglVVUW73RYmyKnxjB56ncJ6NpsVxHGrI/ZLuniVb3DIqQmCHnrNkgcggNeSJPlisRgyJR2b737j/TcDsQUPwv6H5NR4BnroZcb6Z16N2PvyX6yna9Z8qp6JQ0Uf0ughmGHWBSAuyzJqrQ7eqKewY/dzE363C71e39LoWQq5wUwul4uzIBoIBHD01RgyrkZ8eDbvwUWnj623v2DHx4qB51IAzLIAXq8XP/7W0bUVVJtXWIk8wvlN364TA+/1IDMLwmWK/Hq3axmhaBdoGLeklm73ElaBYRgIzkyifHIOO4QQJKM3oJcZq6CgaVp0OTyHw9K/kQI4FiyHfdC0n2CWe5ApFosIPZ7C2tNpXpcDOehGyD/FIbd0euhlhllzFxRzC3fydbG4XRYbB9/tQ41n9m1U7l3lyp9LkfygiZeZCoecmtMqj/+Yxn7Od3v0j50qCO3zAAAAAElFTkSuQmCC";
/**
* @constant
*/
HTMLOperation.REMOVE_ICON = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABwklEQVR42qRTPU8CQRB9K2CCMRJ6NTQajOUaqfxIbLCRghhjQixosLAgFNBQ3l8wsabxLxBJbCyVUBiMCVQEQkOEKBbCnefM3p4eohWXzM3uvHlv52b2hG3bmOWZw4yPn1/XQkCQ9wFxcgZZ0QLKpifpN8Z1n1L13griBBjHhYK0nMT4b+wom53ClAAFQacZJ/m8rNfrSOZy0vxJjPP6IJ2WzWYTO6mUwiwtILiJJSHUKVSWkchkZK1WQzQaxU2pVGUglkjIbreLUCiEx0qlStlFCpfPiPstYDtVKJH9ZFI2Gw1FGA6H6LTbCAaDeGu1FJl6UuYjpwTGzucokZW1NfnS66kyfT4fXns9RaZmlgNcuhZQU+jowLzuOK/HgwEW3E5ZlhLXVWKk11P3wNYNWw+HZdA0sUgx1zjGmD05nckx0ilGjBJdUq3fr7K5e8bGf43RdL7fOPSQb4lI8SLbrUfkUIuY32VTI1bJn5BqDnh4Dodt9ryPUDzyD7aquWoKQohl2i9sAbubwPkTcHkP3FHsg+yT+7sN7G0AF3Xg6sHB3onbdgWWKBDQg/BcTuVt51dQA/JrnIcyIu6rmPV3/hJgACPc0BMEYTg+AAAAAElFTkSuQmCC";
/**
* Renders the operation in HTML as a stub operation with no ingredients.
*
* @returns {string}
*/
HTMLOperation.prototype.to_stub_html = function(remove_icon) {
var html = "<li class='operation'";
if (this.description) {
html += " data-container='body' data-toggle='popover' data-placement='auto right'\
data-content=\"" + this.description + "\" data-html='true' data-trigger='hover'";
}
html += ">" + this.name;
if (remove_icon) {
html += "<img src='data:image/png;base64," + HTMLOperation.REMOVE_ICON +
"' class='op-icon remove-icon'>";
}
if (this.description) {
html += "<img src='data:image/png;base64," + HTMLOperation.INFO_ICON + "' class='op-icon'>";
}
html += "</li>";
return html;
};
/**
* Renders the operation in HTML as a full operation with ingredients.
*
* @returns {string}
*/
HTMLOperation.prototype.to_full_html = function() {
var html = "<div class='arg-title'>" + this.name + "</div>";
for (var i = 0; i < this.ing_list.length; i++) {
html += this.ing_list[i].to_html();
}
html += "<div class='recip-icons'>\
<div class='breakpoint' title='Set breakpoint' break='false'></div>\
<div class='disable-icon recip-icon' title='Disable operation'\
disabled='false'></div>";
html += "</div>\
<div class='clearfix'>&nbsp;</div>";
return html;
};
/**
* Highlights the searched string in the name and description of the operation.
*
* @param {string} search_str
* @param {number} name_pos - The position of the search string in the operation name
* @param {number} desc_pos - The position of the search string in the operation description
*/
HTMLOperation.prototype.highlight_search_string = function(search_str, name_pos, desc_pos) {
if (name_pos >= 0) {
this.name = this.name.slice(0, name_pos) + "<b><u>" +
this.name.slice(name_pos, name_pos + search_str.length) + "</u></b>" +
this.name.slice(name_pos + search_str.length);
}
if (this.description && desc_pos >= 0) {
this.description = this.description.slice(0, desc_pos) + "<b><u>" +
this.description.slice(desc_pos, desc_pos + search_str.length) + "</u></b>" +
this.description.slice(desc_pos + search_str.length);
}
};

View file

@ -0,0 +1,506 @@
/**
* Waiter to handle events related to highlighting in CyberChef.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {HTMLApp} app - The main view object for CyberChef.
*/
var HighlighterWaiter = function(app) {
this.app = app;
this.mouse_button_down = false;
this.mouse_target = null;
};
/**
* HighlighterWaiter data type enum for the input.
* @readonly
* @enum
*/
HighlighterWaiter.INPUT = 0;
/**
* HighlighterWaiter data type enum for the output.
* @readonly
* @enum
*/
HighlighterWaiter.OUTPUT = 1;
/**
* Determines if the current text selection is running backwards or forwards.
* StackOverflow answer id: 12652116
*
* @private
* @returns {boolean}
*/
HighlighterWaiter.prototype._is_selection_backwards = function() {
var backwards = false,
sel = window.getSelection();
if (!sel.isCollapsed) {
var range = document.createRange();
range.setStart(sel.anchorNode, sel.anchorOffset);
range.setEnd(sel.focusNode, sel.focusOffset);
backwards = range.collapsed;
range.detach();
}
return backwards;
};
/**
* Calculates the text offset of a position in an HTML element, ignoring HTML tags.
*
* @private
* @param {element} node - The parent HTML node.
* @param {number} offset - The offset since the last HTML element.
* @returns {number}
*/
HighlighterWaiter.prototype._get_output_html_offset = function(node, offset) {
var sel = window.getSelection(),
range = document.createRange();
range.selectNodeContents(document.getElementById("output-html"));
range.setEnd(node, offset);
sel.removeAllRanges();
sel.addRange(range);
return sel.toString().length;
};
/**
* Gets the current selection offsets in the output HTML, ignoring HTML tags.
*
* @private
* @returns {Object} pos
* @returns {number} pos.start
* @returns {number} pos.end
*/
HighlighterWaiter.prototype._get_output_html_selection_offsets = function() {
var sel = window.getSelection(),
range,
start = 0,
end = 0,
backwards = false;
if (sel.rangeCount) {
range = sel.getRangeAt(sel.rangeCount - 1);
backwards = this._is_selection_backwards();
start = this._get_output_html_offset(range.startContainer, range.startOffset);
end = this._get_output_html_offset(range.endContainer, range.endOffset);
sel.removeAllRanges();
sel.addRange(range);
if (backwards) {
// If selecting backwards, reverse the start and end offsets for the selection to
// prevent deselecting as the drag continues.
sel.collapseToEnd();
sel.extend(sel.anchorNode, range.startOffset);
}
}
return {
start: start,
end: end
};
};
/**
* Handler for input scroll events.
* Scrolls the highlighter pane to match the input textarea position.
*
* @param {event} e
*/
HighlighterWaiter.prototype.input_scroll = function(e) {
var el = e.target;
document.getElementById("input-highlighter").scrollTop = el.scrollTop;
document.getElementById("input-highlighter").scrollLeft = el.scrollLeft;
};
/**
* Handler for output scroll events.
* Scrolls the highlighter pane to match the output textarea position.
*
* @param {event} e
*/
HighlighterWaiter.prototype.output_scroll = function(e) {
var el = e.target;
document.getElementById("output-highlighter").scrollTop = el.scrollTop;
document.getElementById("output-highlighter").scrollLeft = el.scrollLeft;
};
/**
* Handler for input mousedown events.
* Calculates the current selection info, and highlights the corresponding data in the output.
*
* @param {event} e
*/
HighlighterWaiter.prototype.input_mousedown = function(e) {
this.mouse_button_down = true;
this.mouse_target = HighlighterWaiter.INPUT;
this.remove_highlights();
var el = e.target,
start = el.selectionStart,
end = el.selectionEnd;
if (start !== 0 || end !== 0) {
document.getElementById("input-selection-info").innerHTML = this.selection_info(start, end);
this.highlight_output([{start: start, end: end}]);
}
};
/**
* Handler for output mousedown events.
* Calculates the current selection info, and highlights the corresponding data in the input.
*
* @param {event} e
*/
HighlighterWaiter.prototype.output_mousedown = function(e) {
this.mouse_button_down = true;
this.mouse_target = HighlighterWaiter.OUTPUT;
this.remove_highlights();
var el = e.target,
start = el.selectionStart,
end = el.selectionEnd;
if (start !== 0 || end !== 0) {
document.getElementById("output-selection-info").innerHTML = this.selection_info(start, end);
this.highlight_input([{start: start, end: end}]);
}
};
/**
* Handler for output HTML mousedown events.
* Calculates the current selection info.
*
* @param {event} e
*/
HighlighterWaiter.prototype.output_html_mousedown = function(e) {
this.mouse_button_down = true;
this.mouse_target = HighlighterWaiter.OUTPUT;
var sel = this._get_output_html_selection_offsets();
if (sel.start !== 0 || sel.end !== 0) {
document.getElementById("output-selection-info").innerHTML = this.selection_info(sel.start, sel.end);
}
};
/**
* Handler for input mouseup events.
*
* @param {event} e
*/
HighlighterWaiter.prototype.input_mouseup = function(e) {
this.mouse_button_down = false;
};
/**
* Handler for output mouseup events.
*
* @param {event} e
*/
HighlighterWaiter.prototype.output_mouseup = function(e) {
this.mouse_button_down = false;
};
/**
* Handler for output HTML mouseup events.
*
* @param {event} e
*/
HighlighterWaiter.prototype.output_html_mouseup = function(e) {
this.mouse_button_down = false;
};
/**
* Handler for input mousemove events.
* Calculates the current selection info, and highlights the corresponding data in the output.
*
* @param {event} e
*/
HighlighterWaiter.prototype.input_mousemove = function(e) {
// Check that the left mouse button is pressed
if (!this.mouse_button_down ||
e.which != 1 ||
this.mouse_target != HighlighterWaiter.INPUT)
return;
var el = e.target,
start = el.selectionStart,
end = el.selectionEnd;
if (start !== 0 || end !== 0) {
document.getElementById("input-selection-info").innerHTML = this.selection_info(start, end);
this.highlight_output([{start: start, end: end}]);
}
};
/**
* Handler for output mousemove events.
* Calculates the current selection info, and highlights the corresponding data in the input.
*
* @param {event} e
*/
HighlighterWaiter.prototype.output_mousemove = function(e) {
// Check that the left mouse button is pressed
if (!this.mouse_button_down ||
e.which != 1 ||
this.mouse_target != HighlighterWaiter.OUTPUT)
return;
var el = e.target,
start = el.selectionStart,
end = el.selectionEnd;
if (start !== 0 || end !== 0) {
document.getElementById("output-selection-info").innerHTML = this.selection_info(start, end);
this.highlight_input([{start: start, end: end}]);
}
};
/**
* Handler for output HTML mousemove events.
* Calculates the current selection info.
*
* @param {event} e
*/
HighlighterWaiter.prototype.output_html_mousemove = function(e) {
// Check that the left mouse button is pressed
if (!this.mouse_button_down ||
e.which != 1 ||
this.mouse_target != HighlighterWaiter.OUTPUT)
return;
var sel = this._get_output_html_selection_offsets();
if (sel.start !== 0 || sel.end !== 0) {
document.getElementById("output-selection-info").innerHTML = this.selection_info(sel.start, sel.end);
}
};
/**
* Given start and end offsets, writes the HTML for the selection info element with the correct
* padding.
*
* @param {number} start - The start offset.
* @param {number} end - The end offset.
* @returns {string}
*/
HighlighterWaiter.prototype.selection_info = function(start, end) {
var width = end.toString().length;
width = width < 2 ? 2 : width;
var start_str = Utils.pad(start.toString(), width, " ").replace(/ /g, "&nbsp;"),
end_str = Utils.pad(end.toString(), width, " ").replace(/ /g, "&nbsp;"),
len_str = Utils.pad((end-start).toString(), width, " ").replace(/ /g, "&nbsp;");
return "start: " + start_str + "<br>end: " + end_str + "<br>length: " + len_str;
};
/**
* Removes highlighting and selection information.
*/
HighlighterWaiter.prototype.remove_highlights = function() {
document.getElementById("input-highlighter").innerHTML = "";
document.getElementById("output-highlighter").innerHTML = "";
document.getElementById("input-selection-info").innerHTML = "";
document.getElementById("output-selection-info").innerHTML = "";
};
/**
* Generates a list of all the highlight functions assigned to operations in the recipe, if the
* entire recipe supports highlighting.
*
* @returns {Object[]} highlights
* @returns {function} highlights[].f
* @returns {function} highlights[].b
* @returns {Object[]} highlights[].args
*/
HighlighterWaiter.prototype.generate_highlight_list = function() {
var recipe_config = this.app.get_recipe_config(),
highlights = [];
for (var i = 0; i < recipe_config.length; i++) {
if (recipe_config[i].disabled) continue;
// If any breakpoints are set, do not attempt to highlight
if (recipe_config[i].breakpoint) return false;
var op = this.app.operations[recipe_config[i].op];
// If any of the operations do not support highlighting, fail immediately.
if (op.highlight === false || op.highlight === undefined) return false;
highlights.push({
f: op.highlight,
b: op.highlight_reverse,
args: recipe_config[i].args
});
}
return highlights;
};
/**
* Highlights the given offsets in the output.
* We will only highlight if:
* - input hasn't changed since last bake
* - last bake was a full bake
* - all operations in the recipe support highlighting
*
* @param {Object} pos - The position object for the highlight.
* @param {number} pos.start - The start offset.
* @param {number} pos.end - The end offset.
*/
HighlighterWaiter.prototype.highlight_output = function(pos) {
var highlights = this.generate_highlight_list();
if (!highlights || !this.app.auto_bake_) {
return false;
}
for (var i = 0; i < highlights.length; i++) {
// Remove multiple highlights before processing again
pos = [pos[0]];
if (typeof highlights[i].f == "function") {
pos = highlights[i].f(pos, highlights[i].args);
}
}
document.getElementById("output-selection-info").innerHTML = this.selection_info(pos[0].start, pos[0].end);
this.highlight(
document.getElementById("output-text"),
document.getElementById("output-highlighter"),
pos);
};
/**
* Highlights the given offsets in the input.
* We will only highlight if:
* - input hasn't changed since last bake
* - last bake was a full bake
* - all operations in the recipe support highlighting
*
* @param {Object} pos - The position object for the highlight.
* @param {number} pos.start - The start offset.
* @param {number} pos.end - The end offset.
*/
HighlighterWaiter.prototype.highlight_input = function(pos) {
var highlights = this.generate_highlight_list();
if (!highlights || !this.app.auto_bake_) {
return false;
}
for (var i = 0; i < highlights.length; i++) {
// Remove multiple highlights before processing again
pos = [pos[0]];
if (typeof highlights[i].b == "function") {
pos = highlights[i].b(pos, highlights[i].args);
}
}
document.getElementById("input-selection-info").innerHTML = this.selection_info(pos[0].start, pos[0].end);
this.highlight(
document.getElementById("input-text"),
document.getElementById("input-highlighter"),
pos);
};
/**
* Adds the relevant HTML to the specified highlight element such that highlighting appears
* underneath the correct offset.
*
* @param {element} textarea - The input or output textarea.
* @param {element} highlighter - The input or output highlighter element.
* @param {Object} pos - The position object for the highlight.
* @param {number} pos.start - The start offset.
* @param {number} pos.end - The end offset.
*/
HighlighterWaiter.prototype.highlight = function(textarea, highlighter, pos) {
if (!this.app.options.show_highlighter) return false;
if (!this.app.options.attempt_highlight) return false;
// Check if there is a carriage return in the output dish as this will not
// be displayed by the HTML textarea and will mess up highlighting offsets.
if (!this.app.dish_str || this.app.dish_str.indexOf("\r") >= 0) return false;
var start_placeholder = "[start_highlight]",
start_placeholder_regex = /\[start_highlight\]/g,
end_placeholder = "[end_highlight]",
end_placeholder_regex = /\[end_highlight\]/g,
text = textarea.value;
// Put placeholders in position
// If there's only one value, select that
// If there are multiple, ignore the first one and select all others
if (pos.length == 1) {
if (pos[0].end < pos[0].start) return;
text = text.slice(0, pos[0].start) +
start_placeholder + text.slice(pos[0].start, pos[0].end) + end_placeholder +
text.slice(pos[0].end, text.length);
} else {
// O(n^2) - Can anyone improve this without overwriting placeholders?
var result = "",
end_placed = true;
for (var i = 0; i < text.length; i++) {
for (var j = 1; j < pos.length; j++) {
if (pos[j].end < pos[j].start) continue;
if (pos[j].start == i) {
result += start_placeholder;
end_placed = false;
}
if (pos[j].end == i) {
result += end_placeholder;
end_placed = true;
}
}
result += text[i];
}
if (!end_placed) result += end_placeholder;
text = result;
}
var css_class = "hl1";
//if (colour) css_class += "-"+colour;
// Remove HTML tags
text = text.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/\n/g, "&#10;")
// Convert placeholders to tags
.replace(start_placeholder_regex, "<span class=\""+css_class+"\">")
.replace(end_placeholder_regex, "</span>") + "&nbsp;";
// Adjust width to allow for scrollbars
highlighter.style.width = textarea.clientWidth + "px";
highlighter.innerHTML = text;
highlighter.scrollTop = textarea.scrollTop;
highlighter.scrollLeft = textarea.scrollLeft;
};

217
src/js/views/html/InputWaiter.js Executable file
View file

@ -0,0 +1,217 @@
/**
* Waiter to handle events related to the input.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {HTMLApp} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
var InputWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
// Define keys that don't change the input so we don't have to autobake when they are pressed
this.bad_keys = [
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
];
};
/**
* Gets the user's input from the input textarea.
*
* @returns {string}
*/
InputWaiter.prototype.get = function() {
return document.getElementById("input-text").value;
};
/**
* Sets the input in the input textarea.
*
* @param {string} input
*
* @fires Manager#statechange
*/
InputWaiter.prototype.set = function(input) {
document.getElementById("input-text").value = input;
window.dispatchEvent(this.manager.statechange);
};
/**
* Displays information about the input.
*
* @param {number} length - The length of the current input string
* @param {number} lines - The number of the lines in the current input string
*/
InputWaiter.prototype.set_input_info = function(length, lines) {
var width = length.toString().length;
width = width < 2 ? 2 : width;
var length_str = Utils.pad(length.toString(), width, " ").replace(/ /g, "&nbsp;");
var lines_str = Utils.pad(lines.toString(), width, " ").replace(/ /g, "&nbsp;");
document.getElementById("input-info").innerHTML = "length: " + length_str + "<br>lines: " + lines_str;
};
/**
* Handler for input scroll events.
* Scrolls the highlighter pane to match the input textarea position and updates history state.
*
* @param {event} e
*
* @fires Manager#statechange
*/
InputWaiter.prototype.input_change = function(e) {
// Remove highlighting from input and output panes as the offsets might be different now
this.manager.highlighter.remove_highlights();
// Reset recipe progress as any previous processing will be redundant now
this.app.progress = 0;
// Update the input metadata info
var input_text = this.get(),
lines = input_text.count("\n") + 1;
this.set_input_info(input_text.length, lines);
if (this.bad_keys.indexOf(e.keyCode) < 0) {
// Fire the statechange event as the input has been modified
window.dispatchEvent(this.manager.statechange);
}
};
/**
* Handler for input dragover events.
* Gives the user a visual cue to show that items can be dropped here.
*
* @param {event} e
*/
InputWaiter.prototype.input_dragover = function(e) {
// This will be set if we're dragging an operation
if (e.dataTransfer.effectAllowed === "move")
return false;
e.stopPropagation();
e.preventDefault();
e.target.classList.add("dropping-file");
};
/**
* Handler for input dragleave events.
* Removes the visual cue.
*
* @param {event} e
*/
InputWaiter.prototype.input_dragleave = function(e) {
e.stopPropagation();
e.preventDefault();
e.target.classList.remove("dropping-file");
};
/**
* Handler for input drop events.
* Loads the dragged data into the input textarea.
*
* @param {event} e
*/
InputWaiter.prototype.input_drop = function(e) {
// This will be set if we're dragging an operation
if (e.dataTransfer.effectAllowed === "move")
return false;
e.stopPropagation();
e.preventDefault();
var el = e.target,
file = e.dataTransfer.files[0],
text = e.dataTransfer.getData("Text"),
reader = new FileReader(),
input_charcode = "",
offset = 0,
CHUNK_SIZE = 20480; // 20KB
var set_input = function() {
if (input_charcode.length > 100000 && this.app.auto_bake_) {
this.manager.controls.set_auto_bake(false);
this.app.alert("Turned off Auto Bake as the input is large", "warning", 5000);
}
this.set(input_charcode);
var recipe_config = this.app.get_recipe_config();
if (!recipe_config[0] || recipe_config[0].op != "From Hex") {
recipe_config.unshift({op:"From Hex",args:["Space"]});
this.app.set_recipe_config(recipe_config);
}
el.classList.remove("loading_file");
}.bind(this);
var seek = function() {
if (offset >= file.size) {
set_input();
return;
}
el.value = "Processing... " + Math.round(offset / file.size * 100) + "%";
var slice = file.slice(offset, offset + CHUNK_SIZE);
reader.readAsArrayBuffer(slice);
}.bind(this);
reader.onload = function(e) {
var data = new Uint8Array(reader.result);
input_charcode += Utils.to_hex_fast(data);
offset += CHUNK_SIZE;
seek();
}.bind(this);
el.classList.remove("dropping-file");
if (file) {
el.classList.add("loading_file");
seek();
} else if (text) {
this.set(text);
}
};
/**
* Handler for clear IO events.
* Resets the input, output and info areas.
*
* @fires Manager#statechange
*/
InputWaiter.prototype.clear_io_click = function() {
this.manager.highlighter.remove_highlights();
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);
};

263
src/js/views/html/Manager.js Executable file
View file

@ -0,0 +1,263 @@
/**
* This object controls the Waiters responsible for handling events from all areas of the app.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {HTMLApp} app - The main view object for CyberChef.
*/
var Manager = function(app) {
this.app = app;
// Define custom events
/**
* @event Manager#appstart
*/
this.appstart = new CustomEvent("appstart", {bubbles: true});
/**
* @event Manager#operationadd
*/
this.operationadd = new CustomEvent("operationadd", {bubbles: true});
/**
* @event Manager#operationremove
*/
this.operationremove = new CustomEvent("operationremove", {bubbles: true});
/**
* @event Manager#oplistcreate
*/
this.oplistcreate = new CustomEvent("oplistcreate", {bubbles: true});
/**
* @event Manager#statechange
*/
this.statechange = new CustomEvent("statechange", {bubbles: true});
// Define Waiter objects to handle various areas
this.window = new WindowWaiter(this.app);
this.controls = new ControlsWaiter(this.app, this);
this.recipe = new RecipeWaiter(this.app, this);
this.ops = new OperationsWaiter(this.app, this);
this.input = new InputWaiter(this.app, this);
this.output = new OutputWaiter(this.app, this);
this.options = new OptionsWaiter(this.app);
this.highlighter = new HighlighterWaiter(this.app);
this.seasonal = new SeasonalWaiter(this.app, this);
// Object to store dynamic handlers to fire on elements that may not exist yet
this.dynamic_handlers = {};
this.initialise_event_listeners();
};
/**
* Sets up the various components and listeners.
*/
Manager.prototype.setup = function() {
this.recipe.initialise_operation_drag_n_drop();
this.controls.auto_bake_change();
this.seasonal.load();
};
/**
* Main function to handle the creation of the event listeners.
*/
Manager.prototype.initialise_event_listeners = function() {
// Global
window.addEventListener("resize", this.window.window_resize.bind(this.window));
window.addEventListener("blur", this.window.window_blur.bind(this.window));
window.addEventListener("focus", this.window.window_focus.bind(this.window));
window.addEventListener("statechange", this.app.state_change.bind(this.app));
window.addEventListener("popstate", this.app.pop_state.bind(this.app));
// Controls
document.getElementById("bake").addEventListener("click", this.controls.bake_click.bind(this.controls));
document.getElementById("auto-bake").addEventListener("change", this.controls.auto_bake_change.bind(this.controls));
document.getElementById("step").addEventListener("click", this.controls.step_click.bind(this.controls));
document.getElementById("clr-recipe").addEventListener("click", this.controls.clear_recipe_click.bind(this.controls));
document.getElementById("clr-breaks").addEventListener("click", this.controls.clear_breaks_click.bind(this.controls));
document.getElementById("save").addEventListener("click", this.controls.save_click.bind(this.controls));
document.getElementById("save-button").addEventListener("click", this.controls.save_button_click.bind(this.controls));
document.getElementById("save-link-recipe-checkbox").addEventListener("change", this.controls.slr_check_change.bind(this.controls));
document.getElementById("save-link-input-checkbox").addEventListener("change", this.controls.sli_check_change.bind(this.controls));
document.getElementById("load").addEventListener("click", this.controls.load_click.bind(this.controls));
document.getElementById("load-delete-button").addEventListener("click", this.controls.load_delete_click.bind(this.controls));
document.getElementById("load-name").addEventListener("change", this.controls.load_name_change.bind(this.controls));
document.getElementById("load-button").addEventListener("click", this.controls.load_button_click.bind(this.controls));
this.add_multi_event_listener("#save-text", "keyup paste", this.controls.save_text_change, this.controls);
// Operations
this.add_multi_event_listener("#search", "keyup paste search", this.ops.search_operations, this.ops);
this.add_dynamic_listener(".op_list li.operation", "dblclick", this.ops.operation_dblclick, this.ops);
document.getElementById("edit-favourites").addEventListener("click", this.ops.edit_favourites_click.bind(this.ops));
document.getElementById("save-favourites").addEventListener("click", this.ops.save_favourites_click.bind(this.ops));
document.getElementById("reset-favourites").addEventListener("click", this.ops.reset_favourites_click.bind(this.ops));
this.add_dynamic_listener(".op_list .op-icon", "mouseover", this.ops.op_icon_mouseover, this.ops);
this.add_dynamic_listener(".op_list .op-icon", "mouseleave", this.ops.op_icon_mouseleave, this.ops);
this.add_dynamic_listener(".op_list", "oplistcreate", this.ops.op_list_create, this.ops);
this.add_dynamic_listener("li.operation", "operationadd", this.recipe.op_add.bind(this.recipe));
// Recipe
this.add_dynamic_listener(".arg", "keyup", this.recipe.ing_change, this.recipe);
this.add_dynamic_listener(".arg", "change", this.recipe.ing_change, this.recipe);
this.add_dynamic_listener(".disable-icon", "click", this.recipe.disable_click, this.recipe);
this.add_dynamic_listener(".breakpoint", "click", this.recipe.breakpoint_click, this.recipe);
this.add_dynamic_listener("#rec_list li.operation", "dblclick", this.recipe.operation_dblclick, this.recipe);
this.add_dynamic_listener("#rec_list li.operation > div", "dblclick", this.recipe.operation_child_dblclick, this.recipe);
this.add_dynamic_listener("#rec_list .input-group .dropdown-menu a", "click", this.recipe.dropdown_toggle_click, this.recipe);
this.add_dynamic_listener("#rec_list", "operationremove", this.recipe.op_remove.bind(this.recipe));
// Input
this.add_multi_event_listener("#input-text", "keyup paste", this.input.input_change, this.input);
document.getElementById("reset-layout").addEventListener("click", this.app.reset_layout.bind(this.app));
document.getElementById("clr-io").addEventListener("click", this.input.clear_io_click.bind(this.input));
document.getElementById("input-text").addEventListener("dragover", this.input.input_dragover.bind(this.input));
document.getElementById("input-text").addEventListener("dragleave", this.input.input_dragleave.bind(this.input));
document.getElementById("input-text").addEventListener("drop", this.input.input_drop.bind(this.input));
document.getElementById("input-text").addEventListener("scroll", this.highlighter.input_scroll.bind(this.highlighter));
document.getElementById("input-text").addEventListener("mouseup", this.highlighter.input_mouseup.bind(this.highlighter));
document.getElementById("input-text").addEventListener("mousemove", this.highlighter.input_mousemove.bind(this.highlighter));
this.add_multi_event_listener("#input-text", "mousedown dblclick select", this.highlighter.input_mousedown, this.highlighter);
// Output
document.getElementById("save-to-file").addEventListener("click", this.output.save_click.bind(this.output));
document.getElementById("switch").addEventListener("click", this.output.switch_click.bind(this.output));
document.getElementById("undo-switch").addEventListener("click", this.output.undo_switch_click.bind(this.output));
document.getElementById("output-text").addEventListener("scroll", this.highlighter.output_scroll.bind(this.highlighter));
document.getElementById("output-text").addEventListener("mouseup", this.highlighter.output_mouseup.bind(this.highlighter));
document.getElementById("output-text").addEventListener("mousemove", this.highlighter.output_mousemove.bind(this.highlighter));
document.getElementById("output-html").addEventListener("mouseup", this.highlighter.output_html_mouseup.bind(this.highlighter));
document.getElementById("output-html").addEventListener("mousemove", this.highlighter.output_html_mousemove.bind(this.highlighter));
this.add_multi_event_listener("#output-text", "mousedown dblclick select", this.highlighter.output_mousedown, this.highlighter);
this.add_multi_event_listener("#output-html", "mousedown dblclick select", this.highlighter.output_html_mousedown, this.highlighter);
// Options
document.getElementById("options").addEventListener("click", this.options.options_click.bind(this.options));
document.getElementById("reset-options").addEventListener("click", this.options.reset_options_click.bind(this.options));
$(".option-item input:checkbox").on("switchChange.bootstrapSwitch", this.options.switch_change.bind(this.options));
$(".option-item input:checkbox").on("switchChange.bootstrapSwitch", this.options.set_word_wrap.bind(this.options));
this.add_dynamic_listener(".option-item input[type=number]", "keyup", this.options.number_change, this.options);
this.add_dynamic_listener(".option-item input[type=number]", "change", this.options.number_change, this.options);
this.add_dynamic_listener(".option-item select", "change", this.options.select_change, this.options);
// Misc
document.getElementById("alert-close").addEventListener("click", this.app.alert_close_click.bind(this.app));
};
/**
* Adds an event listener to each element in the specified group.
*
* @param {string} selector - A selector string for the element group to add the event to, see
* this.get_all()
* @param {string} event_type - The event to listen for
* @param {function} callback - The function to execute when the event is triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Calls the clickable function whenever any element with the .clickable class is clicked
* this.add_listeners(".clickable", "click", this.clickable, this);
*/
Manager.prototype.add_listeners = function(selector, event_type, callback, scope) {
scope = scope || this;
[].forEach.call(document.querySelectorAll(selector), function(el) {
el.addEventListener(event_type, callback.bind(scope));
});
};
/**
* Adds multiple event listeners to the specified element.
*
* @param {string} selector - A selector string for the element to add the events to
* @param {string} event_types - A space-separated string of all the event types to listen for
* @param {function} callback - The function to execute when the events are triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Calls the search function whenever the the keyup, paste or search events are triggered on the
* // search element
* this.add_multi_event_listener("search", "keyup paste search", this.search, this);
*/
Manager.prototype.add_multi_event_listener = function(selector, event_types, callback, scope) {
var evs = event_types.split(" ");
for (var i = 0; i < evs.length; i++) {
document.querySelector(selector).addEventListener(evs[i], callback.bind(scope));
}
};
/**
* Adds multiple event listeners to each element in the specified group.
*
* @param {string} selector - A selector string for the element group to add the events to
* @param {string} event_types - A space-separated string of all the event types to listen for
* @param {function} callback - The function to execute when the events are triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Calls the save function whenever the the keyup or paste events are triggered on any element
* // with the .saveable class
* this.add_multi_event_listener(".saveable", "keyup paste", this.save, this);
*/
Manager.prototype.add_multi_event_listeners = function(selector, event_types, callback, scope) {
var evs = event_types.split(" ");
for (var i = 0; i < evs.length; i++) {
this.add_listeners(selector, evs[i], callback, scope);
}
};
/**
* Adds an event listener to the global document object which will listen on dynamic elements which
* may not exist in the DOM yet.
*
* @param {string} selector - A selector string for the element(s) to add the event to
* @param {string} event_type - The event(s) to listen for
* @param {function} callback - The function to execute when the event(s) is/are triggered
* @param {Object} [scope=this] - The object to bind to the callback function
*
* @example
* // Pops up an alert whenever any button is clicked, even if it is added to the DOM after this
* // listener is created
* this.add_dynamic_listener("button", "click", alert, this);
*/
Manager.prototype.add_dynamic_listener = function(selector, event_type, callback, scope) {
var event_config = {
selector: selector,
callback: callback.bind(scope || this)
};
if (this.dynamic_handlers.hasOwnProperty(event_type)) {
// Listener already exists, add new handler to the appropriate list
this.dynamic_handlers[event_type].push(event_config);
} else {
this.dynamic_handlers[event_type] = [event_config];
// Set up listener for this new type
document.addEventListener(event_type, this.dynamic_listener_handler.bind(this));
}
};
/**
* Handler for dynamic events. This function is called for any dynamic event and decides which
* callback(s) to execute based on the type and selector.
*
* @param {Event} e - The event to be handled
*/
Manager.prototype.dynamic_listener_handler = function(e) {
var handlers = this.dynamic_handlers[e.type],
matches = e.target.matches ||
e.target.webkitMatchesSelector ||
e.target.mozMatchesSelector ||
e.target.msMatchesSelector ||
e.target.oMatchesSelector;
for (var i = 0; i < handlers.length; i++) {
if (matches && e.target[matches.name](handlers[i].selector)) {
handlers[i].callback(e);
}
}
};

View file

@ -0,0 +1,282 @@
/* globals Sortable */
/**
* Waiter to handle events related to the operations.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {HTMLApp} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
var OperationsWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
this.options = {};
this.remove_intent = false;
};
/**
* Handler for search events.
* Finds operations which match the given search term and displays them under the search box.
*
* @param {event} e
*/
OperationsWaiter.prototype.search_operations = function(e) {
var ops, selected;
if (e.type == "search") { // Search
e.preventDefault();
ops = document.querySelectorAll("#search-results li");
if (ops.length) {
selected = this.get_selected_op(ops);
if (selected > -1) {
this.manager.recipe.add_operation(ops[selected].innerHTML);
this.app.auto_bake();
}
}
}
if (e.keyCode == 13) { // Return
e.preventDefault();
} else if (e.keyCode == 40) { // Down
e.preventDefault();
ops = document.querySelectorAll("#search-results li");
if (ops.length) {
selected = this.get_selected_op(ops);
if (selected > -1) {
ops[selected].classList.remove("selected-op");
}
if (selected == ops.length-1) selected = -1;
ops[selected+1].classList.add("selected-op");
}
} else if (e.keyCode == 38) { // Up
e.preventDefault();
ops = document.querySelectorAll("#search-results li");
if (ops.length) {
selected = this.get_selected_op(ops);
if (selected > -1) {
ops[selected].classList.remove("selected-op");
}
if (selected === 0) selected = ops.length;
ops[selected-1].classList.add("selected-op");
}
} else {
var search_results_el = document.getElementById("search-results"),
el = e.target,
str = el.value;
while (search_results_el.firstChild) {
search_results_el.removeChild(search_results_el.firstChild);
}
$("#categories .in").collapse("hide");
if (str) {
var matched_ops = this.filter_operations(str, true),
matched_ops_html = "";
for (var i = 0; i < matched_ops.length; i++) {
matched_ops_html += matched_ops[i].to_stub_html();
}
search_results_el.innerHTML = matched_ops_html;
search_results_el.dispatchEvent(this.manager.oplistcreate);
}
}
};
/**
* Filters operations based on the search string and returns the matching ones.
*
* @param {string} search_str
* @param {boolean} highlight - Whether or not to highlight the matching string in the operation
* name and description
* @returns {string[]}
*/
OperationsWaiter.prototype.filter_operations = function(search_str, highlight) {
var matched_ops = [],
matched_descs = [];
search_str = search_str.toLowerCase();
for (var op_name in this.app.operations) {
var op = this.app.operations[op_name],
name_pos = op_name.toLowerCase().indexOf(search_str),
desc_pos = op.description.toLowerCase().indexOf(search_str);
if (name_pos >= 0 || desc_pos >= 0) {
var operation = new HTMLOperation(op_name, this.app.operations[op_name], this.app, this.manager);
if (highlight) {
operation.highlight_search_string(search_str, name_pos, desc_pos);
}
if (name_pos < 0) {
matched_ops.push(operation);
} else {
matched_descs.push(operation);
}
}
}
return matched_descs.concat(matched_ops);
};
/**
* Finds the operation which has been selected using keyboard shortcuts. This will have the class
* 'selected-op' set. Returns the index of the operation within the given list.
*
* @param {element[]} ops
* @returns {number}
*/
OperationsWaiter.prototype.get_selected_op = function(ops) {
for (var i = 0; i < ops.length; i++) {
if (ops[i].classList.contains("selected-op")) {
return i;
}
}
return -1;
};
/**
* Handler for oplistcreate events.
*
* @listens Manager#oplistcreate
* @param {event} e
*/
OperationsWaiter.prototype.op_list_create = function(e) {
this.manager.recipe.create_sortable_seed_list(e.target);
$("[data-toggle=popover]").popover();
};
/**
* Handler for operation doubleclick events.
* Adds the operation to the recipe and auto bakes.
*
* @param {event} e
*/
OperationsWaiter.prototype.operation_dblclick = function(e) {
var li = e.target;
this.manager.recipe.add_operation(li.textContent);
this.app.auto_bake();
};
/**
* Handler for edit favourites click events.
* Sets up the 'Edit favourites' pane and displays it.
*
* @param {event} e
*/
OperationsWaiter.prototype.edit_favourites_click = function(e) {
e.preventDefault();
e.stopPropagation();
// Add favourites to modal
var fav_cat = this.app.categories.filter(function(c) {
return c.name == "Favourites";
})[0];
var html = "";
for (var i = 0; i < fav_cat.ops.length; i++) {
var op_name = fav_cat.ops[i];
var operation = new HTMLOperation(op_name, this.app.operations[op_name], this.app, this.manager);
html += operation.to_stub_html(true);
}
var edit_favourites_list = document.getElementById("edit-favourites-list");
edit_favourites_list.innerHTML = html;
this.remove_intent = false;
var editable_list = Sortable.create(edit_favourites_list, {
filter: '.remove-icon',
onFilter: function (evt) {
var el = editable_list.closest(evt.item);
if (el) {
$(el).popover("destroy");
el.parentNode.removeChild(el);
}
},
onEnd: function(evt) {
if (this.remove_intent) evt.item.remove();
}.bind(this),
});
Sortable.utils.on(edit_favourites_list, "dragleave", function() {
this.remove_intent = true;
}.bind(this));
Sortable.utils.on(edit_favourites_list, "dragover", function() {
this.remove_intent = false;
}.bind(this));
$("#edit-favourites-list [data-toggle=popover]").popover();
$("#favourites-modal").modal();
};
/**
* Handler for save favourites click events.
* Saves the selected favourites and reloads them.
*/
OperationsWaiter.prototype.save_favourites_click = function() {
var favourites_list = [],
favs = document.querySelectorAll("#edit-favourites-list li");
for (var i = 0; i < favs.length; i++) {
favourites_list.push(favs[i].textContent);
}
this.app.save_favourites(favourites_list);
this.app.load_favourites();
this.app.populate_operations_list();
this.manager.recipe.initialise_operation_drag_n_drop();
};
/**
* Handler for reset favourites click events.
* Resets favourites to their defaults.
*/
OperationsWaiter.prototype.reset_favourites_click = function() {
this.app.reset_favourites();
};
/**
* Handler for op_icon mouseover events.
* Hides any popovers already showing on the operation so that there aren't two at once.
*
* @param {event} e
*/
OperationsWaiter.prototype.op_icon_mouseover = function(e) {
var op_el = e.target.parentNode;
if (e.target.getAttribute("data-toggle") == "popover") {
$(op_el).popover("hide");
}
};
/**
* Handler for op_icon mouseleave events.
* If this icon created a popover and we're moving back to the operation element, display the
* operation popover again.
*
* @param {event} e
*/
OperationsWaiter.prototype.op_icon_mouseleave = function(e) {
var op_el = e.target.parentNode,
to_el = e.toElement || e.relatedElement;
if (e.target.getAttribute("data-toggle") == "popover" && to_el === op_el) {
$(op_el).popover("show");
}
};

View file

@ -0,0 +1,132 @@
/**
* Waiter to handle events related to the CyberChef options.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {HTMLApp} app - The main view object for CyberChef.
*/
var OptionsWaiter = function(app) {
this.app = app;
};
/**
* Loads options and sets values of switches and inputs to match them.
*
* @param {Object} options
*/
OptionsWaiter.prototype.load = function(options) {
$(".option-item input:checkbox").bootstrapSwitch({
size: "small",
animate: false,
});
for (var option in options) {
this.app.options[option] = options[option];
}
// Set options to match object
var cboxes = document.querySelectorAll("#options-body input[type=checkbox]");
for (var i = 0; i < cboxes.length; i++) {
$(cboxes[i]).bootstrapSwitch("state", this.app.options[cboxes[i].getAttribute("option")]);
}
var 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}));
}
var selects = document.querySelectorAll("#options-body select");
for (i = 0; i < selects.length; i++) {
selects[i].value = this.app.options[selects[i].getAttribute("option")];
selects[i].dispatchEvent(new CustomEvent("change", {bubbles: true}));
}
};
/**
* Handler for options click events.
* Dispays the options pane.
*/
OptionsWaiter.prototype.options_click = function() {
$("#options-modal").modal();
};
/**
* Handler for reset options click events.
* Resets options back to their default values.
*/
OptionsWaiter.prototype.reset_options_click = function() {
this.load(this.app.doptions);
};
/**
* Handler for switch change events.
* Modifies the option state and saves it to local storage.
*
* @param {event} e
* @param {boolean} state
*/
OptionsWaiter.prototype.switch_change = function(e, state) {
var el = e.target,
option = el.getAttribute("option");
this.app.options[option] = state;
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.number_change = function(e) {
var el = e.target,
option = el.getAttribute("option");
this.app.options[option] = parseInt(el.value, 10);
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.select_change = function(e) {
var el = e.target,
option = el.getAttribute("option");
this.app.options[option] = el.value;
localStorage.setItem("options", JSON.stringify(this.app.options));
};
/**
* Sets or unsets word wrap on the input and output depending on the word_wrap option value.
*/
OptionsWaiter.prototype.set_word_wrap = 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.word_wrap) {
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");
}
};

139
src/js/views/html/OutputWaiter.js Executable file
View file

@ -0,0 +1,139 @@
/**
* Waiter to handle events related to the output.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {HTMLApp} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
var OutputWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
};
/**
* Gets the output string from the output textarea.
*
* @returns {string}
*/
OutputWaiter.prototype.get = function() {
return document.getElementById("output-text").value;
};
/**
* Sets the output in the output textarea.
*
* @param {string} data_str - The output string/HTML
* @param {string} type - The data type of the output
* @param {number} duration - The length of time (ms) it took to generate the output
*/
OutputWaiter.prototype.set = function(data_str, type, duration) {
var output_text = document.getElementById("output-text"),
output_html = document.getElementById("output-html"),
output_highlighter = document.getElementById("output-highlighter"),
input_highlighter = document.getElementById("input-highlighter");
if (type == "html") {
output_text.style.display = "none";
output_html.style.display = "block";
output_highlighter.display = "none";
input_highlighter.display = "none";
output_text.value = "";
output_html.innerHTML = data_str;
// Execute script sections
var script_elements = output_html.querySelectorAll("script");
for (var i = 0; i < script_elements.length; i++) {
try {
eval(script_elements[i].innerHTML); // jshint ignore:line
} catch (err) {
console.error(err);
}
}
} else {
output_text.style.display = "block";
output_html.style.display = "none";
output_highlighter.display = "block";
input_highlighter.display = "block";
output_text.value = Utils.printable(data_str, true);
output_html.innerHTML = "";
}
this.manager.highlighter.remove_highlights();
var lines = data_str.count("\n") + 1;
this.set_output_info(data_str.length, lines, duration);
};
/**
* Displays information about the output.
*
* @param {number} length - The length of the current output string
* @param {number} lines - The number of the lines in the current output string
* @param {number} duration - The length of time (ms) it took to generate the output
*/
OutputWaiter.prototype.set_output_info = function(length, lines, duration) {
var width = length.toString().length;
width = width < 4 ? 4 : width;
var length_str = Utils.pad(length.toString(), width, " ").replace(/ /g, "&nbsp;");
var lines_str = Utils.pad(lines.toString(), width, " ").replace(/ /g, "&nbsp;");
var time_str = Utils.pad(duration.toString() + "ms", width, " ").replace(/ /g, "&nbsp;");
document.getElementById("output-info").innerHTML = "time: " + time_str +
"<br>length: " + length_str +
"<br>lines: " + lines_str;
document.getElementById("input-selection-info").innerHTML = "";
document.getElementById("output-selection-info").innerHTML = "";
};
/**
* Handler for save click events.
* Saves the current output to a file, downloaded as a URL octet stream.
*/
OutputWaiter.prototype.save_click = function() {
var data = Utils.to_base64(this.app.dish_str),
filename = window.prompt("Please enter a filename:", "download.dat");
if (filename) {
var el = document.createElement("a");
el.setAttribute("href", "data:application/octet-stream;base64;charset=utf-8," + data);
el.setAttribute("download", filename);
// Firefox requires that the element be added to the DOM before it can be clicked
el.style.display = "none";
document.body.appendChild(el);
el.click();
el.remove();
}
};
/**
* Handler for switch click events.
* Moves the current output into the input textarea.
*/
OutputWaiter.prototype.switch_click = function() {
this.switch_orig_data = this.manager.input.get();
document.getElementById("undo-switch").disabled = false;
this.app.set_input(this.app.dish_str);
};
/**
* Handler for undo switch click events.
* Removes the output from the input and replaces the input that was removed.
*/
OutputWaiter.prototype.undo_switch_click = function() {
this.app.set_input(this.switch_orig_data);
document.getElementById("undo-switch").disabled = true;
};

416
src/js/views/html/RecipeWaiter.js Executable file
View file

@ -0,0 +1,416 @@
/* globals Sortable */
/**
* Waiter to handle events related to the recipe.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {HTMLApp} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
var RecipeWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
this.remove_intent = false;
};
/**
* Sets up the drag and drop capability for operations in the operations and recipe areas.
*/
RecipeWaiter.prototype.initialise_operation_drag_n_drop = function() {
var rec_list = document.getElementById("rec_list"),
op_lists = document.querySelectorAll(".category .op_list");
// Recipe list
Sortable.create(rec_list, {
group: "recipe",
sort: true,
animation: 0,
delay: 0,
filter: ".arg-input,.arg", // Relies on commenting out a line in Sortable.js which calls evt.preventDefault()
setData: function(dataTransfer, drag_el) {
dataTransfer.setData("Text", drag_el.querySelector(".arg-title").textContent);
},
onEnd: function(evt) {
if (this.remove_intent) {
evt.item.remove();
evt.target.dispatchEvent(this.manager.operationremove);
}
}.bind(this)
});
Sortable.utils.on(rec_list, "dragover", function() {
this.remove_intent = false;
}.bind(this));
Sortable.utils.on(rec_list, "dragleave", function() {
this.remove_intent = true;
this.app.progress = 0;
}.bind(this));
// Favourites category
document.querySelector("#categories a").addEventListener("dragover", this.fav_dragover.bind(this));
document.querySelector("#categories a").addEventListener("dragleave", this.fav_dragleave.bind(this));
document.querySelector("#categories a").addEventListener("drop", this.fav_drop.bind(this));
};
/**
* Creates a drag-n-droppable seed list of operations.
*
* @param {element} list_el - The list the initialise
*/
RecipeWaiter.prototype.create_sortable_seed_list = function(list_el) {
Sortable.create(list_el, {
group: {
name: "recipe",
pull: "clone",
put: false
},
sort: false,
setData: function(dataTransfer, drag_el) {
dataTransfer.setData("Text", drag_el.textContent);
},
onStart: function(evt) {
$(evt.item).popover("destroy");
evt.item.setAttribute("data-toggle", "popover-disabled");
},
onEnd: this.op_sort_end.bind(this)
});
};
/**
* Handler for operation sort end events.
* Removes the operation from the list if it has been dropped outside. If not, adds it to the list
* at the appropriate place and initialises it.
*
* @fires Manager#operationadd
* @param {event} evt
*/
RecipeWaiter.prototype.op_sort_end = function(evt) {
if (this.remove_intent) {
if (evt.item.parentNode.id == "rec_list") {
evt.item.remove();
}
return;
}
// Reinitialise the popover on the original element in the ops list because for some reason it
// gets destroyed and recreated.
$(evt.clone).popover();
$(evt.clone).children("[data-toggle=popover]").popover();
if (evt.item.parentNode.id !== "rec_list") {
return;
}
this.build_recipe_operation(evt.item);
evt.item.dispatchEvent(this.manager.operationadd);
};
/**
* Handler for favourite dragover events.
* If the element being dragged is an operation, displays a visual cue so that the user knows it can
* be dropped here.
*
* @param {event} e
*/
RecipeWaiter.prototype.fav_dragover = function(e) {
if (e.dataTransfer.effectAllowed !== "move")
return false;
e.stopPropagation();
e.preventDefault();
if (e.target.className && e.target.className.indexOf("category-title") > -1) {
// Hovering over the a
e.target.classList.add("favourites-hover");
} else if (e.target.parentNode.className && e.target.parentNode.className.indexOf("category-title") > -1) {
// Hovering over the Edit button
e.target.parentNode.classList.add("favourites-hover");
} else if (e.target.parentNode.parentNode.className && e.target.parentNode.parentNode.className.indexOf("category-title") > -1) {
// Hovering over the image on the Edit button
e.target.parentNode.parentNode.classList.add("favourites-hover");
}
};
/**
* Handler for favourite dragleave events.
* Removes the visual cue.
*
* @param {event} e
*/
RecipeWaiter.prototype.fav_dragleave = function(e) {
e.stopPropagation();
e.preventDefault();
document.querySelector("#categories a").classList.remove("favourites-hover");
};
/**
* Handler for favourite drop events.
* Adds the dragged operation to the favourites list.
*
* @param {event} e
*/
RecipeWaiter.prototype.fav_drop = function(e) {
e.stopPropagation();
e.preventDefault();
e.target.classList.remove("favourites-hover");
var op_name = e.dataTransfer.getData("Text");
this.app.add_favourite(op_name);
};
/**
* Handler for ingredient change events.
*
* @fires Manager#statechange
*/
RecipeWaiter.prototype.ing_change = function() {
window.dispatchEvent(this.manager.statechange);
};
/**
* Handler for disable click events.
* Updates the icon status.
*
* @fires Manager#statechange
* @param {event} e
*/
RecipeWaiter.prototype.disable_click = function(e) {
var icon = e.target;
if (icon.getAttribute("disabled") == "false") {
icon.setAttribute("disabled", "true");
icon.classList.add("disable-icon-selected");
icon.parentNode.parentNode.classList.add("disabled");
} else {
icon.setAttribute("disabled", "false");
icon.classList.remove("disable-icon-selected");
icon.parentNode.parentNode.classList.remove("disabled");
}
this.app.progress = 0;
window.dispatchEvent(this.manager.statechange);
};
/**
* Handler for breakpoint click events.
* Updates the icon status.
*
* @fires Manager#statechange
* @param {event} e
*/
RecipeWaiter.prototype.breakpoint_click = function(e) {
var bp = e.target;
if (bp.getAttribute("break") == "false") {
bp.setAttribute("break", "true");
bp.classList.add("breakpoint-selected");
} else {
bp.setAttribute("break", "false");
bp.classList.remove("breakpoint-selected");
}
window.dispatchEvent(this.manager.statechange);
};
/**
* Handler for operation doubleclick events.
* Removes the operation from the recipe and auto bakes.
*
* @fires Manager#statechange
* @param {event} e
*/
RecipeWaiter.prototype.operation_dblclick = function(e) {
e.target.remove();
window.dispatchEvent(this.manager.statechange);
};
/**
* Handler for operation child doubleclick events.
* Removes the operation from the recipe.
*
* @fires Manager#statechange
* @param {event} e
*/
RecipeWaiter.prototype.operation_child_dblclick = function(e) {
e.target.parentNode.remove();
window.dispatchEvent(this.manager.statechange);
};
/**
* Generates a configuration object to represent the current recipe.
*
* @returns {recipe_config}
*/
RecipeWaiter.prototype.get_config = function() {
var config = [], ingredients, ing_list, disabled, bp, item,
operations = document.querySelectorAll("#rec_list li.operation");
for (var i = 0; i < operations.length; i++) {
ingredients = [];
disabled = operations[i].querySelector(".disable-icon");
bp = operations[i].querySelector(".breakpoint");
ing_list = operations[i].querySelectorAll(".arg");
for (var j = 0; j < ing_list.length; j++) {
if (ing_list[j].getAttribute("type") == "checkbox") {
// checkbox
ingredients[j] = ing_list[j].checked;
} else if (ing_list[j].classList.contains("toggle-string")) {
// toggle_string
ingredients[j] = {
option: ing_list[j].previousSibling.children[0].textContent.slice(0, -1),
string: ing_list[j].value
};
} else {
// all others
ingredients[j] = ing_list[j].value;
}
}
item = {
op: operations[i].querySelector(".arg-title").textContent,
args: ingredients
};
if (disabled && disabled.getAttribute("disabled") == "true") {
item.disabled = true;
}
if (bp && bp.getAttribute("break") == "true") {
item.breakpoint = true;
}
config.push(item);
}
return config;
};
/**
* Moves or removes the breakpoint indicator in the recipe based on the position.
*
* @param {number} position
*/
RecipeWaiter.prototype.update_breakpoint_indicator = function(position) {
var operations = document.querySelectorAll("#rec_list li.operation");
for (var i = 0; i < operations.length; i++) {
if (i == position) {
operations[i].classList.add("break");
} else {
operations[i].classList.remove("break");
}
}
};
/**
* Given an operation stub element, this function converts it into a full recipe element with
* arguments.
*
* @param {element} el - The operation stub element from the operations pane
*/
RecipeWaiter.prototype.build_recipe_operation = function(el) {
var op_name = el.textContent;
var op = new HTMLOperation(op_name, this.app.operations[op_name], this.app, this.manager);
el.innerHTML = op.to_full_html();
if (this.app.operations[op_name].flow_control) {
el.classList.add("flow-control-op");
}
// Disable auto-bake if this is a manual op - this should be moved to the 'operationadd'
// handler after event restructuring
if (op.manual_bake && this.app.auto_bake_) {
this.manager.controls.set_auto_bake(false);
this.app.alert("Auto-Bake is disabled by default when using this operation.", "info", 5000);
}
};
/**
* Adds the specified operation to the recipe.
*
* @fires Manager#operationadd
* @param {string} name - The name of the operation to add
* @returns {element}
*/
RecipeWaiter.prototype.add_operation = function(name) {
var item = document.createElement("li");
item.classList.add("operation");
item.innerHTML = name;
this.build_recipe_operation(item);
document.getElementById("rec_list").appendChild(item);
item.dispatchEvent(this.manager.operationadd);
return item;
};
/**
* Removes all operations from the recipe.
*
* @fires Manager#operationremove
*/
RecipeWaiter.prototype.clear_recipe = function() {
var rec_list = document.getElementById("rec_list");
while (rec_list.firstChild) {
rec_list.removeChild(rec_list.firstChild);
}
rec_list.dispatchEvent(this.manager.operationremove);
};
/**
* Handler for operation dropdown events from toggle_string arguments.
* Sets the selected option as the name of the button.
*
* @param {event} e
*/
RecipeWaiter.prototype.dropdown_toggle_click = function(e) {
var el = e.target,
button = el.parentNode.parentNode.previousSibling;
button.innerHTML = el.textContent + " <span class='caret'></span>";
this.ing_change();
};
/**
* Handler for operationadd events.
*
* @listens Manager#operationadd
* @fires Manager#statechange
* @param {event} e
*/
RecipeWaiter.prototype.op_add = function(e) {
window.dispatchEvent(this.manager.statechange);
};
/**
* Handler for operationremove events.
*
* @listens Manager#operationremove
* @fires Manager#statechange
* @param {event} e
*/
RecipeWaiter.prototype.op_remove = function(e) {
window.dispatchEvent(this.manager.statechange);
};

View file

@ -0,0 +1,254 @@
/**
* Waiter to handle seasonal events and easter eggs.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {HTMLApp} app - The main view object for CyberChef.
* @param {Manager} manager - The CyberChef event manager.
*/
var SeasonalWaiter = function(app, manager) {
this.app = app;
this.manager = manager;
};
/**
* Loads all relevant items depending on the current date.
*/
SeasonalWaiter.prototype.load = function() {
var now = new Date();
// Snowfall
if (now.getMonth() == 11 && now.getDate() > 12) { // Dec 13 -> Dec 31
this.app.options.snow = false;
this.create_snow_option();
this.manager.add_dynamic_listener(".option-item input:checkbox[option='snow']", "switchChange.bootstrapSwitch", this.let_it_snow, this);
this.manager.add_window_listener("resize", this.let_it_snow, this);
this.manager.add_listeners(".btn", "click", this.shake_off_snow, this);
if (now.getDate() == 25) this.let_it_snow();
}
// SpiderChef
// if (now.getMonth() == 3 && now.getDate() == 1) { // Apr 1
// this.insert_spider_icons();
// this.insert_spider_text();
// }
// Konami code
this.kkeys = [];
window.addEventListener("keydown", this.konami_code_listener.bind(this));
};
/**
* Replaces chef icons with spider icons.
* #spiderchef
*/
SeasonalWaiter.prototype.insert_spider_icons = function() {
var spider16 = "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAB3UlEQVQ4y2NgGJaAmYGBgVnf0oKJgYGBobWtXamqqoYTn2I4CI+LTzM2NTulpKbu+vPHz2dV5RWlluZmi3j5+KqFJSSEzpw8uQPdAEYYIzo5Kfjrl28rWFlZzjAzMYuEBQao3Lh+g+HGvbsMzExMDN++fWf4/PXLBzY2tqYNK1f2+4eHM2xcuRLigsT09Igf3384MTExbf767etBI319jU8fPsi+//jx/72HDxh5uLkZ7ty7y/Dz1687Avz8n2UUFR3Z2NjOySoqfmdhYGBg+PbtuwI7O8e5H79+8X379t357PnzYo+ePP7y6cuXc9++f69nYGRsvf/w4XdtLS2R799/bBUWFHr57sP7Jbs3b/ZkzswvUP3165fZ7z9//r988WIVAyPDr8tXr576+u3bpb9//7YwMjKeV1dV41NWVGoVEhDgPH761DJREeHaz1+/lqlpafUx6+jrRfz4+fPy+w8fTu/fsf3uw7t3L39+//4cv7DwGQYGhpdPbt9m4BcRFlNWVJC4fuvWASszs4C379792Ldt2xZBUdEdDP5hYSqQGIjDGa965uYKCalpZQwMDAxhMTG9DAwMDLaurhIkJY7A8IgGBgYGBgd3Dz2yUpeFo6O4rasrA9T24ZRxAAMTwMpgEJwLAAAAAElFTkSuQmCC",
spider32 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACYVBMVEUAAAAcJSU2Pz85QkM9RUWEhIWMjI2MkJEcJSU2Pz85QkM9RUWWlpc9RUVXXl4cJSU2Pz85QkM8REU9RUVRWFh6ens9RUVCSkpNVFRdY2McJSU5QkM7REQ9RUVGTk5KUlJQVldcY2Rla2uTk5WampscJSVUWltZX2BrcHF1e3scJSUjLCw9RUVASEhFTU1HTk9bYWJeZGRma2xudHV1eHiZmZocJSUyOjpJUFFQVldSWlpTWVpXXl5YXl5rb3B9fX6RkZIcJSUmLy8tNTU9RUVFTU1IT1BOVldRV1hTWlp0enocJSUfKChJUFBWXV1hZ2hnbGwcJSVETExLUlJLU1NNVVVPVlZYXl9cY2RiaGlobW5rcXFyd3h0eHgcJSUpMTFDS0tQV1dRV1hSWFlWXF1bYWJma2tobW5uc3SsrK0cJSVJUFBMVFROVlZVW1xZX2BdYmNhZ2hjaGhla2tqcHBscHE4Pz9KUlJRWVlSWVlXXF1aYGFbYWFfZWZlampqbW4cJSUgKSkiKysuNjY0PD01PT07QkNES0tHTk5JUFBMUlNMU1NOU1ROVVVPVVZRVlZRV1dSWVlWXFxXXV5aX2BbYWFbYWJcYmJcYmNcY2RdYmNgZmZhZmdkaWpkampkamtlamtla2tma2tma2xnbG1obW5pbG1pb3Bqb3Brb3BtcXJudHVvcHFvcXJvc3NwcXNwdXVxc3RzeXl1eXp2eXl3ent6e3x+gYKAhISBg4SKi4yLi4yWlpeampudnZ6fn6CkpaanqKiur6+vr7C4uLm6urq6u7u8vLy9vb3Av8DR0dL2b74UAAAAgHRSTlMAEBAQEBAQECAgICAgMDBAQEBAQEBAUFBQUGBgYGBgYGBgYGBgcHBwcHCAgICAgICAgICAgICPj4+Pj4+Pj4+Pj5+fn5+fn5+fn5+vr6+vr6+/v7+/v7+/v7+/v7+/z8/Pz8/Pz8/Pz8/P39/f39/f39/f39/f7+/v7+/v7+/v78x6RlYAAAGBSURBVDjLY2AYWUCSgUGAk4GBTdlUhQebvP7yjIgCPQbWzBMnjx5wwJSX37Rwfm1isqj9/iPHTuxYlyeMJi+yunfptBkZOw/uWj9h3vatcycu8eRGlldb3Vsts3ph/cFTh7fN3bCoe2Vf8+TZoQhTvBa6REozVC7cuPvQnmULJm1e2z+308eyJieEBSLPXbKQIUqQIczk+N6eNaumtnZMaWhaHM89m8XVCqJA02Y5w0xmga6yfVsamtrN4xoXNzS0JTHkK3CXy4EVFMumcxUy2LbENTVkZfEzMDAudtJyTmNwS2XQreAFyvOlK9louDNVaXurmjkGgnTMkWDgXswtNouFISEX6Awv+RihQi5OcYY4DtVARpCCFCMGhiJ1hjwFBpagEAaWEpFoC0WQOCOjFMRRwXYMDB4BDLJ+QLYsg7GBGjtasLnEMjCIrWBgyAZ7058FI9x1SoFEnTCDsCyIhynPILYYSFgbYpUDA5bpQBluXzxpI1yYAbd2sCMYRhwAAHB9ZPztbuMUAAAAAElFTkSuQmCC",
spider64 = "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAJZUlEQVR42u1ZaXMU1xXlJ+gHpFITOy5sAcnIYCi2aIL2bTSSZrSP1NpHK41kISQBHgFaQIJBCMwi4TFUGYcPzggwEMcxHVGxQaag5QR/np/QP+Hmnsdr0hpmtEACwulb9aq7p7d3zz333Pt61q2zzTbbbLPNNttss80222yzzTbbVmu7MzKcJRWVkXjntqam6jyURPeGQqeTpqbOqp+evxC5dGlam5m5rE3PzGi8Hzx/4aLzbXDe09HdYxwZHaPc4mLFXVoW9pRXGNv3pDngeHlNLfE2Ljjj4xPOUGjSYKfpq6/+TLdv36bbX39Nt27epGvXvqSLl6bp3LlPtdOnz7jWrPNZ7kLCKCovp5bOTmP/4EHq6vmYMtzuSKbbbQCAHE8Rxd47MjrmuHjxkjF3/z4tLCzQkyc6PX78mB49ekQPHjygub/P0d27f6FrX/6JpqbO0YkT48E1R/sCr9cYHZ+gqrp64mPq+riXcoqKKC0vP9q6VyV/fQOiH+LrsPVY7z82PBKZnb1Bd+7cpfn5eQbgCT1hAADC/MN5uj83R99881eanZ2lL5gN/nrxjihAXwvOJ7l9vuiBQ4dF9LEtLC0V+2rv/ijTX6luaCS3rxT57wADAMTBQ4c9PIIDg4PBwYOHaHhklM5MnSWkwLff/o0+v3qVHv34Iz344QEDc4d8VVXUEAhQXXMzVdQqzKweKq6oABARzOGNOZ+Wl6fD6T25ubQrPT0E5xF93o82tbdjkkZ+iZfAAgbD6fZ6o339A8S0p7HjJ2h4eIQOHf6EujlV9nX3UOj0JDXzfXje+KlTdOPGDeF0T1+fGHg+2JSen08tHZ0CiPySEoPn8vq1IaOgIAzneQK0UzjcQd6qaqrlCVfV1+tpubnRnv5+2p2ZqYMF/oZGPTh0xLhy5Sr9wLn9j++/p5nLn9FxBoLZQJ1dKrkys6iYNeTExEnx3PqWFuF4W9deKq2upkEGCyzyMBC709MFC7r391Fjayv9MSdHZyCU1xJ5FjrNdN6VnU1KS4CjU4Yoh/m8CsezCguFJgAMV05ueP+BfhF5OL+gL9A/f/qJ7t3TaPLMFB09eoy6mTkMGg2PjTELOsS20OcTACgMKqJugqA0NtE7ycn0202b6A+ZmYIVAAKApGZlgRHB/0lqQPAqFEVE9hntM0R0ZblTzeswWdCeU8HAtYW+Uu0AUx+0f/jwoXD+56c/073v7tHU2XMiFbrUfVTNAtfL10FIAQL2QftsBrOEnavld5kg7E7PoF+99x79ev162rJrV9RMi6a2dvKUlQsR5uAgII7/ivMsbEE4g2hggjzC7LQL1OftovoO0WJKUn0gYEAn2hmMXo4QHIXQIfLfsfOXPwuLvB86cpQqamooyEzg1BLMwv04RkoE+B3B4BBBMHEcCwIP0N+ByJdUVhpgBJ7j4WvdANDjeTUglOaWEChfJF7uJzPX2HEPaj1vg7EAbHO5QnAeIPgqKvUB7gtAdbBgcvKMqOnc/NAIVwCcq21qElFnCgvaI9cBBFKhlSPbPzBIbbzduGULpWzfLkDAdZs++sgEwSlZqoIJMg2CzFSNGzODwdBfOi26+w4YTCm9LhDQwQDzdzguFf4FALjciTws8/u1yyx2N2/dovPnL9DRY8PkZ204xtuhoSM0wI7V8DEiirQCCHD+99u2CUdx3Lmvmz7kfemoGDgPEDr4HNKAf1MlAC4wgMGLWFJXQUrklZSEX6rLE2rOyDIQGlhgBUAyYFEZkm2vAGVi4qQ+x83M0389pevXr6OToy07d4qcR+krr/KzqpeJ/IfjGO+npDx3FCKHVPjd1q2LAMBI3ryZ9vL7U56BEzLfD80ACFba876OlGCQV9dAcT0Pyw7PgWij6zPP5Xt9EYgg+n3LosdVzdfz5CI8KY1LH31+5Yro9KanZwjHmPzmHTsoOeVDemfDBuE8dGVnWpqx3unUrE4CDLCAG64XAHB88IFgQV5xMY7DFmc16A6CZvnNBYYVcW+yKj0A/VHTsQ8dwMPNc6X+Gg0VIGbVpzYGWundjRujmGQWi9Eol7+TJ0/R2Nhx2sNlM9YJRPDdDRsM5DGPJB4KHOIhngHhAwixAGAAuDZ2lsuiYnFWBQOYrdEYNochilyiV6YHoH+rRNJkAG+fUw31PzU7Z1EFKPD69CIuQ1Bm6URoh8tFmVym3nc6rZOPyi0cD8HxeHPg3x2InNrbS79JTsYzNXmPuBclsO3ZvKwAOJEGsmI5rT0M+gSf3y9K5LIA1LUEIlL1k0AhCYBH5r9TCqBqib4D+c/1PyInGOThkvuaHCYALhlpbQWBMGR/4IpzTqlpbKQyf0045vdoe0zATHagSYMeWFMkbscnHRYPZjoFJaIiUkz9EJy15j/X3qCsAIqMcFjSWrNE1Iygg0fEmrtLzEUTdT/OhBFht9fHDVCbEUt3LJxi08B8Xj6vTDESriq9lVWqBECgHujqiqAUmufb1X3cfRXoluhjZWiwkOnSUcUS6ZD8LUmmhks6b5j1ezkAkAKZBe5QvPPcNBnoCawMwT66Qxk0R2xwwRAui2iSDGuaPDcubzo3EJq8wcx/9Vmk3QryH42QBQCFF0UagIiJtjX6DskIXTLEucJSHIIIMuO0BOcjn3A3ybU/lu5RCUBc5qA0Ih0Q2EWiCPRk7VfMNhjLW1zETic1tLYZDMKyuSsdfh5l6bwho5+0il4kyA0VohlNcF5FP8DlWo/VB16HYB2hJ0pzgIe2mcXxP2IOumPRY17U0tll8KIkZNb+sppafOxYkQPSaYfchyYoL9GMqWYpTLRIq1QUcT4O3aPQgqVqPwIOIMwDhzX6mQUFIQAgo+9MzcrWrML3mj6+YIKiFCZyhL87RqVQKrEskF+P1BUvfLCAkfRwoPUtq6l5o5+lZb5SolJo6oT8avTCl+c9OTmat6pKW8mLkvBpGzlvsiGuQr4ZEEwA1EQgoR/gNtxIxKBluz+OtMJiF31jHxqXBiAqAUj4WRxpADFM0DCFlv1khvX7Wol4vF4AIldVVxdZqlrIfiCYQPHDy6bAGv7nKYRVY6JewExZVAP+ey5Rv+Ba97aaUHMW5NauLmMZFkegBb/EP14d6NoS9QLWFSzWBmuZza8CQmSpXsAqmGtVy14VALWuuYWWy+W3OteXa4jwceQX6+BKG6J1/8+2VCNkm2222WabbbbZZpttttlmm22rt38DCdA0vq3bcAkAAAAASUVORK5CYII=";
// Favicon
document.querySelector("link[rel=icon]").setAttribute("href", "data:image/png;base64," + spider16);
// Bake button
document.querySelector("#bake img").setAttribute("src", "data:image/png;base64," + spider32);
// About box
document.querySelector(".about-img-left").setAttribute("src", "data:image/png;base64," + spider64);
};
/**
* Replaces all instances of the word "cyber" with "spider".
* #spiderchef
*/
SeasonalWaiter.prototype.insert_spider_text = function() {
// Title
document.title = document.title.replace(/Cyber/g, "Spider");
// Body
SeasonalWaiter.tree_walk(document.body, function(node) {
// process only text nodes
if (node.nodeType == 3) {
node.nodeValue = node.nodeValue.replace(/Cyber/g, "Spider");
}
}, true);
// Bake button
SeasonalWaiter.tree_walk(document.getElementById("bake-group"), function(node) {
// process only text nodes
if (node.nodeType == 3) {
node.nodeValue = node.nodeValue.replace(/Bake/g, "Spin");
}
}, true);
// Recipe title
document.querySelector("#recipe .title").innerHTML = "Web";
};
/**
* Adds an option to make it snow.
* #letitsnow
*/
SeasonalWaiter.prototype.create_snow_option = function() {
var options_body = document.getElementById("options-body"),
option_item = document.createElement("div");
option_item.className = "option-item";
option_item.innerHTML =
"<input type='checkbox' option='snow' checked />\
Let it snow";
options_body.appendChild(option_item);
this.manager.options.load();
};
/**
* Initialises a snowstorm.
* #letitsnow
*/
SeasonalWaiter.prototype.let_it_snow = function() {
$(document).snowfall("clear");
if (!this.app.options.snow) return;
var options = {},
firefox_version = navigator.userAgent.match(/Firefox\/(\d\d?)/);
if (firefox_version && parseInt(firefox_version[1], 10) < 30) {
// Firefox < 30
options = {
flakeCount : 10,
flakeColor : '#fff',
flakePosition: 'absolute',
minSize : 1,
maxSize : 2,
minSpeed : 1,
maxSpeed : 5,
round : false,
shadow : false,
collection : false,
collectionHeight : 20,
deviceorientation : true
};
} else {
// All other browsers
options = {
flakeCount : 35, //35
flakeColor : '#fff',
flakePosition: 'absolute',
minSize : 5,
maxSize : 8,
minSpeed : 1,
maxSpeed : 5,
round : true,
shadow : true,
collection : ".btn",
collectionHeight : 20,
deviceorientation : true
};
}
$(document).snowfall(options);
};
/**
* When a button is clicked, shake the snow off that button.
* #letitsnow
*/
SeasonalWaiter.prototype.shake_off_snow = function(e) {
var el = e.target,
rect = el.getBoundingClientRect(),
canvases = document.querySelectorAll("canvas.snowfall-canvas"),
canvas = null,
remove_func = function() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
$(this).fadeIn();
};
for (var i = 0; i < canvases.length; i++) {
canvas = canvases[i];
if (canvas.style.left == rect.left + "px" && canvas.style.top == (rect.top - 20) + "px") {
var ctx = canvas.getContext("2d");
$(canvas).fadeOut("slow", remove_func);
break;
}
}
};
/**
* Listen for the Konami code sequence of keys. Turn the page upside down if they are all heard in
* sequence.
* #konamicode
*/
SeasonalWaiter.prototype.konami_code_listener = function(e) {
this.kkeys.push(e.keyCode);
var konami = [38,38,40,40,37,39,37,39,66,65];
for (var i = 0; i < this.kkeys.length; i++) {
if (this.kkeys[i] != konami[i]) {
this.kkeys = [];
break;
}
if (i == konami.length - 1) {
$("body").children().toggleClass("konami");
this.kkeys = [];
}
}
};
/**
* Walks through the entire DOM starting at the specified element and operates on each node.
*
* @static
* @param {element} parent - The DOM node to start from
* @param {Function} fn - The callback function to operate on each node
* @param {booleam} all_nodes - Whether to operate on every node or not
*/
SeasonalWaiter.tree_walk = (function() {
// Create closure for constants
var skipTags = {
"SCRIPT": true, "IFRAME": true, "OBJECT": true,
"EMBED": true, "STYLE": true, "LINK": true, "META": true
};
return function(parent, fn, all_nodes) {
var node = parent.firstChild;
while (node && node != parent) {
if (all_nodes || node.nodeType === 1) {
if (fn(node) === false) {
return(false);
}
}
// If it's an element &&
// has children &&
// has a tagname && is not in the skipTags list
// then, we can enumerate children
if (node.nodeType === 1 &&
node.firstChild &&
!(node.tagName && skipTags[node.tagName])) {
node = node.firstChild;
} else if (node.nextSibling) {
node = node.nextSibling;
} else {
// No child and no nextsibling
// Find parent that has a nextSibling
while ((node = node.parentNode) != parent) {
if (node.nextSibling) {
node = node.nextSibling;
break;
}
}
}
}
};
})();

View file

@ -0,0 +1,52 @@
/**
* Waiter to handle events related to the window object.
*
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*
* @constructor
* @param {HTMLApp} app - The main view object for CyberChef.
*/
var WindowWaiter = function(app) {
this.app = app;
};
/**
* Handler for window resize events.
* Resets the layout of CyberChef's panes after 200ms (so that continuous resizing doesn't cause
* continuous resetting).
*/
WindowWaiter.prototype.window_resize = function() {
clearTimeout(this.reset_layout_timeout);
this.reset_layout_timeout = setTimeout(this.app.reset_layout.bind(this.app), 200);
};
/**
* Handler for window blur events.
* Saves the current time so that we can calculate how long the window was unfocussed for when
* focus is returned.
*/
WindowWaiter.prototype.window_blur = function() {
this.window_blur_time = new Date().getTime();
};
/**
* Handler for window focus events.
*
* When a browser tab is unfocused and the browser has to run lots of dynamic content in other
* tabs, it swaps out the memory for that tab.
* If the CyberChef tab has been unfocused for more than a minute, we run a silent bake which will
* force the browser to load and cache all the relevant JavaScript code needed to do a real bake.
* This will stop baking taking a long time when the CyberChef browser tab has been unfocused for
* a long time and the browser has swapped out all its memory.
*/
WindowWaiter.prototype.window_focus = function() {
var unfocused_time = new Date().getTime() - this.window_blur_time;
if (unfocused_time > 60000) {
this.app.silent_bake();
}
};

49
src/js/views/html/main.js Executable file
View file

@ -0,0 +1,49 @@
/* globals moment */
/**
* @author n1474335 [n1474335@gmail.com]
* @copyright Crown Copyright 2016
* @license Apache-2.0
*/
/**
* Main function used to build the CyberChef web app.
*/
var main = function() {
var default_favourites = [
"To Base64",
"From Base64",
"To Hex",
"From Hex",
"To Hexdump",
"From Hexdump",
"URL Decode",
"Regular expression",
"Entropy",
"Fork"
];
var default_options = {
update_url : true,
show_highlighter : true,
treat_as_utf8 : true,
word_wrap : true,
show_errors : true,
error_timeout : 4000,
auto_bake_threshold : 200,
attempt_highlight : true,
snow : false,
};
document.removeEventListener("DOMContentLoaded", main, false);
window.app = new HTMLApp(Categories, OperationConfig, default_favourites, default_options);
window.app.setup();
};
// Fix issues with browsers that don't support console.log()
window.console = console || {log: function() {}, error: function() {}};
window.compile_time = moment.tz("<%= grunt.template.today() %>", "ddd MMM D YYYY HH:mm:ss", "UTC").valueOf();
window.compile_message = "<%= compile_msg %>";
document.addEventListener("DOMContentLoaded", main, false);