diff --git a/.eslintignore b/.eslintignore index 529676da..da0a1615 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ src/core/vendor/** +src/web/static/clippy_assets/** \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js index 11b77452..cac7e842 100755 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -151,7 +151,7 @@ module.exports = function (grunt) { }, configs: ["*.{js,mjs}"], core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*", "!src/core/operations/legacy/**/*"], - web: ["src/web/**/*.{js,mjs}"], + web: ["src/web/**/*.{js,mjs}", "!src/web/static/**/*"], node: ["src/node/**/*.{js,mjs}"], tests: ["tests/**/*.{js,mjs}"], }, diff --git a/src/web/SeasonalWaiter.mjs b/src/web/SeasonalWaiter.mjs index 07c76a04..f516858b 100755 --- a/src/web/SeasonalWaiter.mjs +++ b/src/web/SeasonalWaiter.mjs @@ -5,6 +5,8 @@ */ import clippy from "clippyjs"; +import "./static/clippy_assets/agents/Clippy/agent.js"; +import clippyMap from "./static/clippy_assets/agents/Clippy/map.png"; /** * Waiter to handle seasonal events and easter eggs. @@ -35,8 +37,9 @@ class SeasonalWaiter { // Clippy const now = new Date(); - //if (now.getMonth() === 3 && now.getDate() === 1) { - if (now.getMonth() === 2 && now.getDate() === 22) { + if (now.getMonth() === 3 && now.getDate() === 1) { + this.addClippyOption(); + this.manager.addDynamicListener(".option-item #clippy", "change", this.setupClippy, this); this.setupClippy(); } } @@ -63,26 +66,56 @@ class SeasonalWaiter { } /** - * Sets up Clippy on April Fools Day + * Creates an option in the Options menu for turning Clippy on or off + */ + addClippyOption() { + const optionsBody = document.getElementById("options-body"), + optionItem = document.createElement("span"); + + optionItem.className = "bmd-form-group is-filled"; + optionItem.innerHTML = `
+ +
`; + optionsBody.appendChild(optionItem); + + this.manager.options.load(); + } + + /** + * Sets up Clippy for April Fools Day */ setupClippy() { - //const clippyAssets = "./agents/"; - const clippyAssets = undefined; + // Destroy any previous agents + if (this.clippyAgent) { + this.clippyAgent.closeBalloonImmediately(); + this.clippyAgent.hide(); + } + + if (!this.app.options.clippy) { + this.clippyTimeouts.forEach(t => clearTimeout(t)); + return; + } + + // Set base path to # to prevent external network requests + const clippyAssets = "#"; + // Shim the library to prevent external network requests + shimClippy(clippy); const self = this; clippy.load("Clippy", (agent) => { - window.agent = agent; - shimClippy(agent); + shimClippyAgent(agent); self.clippyAgent = agent; - agent.show(); - //agent.animate(); - agent.speak("Hello, I'm Clippy, your personal cyber assistant!", true); + agent.speak("Hello, I'm Clippy, your personal cyber assistant!"); }, undefined, clippyAssets); // Watch for the Auto Magic button appearing const magic = document.getElementById("magic"); const observer = new MutationObserver((mutationsList, observer) => { + // Read in message and recipe let msg, recipe; for (const mutation of mutationsList) { if (mutation.attributeName === "data-original-title") { @@ -93,42 +126,145 @@ class SeasonalWaiter { } } - // Cancel current animation and hide balloon (after it has finished) - self.clippyAgent._queue.clear(); - self.clippyAgent._queue.next(); - self.clippyAgent.stopCurrent(); - self.clippyAgent._balloon._hold = false; - self.clippyAgent._balloon.hide(); + // Close balloon if it is currently showing a magic hint + const balloon = self.clippyAgent._balloon._balloon; + if (balloon.is(":visible") && balloon.text().indexOf("That looks like encoded data") >= 0) { + self.clippyAgent._balloon.hide(true); + this.clippyAgent._balloon._hidden = true; + } + // If a recipe was found, get Clippy to tell the user if (recipe) { recipe = this.manager.controls.generateStateUrl(true, true, JSON.parse(recipe)); - msg = `That looks like encoded data!

${msg}

Click here to load this recipe.`; + msg = `That looks like encoded data!

${msg}

Click here to load this recipe.`; - // Stop balloon activity immediately and trigger speak again - self.clippyAgent._balloon.pause(); - delete self.clippyAgent._balloon._addWord; - self.clippyAgent._balloon._active = false; - self.clippyAgent._balloon.hide(true); + // Stop current balloon activity immediately and trigger speak again + this.clippyAgent.closeBalloonImmediately(); self.clippyAgent.speak(msg, true); + // self.clippyAgent._queue.next(); } }); - observer.observe(document.getElementById("magic"), { - attributes: true - }); + observer.observe(document.getElementById("magic"), {attributes: true}); + + // Play animations for various things + this.manager.addListeners("#search", "click", () => { + this.clippyAgent.play("Searching"); + }, this); + this.manager.addListeners("#save,#save-to-file", "click", () => { + this.clippyAgent.play("Save"); + }, this); + this.manager.addListeners("#clr-recipe,#clr-io", "click", () => { + this.clippyAgent.play("EmptyTrash"); + }, this); + this.manager.addListeners("#bake", "click", e => { + if (e.target.closest("button").textContent.toLowerCase().indexOf("bake") >= 0) { + this.clippyAgent.play("Thinking"); + } else { + this.clippyAgent.play("EmptyTrash"); + } + this.clippyAgent._queue.clear(); + }, this); + this.manager.addListeners("#input-text", "keydown", () => { + this.clippyAgent.play("Writing"); + this.clippyAgent._queue.clear(); + }, this); + this.manager.addDynamicListener("a.clippyMagicRecipe", "click", (e) => { + this.clippyAgent.play("Congratulate"); + }, this); + + this.clippyTimeouts = []; + // Show challenge after timeout + this.clippyTimeouts.push(setTimeout(() => { + const hex = "1f 8b 08 00 ae a1 9b 5c 00 ff 05 40 a1 12 00 10 0c fd 26 61 5b 76 aa 9d 26 a8 02 02 37 84 f7 fb bb c5 a4 5f 22 c6 09 e5 6e c5 4c 2d 3f e9 30 a6 ea 41 a2 f2 ac 1c 00 00 00"; + self.clippyAgent.speak(`How about a fun challenge?

Try decoding this (click to load):
${hex}`, true); + self.clippyAgent.play("GetAttention"); + }, 1 * 60 * 1000)); + + this.clippyTimeouts.push(setTimeout(() => { + self.clippyAgent.speak("Did you know?

You can load files into CyberChef up to around 500MB using drag and drop or the load file button.", 15000); + self.clippyAgent.play("Wave"); + }, 2 * 60 * 1000)); + + this.clippyTimeouts.push(setTimeout(() => { + self.clippyAgent.speak("Did you know?

You can use the 'Fork' operation to split up your input and run the recipe over each branch separately.

Here's an example.", 15000); + self.clippyAgent.play("Print"); + }, 3 * 60 * 1000)); + + this.clippyTimeouts.push(setTimeout(() => { + self.clippyAgent.speak("Did you know?

The 'Magic' operation uses a number of methods to detect encoded data and the operations which can be used to make sense of it. A technical description of these methods can be found here.", 15000); + self.clippyAgent.play("Alert"); + }, 4 * 60 * 1000)); + + this.clippyTimeouts.push(setTimeout(() => { + self.clippyAgent.speak("Did you know?

You can use parts of the input as arguments to operations.

Click here for an example.", 15000); + self.clippyAgent.play("CheckingSomething"); + }, 5 * 60 * 1000)); } } + /** * Shims various ClippyJS functions to modify behaviour. * + * @param {Clippy} clippy - The Clippy library + */ +function shimClippy(clippy) { + // Shim _loadSounds so that it doesn't actually try to load any sounds + clippy.load._loadSounds = function _loadSounds (name, path) { + let dfd = clippy.load._sounds[name]; + if (dfd) return dfd; + + // set dfd if not defined + dfd = clippy.load._sounds[name] = $.Deferred(); + + // Resolve immediately without loading + dfd.resolve({}); + + return dfd.promise(); + }; + + // Shim _loadMap so that it uses the local copy + clippy.load._loadMap = function _loadMap (path) { + let dfd = clippy.load._maps[path]; + if (dfd) return dfd; + + // set dfd if not defined + dfd = clippy.load._maps[path] = $.Deferred(); + + const src = clippyMap; + const img = new Image(); + + img.onload = dfd.resolve; + img.onerror = dfd.reject; + + // start loading the map; + img.setAttribute("src", src); + + return dfd.promise(); + }; + + // Make sure we don't request the remote map + clippy.Animator.prototype._setupElement = function _setupElement (el) { + const frameSize = this._data.framesize; + el.css("display", "none"); + el.css({ width: frameSize[0], height: frameSize[1] }); + el.css("background", "url('" + clippyMap + "') no-repeat"); + + return el; + }; +} + +/** + * Shims various ClippyJS Agent functions to modify behaviour. + * * @param {Agent} agent - The Clippy Agent */ -function shimClippy(agent) { +function shimClippyAgent(agent) { // Turn off all sounds agent._animator._playSound = () => {}; - // Improve speak function + // Improve speak function to support HTML markup const self = agent._balloon; agent._balloon.speak = (complete, text, hold) => { self._hidden = false; @@ -147,10 +283,11 @@ function shimClippy(agent) { self._complete = complete; self._sayWords(text, hold, complete); + if (hold) agent._queue.next(); }; - // Improve the _sayWords function to allow HTML - agent._balloon.WORD_SPEAK_TIME = 100; + // Improve the _sayWords function to allow HTML and support timeouts + agent._balloon.WORD_SPEAK_TIME = 60; agent._balloon._sayWords = (text, hold, complete) => { self._active = true; self._hold = hold; @@ -158,6 +295,7 @@ function shimClippy(agent) { const time = self.WORD_SPEAK_TIME; const el = self._content; let idx = 1; + clearTimeout(self.holdTimeout); self._addWord = $.proxy(function () { if (!self._active) return; @@ -167,6 +305,12 @@ function shimClippy(agent) { if (!self._hold) { complete(); self.hide(); + } else if (typeof hold === "number") { + self.holdTimeout = setTimeout(() => { + self._hold = false; + complete(); + self.hide(); + }, hold); } } else { el.html(words.slice(0, idx).join(" ")); @@ -178,10 +322,24 @@ function shimClippy(agent) { self._addWord(); }; - // Close the balloon on click + // Add break-word to balloon CSS + agent._balloon._balloon.css("word-break", "break-word"); + + // Close the balloon on click (unless it was a link) agent._balloon._balloon.click(e => { - agent._balloon._finishHideBalloon(); + if (e.target.nodeName !== "A") { + agent._balloon.hide(true); + agent._balloon._hidden = true; + } }); + + // Add function to immediately close the balloon even if it is currently doing something + agent.closeBalloonImmediately = () => { + agent._queue.clear(); + agent._balloon.hide(true); + agent._balloon._hidden = true; + agent._queue.next(); + }; } export default SeasonalWaiter; diff --git a/src/web/html/index.html b/src/web/html/index.html index 119e8ada..9d786f5b 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -591,7 +591,7 @@ What sort of things can I do with CyberChef?
-

There are around 200 operations in CyberChef allowing you to carry out simple and complex tasks easily. Here are some examples:

+

There are around 300 operations in CyberChef allowing you to carry out simple and complex tasks easily. Here are some examples: