HTMLApp.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. /* globals Split */
  2. /**
  3. * HTML view for CyberChef responsible for building the web page and dealing with all user
  4. * interactions.
  5. *
  6. * @author n1474335 [n1474335@gmail.com]
  7. * @copyright Crown Copyright 2016
  8. * @license Apache-2.0
  9. *
  10. * @constructor
  11. * @param {CatConf[]} categories - The list of categories and operations to be populated.
  12. * @param {Object.<string, OpConf>} operations - The list of operation configuration objects.
  13. * @param {String[]} defaultFavourites - A list of default favourite operations.
  14. * @param {Object} options - Default setting for app options.
  15. */
  16. var HTMLApp = function(categories, operations, defaultFavourites, defaultOptions) {
  17. this.categories = categories;
  18. this.operations = operations;
  19. this.dfavourites = defaultFavourites;
  20. this.doptions = defaultOptions;
  21. this.options = Utils.extend({}, defaultOptions);
  22. this.chef = new Chef();
  23. this.manager = new Manager(this);
  24. this.autoBake_ = false;
  25. this.progress = 0;
  26. this.ingId = 0;
  27. window.chef = this.chef;
  28. };
  29. /**
  30. * This function sets up the stage and creates listeners for all events.
  31. *
  32. * @fires Manager#appstart
  33. */
  34. HTMLApp.prototype.setup = function() {
  35. document.dispatchEvent(this.manager.appstart);
  36. this.initialiseSplitter();
  37. this.loadLocalStorage();
  38. this.populateOperationsList();
  39. this.manager.setup();
  40. this.resetLayout();
  41. this.setCompileMessage();
  42. this.loadURIParams();
  43. };
  44. /**
  45. * An error handler for displaying the error to the user.
  46. *
  47. * @param {Error} err
  48. */
  49. HTMLApp.prototype.handleError = function(err) {
  50. console.error(err);
  51. var msg = err.displayStr || err.toString();
  52. this.alert(msg, "danger", this.options.errorTimeout, !this.options.showErrors);
  53. };
  54. /**
  55. * Calls the Chef to bake the current input using the current recipe.
  56. *
  57. * @param {boolean} [step] - Set to true if we should only execute one operation instead of the
  58. * whole recipe.
  59. */
  60. HTMLApp.prototype.bake = function(step) {
  61. var response;
  62. try {
  63. response = this.chef.bake(
  64. this.getInput(), // The user's input
  65. this.getRecipeConfig(), // The configuration of the recipe
  66. this.options, // Options set by the user
  67. this.progress, // The current position in the recipe
  68. step // Whether or not to take one step or execute the whole recipe
  69. );
  70. } catch (err) {
  71. this.handleError(err);
  72. }
  73. if (!response) return;
  74. if (response.error) {
  75. this.handleError(response.error);
  76. }
  77. this.options = response.options;
  78. this.dishStr = response.type === "html" ? Utils.stripHtmlTags(response.result, true) : response.result;
  79. this.progress = response.progress;
  80. this.manager.recipe.updateBreakpointIndicator(response.progress);
  81. this.manager.output.set(response.result, response.type, response.duration);
  82. // If baking took too long, disable auto-bake
  83. if (response.duration > this.options.autoBakeThreshold && this.autoBake_) {
  84. this.manager.controls.setAutoBake(false);
  85. this.alert("Baking took longer than " + this.options.autoBakeThreshold +
  86. "ms, Auto Bake has been disabled.", "warning", 5000);
  87. }
  88. };
  89. /**
  90. * Runs Auto Bake if it is set.
  91. */
  92. HTMLApp.prototype.autoBake = function() {
  93. if (this.autoBake_) {
  94. this.bake();
  95. }
  96. };
  97. /**
  98. * Runs a silent bake forcing the browser to load and cache all the relevant JavaScript code needed
  99. * to do a real bake.
  100. *
  101. * The output will not be modified (hence "silent" bake). This will only actually execute the
  102. * recipe if auto-bake is enabled, otherwise it will just load the recipe, ingredients and dish.
  103. *
  104. * @returns {number} - The number of miliseconds it took to run the silent bake.
  105. */
  106. HTMLApp.prototype.silentBake = function() {
  107. var startTime = new Date().getTime(),
  108. recipeConfig = this.getRecipeConfig();
  109. if (this.autoBake_) {
  110. this.chef.silentBake(recipeConfig);
  111. }
  112. return new Date().getTime() - startTime;
  113. };
  114. /**
  115. * Gets the user's input data.
  116. *
  117. * @returns {string}
  118. */
  119. HTMLApp.prototype.getInput = function() {
  120. var input = this.manager.input.get();
  121. // Save to session storage in case we need to restore it later
  122. sessionStorage.setItem("inputLength", input.length);
  123. sessionStorage.setItem("input", input);
  124. return input;
  125. };
  126. /**
  127. * Sets the user's input data.
  128. *
  129. * @param {string} input - The string to set the input to
  130. */
  131. HTMLApp.prototype.setInput = function(input) {
  132. sessionStorage.setItem("inputLength", input.length);
  133. sessionStorage.setItem("input", input);
  134. this.manager.input.set(input);
  135. };
  136. /**
  137. * Populates the operations accordion list with the categories and operations specified in the
  138. * view constructor.
  139. *
  140. * @fires Manager#oplistcreate
  141. */
  142. HTMLApp.prototype.populateOperationsList = function() {
  143. // Move edit button away before we overwrite it
  144. document.body.appendChild(document.getElementById("edit-favourites"));
  145. var html = "";
  146. for (var i = 0; i < this.categories.length; i++) {
  147. var catConf = this.categories[i],
  148. selected = i === 0,
  149. cat = new HTMLCategory(catConf.name, selected);
  150. for (var j = 0; j < catConf.ops.length; j++) {
  151. var opName = catConf.ops[j],
  152. op = new HTMLOperation(opName, this.operations[opName], this, this.manager);
  153. cat.addOperation(op);
  154. }
  155. html += cat.toHtml();
  156. }
  157. document.getElementById("categories").innerHTML = html;
  158. var opLists = document.querySelectorAll("#categories .op-list");
  159. for (i = 0; i < opLists.length; i++) {
  160. opLists[i].dispatchEvent(this.manager.oplistcreate);
  161. }
  162. // Add edit button to first category (Favourites)
  163. document.querySelector("#categories a").appendChild(document.getElementById("edit-favourites"));
  164. };
  165. /**
  166. * Sets up the adjustable splitter to allow the user to resize areas of the page.
  167. */
  168. HTMLApp.prototype.initialiseSplitter = function() {
  169. this.columnSplitter = Split(["#operations", "#recipe", "#IO"], {
  170. sizes: [20, 30, 50],
  171. minSize: [240, 325, 440],
  172. gutterSize: 4,
  173. onDrag: function() {
  174. this.manager.controls.adjustWidth();
  175. this.manager.output.adjustWidth();
  176. }.bind(this)
  177. });
  178. this.ioSplitter = Split(["#input", "#output"], {
  179. direction: "vertical",
  180. gutterSize: 4,
  181. });
  182. this.resetLayout();
  183. };
  184. /**
  185. * Loads the information previously saved to the HTML5 local storage object so that user options
  186. * and favourites can be restored.
  187. */
  188. HTMLApp.prototype.loadLocalStorage = function() {
  189. // Load options
  190. var lOptions;
  191. if (localStorage.options !== undefined) {
  192. lOptions = JSON.parse(localStorage.options);
  193. }
  194. this.manager.options.load(lOptions);
  195. // Load favourites
  196. this.loadFavourites();
  197. };
  198. /**
  199. * Loads the user's favourite operations from the HTML5 local storage object and populates the
  200. * Favourites category with them.
  201. * If the user currently has no saved favourites, the defaults from the view constructor are used.
  202. */
  203. HTMLApp.prototype.loadFavourites = function() {
  204. var favourites = localStorage.favourites &&
  205. localStorage.favourites.length > 2 ?
  206. JSON.parse(localStorage.favourites) :
  207. this.dfavourites;
  208. favourites = this.validFavourites(favourites);
  209. this.saveFavourites(favourites);
  210. var favCat = this.categories.filter(function(c) {
  211. return c.name === "Favourites";
  212. })[0];
  213. if (favCat) {
  214. favCat.ops = favourites;
  215. } else {
  216. this.categories.unshift({
  217. name: "Favourites",
  218. ops: favourites
  219. });
  220. }
  221. };
  222. /**
  223. * Filters the list of favourite operations that the user had stored and removes any that are no
  224. * longer available. The user is notified if this is the case.
  225. * @param {string[]} favourites - A list of the user's favourite operations
  226. * @returns {string[]} A list of the valid favourites
  227. */
  228. HTMLApp.prototype.validFavourites = function(favourites) {
  229. var validFavs = [];
  230. for (var i = 0; i < favourites.length; i++) {
  231. if (this.operations.hasOwnProperty(favourites[i])) {
  232. validFavs.push(favourites[i]);
  233. } else {
  234. this.alert("The operation \"" + Utils.escapeHtml(favourites[i]) +
  235. "\" is no longer available. It has been removed from your favourites.", "info");
  236. }
  237. }
  238. return validFavs;
  239. };
  240. /**
  241. * Saves a list of favourite operations to the HTML5 local storage object.
  242. *
  243. * @param {string[]} favourites - A list of the user's favourite operations
  244. */
  245. HTMLApp.prototype.saveFavourites = function(favourites) {
  246. localStorage.setItem("favourites", JSON.stringify(this.validFavourites(favourites)));
  247. };
  248. /**
  249. * Resets favourite operations back to the default as specified in the view constructor and
  250. * refreshes the operation list.
  251. */
  252. HTMLApp.prototype.resetFavourites = function() {
  253. this.saveFavourites(this.dfavourites);
  254. this.loadFavourites();
  255. this.populateOperationsList();
  256. this.manager.recipe.initialiseOperationDragNDrop();
  257. };
  258. /**
  259. * Adds an operation to the user's favourites.
  260. *
  261. * @param {string} name - The name of the operation
  262. */
  263. HTMLApp.prototype.addFavourite = function(name) {
  264. var favourites = JSON.parse(localStorage.favourites);
  265. if (favourites.indexOf(name) >= 0) {
  266. this.alert("'" + name + "' is already in your favourites", "info", 2000);
  267. return;
  268. }
  269. favourites.push(name);
  270. this.saveFavourites(favourites);
  271. this.loadFavourites();
  272. this.populateOperationsList();
  273. this.manager.recipe.initialiseOperationDragNDrop();
  274. };
  275. /**
  276. * Checks for input and recipe in the URI parameters and loads them if present.
  277. */
  278. HTMLApp.prototype.loadURIParams = function() {
  279. // Load query string from URI
  280. this.queryString = (function(a) {
  281. if (a === "") return {};
  282. var b = {};
  283. for (var i = 0; i < a.length; i++) {
  284. var p = a[i].split("=");
  285. if (p.length !== 2) {
  286. b[a[i]] = true;
  287. } else {
  288. b[p[0]] = decodeURIComponent(p[1].replace(/\+/g, " "));
  289. }
  290. }
  291. return b;
  292. })(window.location.search.substr(1).split("&"));
  293. // Turn off auto-bake while loading
  294. var autoBakeVal = this.autoBake_;
  295. this.autoBake_ = false;
  296. // Read in recipe from query string
  297. if (this.queryString.recipe) {
  298. try {
  299. var recipeConfig = JSON.parse(this.queryString.recipe);
  300. this.setRecipeConfig(recipeConfig);
  301. } catch(err) {}
  302. } else if (this.queryString.op) {
  303. // If there's no recipe, look for single operations
  304. this.manager.recipe.clearRecipe();
  305. try {
  306. this.manager.recipe.addOperation(this.queryString.op);
  307. } catch(err) {
  308. // If no exact match, search for nearest match and add that
  309. var matchedOps = this.manager.ops.filterOperations(this.queryString.op, false);
  310. if (matchedOps.length) {
  311. this.manager.recipe.addOperation(matchedOps[0].name);
  312. }
  313. // Populate search with the string
  314. var search = document.getElementById("search");
  315. search.value = this.queryString.op;
  316. search.dispatchEvent(new Event("search"));
  317. }
  318. }
  319. // Read in input data from query string
  320. if (this.queryString.input) {
  321. try {
  322. var inputData = Utils.fromBase64(this.queryString.input);
  323. this.setInput(inputData);
  324. } catch(err) {}
  325. }
  326. // Restore auto-bake state
  327. this.autoBake_ = autoBakeVal;
  328. this.autoBake();
  329. };
  330. /**
  331. * Returns the next ingredient ID and increments it for next time.
  332. *
  333. * @returns {number}
  334. */
  335. HTMLApp.prototype.nextIngId = function() {
  336. return this.ingId++;
  337. };
  338. /**
  339. * Gets the current recipe configuration.
  340. *
  341. * @returns {Object[]}
  342. */
  343. HTMLApp.prototype.getRecipeConfig = function() {
  344. var recipeConfig = this.manager.recipe.getConfig();
  345. sessionStorage.setItem("recipeConfig", JSON.stringify(recipeConfig));
  346. return recipeConfig;
  347. };
  348. /**
  349. * Given a recipe configuration, sets the recipe to that configuration.
  350. *
  351. * @param {Object[]} recipeConfig - The recipe configuration
  352. */
  353. HTMLApp.prototype.setRecipeConfig = function(recipeConfig) {
  354. sessionStorage.setItem("recipeConfig", JSON.stringify(recipeConfig));
  355. document.getElementById("rec-list").innerHTML = null;
  356. for (var i = 0; i < recipeConfig.length; i++) {
  357. var item = this.manager.recipe.addOperation(recipeConfig[i].op);
  358. // Populate arguments
  359. var args = item.querySelectorAll(".arg");
  360. for (var j = 0; j < args.length; j++) {
  361. if (args[j].getAttribute("type") === "checkbox") {
  362. // checkbox
  363. args[j].checked = recipeConfig[i].args[j];
  364. } else if (args[j].classList.contains("toggle-string")) {
  365. // toggleString
  366. args[j].value = recipeConfig[i].args[j].string;
  367. args[j].previousSibling.children[0].innerHTML =
  368. Utils.escapeHtml(recipeConfig[i].args[j].option) +
  369. " <span class='caret'></span>";
  370. } else {
  371. // all others
  372. args[j].value = recipeConfig[i].args[j];
  373. }
  374. }
  375. // Set disabled and breakpoint
  376. if (recipeConfig[i].disabled) {
  377. item.querySelector(".disable-icon").click();
  378. }
  379. if (recipeConfig[i].breakpoint) {
  380. item.querySelector(".breakpoint").click();
  381. }
  382. this.progress = 0;
  383. }
  384. };
  385. /**
  386. * Resets the splitter positions to default.
  387. */
  388. HTMLApp.prototype.resetLayout = function() {
  389. this.columnSplitter.setSizes([20, 30, 50]);
  390. this.ioSplitter.setSizes([50, 50]);
  391. this.manager.controls.adjustWidth();
  392. this.manager.output.adjustWidth();
  393. };
  394. /**
  395. * Sets the compile message.
  396. */
  397. HTMLApp.prototype.setCompileMessage = function() {
  398. // Display time since last build and compile message
  399. var now = new Date(),
  400. timeSinceCompile = Utils.fuzzyTime(now.getTime() - window.compileTime),
  401. compileInfo = "<span style=\"font-weight: normal\">Last build: " +
  402. timeSinceCompile.substr(0, 1).toUpperCase() + timeSinceCompile.substr(1) + " ago";
  403. if (window.compileMessage !== "") {
  404. compileInfo += " - " + window.compileMessage;
  405. }
  406. compileInfo += "</span>";
  407. document.getElementById("notice").innerHTML = compileInfo;
  408. };
  409. /**
  410. * Pops up a message to the user and writes it to the console log.
  411. *
  412. * @param {string} str - The message to display (HTML supported)
  413. * @param {string} style - The colour of the popup
  414. * "danger" = red
  415. * "warning" = amber
  416. * "info" = blue
  417. * "success" = green
  418. * @param {number} timeout - The number of milliseconds before the popup closes automatically
  419. * 0 for never (until the user closes it)
  420. * @param {boolean} [silent=false] - Don't show the message in the popup, only print it to the
  421. * console
  422. *
  423. * @example
  424. * // Pops up a red box with the message "[current time] Error: Something has gone wrong!"
  425. * // that will need to be dismissed by the user.
  426. * this.alert("Error: Something has gone wrong!", "danger", 0);
  427. *
  428. * // Pops up a blue information box with the message "[current time] Happy Christmas!"
  429. * // that will disappear after 5 seconds.
  430. * this.alert("Happy Christmas!", "info", 5000);
  431. */
  432. HTMLApp.prototype.alert = function(str, style, timeout, silent) {
  433. var time = new Date();
  434. console.log("[" + time.toLocaleString() + "] " + str);
  435. if (silent) return;
  436. style = style || "danger";
  437. timeout = timeout || 0;
  438. var alertEl = document.getElementById("alert"),
  439. alertContent = document.getElementById("alert-content");
  440. alertEl.classList.remove("alert-danger");
  441. alertEl.classList.remove("alert-warning");
  442. alertEl.classList.remove("alert-info");
  443. alertEl.classList.remove("alert-success");
  444. alertEl.classList.add("alert-" + style);
  445. // If the box hasn't been closed, append to it rather than replacing
  446. if (alertEl.style.display === "block") {
  447. alertContent.innerHTML +=
  448. "<br><br>[" + time.toLocaleTimeString() + "] " + str;
  449. } else {
  450. alertContent.innerHTML =
  451. "[" + time.toLocaleTimeString() + "] " + str;
  452. }
  453. // Stop the animation if it is in progress
  454. $("#alert").stop();
  455. alertEl.style.display = "block";
  456. alertEl.style.opacity = 1;
  457. if (timeout > 0) {
  458. clearTimeout(this.alertTimeout);
  459. this.alertTimeout = setTimeout(function(){
  460. $("#alert").slideUp(100);
  461. }, timeout);
  462. }
  463. };
  464. /**
  465. * Pops up a box asking the user a question and sending the answer to a specified callback function.
  466. *
  467. * @param {string} title - The title of the box
  468. * @param {string} body - The question (HTML supported)
  469. * @param {function} callback - A function accepting one boolean argument which handles the
  470. * response e.g. function(answer) {...}
  471. * @param {Object} [scope=this] - The object to bind to the callback function
  472. *
  473. * @example
  474. * // Pops up a box asking if the user would like a cookie. Prints the answer to the console.
  475. * this.confirm("Question", "Would you like a cookie?", function(answer) {console.log(answer);});
  476. */
  477. HTMLApp.prototype.confirm = function(title, body, callback, scope) {
  478. scope = scope || this;
  479. document.getElementById("confirm-title").innerHTML = title;
  480. document.getElementById("confirm-body").innerHTML = body;
  481. document.getElementById("confirm-modal").style.display = "block";
  482. this.confirmClosed = false;
  483. $("#confirm-modal").modal()
  484. .one("show.bs.modal", function(e) {
  485. this.confirmClosed = false;
  486. }.bind(this))
  487. .one("click", "#confirm-yes", function() {
  488. this.confirmClosed = true;
  489. callback.bind(scope)(true);
  490. $("#confirm-modal").modal("hide");
  491. }.bind(this))
  492. .one("hide.bs.modal", function(e) {
  493. if (!this.confirmClosed)
  494. callback.bind(scope)(false);
  495. this.confirmClosed = true;
  496. }.bind(this));
  497. };
  498. /**
  499. * Handler for the alert close button click event.
  500. * Closes the alert box.
  501. */
  502. HTMLApp.prototype.alertCloseClick = function() {
  503. document.getElementById("alert").style.display = "none";
  504. };
  505. /**
  506. * Handler for CyerChef statechange events.
  507. * Fires whenever the input or recipe changes in any way.
  508. *
  509. * @listens Manager#statechange
  510. * @param {event} e
  511. */
  512. HTMLApp.prototype.stateChange = function(e) {
  513. this.autoBake();
  514. // Update the current history state (not creating a new one)
  515. if (this.options.updateUrl) {
  516. this.lastStateUrl = this.manager.controls.generateStateUrl(true, true);
  517. window.history.replaceState({}, "CyberChef", this.lastStateUrl);
  518. }
  519. };
  520. /**
  521. * Handler for the history popstate event.
  522. * Reloads parameters from the URL.
  523. *
  524. * @param {event} e
  525. */
  526. HTMLApp.prototype.popState = function(e) {
  527. if (window.location.href.split("#")[0] !== this.lastStateUrl) {
  528. this.loadURIParams();
  529. }
  530. };
  531. /**
  532. * Function to call an external API from this view.
  533. */
  534. HTMLApp.prototype.callApi = function(url, type, data, dataType, contentType) {
  535. type = type || "POST";
  536. data = data || {};
  537. dataType = dataType || undefined;
  538. contentType = contentType || "application/json";
  539. var response = null,
  540. success = false;
  541. $.ajax({
  542. url: url,
  543. async: false,
  544. type: type,
  545. data: data,
  546. dataType: dataType,
  547. contentType: contentType,
  548. success: function(data) {
  549. success = true;
  550. response = data;
  551. },
  552. error: function(data) {
  553. success = false;
  554. response = data;
  555. },
  556. });
  557. return {
  558. success: success,
  559. response: response
  560. };
  561. };