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 @@
    - + + + + + + +