From 45886c704308a1c70cf0dcb1c83ff669e1f0ce04 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 18 Aug 2026 14:05:39 -0700 Subject: [PATCH 01/11] Script Loader: Restore the admin easter egg behind the command palette. The revisions easter egg was removed in [24820] because the revisions UI it hooked into was rewritten and its trigger disappeared with it, not because the egg was unwanted. This gives the payload a new home: a command palette loader that returns no commands, and so stays invisible, until its phrase is typed. The 2013 original shipped as a Dean Edwards packed blob. It is unpacked here into readable source and no longer depends on jQuery, since obfuscated code is not GPL source. See #15262. The trigger phrase is still compared through the original Dvorak/QWERTY cipher so that it is not a readable string in the admin bundle; that is the only obfuscation retained, and it is aimed at users rather than developers. The payload is fetched only once the egg has been invoked, so a normal admin page load carries nothing but the loader. It renders into a self-contained overlay instead of taking over the document as the original did. See #24852. --- Gruntfile.js | 2 + src/js/_enqueues/admin/easter-egg-loader.js | 93 ++++++++++ src/js/_enqueues/admin/easter-egg.js | 196 ++++++++++++++++++++ src/wp-includes/script-loader.php | 27 +++ 4 files changed, 318 insertions(+) create mode 100644 src/js/_enqueues/admin/easter-egg-loader.js create mode 100644 src/js/_enqueues/admin/easter-egg.js diff --git a/Gruntfile.js b/Gruntfile.js index 61f18481e23a8..b013cefa968aa 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -513,6 +513,8 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-admin/js/common.js' ]: [ './src/js/_enqueues/admin/common.js' ], [ WORKING_DIR + 'wp-admin/js/custom-background.js' ]: [ './src/js/_enqueues/admin/custom-background.js' ], [ WORKING_DIR + 'wp-admin/js/custom-header.js' ]: [ './src/js/_enqueues/admin/custom-header.js' ], + [ WORKING_DIR + 'wp-admin/js/easter-egg.js' ]: [ './src/js/_enqueues/admin/easter-egg.js' ], + [ WORKING_DIR + 'wp-admin/js/easter-egg-loader.js' ]: [ './src/js/_enqueues/admin/easter-egg-loader.js' ], [ WORKING_DIR + 'wp-admin/js/customize-controls.js' ]: [ './src/js/_enqueues/wp/customize/controls.js' ], [ WORKING_DIR + 'wp-admin/js/customize-nav-menus.js' ]: [ './src/js/_enqueues/wp/customize/nav-menus.js' ], [ WORKING_DIR + 'wp-admin/js/customize-widgets.js' ]: [ './src/js/_enqueues/wp/customize/widgets.js' ], diff --git a/src/js/_enqueues/admin/easter-egg-loader.js b/src/js/_enqueues/admin/easter-egg-loader.js new file mode 100644 index 0000000000000..c231ee291559a --- /dev/null +++ b/src/js/_enqueues/admin/easter-egg-loader.js @@ -0,0 +1,93 @@ +/** + * Hides the admin easter egg behind the command palette. + * + * This registers a command *loader* rather than a static command. A loader is handed the + * text currently typed into the palette, so returning an empty list of commands keeps the + * entry invisible until the phrase is typed. A static command would always be listed. + * + * The phrase is compared in its Dvorak/QWERTY-substituted form, using the same cipher the + * 2013 original used, so it is not a readable string in the admin bundle. That is the one + * piece of deliberate obfuscation here and it is aimed at users, not at other developers: + * the mystery is meant to live in the trigger. Everything else, including the payload in + * easter-egg.js, ships as readable source. + * + * @output wp-admin/js/easter-egg-loader.js + */ + +( function ( wp, settings ) { + 'use strict'; + + if ( ! wp || ! wp.data || ! settings ) { + return; + } + + var KEY = 'ishdg;rsdkot', + LABEL = 'Jre. co Lr.ypf', + FROM = '\',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', + TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; + + /** + * Applies the Dvorak/QWERTY substitution cipher. + * + * @param {string} value Text to substitute. + * @return {string} Substituted text. + */ + function dvortr( value ) { + var map = {}, + i; + + for ( i = 0; i < FROM.length; i++ ) { + map[ FROM.charAt( i ) ] = TO.charAt( i ); + } + + return value.replace( /[\s\S]/g, function ( character ) { + return map[ character ] || character; + } ); + } + + /** + * Fetches the payload if it is not already loaded, then plays it. + */ + function play() { + var script; + + if ( wp.easterEgg ) { + wp.easterEgg.run( settings.name ); + return; + } + + script = document.createElement( 'script' ); + script.src = settings.src; + + script.onload = function () { + if ( wp.easterEgg ) { + wp.easterEgg.run( settings.name ); + } + }; + + document.head.appendChild( script ); + } + + wp.data.dispatch( 'core/commands' ).registerCommandLoader( { + name: 'core/easter-egg', + hook: function ( args ) { + var search = ( ( args && args.search ) || '' ).toLowerCase().replace( /[^a-z0-9]/g, '' ); + + if ( ! search || dvortr( search ) !== KEY ) { + return { commands: [], isLoading: false }; + } + + return { + commands: [ { + name: 'core/easter-egg', + label: dvortr( LABEL ), + callback: function ( commandArgs ) { + commandArgs.close(); + play(); + } + } ], + isLoading: false + }; + } + } ); +}( window.wp, window.wpEasterEgg ) ); diff --git a/src/js/_enqueues/admin/easter-egg.js b/src/js/_enqueues/admin/easter-egg.js new file mode 100644 index 0000000000000..4e09edc21f2c7 --- /dev/null +++ b/src/js/_enqueues/admin/easter-egg.js @@ -0,0 +1,196 @@ +/** + * The WordPress admin easter egg. + * + * Originally written by Matt Mullenweg and shipped in WordPress 3.6 as + * wp-admin/js/revisions-js.php, where it triggered on comparing a post revision to + * itself. Removed in r24820 (see #24852) because that revisions UI was rewritten and + * the trigger disappeared with it, not because the egg was unwanted. Restored here + * with a new home behind the command palette. + * + * The 2013 original shipped as a Dean Edwards packed blob with its dialogue run + * through the Dvorak/QWERTY substitution cipher in dvortr(). Both are unwound here: + * obfuscated code is not GPL source (see #15262), and the mystery is meant to live in + * the trigger, not in unreadable code. This file is only fetched once the egg has + * already been invoked, so it never loads on a normal admin screen. + * + * Timings, dialogue, and staging are faithful to the original. + * + * @output wp-admin/js/easter-egg.js + */ + +( function ( wp ) { + 'use strict'; + + var TYPE_SPEED = 100, // Milliseconds per character. + LINE_PAUSE = 2000, // Between lines. + START_DELAY = 3000, // Before the first line. + FADE_TIME = 3000, // Cursor fade-in, once the lights go out. + ACT_PAUSE = 4000; // Between the two acts. + + var ACT_ONE = [ + 'Self-comparison detected.', + 'Initiating infinite loop eschewal protocol.', + 'Self destruct in... 3', + '2', + '1' + ]; + + // %s is replaced with the current user's display name. + var ACT_TWO = [ + 'Wake up, %s...', + 'The Matrix has you...', + 'Follow the white rabbit.' + ]; + + var STYLE = [ + '.wp-easter-egg{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', + 'font-family:courier,monospace;font-size:16px;line-height:1.6;', + 'background:#fff;color:#000;overflow:hidden;', + 'transition:background-color .6s linear,color .6s linear}', + '.wp-easter-egg.is-dark{background:#000;color:#0f0}', + '.wp-easter-egg p{margin:0;white-space:pre-wrap}', + '.wp-easter-egg .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', + '.wp-easter-egg .cursor.is-visible{opacity:1;animation:wp-easter-egg-blink 1s step-end infinite}', + '@keyframes wp-easter-egg-blink{50%{opacity:0}}', + '@media (prefers-reduced-motion:reduce){', + '.wp-easter-egg .cursor.is-visible{animation:none}', + '.wp-easter-egg,.wp-easter-egg .cursor{transition:none}}' + ].join( '' ); + + /** + * Plays the scene. + * + * @param {string} displayName The current user's display name. + */ + function run( displayName ) { + var aborted = false, + timer = null; + + var style = document.createElement( 'style' ); + style.textContent = STYLE; + + var overlay = document.createElement( 'div' ); + overlay.className = 'wp-easter-egg'; + + var line = document.createElement( 'p' ); + + var cursor = document.createElement( 'span' ); + cursor.className = 'cursor'; + cursor.textContent = '▌'; + + line.appendChild( cursor ); + overlay.appendChild( line ); + + function wait( ms, next ) { + timer = window.setTimeout( function () { + if ( ! aborted ) { + next(); + } + }, ms ); + } + + function clear() { + while ( line.firstChild !== cursor ) { + line.removeChild( line.firstChild ); + } + } + + function newline() { + line.insertBefore( document.createElement( 'br' ), cursor ); + } + + /** + * Types one line, a character at a time, ahead of the cursor. + */ + function type( text, done ) { + var chars = text.split( '' ); + + ( function next() { + if ( aborted ) { + return; + } + + if ( ! chars.length ) { + done(); + return; + } + + line.insertBefore( document.createTextNode( chars.shift() ), cursor ); + wait( TYPE_SPEED, next ); + }() ); + } + + /** + * Act one: typed on the lights-on screen, each line under the last. + */ + function actOne( index ) { + if ( index >= ACT_ONE.length ) { + lightsOut(); + return; + } + + type( ACT_ONE[ index ], function () { + newline(); + wait( LINE_PAUSE, function () { + actOne( index + 1 ); + } ); + } ); + } + + /** + * The turn: screen to black, text to green, cursor fades up alone. + */ + function lightsOut() { + overlay.className = 'wp-easter-egg is-dark'; + clear(); + cursor.className = 'cursor is-visible'; + wait( ACT_PAUSE, function () { + actTwo( 0 ); + } ); + } + + /** + * Act two: one line at a time, cleared between each. + */ + function actTwo( index ) { + if ( index >= ACT_TWO.length ) { + wait( LINE_PAUSE, function () { + clear(); + cursor.className = 'cursor'; + } ); + return; + } + + type( ACT_TWO[ index ].replace( '%s', displayName ), function () { + wait( LINE_PAUSE, function () { + clear(); + actTwo( index + 1 ); + } ); + } ); + } + + function abort( event ) { + if ( event && 'keydown' === event.type && 'Escape' !== event.key ) { + return; + } + + aborted = true; + window.clearTimeout( timer ); + document.removeEventListener( 'keydown', abort ); + overlay.remove(); + style.remove(); + } + + document.addEventListener( 'keydown', abort ); + overlay.addEventListener( 'click', abort ); + + document.head.appendChild( style ); + document.body.appendChild( overlay ); + + wait( START_DELAY, function () { + actOne( 0 ); + } ); + } + + wp.easterEgg = { run: run }; +}( window.wp = window.wp || {} ) ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index a364439f0abbb..4ed3e2510c63b 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1242,6 +1242,8 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'password-toggle', "/wp-admin/js/password-toggle$suffix.js", array(), false, 1 ); $scripts->set_translations( 'password-toggle' ); + $scripts->add( 'wp-easter-egg-loader', "/wp-admin/js/easter-egg-loader$suffix.js", array( 'wp-data', 'wp-core-commands' ), false, 1 ); + $scripts->add( 'application-passwords', "/wp-admin/js/application-passwords$suffix.js", array( 'jquery', 'wp-util', 'wp-api-request', 'wp-date', 'wp-i18n', 'wp-hooks' ), false, 1 ); $scripts->set_translations( 'application-passwords' ); @@ -3617,6 +3619,31 @@ function wp_enqueue_command_palette_assets() { wp_json_encode( $command_palette_settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) ) ); + + /* + * The admin easter egg. The loader is small and stays inert until its phrase is + * typed into the palette; the payload it plays is fetched only at that point, so + * nothing else is added to a normal admin page load. See easter-egg-loader.js. + */ + wp_enqueue_script( 'wp-easter-egg-loader' ); + wp_add_inline_script( + 'wp-easter-egg-loader', + sprintf( + 'window.wpEasterEgg = %s;', + wp_json_encode( + array( + 'src' => add_query_arg( + 'ver', + get_bloginfo( 'version' ), + admin_url( 'js/easter-egg' . wp_scripts_get_suffix() . '.js' ) + ), + 'name' => wp_get_current_user()->display_name, + ), + JSON_HEX_TAG | JSON_UNESCAPED_SLASHES + ) + ), + 'before' + ); } /** From a27c5b52e3ab4283d54aa7803d44b1bd0da8f900 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 18 Aug 2026 14:18:25 -0700 Subject: [PATCH 02/11] Script Loader: Fix overlay coverage, accessibility, and ending of the admin easter egg. Browser testing surfaced three problems with the overlay. The page behind it stayed scrollable, so the overlay measured 15px narrower than the viewport and left a visible scrollbar strip down the side. Scrolling is now locked on the document element while the scene plays and restored when it is dismissed. The scene inserts text one character at a time, and each character landed in the accessibility tree as its own node. The overlay is purely decorative, so it is now marked aria-hidden. Act two ended on an empty black screen with no indication of how to leave it. It now closes on the line the original used as its noscript fallback, which both ends the scene deliberately and hints that the page is still there. --- src/js/_enqueues/admin/easter-egg.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/js/_enqueues/admin/easter-egg.js b/src/js/_enqueues/admin/easter-egg.js index 4e09edc21f2c7..4761e9e9fc934 100644 --- a/src/js/_enqueues/admin/easter-egg.js +++ b/src/js/_enqueues/admin/easter-egg.js @@ -42,6 +42,9 @@ 'Follow the white rabbit.' ]; + // Left on screen at the end, as the original's noscript fallback did. + var CLOSING = "Don't let this happen again."; + var STYLE = [ '.wp-easter-egg{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', 'font-family:courier,monospace;font-size:16px;line-height:1.6;', @@ -63,14 +66,16 @@ * @param {string} displayName The current user's display name. */ function run( displayName ) { - var aborted = false, - timer = null; + var aborted = false, + timer = null, + previousOverflow = document.documentElement.style.overflow; var style = document.createElement( 'style' ); style.textContent = STYLE; var overlay = document.createElement( 'div' ); overlay.className = 'wp-easter-egg'; + overlay.setAttribute( 'aria-hidden', 'true' ); var line = document.createElement( 'p' ); @@ -154,8 +159,7 @@ */ function actTwo( index ) { if ( index >= ACT_TWO.length ) { - wait( LINE_PAUSE, function () { - clear(); + type( CLOSING, function () { cursor.className = 'cursor'; } ); return; @@ -177,6 +181,7 @@ aborted = true; window.clearTimeout( timer ); document.removeEventListener( 'keydown', abort ); + document.documentElement.style.overflow = previousOverflow; overlay.remove(); style.remove(); } @@ -186,6 +191,7 @@ document.head.appendChild( style ); document.body.appendChild( overlay ); + document.documentElement.style.overflow = 'hidden'; wait( START_DELAY, function () { actOne( 0 ); From a686785a14cb2df23774d37d3927544c2152a104 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 18 Aug 2026 15:00:01 -0700 Subject: [PATCH 03/11] Script Loader: Use single quotes for the easter egg closing line. JSHint enforces single-quoted strings in core JS, and jshint:core failed on this one double-quoted literal. It was written that way only because the line contains an apostrophe, so escape the apostrophe instead. --- src/js/_enqueues/admin/easter-egg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/_enqueues/admin/easter-egg.js b/src/js/_enqueues/admin/easter-egg.js index 4761e9e9fc934..24687ba2681dc 100644 --- a/src/js/_enqueues/admin/easter-egg.js +++ b/src/js/_enqueues/admin/easter-egg.js @@ -43,7 +43,7 @@ ]; // Left on screen at the end, as the original's noscript fallback did. - var CLOSING = "Don't let this happen again."; + var CLOSING = 'Don\'t let this happen again.'; var STYLE = [ '.wp-easter-egg{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', From 9597553d725af04c2e9fa09c622b69733e4cd77e Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 18 Aug 2026 15:27:05 -0700 Subject: [PATCH 04/11] Script Loader: Trigger the easter egg without listing a command. A registered command appears in the palette as soon as its phrase matches, so anyone typing near the phrase is shown the egg rather than discovering it. Nothing is registered now. A keydown handler watches for Enter while the palette is open and acts on the phrase itself, which leaves the palette showing its ordinary "No results found." state throughout, indistinguishable from a search that missed. The handler runs in the capture phase, so it is guarded on the palette being open and the phrase matching before it touches the event. Verified that ordinary palette commands still run and that Enter on a genuinely empty search does nothing. Also drops the now-unused label constant, since no label is rendered. --- src/js/_enqueues/admin/easter-egg-loader.js | 75 ++++++++++++--------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/src/js/_enqueues/admin/easter-egg-loader.js b/src/js/_enqueues/admin/easter-egg-loader.js index c231ee291559a..6e4f9bb0392b9 100644 --- a/src/js/_enqueues/admin/easter-egg-loader.js +++ b/src/js/_enqueues/admin/easter-egg-loader.js @@ -1,15 +1,18 @@ /** * Hides the admin easter egg behind the command palette. * - * This registers a command *loader* rather than a static command. A loader is handed the - * text currently typed into the palette, so returning an empty list of commands keeps the - * entry invisible until the phrase is typed. A static command would always be listed. + * The palette is used as the doorway, but the egg deliberately does not register a + * command. A registered command would be listed the moment its phrase matched, which + * gives the thing away to anyone who happens to type near it. Instead this watches for + * Enter while the palette is open and acts on the phrase itself, so the palette shows + * its ordinary "No results found." state throughout and looks exactly like a search + * that found nothing. * - * The phrase is compared in its Dvorak/QWERTY-substituted form, using the same cipher the - * 2013 original used, so it is not a readable string in the admin bundle. That is the one - * piece of deliberate obfuscation here and it is aimed at users, not at other developers: - * the mystery is meant to live in the trigger. Everything else, including the payload in - * easter-egg.js, ships as readable source. + * The phrase is compared in its Dvorak/QWERTY-substituted form, using the same cipher + * the 2013 original used, so it is not a readable string in the admin bundle. That is + * the one piece of deliberate obfuscation here and it is aimed at users, not at other + * developers: the mystery is meant to live in the trigger. Everything else, including + * the payload in easter-egg.js, ships as readable source. * * @output wp-admin/js/easter-egg-loader.js */ @@ -21,10 +24,9 @@ return; } - var KEY = 'ishdg;rsdkot', - LABEL = 'Jre. co Lr.ypf', - FROM = '\',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', - TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; + var KEY = 'ishdg;rsdkot', + FROM = '\',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', + TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; /** * Applies the Dvorak/QWERTY substitution cipher. @@ -68,26 +70,37 @@ document.head.appendChild( script ); } - wp.data.dispatch( 'core/commands' ).registerCommandLoader( { - name: 'core/easter-egg', - hook: function ( args ) { - var search = ( ( args && args.search ) || '' ).toLowerCase().replace( /[^a-z0-9]/g, '' ); + /** + * Acts on the phrase when it is submitted from an open command palette. + * + * @param {KeyboardEvent} event The keydown event. + */ + function onKeydown( event ) { + var store, + value; - if ( ! search || dvortr( search ) !== KEY ) { - return { commands: [], isLoading: false }; - } + if ( 'Enter' !== event.key || ! event.target || ! event.target.value ) { + return; + } + + store = wp.data.select( 'core/commands' ); + + if ( ! store || ! store.isOpen() ) { + return; + } - return { - commands: [ { - name: 'core/easter-egg', - label: dvortr( LABEL ), - callback: function ( commandArgs ) { - commandArgs.close(); - play(); - } - } ], - isLoading: false - }; + value = event.target.value.toLowerCase().replace( /[^a-z0-9]/g, '' ); + + if ( ! value || dvortr( value ) !== KEY ) { + return; } - } ); + + event.preventDefault(); + event.stopPropagation(); + + wp.data.dispatch( 'core/commands' ).close(); + play(); + } + + document.addEventListener( 'keydown', onKeydown, true ); }( window.wp, window.wpEasterEgg ) ); From b2150a3cf55eaa24baca96fa7d7298c853e82fa3 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 18 Aug 2026 16:21:27 -0700 Subject: [PATCH 05/11] Script Loader: Rename the easter egg files to teletype. The loader is enqueued by name on every admin screen that loads the command palette, so its src appears in the markup of an ordinary page load. A file called easter-egg-loader.js announces that something is hidden before anyone goes looking, which is most of what the egg has to offer. Name both files for what they do instead. The payload types lines onto a fullscreen overlay, so teletype.js and teletype-loader.js describe the mechanism while giving away nothing about the surprise. The script handle, the config global, and the overlay class follow. The docblocks still say plainly what this is. Minification strips them, so they never reach a production page, and a core developer reading the source should not have to guess. --- Gruntfile.js | 4 ++-- ...aster-egg-loader.js => teletype-loader.js} | 18 ++++++++------ .../admin/{easter-egg.js => teletype.js} | 24 +++++++++---------- src/wp-includes/script-loader.php | 12 +++++----- 4 files changed, 31 insertions(+), 27 deletions(-) rename src/js/_enqueues/admin/{easter-egg-loader.js => teletype-loader.js} (83%) rename src/js/_enqueues/admin/{easter-egg.js => teletype.js} (87%) diff --git a/Gruntfile.js b/Gruntfile.js index b013cefa968aa..7474633e363db 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -513,8 +513,8 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-admin/js/common.js' ]: [ './src/js/_enqueues/admin/common.js' ], [ WORKING_DIR + 'wp-admin/js/custom-background.js' ]: [ './src/js/_enqueues/admin/custom-background.js' ], [ WORKING_DIR + 'wp-admin/js/custom-header.js' ]: [ './src/js/_enqueues/admin/custom-header.js' ], - [ WORKING_DIR + 'wp-admin/js/easter-egg.js' ]: [ './src/js/_enqueues/admin/easter-egg.js' ], - [ WORKING_DIR + 'wp-admin/js/easter-egg-loader.js' ]: [ './src/js/_enqueues/admin/easter-egg-loader.js' ], + [ WORKING_DIR + 'wp-admin/js/teletype.js' ]: [ './src/js/_enqueues/admin/teletype.js' ], + [ WORKING_DIR + 'wp-admin/js/teletype-loader.js' ]: [ './src/js/_enqueues/admin/teletype-loader.js' ], [ WORKING_DIR + 'wp-admin/js/customize-controls.js' ]: [ './src/js/_enqueues/wp/customize/controls.js' ], [ WORKING_DIR + 'wp-admin/js/customize-nav-menus.js' ]: [ './src/js/_enqueues/wp/customize/nav-menus.js' ], [ WORKING_DIR + 'wp-admin/js/customize-widgets.js' ]: [ './src/js/_enqueues/wp/customize/widgets.js' ], diff --git a/src/js/_enqueues/admin/easter-egg-loader.js b/src/js/_enqueues/admin/teletype-loader.js similarity index 83% rename from src/js/_enqueues/admin/easter-egg-loader.js rename to src/js/_enqueues/admin/teletype-loader.js index 6e4f9bb0392b9..13cf9f2aacc72 100644 --- a/src/js/_enqueues/admin/easter-egg-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -12,9 +12,13 @@ * the 2013 original used, so it is not a readable string in the admin bundle. That is * the one piece of deliberate obfuscation here and it is aimed at users, not at other * developers: the mystery is meant to live in the trigger. Everything else, including - * the payload in easter-egg.js, ships as readable source. + * the payload in teletype.js, ships as readable source. * - * @output wp-admin/js/easter-egg-loader.js + * The files are named for what they do rather than for what they are. This one is + * enqueued by name on every admin screen that loads the palette, so a src ending in + * easter-egg.js would answer the question before anyone thought to ask it. + * + * @output wp-admin/js/teletype-loader.js */ ( function ( wp, settings ) { @@ -53,8 +57,8 @@ function play() { var script; - if ( wp.easterEgg ) { - wp.easterEgg.run( settings.name ); + if ( wp.teletype ) { + wp.teletype.run( settings.name ); return; } @@ -62,8 +66,8 @@ script.src = settings.src; script.onload = function () { - if ( wp.easterEgg ) { - wp.easterEgg.run( settings.name ); + if ( wp.teletype ) { + wp.teletype.run( settings.name ); } }; @@ -103,4 +107,4 @@ } document.addEventListener( 'keydown', onKeydown, true ); -}( window.wp, window.wpEasterEgg ) ); +}( window.wp, window.wpTeletype ) ); diff --git a/src/js/_enqueues/admin/easter-egg.js b/src/js/_enqueues/admin/teletype.js similarity index 87% rename from src/js/_enqueues/admin/easter-egg.js rename to src/js/_enqueues/admin/teletype.js index 24687ba2681dc..1feff3b8b7db7 100644 --- a/src/js/_enqueues/admin/easter-egg.js +++ b/src/js/_enqueues/admin/teletype.js @@ -15,7 +15,7 @@ * * Timings, dialogue, and staging are faithful to the original. * - * @output wp-admin/js/easter-egg.js + * @output wp-admin/js/teletype.js */ ( function ( wp ) { @@ -46,18 +46,18 @@ var CLOSING = 'Don\'t let this happen again.'; var STYLE = [ - '.wp-easter-egg{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', + '.wp-teletype{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', 'font-family:courier,monospace;font-size:16px;line-height:1.6;', 'background:#fff;color:#000;overflow:hidden;', 'transition:background-color .6s linear,color .6s linear}', - '.wp-easter-egg.is-dark{background:#000;color:#0f0}', - '.wp-easter-egg p{margin:0;white-space:pre-wrap}', - '.wp-easter-egg .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', - '.wp-easter-egg .cursor.is-visible{opacity:1;animation:wp-easter-egg-blink 1s step-end infinite}', - '@keyframes wp-easter-egg-blink{50%{opacity:0}}', + '.wp-teletype.is-dark{background:#000;color:#0f0}', + '.wp-teletype p{margin:0;white-space:pre-wrap}', + '.wp-teletype .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', + '.wp-teletype .cursor.is-visible{opacity:1;animation:wp-teletype-blink 1s step-end infinite}', + '@keyframes wp-teletype-blink{50%{opacity:0}}', '@media (prefers-reduced-motion:reduce){', - '.wp-easter-egg .cursor.is-visible{animation:none}', - '.wp-easter-egg,.wp-easter-egg .cursor{transition:none}}' + '.wp-teletype .cursor.is-visible{animation:none}', + '.wp-teletype,.wp-teletype .cursor{transition:none}}' ].join( '' ); /** @@ -74,7 +74,7 @@ style.textContent = STYLE; var overlay = document.createElement( 'div' ); - overlay.className = 'wp-easter-egg'; + overlay.className = 'wp-teletype'; overlay.setAttribute( 'aria-hidden', 'true' ); var line = document.createElement( 'p' ); @@ -146,7 +146,7 @@ * The turn: screen to black, text to green, cursor fades up alone. */ function lightsOut() { - overlay.className = 'wp-easter-egg is-dark'; + overlay.className = 'wp-teletype is-dark'; clear(); cursor.className = 'cursor is-visible'; wait( ACT_PAUSE, function () { @@ -198,5 +198,5 @@ } ); } - wp.easterEgg = { run: run }; + wp.teletype = { run: run }; }( window.wp = window.wp || {} ) ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 4ed3e2510c63b..1070289f215e6 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1242,7 +1242,7 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'password-toggle', "/wp-admin/js/password-toggle$suffix.js", array(), false, 1 ); $scripts->set_translations( 'password-toggle' ); - $scripts->add( 'wp-easter-egg-loader', "/wp-admin/js/easter-egg-loader$suffix.js", array( 'wp-data', 'wp-core-commands' ), false, 1 ); + $scripts->add( 'wp-teletype-loader', "/wp-admin/js/teletype-loader$suffix.js", array( 'wp-data', 'wp-core-commands' ), false, 1 ); $scripts->add( 'application-passwords', "/wp-admin/js/application-passwords$suffix.js", array( 'jquery', 'wp-util', 'wp-api-request', 'wp-date', 'wp-i18n', 'wp-hooks' ), false, 1 ); $scripts->set_translations( 'application-passwords' ); @@ -3623,19 +3623,19 @@ function wp_enqueue_command_palette_assets() { /* * The admin easter egg. The loader is small and stays inert until its phrase is * typed into the palette; the payload it plays is fetched only at that point, so - * nothing else is added to a normal admin page load. See easter-egg-loader.js. + * nothing else is added to a normal admin page load. See teletype-loader.js. */ - wp_enqueue_script( 'wp-easter-egg-loader' ); + wp_enqueue_script( 'wp-teletype-loader' ); wp_add_inline_script( - 'wp-easter-egg-loader', + 'wp-teletype-loader', sprintf( - 'window.wpEasterEgg = %s;', + 'window.wpTeletype = %s;', wp_json_encode( array( 'src' => add_query_arg( 'ver', get_bloginfo( 'version' ), - admin_url( 'js/easter-egg' . wp_scripts_get_suffix() . '.js' ) + admin_url( 'js/teletype' . wp_scripts_get_suffix() . '.js' ) ), 'name' => wp_get_current_user()->display_name, ), From 182a99462160541672a823752f9e16c0cc4fac67 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 10:51:05 -0700 Subject: [PATCH 06/11] Script Loader: Correct the easter egg's provenance and cipher its dialogue. The doc block credited WordPress 3.6, but 3.6 is where the egg was removed. r8306 added it on July 10, 2008, when trunk was 2.6-beta3, so it first shipped in 2.6 and survived through 3.5.x before r24820 dropped it. Store the dialogue in its Dvorak/QWERTY-substituted form, as the original did, so the payoff does not turn up in a plain-text search of wp-admin/js. The decoder ships alongside it and all of the logic stays readable. --- src/js/_enqueues/admin/teletype-loader.js | 8 +-- src/js/_enqueues/admin/teletype.js | 77 +++++++++++++++++------ 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index 13cf9f2aacc72..b1c25a111c112 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -9,10 +9,10 @@ * that found nothing. * * The phrase is compared in its Dvorak/QWERTY-substituted form, using the same cipher - * the 2013 original used, so it is not a readable string in the admin bundle. That is - * the one piece of deliberate obfuscation here and it is aimed at users, not at other - * developers: the mystery is meant to live in the trigger. Everything else, including - * the payload in teletype.js, ships as readable source. + * the 2008 original used, so it is not a readable string in the admin bundle. The payload + * in teletype.js stores its dialogue the same way. Both are aimed at users rather than at + * other developers: the cipher and its decoder are in plain sight, and everything that is + * actually logic ships as readable source. * * The files are named for what they do rather than for what they are. This one is * enqueued by name on every admin screen that loads the palette, so a src ending in diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index 1feff3b8b7db7..f85a1be9f2ba0 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -1,17 +1,23 @@ /** * The WordPress admin easter egg. * - * Originally written by Matt Mullenweg and shipped in WordPress 3.6 as - * wp-admin/js/revisions-js.php, where it triggered on comparing a post revision to - * itself. Removed in r24820 (see #24852) because that revisions UI was rewritten and - * the trigger disappeared with it, not because the egg was unwanted. Restored here - * with a new home behind the command palette. + * Originally written by Matt Mullenweg and committed in r8306 (July 10, 2008) as + * wp-admin/js/revisions-js.php, under the cover of "Javascriptify revision selections to + * deal elegantly while comparing." Trunk was 2.6-beta3 at the time, so it first shipped + * in WordPress 2.6 and survived through 3.5.x. wp-admin/revision.php included it when a + * post revision was compared to itself. Removed in r24820 (see #24852) for 3.6 because + * that revisions UI was rewritten and the trigger disappeared with it, not because the + * egg was unwanted: "There will be opportunities for other easter eggs. This one has had + * its decade." Restored here with a new home behind the command palette. * - * The 2013 original shipped as a Dean Edwards packed blob with its dialogue run - * through the Dvorak/QWERTY substitution cipher in dvortr(). Both are unwound here: - * obfuscated code is not GPL source (see #15262), and the mystery is meant to live in - * the trigger, not in unreadable code. This file is only fetched once the egg has - * already been invoked, so it never loads on a normal admin screen. + * The original shipped as a Dean Edwards packed blob whose dialogue was additionally run + * through the Dvorak/QWERTY substitution cipher in dvortr(). #15262 raised that packed + * code is not GPL source, and r16826 answered it with a comment pointing at the ticket + * rather than with readable code. The packing is dropped here: all of the logic ships as + * ordinary source. The cipher is kept, on the dialogue only, so the payoff does not turn + * up in a plain-text search of wp-admin/js. It hides the lines from a grep, not from a + * reader, and the decoder sits a few lines below. This file is also only fetched once the + * egg has already been invoked, so it never loads on a normal admin screen. * * Timings, dialogue, and staging are faithful to the original. * @@ -27,23 +33,52 @@ FADE_TIME = 3000, // Cursor fade-in, once the lights go out. ACT_PAUSE = 4000; // Between the two acts. + /* + * The dialogue is stored in its Dvorak/QWERTY-substituted form and run back through + * dvortr() as each line is typed, exactly as the 2008 original stored it. The point is + * to keep the payoff out of a plain-text search of wp-admin/js, not to hide anything + * from anyone reading this file: the cipher is right here, and every line of logic + * around it is ordinary source. + */ var ACT_ONE = [ - 'Self-comparison detected.', - 'Initiating infinite loop eschewal protocol.', - 'Self destruct in... 3', + 'O.nu[jrmlapcorb e.y.jy.ev', + 'Cbcycaycbi cbucbcy. nrrl .ojd.,an lpryrjrnv', + 'O.nu e.oypgjy cbvvv 3', '2', '1' ]; // %s is replaced with the current user's display name. var ACT_TWO = [ - 'Wake up, %s...', - 'The Matrix has you...', - 'Follow the white rabbit.' + 'PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', + TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; + + /** + * Applies the Dvorak/QWERTY substitution cipher. + * + * @param {string} value Text to substitute. + * @return {string} Substituted text. + */ + function dvortr( value ) { + var map = {}, + i; + + for ( i = 0; i < FROM.length; i++ ) { + map[ FROM.charAt( i ) ] = TO.charAt( i ); + } + + return value.replace( /[\s\S]/g, function ( character ) { + return map[ character ] || character; + } ); + } var STYLE = [ '.wp-teletype{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', @@ -134,7 +169,7 @@ return; } - type( ACT_ONE[ index ], function () { + type( dvortr( ACT_ONE[ index ] ), function () { newline(); wait( LINE_PAUSE, function () { actOne( index + 1 ); @@ -159,13 +194,15 @@ */ function actTwo( index ) { if ( index >= ACT_TWO.length ) { - type( CLOSING, function () { + type( dvortr( CLOSING ), function () { cursor.className = 'cursor'; } ); return; } - type( ACT_TWO[ index ].replace( '%s', displayName ), function () { + type( dvortr( ACT_TWO[ index ] ).replace( '%s', function () { + return displayName; + } ), function () { wait( LINE_PAUSE, function () { clear(); actTwo( index + 1 ); From bbd8f3a8455db968b6b5a9d0f623467ddb9690f4 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 16:40:12 -0700 Subject: [PATCH 07/11] Script Loader: Trim the easter egg's inline commentary. Cut the provenance narrative and the rationale walkthroughs from the file headers, leaving the facts a reader of the code needs. The history belongs on the ticket. --- src/js/_enqueues/admin/teletype-loader.js | 20 ++++----------- src/js/_enqueues/admin/teletype.js | 30 ++++------------------- src/wp-includes/script-loader.php | 5 ++-- 3 files changed, 12 insertions(+), 43 deletions(-) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index b1c25a111c112..4b4102aa4e6dd 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -1,22 +1,12 @@ /** * Hides the admin easter egg behind the command palette. * - * The palette is used as the doorway, but the egg deliberately does not register a - * command. A registered command would be listed the moment its phrase matched, which - * gives the thing away to anyone who happens to type near it. Instead this watches for - * Enter while the palette is open and acts on the phrase itself, so the palette shows - * its ordinary "No results found." state throughout and looks exactly like a search - * that found nothing. + * No command is registered: a registered command would be listed as soon as its phrase + * matched. This listens for Enter while the palette is open instead, so the palette shows + * its ordinary "No results found." state throughout. * - * The phrase is compared in its Dvorak/QWERTY-substituted form, using the same cipher - * the 2008 original used, so it is not a readable string in the admin bundle. The payload - * in teletype.js stores its dialogue the same way. Both are aimed at users rather than at - * other developers: the cipher and its decoder are in plain sight, and everything that is - * actually logic ships as readable source. - * - * The files are named for what they do rather than for what they are. This one is - * enqueued by name on every admin screen that loads the palette, so a src ending in - * easter-egg.js would answer the question before anyone thought to ask it. + * The phrase is compared in its Dvorak/QWERTY-substituted form so that it is not a + * readable string in the admin bundle. * * @output wp-admin/js/teletype-loader.js */ diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index f85a1be9f2ba0..f85e1189f8272 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -1,25 +1,12 @@ /** * The WordPress admin easter egg. * - * Originally written by Matt Mullenweg and committed in r8306 (July 10, 2008) as - * wp-admin/js/revisions-js.php, under the cover of "Javascriptify revision selections to - * deal elegantly while comparing." Trunk was 2.6-beta3 at the time, so it first shipped - * in WordPress 2.6 and survived through 3.5.x. wp-admin/revision.php included it when a - * post revision was compared to itself. Removed in r24820 (see #24852) for 3.6 because - * that revisions UI was rewritten and the trigger disappeared with it, not because the - * egg was unwanted: "There will be opportunities for other easter eggs. This one has had - * its decade." Restored here with a new home behind the command palette. + * Originally written by Matt Mullenweg for WordPress 2.6 and removed in 3.6 along with + * the revisions UI that triggered it. Restored here behind the command palette, with the + * timings, dialogue, and staging of the original. See #65907. * - * The original shipped as a Dean Edwards packed blob whose dialogue was additionally run - * through the Dvorak/QWERTY substitution cipher in dvortr(). #15262 raised that packed - * code is not GPL source, and r16826 answered it with a comment pointing at the ticket - * rather than with readable code. The packing is dropped here: all of the logic ships as - * ordinary source. The cipher is kept, on the dialogue only, so the payoff does not turn - * up in a plain-text search of wp-admin/js. It hides the lines from a grep, not from a - * reader, and the decoder sits a few lines below. This file is also only fetched once the - * egg has already been invoked, so it never loads on a normal admin screen. - * - * Timings, dialogue, and staging are faithful to the original. + * The dialogue is stored in a Dvorak/QWERTY substitution cipher so that the payoff does + * not turn up in a plain-text search of wp-admin/js. The decoder is dvortr(), below. * * @output wp-admin/js/teletype.js */ @@ -33,13 +20,6 @@ FADE_TIME = 3000, // Cursor fade-in, once the lights go out. ACT_PAUSE = 4000; // Between the two acts. - /* - * The dialogue is stored in its Dvorak/QWERTY-substituted form and run back through - * dvortr() as each line is typed, exactly as the 2008 original stored it. The point is - * to keep the payoff out of a plain-text search of wp-admin/js, not to hide anything - * from anyone reading this file: the cipher is right here, and every line of logic - * around it is ordinary source. - */ var ACT_ONE = [ 'O.nu[jrmlapcorb e.y.jy.ev', 'Cbcycaycbi cbucbcy. nrrl .ojd.,an lpryrjrnv', diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 1070289f215e6..d1cd20a3842cf 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3621,9 +3621,8 @@ function wp_enqueue_command_palette_assets() { ); /* - * The admin easter egg. The loader is small and stays inert until its phrase is - * typed into the palette; the payload it plays is fetched only at that point, so - * nothing else is added to a normal admin page load. See teletype-loader.js. + * The admin easter egg. The loader stays inert until its phrase is typed into the + * palette, and only then fetches the payload it plays. */ wp_enqueue_script( 'wp-teletype-loader' ); wp_add_inline_script( From 523040354eec0ec1fa2fdc94fd3f5d8e78ce1d8c Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 17:28:59 -0700 Subject: [PATCH 08/11] Script Loader: Soften the easter egg's dialogue and let it exit on its own. The self-comparison line no longer describes how the egg is reached, and a self-destruct countdown risks reading as a compromised site. Announce a routine maintenance cycle instead, and drop the closing line: the scene now clears itself and hands the admin page back. See #65907. --- src/js/_enqueues/admin/teletype.js | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index f85e1189f8272..aa9f517851dfa 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -2,8 +2,8 @@ * The WordPress admin easter egg. * * Originally written by Matt Mullenweg for WordPress 2.6 and removed in 3.6 along with - * the revisions UI that triggered it. Restored here behind the command palette, with the - * timings, dialogue, and staging of the original. See #65907. + * the revisions UI that triggered it. Restored here behind the command palette, keeping + * the timings and staging of the original. See #65907. * * The dialogue is stored in a Dvorak/QWERTY substitution cipher so that the payoff does * not turn up in a plain-text search of wp-admin/js. The decoder is dvortr(), below. @@ -18,12 +18,12 @@ LINE_PAUSE = 2000, // Between lines. START_DELAY = 3000, // Before the first line. FADE_TIME = 3000, // Cursor fade-in, once the lights go out. - ACT_PAUSE = 4000; // Between the two acts. + ACT_PAUSE = 4000; // Between the two acts, and before the exit. var ACT_ONE = [ - 'O.nu[jrmlapcorb e.y.jy.ev', + 'O.nu[p.u.p.bj. e.y.jy.ev', 'Cbcycaycbi cbucbcy. nrrl .ojd.,an lpryrjrnv', - 'O.nu e.oypgjy cbvvv 3', + 'Pgbbcbi a prgycb. macby.babj. jfjn.v Xajt cbvvv 3', '2', '1' ]; @@ -35,9 +35,6 @@ 'Urnnr, yd. ,dcy. paxxcyv' ]; - // Left on screen at the end, as the original's noscript fallback did. - var CLOSING = 'Erb-y n.y ydco dall.b aiacbv'; - var FROM = '\',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; @@ -170,19 +167,18 @@ } /** - * Act two: one line at a time, cleared between each. + * Act two: one line at a time, cleared between each. The last line holds, then + * the scene takes itself down and gives the admin page back. */ function actTwo( index ) { - if ( index >= ACT_TWO.length ) { - type( dvortr( CLOSING ), function () { - cursor.className = 'cursor'; - } ); - return; - } - type( dvortr( ACT_TWO[ index ] ).replace( '%s', function () { return displayName; } ), function () { + if ( index + 1 >= ACT_TWO.length ) { + wait( ACT_PAUSE, abort ); + return; + } + wait( LINE_PAUSE, function () { clear(); actTwo( index + 1 ); From 3517a6276b045f5eb0439d80405cf06ee4ccd1b3 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 17:30:16 -0700 Subject: [PATCH 09/11] Script Loader: Only claim Enter when the command palette found nothing. The loader swallowed every Enter whose search text matched the phrase, so a post titled with it could never be opened from the palette. Defer to the active option whenever the palette has one. See #65907. --- src/js/_enqueues/admin/teletype-loader.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index 4b4102aa4e6dd..5e058f9d9510e 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -71,9 +71,10 @@ */ function onKeydown( event ) { var store, + input = event.target, value; - if ( 'Enter' !== event.key || ! event.target || ! event.target.value ) { + if ( 'Enter' !== event.key || ! input || ! input.value ) { return; } @@ -83,7 +84,20 @@ return; } - value = event.target.value.toLowerCase().replace( /[^a-z0-9]/g, '' ); + /* + * The palette's search field is a combobox: while any command matches, one of + * them is the active option and Enter belongs to it. Only an empty result list + * is ours to take, otherwise a post that happens to be named for the phrase + * would never open. + */ + if ( + 'combobox' !== input.getAttribute( 'role' ) || + input.getAttribute( 'aria-activedescendant' ) + ) { + return; + } + + value = input.value.toLowerCase().replace( /[^a-z0-9]/g, '' ); if ( ! value || dvortr( value ) !== KEY ) { return; From f7bcdedff1069129c8cb9c358d9a547e640a375b Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 17:40:41 -0700 Subject: [PATCH 10/11] Script Loader: Add the falling glyph rain to the easter egg's Matrix act. The act was green text on black and nothing more, which is the part of the reference everyone actually pictures. Draw the rain on a canvas behind the dialogue, and sit it out when reduced motion is preferred. See #65907. --- src/js/_enqueues/admin/teletype.js | 118 ++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index aa9f517851dfa..36e0a63ee0b7c 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -18,7 +18,12 @@ LINE_PAUSE = 2000, // Between lines. START_DELAY = 3000, // Before the first line. FADE_TIME = 3000, // Cursor fade-in, once the lights go out. - ACT_PAUSE = 4000; // Between the two acts, and before the exit. + ACT_PAUSE = 4000, // Between the two acts, and before the exit. + RAIN_SIZE = 16, // Glyph size of the falling rain, in pixels. + RAIN_SPEED = 60; // Milliseconds between rain rows. + + // Half-width katakana and digits, as the films used. + var GLYPHS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789'; var ACT_ONE = [ 'O.nu[p.u.p.bj. e.y.jy.ev', @@ -63,7 +68,8 @@ 'background:#fff;color:#000;overflow:hidden;', 'transition:background-color .6s linear,color .6s linear}', '.wp-teletype.is-dark{background:#000;color:#0f0}', - '.wp-teletype p{margin:0;white-space:pre-wrap}', + '.wp-teletype p{position:relative;z-index:1;margin:0;white-space:pre-wrap}', + '.wp-teletype .rain{position:absolute;inset:0;opacity:.5}', '.wp-teletype .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', '.wp-teletype .cursor.is-visible{opacity:1;animation:wp-teletype-blink 1s step-end infinite}', '@keyframes wp-teletype-blink{50%{opacity:0}}', @@ -72,6 +78,100 @@ '.wp-teletype,.wp-teletype .cursor{transition:none}}' ].join( '' ); + /** + * Runs the falling glyph rain behind the scene. + * + * @param {HTMLElement} parent Element to render into. + * @return {Function} Stops the rain and removes it. + */ + function rain( parent ) { + var canvas = document.createElement( 'canvas' ), + context = canvas.getContext( '2d' ), + columns = [], + width = 0, + height = 0, + frame = null, + last = 0; + + canvas.className = 'rain'; + canvas.setAttribute( 'aria-hidden', 'true' ); + + function resize() { + var ratio = window.devicePixelRatio || 1, + count, + i; + + width = parent.clientWidth; + height = parent.clientHeight; + + canvas.width = width * ratio; + canvas.height = height * ratio; + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + + context.setTransform( ratio, 0, 0, ratio, 0, 0 ); + context.font = RAIN_SIZE + 'px courier, monospace'; + + count = Math.ceil( width / RAIN_SIZE ); + + /* + * Columns keep their position across a resize so the rain does not restart, + * and any new ones start at a random height so the edge does not arrive as + * a straight line. + */ + for ( i = columns.length; i < count; i++ ) { + columns.push( Math.random() * height ); + } + + columns.length = count; + } + + function draw() { + var i; + + // Painting over the last frame rather than clearing it leaves the trails. + context.fillStyle = 'rgba(0,0,0,.08)'; + context.fillRect( 0, 0, width, height ); + context.fillStyle = '#0f0'; + + for ( i = 0; i < columns.length; i++ ) { + context.fillText( + GLYPHS.charAt( Math.floor( Math.random() * GLYPHS.length ) ), + i * RAIN_SIZE, + columns[ i ] + ); + + if ( columns[ i ] > height && Math.random() > 0.975 ) { + columns[ i ] = 0; + } else { + columns[ i ] += RAIN_SIZE; + } + } + } + + function tick( now ) { + frame = window.requestAnimationFrame( tick ); + + if ( now - last < RAIN_SPEED ) { + return; + } + + last = now; + draw(); + } + + resize(); + parent.appendChild( canvas ); + window.addEventListener( 'resize', resize ); + frame = window.requestAnimationFrame( tick ); + + return function () { + window.cancelAnimationFrame( frame ); + window.removeEventListener( 'resize', resize ); + canvas.remove(); + }; + } + /** * Plays the scene. * @@ -80,6 +180,7 @@ function run( displayName ) { var aborted = false, timer = null, + stopRain = null, previousOverflow = document.documentElement.style.overflow; var style = document.createElement( 'style' ); @@ -155,12 +256,18 @@ } /** - * The turn: screen to black, text to green, cursor fades up alone. + * The turn: screen to black, text to green, rain starts, cursor fades up alone. */ function lightsOut() { overlay.className = 'wp-teletype is-dark'; clear(); cursor.className = 'cursor is-visible'; + + // The rain is decoration, so it sits out a reduced-motion preference. + if ( ! window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ) { + stopRain = rain( overlay ); + } + wait( ACT_PAUSE, function () { actTwo( 0 ); } ); @@ -193,6 +300,11 @@ aborted = true; window.clearTimeout( timer ); + + if ( stopRain ) { + stopRain(); + } + document.removeEventListener( 'keydown', abort ); document.documentElement.style.overflow = previousOverflow; overlay.remove(); From 57e9174a1da880f69959bcc9707172b027ef956f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 23:17:34 -0700 Subject: [PATCH 11/11] Script Loader: Give the easter egg to screen reader users too. The scene was aria-hidden end to end, so a blind user got forty-five seconds of nothing while focus sat on a page they could no longer see. Make it a modal dialog that takes focus and gives it back, narrate whole lines through a live region rather than the character-by-character surface, describe the rain once, and add the visible Exit control that was missing for everyone. See #65907. --- src/js/_enqueues/admin/teletype.js | 88 +++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index 36e0a63ee0b7c..95c39c60f1ff4 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -68,8 +68,17 @@ 'background:#fff;color:#000;overflow:hidden;', 'transition:background-color .6s linear,color .6s linear}', '.wp-teletype.is-dark{background:#000;color:#0f0}', + // Focused only to move the reading position in, never by tabbing to it. + '.wp-teletype:focus{outline:none}', '.wp-teletype p{position:relative;z-index:1;margin:0;white-space:pre-wrap}', '.wp-teletype .rain{position:absolute;inset:0;opacity:.5}', + '.wp-teletype .narration{position:absolute;width:1px;height:1px;margin:-1px;', + 'padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}', + '.wp-teletype .exit{position:absolute;z-index:2;top:1.5em;right:1.5em;', + 'padding:.7em 1.8em;border:0;border-radius:999px;background:#2271b1;color:#fff;', + 'font-family:inherit;font-size:14px;line-height:1;cursor:pointer}', + '.wp-teletype .exit:hover{background:#135e96}', + '.wp-teletype .exit:focus-visible{outline:2px solid #fff;outline-offset:2px}', '.wp-teletype .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', '.wp-teletype .cursor.is-visible{opacity:1;animation:wp-teletype-blink 1s step-end infinite}', '@keyframes wp-teletype-blink{50%{opacity:0}}', @@ -181,16 +190,39 @@ var aborted = false, timer = null, stopRain = null, + previousFocus = document.activeElement, previousOverflow = document.documentElement.style.overflow; var style = document.createElement( 'style' ); style.textContent = STYLE; + /* + * The scene is a modal dialog so that it is reachable and escapable rather than + * something that happens silently over the top of an admin page. The dialogue + * is English whatever the profile language, so it says so. + */ var overlay = document.createElement( 'div' ); overlay.className = 'wp-teletype'; - overlay.setAttribute( 'aria-hidden', 'true' ); - + overlay.setAttribute( 'role', 'dialog' ); + overlay.setAttribute( 'aria-modal', 'true' ); + overlay.setAttribute( 'aria-label', 'A WordPress easter egg. Press Escape to leave.' ); + overlay.setAttribute( 'lang', 'en' ); + overlay.setAttribute( 'tabindex', '-1' ); + + // Typed a character at a time, so it reaches the accessibility tree as whole + // lines through the live region below instead. var line = document.createElement( 'p' ); + line.setAttribute( 'aria-hidden', 'true' ); + + var narration = document.createElement( 'div' ); + narration.className = 'narration'; + narration.setAttribute( 'aria-live', 'polite' ); + narration.setAttribute( 'aria-atomic', 'true' ); + + var exit = document.createElement( 'button' ); + exit.type = 'button'; + exit.className = 'exit'; + exit.textContent = 'Exit'; var cursor = document.createElement( 'span' ); cursor.className = 'cursor'; @@ -198,6 +230,17 @@ line.appendChild( cursor ); overlay.appendChild( line ); + overlay.appendChild( narration ); + overlay.appendChild( exit ); + + /** + * Hands a whole line to the live region. + * + * @param {string} text Line to announce. + */ + function say( text ) { + narration.textContent = text; + } function wait( ms, next ) { timer = window.setTimeout( function () { @@ -247,6 +290,7 @@ return; } + say( dvortr( ACT_ONE[ index ] ) ); type( dvortr( ACT_ONE[ index ] ), function () { newline(); wait( LINE_PAUSE, function () { @@ -268,6 +312,9 @@ stopRain = rain( overlay ); } + // The rain is the whole joke of this act, so describe it once. + say( 'The screen goes black. Green code rains down it.' ); + wait( ACT_PAUSE, function () { actTwo( 0 ); } ); @@ -278,9 +325,12 @@ * the scene takes itself down and gives the admin page back. */ function actTwo( index ) { - type( dvortr( ACT_TWO[ index ] ).replace( '%s', function () { + var text = dvortr( ACT_TWO[ index ] ).replace( '%s', function () { return displayName; - } ), function () { + } ); + + say( text ); + type( text, function () { if ( index + 1 >= ACT_TWO.length ) { wait( ACT_PAUSE, abort ); return; @@ -293,8 +343,8 @@ } ); } - function abort( event ) { - if ( event && 'keydown' === event.type && 'Escape' !== event.key ) { + function abort() { + if ( aborted ) { return; } @@ -305,18 +355,38 @@ stopRain(); } - document.removeEventListener( 'keydown', abort ); + document.removeEventListener( 'keydown', onKeydown ); document.documentElement.style.overflow = previousOverflow; overlay.remove(); style.remove(); + + if ( previousFocus && previousFocus.focus ) { + previousFocus.focus(); + } + } + + function onKeydown( event ) { + if ( 'Escape' === event.key ) { + abort(); + return; + } + + // The scene has one control, so the trap has one stop. + if ( 'Tab' === event.key ) { + event.preventDefault(); + exit.focus(); + } } - document.addEventListener( 'keydown', abort ); - overlay.addEventListener( 'click', abort ); + document.addEventListener( 'keydown', onKeydown ); + overlay.addEventListener( 'click', function () { + abort(); + } ); document.head.appendChild( style ); document.body.appendChild( overlay ); document.documentElement.style.overflow = 'hidden'; + overlay.focus(); wait( START_DELAY, function () { actOne( 0 );