Recent Commits to wp-pluginsused:master (11)

  • 08/08/2026 23:09

    Let the translation decide the summary word order

    Let the translation decide the summary word order
    
    The three counts were separately pluralised, which was always right, and
    then joined by a bare translated "and" with the spacing and the full stop
    concatenated around it. So the order the counts appear in was a fact about
    English held in PHP: a translator handed only "and" cannot put the
    inactive count first, and several languages want to.
    
    One joining string with numbered placeholders. The <strong> stays outside
    the msgid for the reason the comment above it already gives -- a translator
    has no reason to be handed markup. Rendered output is byte-identical, so
    the new test reorders the translation and asserts the sentence follows;
    that is the only thing that can tell the two apart.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_015aLvFGxYRW475ZnGjbLopC
  • 08/08/2026 21:37

    Guard against padding inside a translatable string

    Guard against padding inside a translatable string
    
    A translator sees a msgid in a list, where a leading or trailing space is
    invisible and a run of them is indistinguishable from one. They cannot
    reproduce what they cannot see, so padding survives only in English -- and
    where it was separating a label from a value concatenated after it, the
    translated string comes out run together.
    
    Three plugins in the family had it. This one does not, and the shared
    metadata fixture is where it is kept that way.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_015aLvFGxYRW475ZnGjbLopC
  • 08/08/2026 18:24

    Notice a plugin that arrived without WordPress being told

    Notice a plugin that arrived without WordPress being told
    
    Caching the headers behind hooks and an expiry got the scan off the front
    end, and broke the plugin's one job on the way: a plugin uploaded over FTP,
    unzipped by hand or dropped in by a provisioning script fires none of those
    hooks, so the listing went on not listing it until the day ran out. The
    owner refreshes, sees the old list, and concludes the thing is broken. The
    e2e suite writes its hostile fixture straight to disk and caught exactly
    this.
    
    The entry names of the plugins directory are read on every request now and
    the stored headers are only reused while they still match. That is one
    directory read against get_plugins()' directory read plus a file opened and
    parsed for every plugin in it -- listing a directory was never the
    expensive part, and it is the whole of the saving. The expiry drops to an
    hour and covers what the fingerprint cannot see: a header edited inside a
    file whose name did not change, when WordPress was not the one editing it.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_015aLvFGxYRW475ZnGjbLopC
  • 08/08/2026 14:48

    Stop scanning the plugins directory on every front-end request

    Stop scanning the plugins directory on every front-end request
    
    get_plugins() opens the plugins directory, opens every plugin file in it and
    reads the first 8KB of each. Core caches the result -- in the `plugins`
    group, which wp_start_object_cache() registers as non-persistent, so the
    scan runs again on every request no matter what object cache the site has
    installed. That is an admin-screen cost, and these shortcodes run on the
    front end: any visitor arriving past the page cache pays it, as often as
    they care to ask, on a page whose whole content is a list that changes maybe
    twice a year.
    
    The headers are kept in a site transient for a day. A site transient rather
    than a plain one because the plugins directory is one directory for the
    whole network -- a per-site transient would leave every other site reading a
    copy the deletion never reached. Installing, updating and deleting a plugin
    discard it because they change what is on disk; activating and deactivating
    do not, but they are the moment a plugin uploaded over FTP is first noticed,
    and a plugin the cache has never seen is missing from the listing rather
    than merely on the wrong side of it. The expiry covers an upload nobody
    follows with any of the five.
    
    The hidden-plugins list and the version switch are applied to the headers on
    every request rather than baked into them. Filtering is free next to the
    scan, and caching before that step means a settings save needs no
    invalidation at all -- which is one fewer way for a hidden plugin to stay
    published.
    
    reset_plugin_state() in the test case now flushes both, so a suite that
    writes fixture plugins to disk between tests is not reading yesterday's.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_015aLvFGxYRW475ZnGjbLopC
  • 08/08/2026 13:52

    Stop keeping a logged-in session where it can be picked up

    Stop keeping a logged-in session where it can be picked up
    
    artifacts/storage-states/admin.json is a real WordPress auth cookie pair,
    written by global-setup.js so the browser tests do not each log in through
    the form. It is scoped to the ephemeral wp-env container and worthless once
    that is gone, so nothing real leaks -- but it was sitting on disk in every
    plugin between runs, and two things sweep that directory up.
    
    The release rsyncs the working tree rather than a clean export, so the only
    thing keeping it off wordpress.org is one --exclude in a procedure that
    lives outside these repositories. This repository's own notes record that
    exact file class having shipped once already.
    
    And CI uploaded the whole of artifacts/ on failure, cookie included, for
    seven days. Narrowed to artifacts/test-results/, which is what the traces
    and screenshots are actually in.
    
    bin/test-e2e.sh no longer exec's Playwright, so it can remove the session
    whichever way the suite ends; global-setup.js writes it again on the next
    run. A session cookie is not a diagnostic, and keeping one is a habit rather
    than a decision.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_015aLvFGxYRW475ZnGjbLopC
  • 08/08/2026 13:00

    Gate the block-renderer route, and match hidden names the way they ar…

    Gate the block-renderer route, and match hidden names the way they are stored
    
    Registering a dynamic block is what creates /wp/v2/block-renderer/<name>,
    and core gates that route on edit_posts. So merely having this plugin active
    published the site's whole inventory -- inactive plugins and exact versions
    included -- to every Contributor, on a site that had never placed a listing
    anywhere. Core keeps the same inventory behind a higher capability of its
    own, and an inactive plugin is by definition one nobody has been updating.
    
    Gated on the route rather than inside the render callbacks, and that is
    load-bearing: do_blocks() also runs during a REST render of post content, so
    a check on REST_REQUEST would blank the block for every reader of a headless
    site. Dropping ServerSideRender from the editor would not close it either --
    the route exists because the block is dynamic.
    
    manage_options rather than activate_plugins, because under multisite
    map_meta_cap() resolves the latter to manage_network_plugins for anyone who
    is not a super admin, which would have locked every site administrator on
    every network out of previewing a block on their own site. The multisite
    suite caught that. manage_options already decides which plugins are hidden,
    so the person configuring the list is the person who can preview it.
    
    Separately: the hidden-plugins list is stored through sanitize_text_field()
    and was compared against the raw Name header, which get_plugins() applies
    nothing but trim() to. Any plugin whose name holds a double space, a tab or
    a percent-octet could be ticked, saved with a success notice, and go on
    being listed -- with the checkbox coming back unticked, so it read as a save
    that had not worked. Both sides go through one normaliser now. Every
    existing fixture survives sanitisation byte for byte, including the "evil"
    one whose double quote passes straight through, which is why the round-trip
    looked covered; zzz-gappy is the shape that exercises it.
    
    And the per-request cache is dropped on switch_blog, so one request
    rendering for two sites no longer applies the first site's hidden list to
    the second.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_015aLvFGxYRW475ZnGjbLopC
  • 08/08/2026 7:05

    The metadata fixture keeps its own copy of the canonical sections

    The metadata fixture keeps its own copy of the canonical sections
    
    Adding Installation to the README meant adding it in two places, and
    only one of them was done: bin/verify.py went green while every
    plugin's own PHPUnit suite went red on an array diff whose whole
    content was "+ 1 => 'Installation'".
    
    The list lives in the shared fixture as well as in the checker, and
    nothing links them -- so the checker passing says nothing about the
    suite passing, which is exactly the sort of second source of truth
    this collection keeps finding.
  • 08/08/2026 6:46

    An Installation section, because wordpress.org gives it a tab

    An Installation section, because wordpress.org gives it a tab
    
    Every plugin gets one, including the ones whose honest answer is
    "activate it and you are done" -- a tab present on some of these and
    missing on others reads as an omission rather than as a decision. It
    sits between Description and Usage, which is the order a reader works
    in.
    
    Not boilerplate: "upload to wp-content/plugins/" is advice from before
    WordPress had a plugin installer and is not here. Step one is install
    and activate; everything after it is specific to this plugin, and
    where there is genuinely nothing to do it says so and says what
    happens instead.
    
    Setup steps that were sitting in Usage -- or, worse, in Description --
    moved here, which is where somebody looking for them will go.
  • 08/08/2026 2:59

    tests: a block's dependencies are core's, and are read off disk

    tests: a block's dependencies are core's, and are read off disk
    
    The shared metadata fixture demanded an empty dependency array from every
    registered handle whose name starts with the slug. A block's editor script
    handle is minted by core from block.json and its dependencies are written by
    the build, so wp-blocks and wp-block-editor read as a §6 violation when they
    are what a block is. Block scripts are now held to their own rule: every
    dependency must be a handle WordPress itself registers, and none may be
    jQuery, because core ships jquery and "core provides it" alone would reopen
    the one door §6 exists to close.
    
    They are read from the build/*/*.asset.php manifest rather than from
    wp_scripts(). A block's handles are registered once, at the init the
    bootstrap fires, into a process-wide global that several plugins rebuild in
    set_up() - so whether the old loop saw a block at all depended on run order,
    and in three plugins it saw nothing and passed.
    
    fire_init() is for the tests that fire init a second time to watch a front
    end request: init has already fired once, so the second one re-registers the
    blocks, which is a _doing_it_wrong() notice. A real second request starts
    with an empty block registry, so this empties it of the plugin's own blocks
    first. The guard belongs here and not in the plugin - init fires once per
    request, and a second registration in production would be a real bug.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011ZG7R8RPFwPvWnSpUdKpoD
  • 08/08/2026 2:18

    Three blocks, one per shortcode, sharing the shortcodes' renderer

    Three blocks, one per shortcode, sharing the shortcodes' renderer
    
    Each block's render callback calls WP_PluginsUsed_Template::render() with the
    same string its shortcode's callback passes. Neither entry point calls the
    other: no do_shortcode() in a block, no block lookup in a shortcode, so either
    can be unregistered and the other carries on. All three shortcodes stay
    registered, documented and undeprecated, and the major version does not move.
    
    Three blocks rather than one with a listing-type attribute, even though the
    three shortcode callbacks differ only in that string. None of the shortcodes
    takes an attribute, so a type attribute would exist on one entry point and not
    the other; and a block name is fixed in post_content for the life of the post
    where an attribute is a value a later default can flip. Which listing a page
    shows belongs in the name.
    
    The hidden-plugins setting is the one thing here that is a disclosure rather
    than a nicety: somebody ticks a plugin because they do not want visitors
    knowing it is installed. Both the setting and the filter reach the block,
    because the block does no collecting of its own -- tests/test-blocks.php pins
    that through the render callback and through do_blocks().
    
    This adds the first JavaScript build to the plugin. build/ is generated,
    gitignored and shipped; src/ is committed and not shipped. bin/build compiles
    and then walks build/ writing the silence-is-golden guards webpack knows
    nothing about, and bin/test.sh, bin/test-e2e.sh and the PHPUnit CI job all
    build first, so a checkout that has never been built cannot silently test the
    previous build.
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_011ZG7R8RPFwPvWnSpUdKpoD
  • 08/08/2026 0:04

    There is no link setting; the screen has two fields and neither is one

    There is no link setting; the screen has two fields and neither is one
    
    The caption said the screen chooses "what each entry shows and where it
    links". add_settings_field() is called twice -- show_version and
    hidden_plugins -- so the screen offers the version toggle and the hide list and
    nothing about linking.
    
    Seventh caption corrected, and the third to promise a control that does not
    exist.

Log in