/** * Waiter to handle events related to highlighting in CyberChef. * * @author n1474335 [n1474335@gmail.com] * @copyright Crown Copyright 2016 * @license Apache-2.0 * * @constructor * @param {HTMLApp} app - The main view object for CyberChef. */ var HighlighterWaiter = function(app) { this.app = app; this.mouse_button_down = false; this.mouse_target = null; }; /** * HighlighterWaiter data type enum for the input. * @readonly * @enum */ HighlighterWaiter.INPUT = 0; /** * HighlighterWaiter data type enum for the output. * @readonly * @enum */ HighlighterWaiter.OUTPUT = 1; /** * Determines if the current text selection is running backwards or forwards. * StackOverflow answer id: 12652116 * * @private * @returns {boolean} */ HighlighterWaiter.prototype._is_selection_backwards = function() { var backwards = false, sel = window.getSelection(); if (!sel.isCollapsed) { var range = document.createRange(); range.setStart(sel.anchorNode, sel.anchorOffset); range.setEnd(sel.focusNode, sel.focusOffset); backwards = range.collapsed; range.detach(); } return backwards; }; /** * Calculates the text offset of a position in an HTML element, ignoring HTML tags. * * @private * @param {element} node - The parent HTML node. * @param {number} offset - The offset since the last HTML element. * @returns {number} */ HighlighterWaiter.prototype._get_output_html_offset = function(node, offset) { var sel = window.getSelection(), range = document.createRange(); range.selectNodeContents(document.getElementById("output-html")); range.setEnd(node, offset); sel.removeAllRanges(); sel.addRange(range); return sel.toString().length; }; /** * Gets the current selection offsets in the output HTML, ignoring HTML tags. * * @private * @returns {Object} pos * @returns {number} pos.start * @returns {number} pos.end */ HighlighterWaiter.prototype._get_output_html_selection_offsets = function() { var sel = window.getSelection(), range, start = 0, end = 0, backwards = false; if (sel.rangeCount) { range = sel.getRangeAt(sel.rangeCount - 1); backwards = this._is_selection_backwards(); start = this._get_output_html_offset(range.startContainer, range.startOffset); end = this._get_output_html_offset(range.endContainer, range.endOffset); sel.removeAllRanges(); sel.addRange(range); if (backwards) { // If selecting backwards, reverse the start and end offsets for the selection to // prevent deselecting as the drag continues. sel.collapseToEnd(); sel.extend(sel.anchorNode, range.startOffset); } } return { start: start, end: end }; }; /** * Handler for input scroll events. * Scrolls the highlighter pane to match the input textarea position. * * @param {event} e */ HighlighterWaiter.prototype.input_scroll = function(e) { var el = e.target; document.getElementById("input-highlighter").scrollTop = el.scrollTop; document.getElementById("input-highlighter").scrollLeft = el.scrollLeft; }; /** * Handler for output scroll events. * Scrolls the highlighter pane to match the output textarea position. * * @param {event} e */ HighlighterWaiter.prototype.output_scroll = function(e) { var el = e.target; document.getElementById("output-highlighter").scrollTop = el.scrollTop; document.getElementById("output-highlighter").scrollLeft = el.scrollLeft; }; /** * Handler for input mousedown events. * Calculates the current selection info, and highlights the corresponding data in the output. * * @param {event} e */ HighlighterWaiter.prototype.input_mousedown = function(e) { this.mouse_button_down = true; this.mouse_target = HighlighterWaiter.INPUT; this.remove_highlights(); var el = e.target, start = el.selectionStart, end = el.selectionEnd; if (start !== 0 || end !== 0) { document.getElementById("input-selection-info").innerHTML = this.selection_info(start, end); this.highlight_output([{start: start, end: end}]); } }; /** * Handler for output mousedown events. * Calculates the current selection info, and highlights the corresponding data in the input. * * @param {event} e */ HighlighterWaiter.prototype.output_mousedown = function(e) { this.mouse_button_down = true; this.mouse_target = HighlighterWaiter.OUTPUT; this.remove_highlights(); var el = e.target, start = el.selectionStart, end = el.selectionEnd; if (start !== 0 || end !== 0) { document.getElementById("output-selection-info").innerHTML = this.selection_info(start, end); this.highlight_input([{start: start, end: end}]); } }; /** * Handler for output HTML mousedown events. * Calculates the current selection info. * * @param {event} e */ HighlighterWaiter.prototype.output_html_mousedown = function(e) { this.mouse_button_down = true; this.mouse_target = HighlighterWaiter.OUTPUT; var sel = this._get_output_html_selection_offsets(); if (sel.start !== 0 || sel.end !== 0) { document.getElementById("output-selection-info").innerHTML = this.selection_info(sel.start, sel.end); } }; /** * Handler for input mouseup events. * * @param {event} e */ HighlighterWaiter.prototype.input_mouseup = function(e) { this.mouse_button_down = false; }; /** * Handler for output mouseup events. * * @param {event} e */ HighlighterWaiter.prototype.output_mouseup = function(e) { this.mouse_button_down = false; }; /** * Handler for output HTML mouseup events. * * @param {event} e */ HighlighterWaiter.prototype.output_html_mouseup = function(e) { this.mouse_button_down = false; }; /** * Handler for input mousemove events. * Calculates the current selection info, and highlights the corresponding data in the output. * * @param {event} e */ HighlighterWaiter.prototype.input_mousemove = function(e) { // Check that the left mouse button is pressed if (!this.mouse_button_down || e.which !== 1 || this.mouse_target !== HighlighterWaiter.INPUT) return; var el = e.target, start = el.selectionStart, end = el.selectionEnd; if (start !== 0 || end !== 0) { document.getElementById("input-selection-info").innerHTML = this.selection_info(start, end); this.highlight_output([{start: start, end: end}]); } }; /** * Handler for output mousemove events. * Calculates the current selection info, and highlights the corresponding data in the input. * * @param {event} e */ HighlighterWaiter.prototype.output_mousemove = function(e) { // Check that the left mouse button is pressed if (!this.mouse_button_down || e.which !== 1 || this.mouse_target !== HighlighterWaiter.OUTPUT) return; var el = e.target, start = el.selectionStart, end = el.selectionEnd; if (start !== 0 || end !== 0) { document.getElementById("output-selection-info").innerHTML = this.selection_info(start, end); this.highlight_input([{start: start, end: end}]); } }; /** * Handler for output HTML mousemove events. * Calculates the current selection info. * * @param {event} e */ HighlighterWaiter.prototype.output_html_mousemove = function(e) { // Check that the left mouse button is pressed if (!this.mouse_button_down || e.which !== 1 || this.mouse_target !== HighlighterWaiter.OUTPUT) return; var sel = this._get_output_html_selection_offsets(); if (sel.start !== 0 || sel.end !== 0) { document.getElementById("output-selection-info").innerHTML = this.selection_info(sel.start, sel.end); } }; /** * Given start and end offsets, writes the HTML for the selection info element with the correct * padding. * * @param {number} start - The start offset. * @param {number} end - The end offset. * @returns {string} */ HighlighterWaiter.prototype.selection_info = function(start, end) { var width = end.toString().length; width = width < 2 ? 2 : width; var start_str = Utils.pad(start.toString(), width, " ").replace(/ /g, " "), end_str = Utils.pad(end.toString(), width, " ").replace(/ /g, " "), len_str = Utils.pad((end-start).toString(), width, " ").replace(/ /g, " "); return "start: " + start_str + "
end: " + end_str + "
length: " + len_str; }; /** * Removes highlighting and selection information. */ HighlighterWaiter.prototype.remove_highlights = function() { document.getElementById("input-highlighter").innerHTML = ""; document.getElementById("output-highlighter").innerHTML = ""; document.getElementById("input-selection-info").innerHTML = ""; document.getElementById("output-selection-info").innerHTML = ""; }; /** * Generates a list of all the highlight functions assigned to operations in the recipe, if the * entire recipe supports highlighting. * * @returns {Object[]} highlights * @returns {function} highlights[].f * @returns {function} highlights[].b * @returns {Object[]} highlights[].args */ HighlighterWaiter.prototype.generate_highlight_list = function() { var recipe_config = this.app.get_recipe_config(), highlights = []; for (var i = 0; i < recipe_config.length; i++) { if (recipe_config[i].disabled) continue; // If any breakpoints are set, do not attempt to highlight if (recipe_config[i].breakpoint) return false; var op = this.app.operations[recipe_config[i].op]; // If any of the operations do not support highlighting, fail immediately. if (op.highlight === false || op.highlight === undefined) return false; highlights.push({ f: op.highlight, b: op.highlight_reverse, args: recipe_config[i].args }); } return highlights; }; /** * Highlights the given offsets in the output. * We will only highlight if: * - input hasn't changed since last bake * - last bake was a full bake * - all operations in the recipe support highlighting * * @param {Object} pos - The position object for the highlight. * @param {number} pos.start - The start offset. * @param {number} pos.end - The end offset. */ HighlighterWaiter.prototype.highlight_output = function(pos) { var highlights = this.generate_highlight_list(); if (!highlights || !this.app.auto_bake_) { return false; } for (var i = 0; i < highlights.length; i++) { // Remove multiple highlights before processing again pos = [pos[0]]; if (typeof highlights[i].f == "function") { pos = highlights[i].f(pos, highlights[i].args); } } document.getElementById("output-selection-info").innerHTML = this.selection_info(pos[0].start, pos[0].end); this.highlight( document.getElementById("output-text"), document.getElementById("output-highlighter"), pos); }; /** * Highlights the given offsets in the input. * We will only highlight if: * - input hasn't changed since last bake * - last bake was a full bake * - all operations in the recipe support highlighting * * @param {Object} pos - The position object for the highlight. * @param {number} pos.start - The start offset. * @param {number} pos.end - The end offset. */ HighlighterWaiter.prototype.highlight_input = function(pos) { var highlights = this.generate_highlight_list(); if (!highlights || !this.app.auto_bake_) { return false; } for (var i = 0; i < highlights.length; i++) { // Remove multiple highlights before processing again pos = [pos[0]]; if (typeof highlights[i].b == "function") { pos = highlights[i].b(pos, highlights[i].args); } } document.getElementById("input-selection-info").innerHTML = this.selection_info(pos[0].start, pos[0].end); this.highlight( document.getElementById("input-text"), document.getElementById("input-highlighter"), pos); }; /** * Adds the relevant HTML to the specified highlight element such that highlighting appears * underneath the correct offset. * * @param {element} textarea - The input or output textarea. * @param {element} highlighter - The input or output highlighter element. * @param {Object} pos - The position object for the highlight. * @param {number} pos.start - The start offset. * @param {number} pos.end - The end offset. */ HighlighterWaiter.prototype.highlight = function(textarea, highlighter, pos) { if (!this.app.options.show_highlighter) return false; if (!this.app.options.attempt_highlight) return false; // Check if there is a carriage return in the output dish as this will not // be displayed by the HTML textarea and will mess up highlighting offsets. if (!this.app.dish_str || this.app.dish_str.indexOf("\r") >= 0) return false; var start_placeholder = "[start_highlight]", start_placeholder_regex = /\[start_highlight\]/g, end_placeholder = "[end_highlight]", end_placeholder_regex = /\[end_highlight\]/g, text = textarea.value; // Put placeholders in position // If there's only one value, select that // If there are multiple, ignore the first one and select all others if (pos.length === 1) { if (pos[0].end < pos[0].start) return; text = text.slice(0, pos[0].start) + start_placeholder + text.slice(pos[0].start, pos[0].end) + end_placeholder + text.slice(pos[0].end, text.length); } else { // O(n^2) - Can anyone improve this without overwriting placeholders? var result = "", end_placed = true; for (var i = 0; i < text.length; i++) { for (var j = 1; j < pos.length; j++) { if (pos[j].end < pos[j].start) continue; if (pos[j].start === i) { result += start_placeholder; end_placed = false; } if (pos[j].end === i) { result += end_placeholder; end_placed = true; } } result += text[i]; } if (!end_placed) result += end_placeholder; text = result; } var css_class = "hl1"; //if (colour) css_class += "-"+colour; // Remove HTML tags text = text.replace(/&/g, "&") .replace(//g, ">") .replace(/\n/g, " ") // Convert placeholders to tags .replace(start_placeholder_regex, "") .replace(end_placeholder_regex, "") + " "; // Adjust width to allow for scrollbars highlighter.style.width = textarea.clientWidth + "px"; highlighter.innerHTML = text; highlighter.scrollTop = textarea.scrollTop; highlighter.scrollLeft = textarea.scrollLeft; };