From b1b0be254bee90b6881fe48df49232b5fe6768da Mon Sep 17 00:00:00 2001 From: Robin Scholtes Date: Fri, 16 Jun 2023 12:09:04 +1200 Subject: [PATCH] introducing and refactoring to using custom components for building category / cat lists / operation lists. This allows us to group relevant functionality more efficiently, easier maintenance in the future. We could, one by one, refactor and encapsulate components across CC in this manner. A very convenient and much needed effect of this implementation is that these components can hold functionality and references exclusively pertaining to themselves. Separating said functionality increases code compartmentalisation while references and event listeners requiring these references are guaranteed to be up to date with the component at all times. --- README.md | 2 +- src/web/App.mjs | 165 ++++------------ src/web/HTMLCategory.mjs | 9 +- src/web/HTMLOperation.mjs | 72 +------ src/web/Manager.mjs | 2 +- src/web/TODO.md | 0 src/web/components/c-category-li.mjs | 184 ++++++++++++++++++ src/web/components/c-category-list.mjs | 40 ++++ src/web/components/c-operation-li.mjs | 176 +++++++++++++++++ src/web/html/index.html | 15 +- src/web/stylesheets/components/_operation.css | 21 +- src/web/stylesheets/components/_recipe.css | 1 + .../components/operations/_categories.css | 5 +- .../components/operations/_operations.css | 5 +- src/web/waiters/OperationsWaiter.mjs | 88 ++------- src/web/waiters/RecipeWaiter.mjs | 14 +- src/web/waiters/WindowWaiter.mjs | 3 +- 17 files changed, 489 insertions(+), 313 deletions(-) create mode 100644 src/web/TODO.md create mode 100644 src/web/components/c-category-li.mjs create mode 100644 src/web/components/c-category-list.mjs create mode 100644 src/web/components/c-operation-li.mjs 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.

- `); + 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 = `
${this.name} @@ -55,9 +52,9 @@ class HTMLCategory { } 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.

    + `); + + 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 @@
    - + + + + + + +