App.js 21 KB

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