ControlsWaiter.mjs 11 KB

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