ControlsWaiter.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import Utils from "../core/Utils.js";
  2. /**
  3. * Waiter to handle events related to the CyberChef controls (i.e. Bake, Step, Save, Load etc.)
  4. *
  5. * @author n1474335 [n1474335@gmail.com]
  6. * @copyright Crown Copyright 2016
  7. * @license Apache-2.0
  8. *
  9. * @constructor
  10. * @param {App} app - The main view object for CyberChef.
  11. * @param {Manager} manager - The CyberChef event manager.
  12. */
  13. const ControlsWaiter = function(app, manager) {
  14. this.app = app;
  15. this.manager = manager;
  16. };
  17. /**
  18. * Adjusts the display properties of the control buttons so that they fit within the current width
  19. * without wrapping or overflowing.
  20. */
  21. ControlsWaiter.prototype.adjustWidth = function() {
  22. const controls = document.getElementById("controls");
  23. const step = document.getElementById("step");
  24. const clrBreaks = document.getElementById("clr-breaks");
  25. const saveImg = document.querySelector("#save img");
  26. const loadImg = document.querySelector("#load img");
  27. const stepImg = document.querySelector("#step img");
  28. const clrRecipImg = document.querySelector("#clr-recipe img");
  29. const clrBreaksImg = document.querySelector("#clr-breaks img");
  30. if (controls.clientWidth < 470) {
  31. step.childNodes[1].nodeValue = " Step";
  32. } else {
  33. step.childNodes[1].nodeValue = " Step through";
  34. }
  35. if (controls.clientWidth < 400) {
  36. saveImg.style.display = "none";
  37. loadImg.style.display = "none";
  38. stepImg.style.display = "none";
  39. clrRecipImg.style.display = "none";
  40. clrBreaksImg.style.display = "none";
  41. } else {
  42. saveImg.style.display = "inline";
  43. loadImg.style.display = "inline";
  44. stepImg.style.display = "inline";
  45. clrRecipImg.style.display = "inline";
  46. clrBreaksImg.style.display = "inline";
  47. }
  48. if (controls.clientWidth < 330) {
  49. clrBreaks.childNodes[1].nodeValue = " Clear breaks";
  50. } else {
  51. clrBreaks.childNodes[1].nodeValue = " Clear breakpoints";
  52. }
  53. };
  54. /**
  55. * Checks or unchecks the Auto Bake checkbox based on the given value.
  56. *
  57. * @param {boolean} value - The new value for Auto Bake.
  58. */
  59. ControlsWaiter.prototype.setAutoBake = function(value) {
  60. const autoBakeCheckbox = document.getElementById("auto-bake");
  61. if (autoBakeCheckbox.checked !== value) {
  62. autoBakeCheckbox.click();
  63. }
  64. };
  65. /**
  66. * Handler to trigger baking.
  67. */
  68. ControlsWaiter.prototype.bakeClick = function() {
  69. if (document.getElementById("bake").textContent.indexOf("Bake") > 0) {
  70. this.app.bake();
  71. } else {
  72. this.manager.worker.cancelBake();
  73. }
  74. };
  75. /**
  76. * Handler for the 'Step through' command. Executes the next step of the recipe.
  77. */
  78. ControlsWaiter.prototype.stepClick = function() {
  79. this.app.bake(true);
  80. };
  81. /**
  82. * Handler for changes made to the Auto Bake checkbox.
  83. */
  84. ControlsWaiter.prototype.autoBakeChange = function() {
  85. const autoBakeLabel = document.getElementById("auto-bake-label");
  86. const autoBakeCheckbox = document.getElementById("auto-bake");
  87. this.app.autoBake_ = autoBakeCheckbox.checked;
  88. if (autoBakeCheckbox.checked) {
  89. autoBakeLabel.classList.add("btn-success");
  90. autoBakeLabel.classList.remove("btn-default");
  91. } else {
  92. autoBakeLabel.classList.add("btn-default");
  93. autoBakeLabel.classList.remove("btn-success");
  94. }
  95. };
  96. /**
  97. * Handler for the 'Clear recipe' command. Removes all operations from the recipe.
  98. */
  99. ControlsWaiter.prototype.clearRecipeClick = function() {
  100. this.manager.recipe.clearRecipe();
  101. };
  102. /**
  103. * Handler for the 'Clear breakpoints' command. Removes all breakpoints from operations in the
  104. * recipe.
  105. */
  106. ControlsWaiter.prototype.clearBreaksClick = function() {
  107. const bps = document.querySelectorAll("#rec-list li.operation .breakpoint");
  108. for (let i = 0; i < bps.length; i++) {
  109. bps[i].setAttribute("break", "false");
  110. bps[i].classList.remove("breakpoint-selected");
  111. }
  112. };
  113. /**
  114. * Populates the save disalog box with a URL incorporating the recipe and input.
  115. *
  116. * @param {Object[]} [recipeConfig] - The recipe configuration object array.
  117. */
  118. ControlsWaiter.prototype.initialiseSaveLink = function(recipeConfig) {
  119. recipeConfig = recipeConfig || this.app.getRecipeConfig();
  120. const includeRecipe = document.getElementById("save-link-recipe-checkbox").checked;
  121. const includeInput = document.getElementById("save-link-input-checkbox").checked;
  122. const saveLinkEl = document.getElementById("save-link");
  123. const saveLink = this.generateStateUrl(includeRecipe, includeInput, recipeConfig);
  124. saveLinkEl.innerHTML = Utils.truncate(saveLink, 120);
  125. saveLinkEl.setAttribute("href", saveLink);
  126. };
  127. /**
  128. * Generates a URL containing the current recipe and input state.
  129. *
  130. * @param {boolean} includeRecipe - Whether to include the recipe in the URL.
  131. * @param {boolean} includeInput - Whether to include the input in the URL.
  132. * @param {Object[]} [recipeConfig] - The recipe configuration object array.
  133. * @param {string} [baseURL] - The CyberChef URL, set to the current URL if not included
  134. * @returns {string}
  135. */
  136. ControlsWaiter.prototype.generateStateUrl = function(includeRecipe, includeInput, recipeConfig, baseURL) {
  137. recipeConfig = recipeConfig || this.app.getRecipeConfig();
  138. const link = baseURL || window.location.protocol + "//" +
  139. window.location.host +
  140. window.location.pathname;
  141. const recipeStr = Utils.generatePrettyRecipe(recipeConfig);
  142. const inputStr = Utils.toBase64(this.app.getInput(), "A-Za-z0-9+/"); // B64 alphabet with no padding
  143. includeRecipe = includeRecipe && (recipeConfig.length > 0);
  144. // Only inlcude input if it is less than 50KB (51200 * 4/3 as it is Base64 encoded)
  145. includeInput = includeInput && (inputStr.length > 0) && (inputStr.length <= 68267);
  146. const params = [
  147. includeRecipe ? ["recipe", recipeStr] : undefined,
  148. includeInput ? ["input", inputStr] : undefined,
  149. ];
  150. const hash = params
  151. .filter(v => v)
  152. .map(([key, value]) => `${key}=${Utils.encodeURIFragment(value)}`)
  153. .join("&");
  154. if (hash) {
  155. return `${link}#${hash}`;
  156. }
  157. return link;
  158. };
  159. /**
  160. * Handler for changes made to the save dialog text area. Re-initialises the save link.
  161. */
  162. ControlsWaiter.prototype.saveTextChange = function(e) {
  163. try {
  164. const recipeConfig = Utils.parseRecipeConfig(e.target.value);
  165. this.initialiseSaveLink(recipeConfig);
  166. } catch (err) {}
  167. };
  168. /**
  169. * Handler for the 'Save' command. Pops up the save dialog box.
  170. */
  171. ControlsWaiter.prototype.saveClick = function() {
  172. const recipeConfig = this.app.getRecipeConfig();
  173. const recipeStr = JSON.stringify(recipeConfig);
  174. document.getElementById("save-text-chef").value = Utils.generatePrettyRecipe(recipeConfig, true);
  175. document.getElementById("save-text-clean").value = JSON.stringify(recipeConfig, null, 2)
  176. .replace(/{\n\s+"/g, "{ \"")
  177. .replace(/\[\n\s{3,}/g, "[")
  178. .replace(/\n\s{3,}]/g, "]")
  179. .replace(/\s*\n\s*}/g, " }")
  180. .replace(/\n\s{6,}/g, " ");
  181. document.getElementById("save-text-compact").value = recipeStr;
  182. this.initialiseSaveLink(recipeConfig);
  183. $("#save-modal").modal();
  184. };
  185. /**
  186. * Handler for the save link recipe checkbox change event.
  187. */
  188. ControlsWaiter.prototype.slrCheckChange = function() {
  189. this.initialiseSaveLink();
  190. };
  191. /**
  192. * Handler for the save link input checkbox change event.
  193. */
  194. ControlsWaiter.prototype.sliCheckChange = function() {
  195. this.initialiseSaveLink();
  196. };
  197. /**
  198. * Handler for the 'Load' command. Pops up the load dialog box.
  199. */
  200. ControlsWaiter.prototype.loadClick = function() {
  201. this.populateLoadRecipesList();
  202. $("#load-modal").modal();
  203. };
  204. /**
  205. * Saves the recipe specified in the save textarea to local storage.
  206. */
  207. ControlsWaiter.prototype.saveButtonClick = function() {
  208. if (!this.app.isLocalStorageAvailable()) {
  209. this.app.alert(
  210. "Your security settings do not allow access to local storage so your recipe cannot be saved.",
  211. "danger",
  212. 5000
  213. );
  214. return false;
  215. }
  216. const recipeName = Utils.escapeHtml(document.getElementById("save-name").value);
  217. const recipeStr = document.querySelector("#save-texts .tab-pane.active textarea").value;
  218. if (!recipeName) {
  219. this.app.alert("Please enter a recipe name", "danger", 2000);
  220. return;
  221. }
  222. let savedRecipes = localStorage.savedRecipes ?
  223. JSON.parse(localStorage.savedRecipes) : [],
  224. recipeId = localStorage.recipeId || 0;
  225. savedRecipes.push({
  226. id: ++recipeId,
  227. name: recipeName,
  228. recipe: recipeStr
  229. });
  230. localStorage.savedRecipes = JSON.stringify(savedRecipes);
  231. localStorage.recipeId = recipeId;
  232. this.app.alert("Recipe saved as \"" + recipeName + "\".", "success", 2000);
  233. };
  234. /**
  235. * Populates the list of saved recipes in the load dialog box from local storage.
  236. */
  237. ControlsWaiter.prototype.populateLoadRecipesList = function() {
  238. if (!this.app.isLocalStorageAvailable()) return false;
  239. const loadNameEl = document.getElementById("load-name");
  240. // Remove current recipes from select
  241. let i = loadNameEl.options.length;
  242. while (i--) {
  243. loadNameEl.remove(i);
  244. }
  245. // Add recipes to select
  246. const savedRecipes = localStorage.savedRecipes ?
  247. JSON.parse(localStorage.savedRecipes) : [];
  248. for (i = 0; i < savedRecipes.length; i++) {
  249. const opt = document.createElement("option");
  250. opt.value = savedRecipes[i].id;
  251. // Unescape then re-escape in case localStorage has been corrupted
  252. opt.innerHTML = Utils.escapeHtml(Utils.unescapeHtml(savedRecipes[i].name));
  253. loadNameEl.appendChild(opt);
  254. }
  255. // Populate textarea with first recipe
  256. document.getElementById("load-text").value = savedRecipes.length ? savedRecipes[0].recipe : "";
  257. };
  258. /**
  259. * Removes the currently selected recipe from local storage.
  260. */
  261. ControlsWaiter.prototype.loadDeleteClick = function() {
  262. if (!this.app.isLocalStorageAvailable()) return false;
  263. const id = parseInt(document.getElementById("load-name").value, 10);
  264. const rawSavedRecipes = localStorage.savedRecipes ?
  265. JSON.parse(localStorage.savedRecipes) : [];
  266. const savedRecipes = rawSavedRecipes.filter(r => r.id !== id);
  267. localStorage.savedRecipes = JSON.stringify(savedRecipes);
  268. this.populateLoadRecipesList();
  269. };
  270. /**
  271. * Displays the selected recipe in the load text box.
  272. */
  273. ControlsWaiter.prototype.loadNameChange = function(e) {
  274. if (!this.app.isLocalStorageAvailable()) return false;
  275. const el = e.target;
  276. const savedRecipes = localStorage.savedRecipes ?
  277. JSON.parse(localStorage.savedRecipes) : [];
  278. const id = parseInt(el.value, 10);
  279. const recipe = savedRecipes.find(r => r.id === id);
  280. document.getElementById("load-text").value = recipe.recipe;
  281. };
  282. /**
  283. * Loads the selected recipe and populates the Recipe with its operations.
  284. */
  285. ControlsWaiter.prototype.loadButtonClick = function() {
  286. try {
  287. const recipeConfig = Utils.parseRecipeConfig(document.getElementById("load-text").value);
  288. this.app.setRecipeConfig(recipeConfig);
  289. this.app.autoBake();
  290. $("#rec-list [data-toggle=popover]").popover();
  291. } catch (e) {
  292. this.app.alert("Invalid recipe", "danger", 2000);
  293. }
  294. };
  295. /**
  296. * Populates the bug report information box with useful technical info.
  297. *
  298. * @param {event} e
  299. */
  300. ControlsWaiter.prototype.supportButtonClick = function(e) {
  301. e.preventDefault();
  302. const reportBugInfo = document.getElementById("report-bug-info");
  303. const saveLink = this.generateStateUrl(true, true, null, "https://gchq.github.io/CyberChef/");
  304. if (reportBugInfo) {
  305. reportBugInfo.innerHTML = "* Version: " + PKG_VERSION + "\n" +
  306. "* Compile time: " + COMPILE_TIME + "\n" +
  307. "* User-Agent: \n" + navigator.userAgent + "\n" +
  308. "* [Link to reproduce](" + saveLink + ")\n\n";
  309. }
  310. };
  311. /**
  312. * Shows the stale indicator to show that the input or recipe has changed
  313. * since the last bake.
  314. */
  315. ControlsWaiter.prototype.showStaleIndicator = function() {
  316. const staleIndicator = document.getElementById("stale-indicator");
  317. staleIndicator.style.visibility = "visible";
  318. staleIndicator.style.opacity = 1;
  319. };
  320. /**
  321. * Hides the stale indicator to show that the input or recipe has not changed
  322. * since the last bake.
  323. */
  324. ControlsWaiter.prototype.hideStaleIndicator = function() {
  325. const staleIndicator = document.getElementById("stale-indicator");
  326. staleIndicator.style.opacity = 0;
  327. staleIndicator.style.visibility = "hidden";
  328. };
  329. /**
  330. * Switches the Bake button between 'Bake' and 'Cancel' functions.
  331. *
  332. * @param {boolean} cancel - Whether to change to cancel or not
  333. */
  334. ControlsWaiter.prototype.toggleBakeButtonFunction = function(cancel) {
  335. const bakeButton = document.getElementById("bake"),
  336. btnText = bakeButton.querySelector("span");
  337. if (cancel) {
  338. btnText.innerText = "Cancel";
  339. bakeButton.classList.remove("btn-success");
  340. bakeButton.classList.add("btn-danger");
  341. } else {
  342. btnText.innerText = "Bake!";
  343. bakeButton.classList.remove("btn-danger");
  344. bakeButton.classList.add("btn-success");
  345. }
  346. };
  347. export default ControlsWaiter;