diff --git a/README.md b/README.md
index 82b80d86..588280bb 100755
--- a/README.md
+++ b/README.md
@@ -90,7 +90,7 @@ CyberChef is built to support
## Node.js support
-CyberChef is built to fully support Node.js `v16`. For more information, see the Node API page in the project [wiki pages](https://github.com/gchq/CyberChef/wiki)
+CyberChef is built to fully support Node.js `v18`. For more information, see the Node API page in the project [wiki pages](https://github.com/gchq/CyberChef/wiki)
## Contributing
diff --git a/src/web/App.mjs b/src/web/App.mjs
index 12cadabe..85b4f897 100755
--- a/src/web/App.mjs
+++ b/src/web/App.mjs
@@ -7,11 +7,11 @@
import Utils, { debounce } from "../core/Utils.mjs";
import {fromBase64} from "../core/lib/Base64.mjs";
import Manager from "./Manager.mjs";
-import HTMLCategory from "./HTMLCategory.mjs";
-import HTMLOperation from "./HTMLOperation.mjs";
import Split from "split.js";
import moment from "moment-timezone";
import cptable from "codepage";
+import {CCategoryLi} from "./components/c-category-li.mjs";
+import {CCategoryList} from "./components/c-category-list.mjs";
/**
@@ -65,9 +65,9 @@ class App {
setup() {
document.dispatchEvent(this.manager.appstart);
- this.initialiseUI();
this.loadLocalStorage();
- this.buildOperationsList();
+ this.buildUI();
+ this.buildCategoryList();
this.manager.setup();
this.manager.output.saveBombe();
this.uriParams = this.getURIParams();
@@ -249,7 +249,7 @@ class App {
*
* @param {boolean} [minimise=false] - Set this flag if attempting to minimise frames to 0 width
*/
- initialiseUI() {
+ buildUI() {
if (this.isMobileView()) {
this.setMobileUI();
} else {
@@ -416,54 +416,41 @@ class App {
}
favourites.push(name);
- this.updateFavourites(favourites, false);
+ this.updateFavourites(favourites);
}
/**
* Update favourites in localstorage, load the updated
- * favourites and re-render the favourites category.
- *
- * Apply 'favourite' classes and set icons appropriately
+ * favourites and re-render c-category-li [favourites]
*
* @param {string[]} favourites
- * @param {boolean} updateAllIcons
*/
- updateFavourites(favourites, updateAllIcons = true) {
+ updateFavourites(favourites) {
this.saveFavourites(favourites);
this.loadFavourites();
- this.buildFavouritesCategory();
- if (updateAllIcons) {
- this.manager.ops.updateOpsFavouriteIcons();
- }
+ /* Rebuild only the favourites category */
- if (!this.isMobileView()) {
- this.manager.recipe.initialiseOperationDragNDrop();
+ // double-check if the first category is indeed "catFavourites",
+ if (document.querySelector("c-category-list > ul > c-category-li > li > a[data-target='#catFavourites']")) {
+ // then destroy
+ document.querySelectorAll("c-category-list > ul > c-category-li")[0].remove();
+
+ // and rebuild it
+ const favCatConfig = this.categories.find( catConfig => catConfig.name === "Favourites");
+ const favouriteCategory = new CCategoryLi(
+ this,
+ favCatConfig,
+ this.operations,
+ false
+ );
+
+ // finally prepend it to c-category-list
+ document.querySelector("c-category-list > ul").prepend(favouriteCategory);
}
}
-
- /**
- * Build the Favourites category and insert it into #categories
- */
- buildFavouritesCategory() {
- // Move the edit button away before we re-render the favourite category
- // A note: this is hacky and should be solved in a proper way once the entire
- // codebase and architecture gets a thorough refactoring.
- document.body.appendChild(document.getElementById("edit-favourites"));
-
- // rerender the favourites category
- const catConf = this.categories.find((cat) => cat.name === "Favourites");
- this.removeCategoryFromDOM("catFavourites");
- this.addCategoryToElement(catConf, document.getElementById("categories"));
-
- // Restore "edit favourites" after the favourites category has been re-rendered, and reinitialize the listener
- this.setEditFavourites();
- document.getElementById("edit-favourites").addEventListener("click", this.manager.ops.editFavouritesClick.bind(this.manager.ops));
- }
-
-
/**
* Gets the URI params from the window and parses them to extract the actual values.
*
@@ -862,19 +849,19 @@ class App {
* @param {boolean} minimise
*/
setDesktopUI(minimise) {
- $("[data-toggle=tooltip]").tooltip("enable");
this.setCompileMessage();
this.setDesktopSplitter(minimise);
this.adjustComponentSizes();
+ $("[data-toggle=tooltip]").tooltip("enable");
}
/**
* Set mobile UI ( on init and on window resize events )
*/
setMobileUI() {
- $("[data-toggle=tooltip]").tooltip("disable");
this.setMobileSplitter();
this.assignAvailableHeight();
+ $("[data-toggle=tooltip]").tooltip("disable");
}
/**
@@ -902,98 +889,18 @@ class App {
document.getElementById("operations-dropdown").style.maxHeight = `${window.innerHeight - (bannerHeight+operationsHeight)}px`;
}
-
/**
- * Create the template for a single category
- *
- * @param {object} catConf
- * @param {boolean} selected
+ * @fires Manager#oplistcreate in nested c-category-li build() function
*/
- createCategory(catConf, selected) {
- const cat = new HTMLCategory(catConf.name, selected);
+ buildCategoryList() {
+ const categoryList = new CCategoryList(
+ this,
+ this.categories,
+ this.operations
+ );
+ categoryList.build();
- catConf.ops.forEach(opName => {
- if (!(opName in this.operations)) {
- log.warn(`${opName} could not be found.`);
- return;
- }
-
- const op = new HTMLOperation(opName, this.operations[opName], this, this.manager);
- cat.addOperation(op);
- });
-
- return cat;
- }
-
- /**
- * Add a category to an element
- *
- * @param {object} catConf
- * @param {HTMLElement} targetElement
- * @param {boolean} selected ( false by default )
- */
- addCategoryToElement(catConf, targetElement, selected = false) {
- const cat = this.createCategory(catConf, selected);
- const catName = "cat" + cat.name.replace(/[\s/\-:_]/g, "");
-
- if (catConf.name === "Favourites") {
- targetElement.innerHTML = cat.toHtml() + targetElement.innerHTML;
- } else {
- targetElement.innerHTML += cat.toHtml();
- }
-
- targetElement.querySelector(`#${catName} > .op-list`).dispatchEvent(this.manager.oplistcreate);
- }
-
- /**
- * Remove a category from the DOM
- *
- * @param {string} catName
- */
- removeCategoryFromDOM(catName) {
- document.querySelector(`#${catName}`).parentNode.remove();
- }
-
- /**
- * Build the #operations accordion list with the categories and operations specified in the
- * view constructor.
- *
- * @fires Manager#oplistcreate
- */
- buildOperationsList() {
- const targetElement = document.getElementById("categories");
-
- // Move the edit button away before we overwrite the categories.
- // A note: this is hacky and should be solved in a proper way once the entire
- // codebase and architecture gets a thorough refactoring.
- document.body.appendChild(document.getElementById("edit-favourites"));
-
- this.categories.forEach((catConf, index) => {
- this.addCategoryToElement(catConf, targetElement, index === 0);
- });
-
- this.setEditFavourites();
-
- this.manager.ops.updateListItemsClasses("#catFavourites > .op-list", "favourite");
- this.manager.ops.updateListItemsClasses("#rec-list", "selected");
- }
-
-
- /**
- * Set appropriate attributes and values and append "edit-favourites"
- * to the favourites category
- */
- setEditFavourites() {
- // Add edit button to first category (Favourites)
- const favCat = document.querySelector("#categories a[data-target='#catFavourites']");
- favCat.appendChild(document.getElementById("edit-favourites"));
- favCat.setAttribute("data-help-title", "Favourite operations");
- favCat.setAttribute("data-help", `
This category displays your favourite operations.
-
-
To add: drag an operation over the Favourites category
-
To reorder: Click on the 'Edit favourites' button and drag operations up and down in the list provided
-
To remove: Click on the 'Edit favourites' button and hit the delete button next to the operation you want to remove
-
`);
+ document.querySelector("#categories").appendChild( categoryList );
}
}
diff --git a/src/web/HTMLCategory.mjs b/src/web/HTMLCategory.mjs
index 74f88a5e..89a7ef36 100755
--- a/src/web/HTMLCategory.mjs
+++ b/src/web/HTMLCategory.mjs
@@ -35,14 +35,11 @@ class HTMLCategory {
/**
* Renders the category and all operations within it in HTML.
*
- * @returns {string}
- *
- * @TODO: it will be better if this, and the other HTMLX.js, toHTML() functions
- * created HTML elements rather than insert and change stringified html, but that
- * would be part of a bigger refactoring adventure
+ * @returns {HTMLElement}
*/
toHtml() {
const catName = "cat" + this.name.replace(/[\s/\-:_]/g, "");
+
let html = `
";
+
return html;
}
-
}
export default HTMLCategory;
diff --git a/src/web/HTMLOperation.mjs b/src/web/HTMLOperation.mjs
index 01c623cc..9c19b06f 100755
--- a/src/web/HTMLOperation.mjs
+++ b/src/web/HTMLOperation.mjs
@@ -7,6 +7,7 @@
import HTMLIngredient from "./HTMLIngredient.mjs";
import Utils from "../core/Utils.mjs";
import url from "url";
+import {COperationLi} from "./components/c-operation-li.mjs";
/**
@@ -40,47 +41,6 @@ class HTMLOperation {
}
- /**
- * Renders the operation in HTML as a stub operation with no ingredients.
- *
- * @returns {string}
- */
- toStubHtml(removeIcon) {
- // this.name is polluted with HTML if it originates from search-results, so before
- // returning the HTML we purge this.name from any HTML for the data-name attribute
- const name = this.name.replace(/(<([^>]+)>)/ig, "");
-
- // check if local storage is available *and* has favourites at all ( otherwise default favs are used )
- const isFavourite = this.app.isLocalStorageAvailable() && localStorage.favourites?.includes(name);
-
- let html = `
${titleFromWikiLink(this.infoURL)}` : "";
-
- html += ` data-container='body' data-toggle='popover' data-placement='left'
- data-content="${this.description}${infoLink}" data-html='true' data-trigger='hover'
- data-boundary='viewport'`;
- }
-
- html += ">" + this.name;
-
- if (removeIcon) {
- html += "delete";
- } else if (!removeIcon && this.app.isMobileView()) {
- html += "check";
- }
-
- if (this.app.isMobileView()) {
- html += `${isFavourite ? "star" : "star_outline"}`;
- }
-
- html += "
";
-
- return html;
- }
-
-
/**
* Renders the operation in HTML as a full operation with ingredients.
*
@@ -154,36 +114,6 @@ class HTMLOperation {
this.description = desc;
}
}
-
-}
-
-
-/**
- * Given a URL for a Wikipedia (or other wiki) page, this function returns a link to that page.
- *
- * @param {string} urlStr
- * @returns {string}
- */
-function titleFromWikiLink(urlStr) {
- const urlObj = url.parse(urlStr);
- let wikiName = "",
- pageTitle = "";
-
- switch (urlObj.host) {
- case "forensicswiki.xyz":
- wikiName = "Forensics Wiki";
- pageTitle = urlObj.query.substr(6).replace(/_/g, " "); // Chop off 'title='
- break;
- case "wikipedia.org":
- wikiName = "Wikipedia";
- pageTitle = urlObj.pathname.substr(6).replace(/_/g, " "); // Chop off '/wiki/'
- break;
- default:
- // Not a wiki link, return full URL
- return `More Informationopen_in_new`;
- }
-
- return `${pageTitle}open_in_new on ${wikiName}`;
}
export default HTMLOperation;
diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs
index a04291b1..758a8ae2 100755
--- a/src/web/Manager.mjs
+++ b/src/web/Manager.mjs
@@ -156,7 +156,7 @@ class Manager {
document.getElementById("reset-favourites").addEventListener("click", this.ops.resetFavouritesClick.bind(this.ops));
this.addDynamicListener(".op-list", "oplistcreate", this.ops.opListCreate, this.ops);
this.addDynamicListener("li.operation", "operationadd", this.recipe.opAdd, this.recipe);
- this.addDynamicListener(".icon-add-favourite", "click", this.ops.onIconFavouriteClick, this.ops);
+ // this.addDynamicListener(".op-icon.star-icon", "click", this.ops.onIconFavouriteClick, this.ops);
// Recipe
this.addDynamicListener(".arg:not(select)", "input", this.recipe.ingChange, this.recipe);
diff --git a/src/web/TODO.md b/src/web/TODO.md
new file mode 100644
index 00000000..e69de29b
diff --git a/src/web/components/c-category-li.mjs b/src/web/components/c-category-li.mjs
new file mode 100644
index 00000000..705da1c4
--- /dev/null
+++ b/src/web/components/c-category-li.mjs
@@ -0,0 +1,184 @@
+import {COperationLi} from "./c-operation-li.mjs";
+
+/**
+ * c(ustom element)-category-li ( list item )
+ *
+ * @param {App} app - The main view object for CyberChef
+ * @param {CatConf} category - The category and operations to be populated.
+ * @param {Object.} operations - The list of operation configuration objects.
+ * @param {Boolean} isExpanded - expand the category on init or not
+ * */
+export class CCategoryLi extends HTMLElement {
+ constructor(
+ app,
+ category,
+ operations,
+ isExpanded
+ ) {
+ super();
+
+ this.app = app;
+ this.category = category;
+ this.operations = operations;
+ this.label = category.name;
+ this.isExpanded = isExpanded;
+
+ this.build();
+
+ this.addEventListener("click", this.handleClick.bind(this));
+ }
+
+ // /**
+ // * Handle click
+ // *
+ // * @param {Event} e
+ // */
+ // handleClick(e) {
+ // if (e.target === this.querySelector("button")) {
+ // // todo back to this "hitbox" issue w the icon inside the button
+ // this.app.manager.ops.editFavouritesClick(e);
+ // }
+ // }
+
+ /**
+ * Build the li element
+ */
+ buildListItem() {
+ const li = document.createElement("li");
+
+ li.classList.add("panel");
+ li.classList.add("category");
+
+ return li;
+ };
+
+ /**
+ * Build the anchor element
+ */
+ buildAnchor() {
+ const a = document.createElement("a");
+
+ a.classList.add("category-title");
+
+ a.setAttribute("data-toggle", "collapse");
+ a.setAttribute("data-target", `#${"cat" + this.label.replace(/[\s/\-:_]/g, "")}`);
+
+ a.innerText = this.label;
+
+ if (this.label === "Favourites"){
+ const editFavouritesButton = this.buildEditFavourites(a);
+
+ a.setAttribute("data-help-title", "Favourite operations");
+ a.setAttribute("data-help", `
This category displays your favourite operations.
+
+
To add: Click on the star icon of an operation or drag an operation over the Favourites category on desktop devices
+
To reorder: Click on the 'Edit favourites' button and drag operations up and down in the list provided
+
To remove: Click on the 'Edit favourites' button and hit the delete button next to the operation you want to remove
+
`);
+
+ a.appendChild(editFavouritesButton);
+ }
+
+ return a;
+ };
+
+ /**
+ * Build the collapsable panel that contains the op-list for this category
+ */
+ buildCollapsablePanel(){
+ const div = document.createElement("div");
+
+ div.setAttribute("id", `${"cat" + this.label.replace(/[\s/\-:_]/g, "")}`);
+ div.setAttribute("data-parent", "#categories");
+
+ div.classList.add("panel-collapse");
+ div.classList.add("collapse");
+
+ if (this.isExpanded) {
+ div.classList.add("show");
+ }
+
+ return div;
+ };
+
+ /**
+ * Build the op-list for this category
+ *
+ * @param {string[]} opNames
+ */
+ buildOperationList(opNames) {
+ return opNames.map(opName => {
+ if (!(opName in this.operations)) {
+ log.warn(`${opName} could not be found.`);
+ return;
+ }
+
+ return new COperationLi(
+ this.app,
+ opName,
+ {
+ class: "check-icon",
+ innerText: "check"
+ },
+ this.operations[opName]
+ );
+ });
+ }
+
+ /**
+ * Build c-category-li and dispatch event oplistcreate
+ */
+ build() {
+ const ul = document.createElement("ul");
+ ul.classList.add("op-list");
+
+ const li = this.buildListItem();
+ const a = this.buildAnchor();
+ const div = this.buildCollapsablePanel();
+
+ li.appendChild(a);
+ li.appendChild(div);
+ div.appendChild(ul);
+ this.appendChild(li);
+
+ this.buildOperationList(this.category.ops).forEach(operationListItem =>
+ ul.appendChild(operationListItem)
+ );
+
+ ul.dispatchEvent(this.app.manager.oplistcreate);
+ }
+
+ /**
+ * Append a c-operation-li to this op-list
+ *
+ * @param {HTMLElement} cOperationLiElement
+ */
+ appendOperation(cOperationLiElement) {
+ this.querySelector('li > div > ul').appendChild(cOperationLiElement);
+ }
+
+ /**
+ * If this category is Favourites, build and return the star icon to the category
+ */
+ buildEditFavourites() {
+ const button = document.createElement("button");
+ const icon = document.createElement("i");
+
+ button.setAttribute("id", "edit-favourites");
+ button.setAttribute("type", "button");
+ button.setAttribute("data-toggle", "tooltip");
+ button.setAttribute("title", "Edit favourites");
+ button.classList.add("btn");
+ button.classList.add("btn-warning");
+ button.classList.add("bmd-btn-icon");
+
+ icon.classList.add("material-icons");
+ icon.innerText = "star";
+
+ button.appendChild(icon);
+
+ return button;
+ }
+}
+
+customElements.define("c-category-li", CCategoryLi);
diff --git a/src/web/components/c-category-list.mjs b/src/web/components/c-category-list.mjs
new file mode 100644
index 00000000..2f259c94
--- /dev/null
+++ b/src/web/components/c-category-list.mjs
@@ -0,0 +1,40 @@
+import {CCategoryLi} from "./c-category-li.mjs";
+
+/**
+ * c(ustom element)-category-list
+ *
+ * @param {App} app - The main view object for CyberChef
+ * @param {CatConf[]} categories - The list of categories and operations to be populated.
+ * @param {Object.} operations - The list of operation configuration objects.
+ **/
+export class CCategoryList extends HTMLElement {
+ constructor( app, categories, operations ) {
+ super();
+
+ this.app = app;
+ this.categories = categories;
+ this.operations = operations;
+ }
+
+ /**
+ * Build c-category-list
+ */
+ build() {
+ const ul = document.createElement("ul");
+
+ this.categories.forEach((category, index) => {
+ const cat = new CCategoryLi(
+ this.app,
+ category,
+ this.operations,
+ index === 0
+ );
+
+ ul.appendChild(cat);
+ });
+
+ this.append(ul);
+ }
+}
+
+customElements.define("c-category-list", CCategoryList);
diff --git a/src/web/components/c-operation-li.mjs b/src/web/components/c-operation-li.mjs
new file mode 100644
index 00000000..716d5c70
--- /dev/null
+++ b/src/web/components/c-operation-li.mjs
@@ -0,0 +1,176 @@
+import url from "url";
+
+/**
+ * c(ustom element)-operation-li ( list item )
+ *
+ * @param {App} app - The main view object for CyberChef
+ * @param {string} name - The name of the operation
+ * @param {Object} icon - { class: string, innerText: string } - The optional and customizable icon displayed on the right side of the operation
+ * @param {Object} config - The configuration object for this operation.
+ */
+export class COperationLi extends HTMLElement {
+ constructor(
+ app,
+ name,
+ icon,
+ config
+ ) {
+ super();
+
+ this.app = app;
+ this.name = name;
+ this.isFavourite = this.app.isLocalStorageAvailable() && localStorage.favourites?.includes(name);
+ this.icon = icon;
+ this.config = config;
+
+ this.build();
+
+ this.addEventListener('click', this.handleClick.bind(this));
+ this.addEventListener('dblclick', this.handleDoubleClick.bind(this));
+ }
+
+ /**
+ * @fires OperationsWaiter#operationDblclick on double click
+ * @param {Event} e
+ */
+ handleDoubleClick(e) {
+ if (e.target === this.querySelector("li")) {
+ this.querySelector("li.operation").classList.add("selected");
+ }
+ }
+
+ /**
+ * Handle click
+ * @param {Event} e
+ */
+ handleClick(e) {
+ if (e.target === this.querySelector("i.star-icon")) {
+ this.app.addFavourite(this.name);
+ this.updateFavourite(true);
+ }
+ }
+
+ /**
+ * Given a URL for a Wikipedia (or other wiki) page, this function returns a link to that page.
+ *
+ * @param {string} urlStr
+ * @returns {string}
+ */
+ titleFromWikiLink(urlStr) {
+ const urlObj = url.parse(urlStr);
+ let wikiName = "",
+ pageTitle = "";
+
+ switch (urlObj.host) {
+ case "forensicswiki.xyz":
+ wikiName = "Forensics Wiki";
+ pageTitle = urlObj.query.substr(6).replace(/_/g, " "); // Chop off 'title='
+ break;
+ case "wikipedia.org":
+ wikiName = "Wikipedia";
+ pageTitle = urlObj.pathname.substr(6).replace(/_/g, " "); // Chop off '/wiki/'
+ break;
+ default:
+ // Not a wiki link, return full URL
+ return `More Informationopen_in_new`;
+ }
+
+ return `${pageTitle}open_in_new on ${wikiName}`;
+ }
+
+ /**
+ * Build the li element
+ */
+ buildListItem() {
+ const li = document.createElement("li");
+
+ li.setAttribute("data-name", this.name);
+ li.classList.add("operation");
+
+ if (this.isFavourite) {
+ li.classList.add("favourite");
+ }
+
+ li.textContent = this.name;
+
+ if (this.config.description){
+ let dataContent = this.config.description;
+
+ if (this.config.infoURL) {
+ dataContent += `${this.titleFromWikiLink(this.config.infoURL)}`;
+ }
+
+ li.setAttribute("data-container", "body");
+ li.setAttribute("data-toggle", "popover");
+ li.setAttribute("data-placement", "left");
+ li.setAttribute("data-html", "true");
+ li.setAttribute("data-trigger", "hover");
+ li.setAttribute("data-boundary", "viewport");
+ li.setAttribute("data-content", dataContent);
+ }
+
+ return li;
+ }
+
+ /**
+ * Build the operation list item right side icon
+ */
+ buildIcon() {
+ const icon = document.createElement("i");
+
+ icon.classList.add("material-icons");
+ icon.classList.add("op-icon");
+ icon.classList.add(this.icon.class);
+
+ icon.innerText = this.icon.innerText;
+
+ return icon;
+ }
+
+ /**
+ * Build the star icon
+ */
+ buildStarIcon() {
+ const icon = document.createElement("i");
+ icon.setAttribute("title", this.name);
+
+ icon.classList.add("material-icons");
+ icon.classList.add("op-icon");
+ icon.classList.add("star-icon");
+
+ if (this.isFavourite){
+ icon.innerText = "star";
+ } else {
+ icon.innerText = "star_outline";
+ }
+
+ return icon;
+ }
+
+ /**
+ * Build c-operation-li
+ */
+ build() {
+ const li = this.buildListItem();
+ const icon = this.buildIcon();
+ const starIcon = this.buildStarIcon();
+
+ li.appendChild(icon);
+ li.appendChild(starIcon);
+
+ this.appendChild(li);
+ }
+
+ updateFavourite(isFavourite) {
+ if (isFavourite) {
+ this.querySelector("li").classList.add("favourite");
+ this.querySelector("i.star-icon").innerText = "star";
+ } else {
+ this.querySelector("li").classList.remove("favourite");
+ this.querySelector("i.star-icon").innerText = "star_outline";
+ }
+ }
+}
+
+
+customElements.define("c-operation-li", COperationLi);
diff --git a/src/web/html/index.html b/src/web/html/index.html
index daa0af3e..4e7ec022 100755
--- a/src/web/html/index.html
+++ b/src/web/html/index.html
@@ -142,13 +142,13 @@
-
+
+
+
+
+
+
+