Recent Commits to wp-pluginsused:master

  • 31/07/2026 23:08

    Drop the eslint job, which could never have passed here

    Drop the eslint job, which could never have passed here
    
    This plugin ships no JavaScript: no js/ directory, no scripts, no
    package.json. The workflow carried a "JS coding standards and tests" job
    anyway, and it did not fail at the linter -- it never reached one. It died
    at actions/setup-node, looking for a lock file:
    
        Dependencies lock file is not found in /home/runner/work/...
        Supported file patterns: package-lock.json, npm-shrinkwrap.json, yarn.lock
    
    A job that cannot pass is worse than no job. It puts a red mark on every
    push for a reason nobody can act on, and teaches everyone to stop reading
    the marks.
    
    The shared standard said "a plugin with no js/ directory deletes the whole
    eslint job". This plugin has no js/ directory, so the job should have gone
    when the workflow was written; the check that compares a workflow to the
    template was tolerating its absence rather than requiring it, so nothing
    noticed either way.
  • 30/07/2026 13:31

    Name the screen, not the plugin, in the page heading

    Name the screen, not the plugin, in the page heading
    
    "WP-PluginsUsed" as the h1 duplicated the sidebar entry. The heading says what
    the screen is now -- Plugins Used -- matching Ban Options, Manage Ratings, Stats
    and the rest. STANDARDS.md 4.1: the prefix belongs in the menu title, never in
    the heading.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_018W7cKqYNL1gUFTi38VRHaX
  • 30/07/2026 11:52

    Correct the record on wp_get_sites(), here and in the CI comment

    Correct the record on wp_get_sites(), here and in the CI comment
    
    This plugin repeated a claim that runs through the whole collection: that
    wp_get_sites() was removed in WordPress 5.1 and that calling it fatals. Neither
    is true. It was deprecated in 4.6 and still ships in ms-deprecated.php, which is
    loaded for multisite only -- so on a single site it merely looks absent.
    
    What it actually does is worse than a crash: it returns only the first 100 sites,
    so network activation and uninstall quietly did their work on a hundred sites and
    reported success. Comments, docblocks, assertion messages and the test method
    names that encoded the false version are all corrected to say that.
    
    No behaviour changes -- the code already used get_sites() with 'number' => 0.
    The CI matrix comment carried the same claim and moves with the shared template.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_018W7cKqYNL1gUFTi38VRHaX
  • 30/07/2026 10:44

    Stop claiming what the reader is upgrading from

    Stop claiming what the reader is upgrading from
    
    The Upgrade Notice said the floors moved 'up from 6.0 and 7.4', or advised
    anyone 'still on PHP 7.4' to move. Both are wrong about the version
    actually on wordpress.org.
    
    Queried against plugins.svn.wordpress.org, the released readmes declare
    WordPress 2.8 to 5.5, and fifteen of the nineteen declare no Requires PHP
    at all. Three plugins had ended up asserting three different
    predecessors, none of them right - and telling a reader who may be on 5.6
    that they are on 7.4 is worse than saying nothing, because it reads as
    confirmation they are fine.
    
    What the notice can state truthfully is the requirement, the consequence
    and how to check: an older site is not offered the update, and Site
    Health reports the PHP version. That holds wherever the reader is coming
    from.
  • 29/07/2026 21:50

    Emit the markup kses leaves, and assert loss rather than bytes

    Emit the markup kses leaves, and assert loss rather than bytes
    
    The plugin has two ways out: the shortcodes return their markup, while
    display_pluginsused() echoes it through wp_kses(). kses normalises as
    well as filters, so the two paths were emitting different bytes for the
    same listing - viewBox lowercased to viewbox, a space added before a
    self-closing slash, &#039; rewritten to &apos; inside a title. A theme
    calling the template tag got markup the shortcode never produced.
    
    The icon now spells its attributes the way kses leaves them, so the two
    paths agree. Lowercase viewbox is safe and is what the echoing path has
    always shipped: an HTML parser applies its SVG case-fixup table, so it
    reaches the DOM as viewBox and the icon still scales.
    
    The entity rewriting cannot be pre-empted that way, so the two tests now
    assert what they mean rather than byte-identity. The allow-list test
    compares inventories of tags and attribute names, since an incomplete
    allow list drops a tag or an attribute and that is the failure worth
    catching. The template-tag test compares the echoed output against
    wp_kses( render(), allowed_html() ), which is what it should equal.
    
    Also made the settings_fields() assertion quote-agnostic: core quotes
    option_page with single quotes, which is not part of any contract.
    
    158 tests, 442 assertions, green.
  • 29/07/2026 21:28

    Match wp_enqueue_script( with its bracket, not as a substring

    Match wp_enqueue_script( with its bracket, not as a substring
    
    The no-jQuery test forbade the bare string 'wp_enqueue_script', which is
    also a substring of the ACTION name wp_enqueue_scripts - the hook a
    plugin legitimately uses to enqueue a stylesheet. Registering no scripts
    is not the same as never touching the hook scripts are registered on.
    
    wp-stats failed on exactly this: it hooks wp_enqueue_scripts to add its
    one sheet. The others here pass today only because they happen not to,
    so this is a latent false failure waiting for the first stylesheet.
    
    Found by the first PHPUnit run of this work.
  • 29/07/2026 11:33

    Re-lock composer against the PHP 8.2 floor

    Re-lock composer against the PHP 8.2 floor
    
    composer.json moved to >=8.2 with the floor raise but the lock file still
    recorded >=7.4. Composer treats that as an error rather than a warning,
    so 'composer install' would have failed inside the tests container before
    PHPUnit ran a single test - and CI does exactly that on all six matrix
    rows.
    
    bin/verify.py now compares the two, so the pair cannot drift again.
  • 29/07/2026 11:29

    Step 12: drop the last two phpcs suppressions and fix the code under …

    Step 12: drop the last two phpcs suppressions and fix the code under them
    
    Both were in includes/, both had a reason written next to them, and the target
    for this collection is zero.
    
    display_pluginsused() echoed its markup behind an EscapeOutput ignore. It now
    escapes at the sink like everything else, through wp_kses() with an allow-list
    that says exactly which tags and attributes the listings use. A test asserts
    wp_kses() leaves all three listings byte-identical, so the day the markup
    grows a tag the list has not got, the suite says so rather than the template
    tag quietly printing less than the shortcodes do.
    
    The summary sentence carried an I18n.NoHtmlWrappedStrings ignore arguing that
    rewording the msgid would orphan its translations. That is true and it is
    still the right trade: a translator has no business being handed <strong>, and
    a msgid wrapped in a tag is a translation waiting to lose it. The tags moved
    into the concatenation, the rendered string is byte-for-byte what it was, and
    the three orphaned msgids are recorded as a NOTE in the changelog.
    
    phpcs and php -l are clean across the plugin, and bin/verify.py reports zero.
  • 29/07/2026 11:27

    Step 11: put the suite on the house conventions and add the shared tests

    Step 11: put the suite on the house conventions and add the shared tests
    
    Files and classes follow 7.1: helper-fixtures.php becomes helper-testcase.php
    because what it declares is the base test case, test-stats.php becomes
    test-summary.php so nothing confuses it with the WP-Stats integration this
    plugin does not have, and every class is WP_PluginsUsed_<Area>_Test extending
    WP_PluginsUsed_TestCase. test-bootstrap.php and test-uninstall.php extended
    WP_UnitTestCase directly and now go through the shared base like the rest.
    
    Discovery is by the test- prefix now, so a misnamed file would be silently
    skipped rather than fail: 109 test methods before this step, 150 after, none
    lost in the rename.
    
    tests/test-metadata.php carries the fourteen shared assertions from 7.2, plus
    three specific to what this plugin just did: that every fired hook carries the
    prefix, that no apply_filters_deprecated shim crept back in, and that the
    Upgrade Notice names all three renamed filters in both spellings. The last one
    is the guard that matters -- the rename is only survivable for a site owner if
    the notice actually lists it.
    
    run_uninstall() is shared rather than repeated, because uninstall.php declares
    a function and so can only be included once per process. Two files assert
    against it; the second include would have been a silent no-op and the test
    would have passed on rows nothing had deleted.
  • 29/07/2026 11:27

    Step 2 (redo): re-sync the shared config files after the templates moved

    Step 2 (redo): re-sync the shared config files after the templates moved
    
    _standards/templates/phpcs.xml and phpunit-multisite.xml.dist changed while
    this plugin was being worked through, so the copies taken at 516e33a had
    drifted from the shared originals. Both are re-copied verbatim.
    
    Neither new exclusion applies here -- WP-PluginsUsed owns no database table
    and has no list table -- but the point of these files is that all nineteen
    carry the same one, so it is copied whole rather than trimmed to fit.
  • 29/07/2026 11:22

    Step 10: put the readme on the canonical section set

    Step 10: put the readme on the canonical section set
    
    The six h2s in the order the standard fixes them: Description, Usage,
    Frequently Asked Questions, Screenshots, Changelog, Upgrade Notice. Usage was
    an h3 buried in Description and the changelog sat above the screenshots.
    
    The License line gets its two trailing spaces back. Without them Markdown ran
    "License: GPLv2 or later" and the License URI together into one line on
    GitHub, which is invisible in the source and in a diff.
    
    Description gains a Features list and keeps Donations as its last h3, in the
    one wording all nineteen use, without the stray bullet it carried. Development
    and Credits are gone: the repo link belongs in the repo, and the Flaticon
    credit was for the wordpress.org listing icon rather than anything this plugin
    ships -- worth re-adding to the .org assets if it is still owed.
    
    Changelog entries are sorted into BREAKING, NEW, CHANGED, FIXED, NOTE within
    2.0.0. The security fix moves from a sixth prefix, SECURITY:, to FIXED:, the
    raised floors become their own BREAKING line, and four 2008-era bullets that
    were indented with two spaces are normalised so they parse as bullets at all.
  • 29/07/2026 11:20

    Step 9: scope the emitted class names and drop the last inline style

    Step 9: scope the emitted class names and drop the last inline style
    
    The plugin ships no stylesheet, but it does emit markup, and the two class
    names on the state icons were unscoped. They become wp-pluginsused-icon,
    wp-pluginsused-icon-active and wp-pluginsused-icon-inactive, so a theme has
    one prefix to target and no chance of colliding with anything else.
    
    The SVGs lose their style="vertical-align: middle" as well. It was the last
    inline CSS in the plugin, and the icons still inherit their colour through
    currentColor, which is the part that actually matters for a theme.
    
    There is no css/ or js/ directory to normalise -- the plugin has neither, and
    the SVG is built in PHP -- so this is the whole of steps 7 to 9. Steps 7 and 8
    are no-ops: no JavaScript, and the GIF markers were already replaced by inline
    SVG earlier in this major.
  • 29/07/2026 11:19

    Step 6: put the settings screen on the house rules

    Step 6: put the settings screen on the house rules
    
    The screen was already Settings API rather than hand-written markup, so this
    is the naming and the details around it:
    
    * CAPABILITY, SECTION_DISPLAY and SECTION_HIDDEN join PAGE and GROUP, and the
      two section ids and both field ids gain the plugin's prefix.
    * Every capability check goes through capability(), which fires
      wp_pluginsused_capability with a context, so a site hands the screen to a
      different role in one place rather than by hunting current_user_can() calls.
    * register() and render() become register_settings() and render_page(),
      matching the other settings screens in the collection.
    * The hidden-plugins list loses its inline style attribute and separates its
      labels with <br />, the way core's own checkbox lists do. That was the only
      inline CSS in the plugin.
    
    Five tests follow, including two standing guards: no inline style attribute
    reaches the screen, and no file hand-writes a form table.
  • 29/07/2026 11:17

    Step 5: prefix the three public filters and write the Upgrade Notice

    Step 5: prefix the three public filters and write the Upgrade Notice
    
        pluginsused_show_version    ->  wp_pluginsused_show_version
        pluginsused_hidden_plugins  ->  wp_pluginsused_hidden_plugins
        pluginsused_plugins_used    ->  wp_pluginsused_plugins_used
    
    All three shipped in 1.50, so this genuinely breaks third-party code. The old
    names are dropped outright -- no apply_filters_deprecated() shims, which was
    the decision taken for the whole collection: a shim keeping an unprefixed name
    alive keeps the clash the rename exists to remove.
    
    Two more unprefixed identifiers go with them, for the same reason and in the
    same release rather than one at a time: the PLUGINSUSED_SHOW_VERSION constant
    and the $pluginsused_hidden_plugins global, which were how 1.50 was configured
    by editing the plugin's own file. The settings screen added in this major does
    both jobs and survives updates, which is what those two never did.
    
    The Upgrade Notice is written for a site owner: what silently stops working,
    where in their site to look for it, what the symptom will be weeks later, and
    the reassurance that no code of their own means nothing to do. The changelog
    carries the same two BREAKING lines in developer shorthand.
    
    Tests follow: the filter call sites are renamed, and the two that asserted the
    legacy global and constant still worked now assert that they do not.
  • 29/07/2026 11:13

    Step 4: give the plugin the two canonical option rows

    Step 4: give the plugin the two canonical option rows
    
    Settings move from pluginsused_options to wp_pluginsused_options, and the
    version markers get a row of their own, wp_pluginsused_version, holding
    'plugin' and 'db' and nothing else. Keeping them apart means the settings
    screen and the upgrade routine can never overwrite each other, and the
    sanitise callback never has to rescue a value the form did not post.
    
    There was no migration here to retarget -- 1.50, the last release, stored
    nothing at all, so the unprefixed row only exists on an install that ran a
    development build of 2.0.0. maybe_upgrade() is that one migration: it folds
    the old row in, re-sanitises it, deletes it, then stamps both markers in a
    single write so a half-finished upgrade cannot record itself as complete. It
    runs on activation and on every admin load, because activation hooks do not
    fire on update, which is the usual reason a migration never runs.
    
    uninstall.php removes all three rows -- both current ones and the unprefixed
    leftover, for an install removed before it ever reached wp-admin.
    
    Seven new tests cover the fold, the re-sanitise, the refusal to overwrite a
    newer prefixed row, idempotency, and the shape of the marker row.
  • 29/07/2026 11:04

    Step 3: prefix the classes, add the six constants, raise the floors

    Step 3: prefix the classes, add the six constants, raise the floors
    
    PluginsUsed, PluginsUsed_Options, PluginsUsed_Settings and
    PluginsUsed_Template become WP_PluginsUsed_*, each in the class-wp-pluginsused-*
    file its name now implies. Nothing unprefixed is left in the global namespace
    except the template tag and the deprecated shims, which are the documented
    public API and keep the names they shipped with.
    
    The plugin file gains the four constants it was missing -- DB_VERSION, SLUG,
    DIR and URL -- and loads includes/ through DIR rather than __DIR__.
    
    Floors go to WordPress 6.8 and PHP 8.2 in the plugin header and the readme;
    phpcs.xml, .wp-env.json, composer.json and the CI matrix already carried them
    from the templates. There is no version-guarded back-compat shim to delete:
    the plugin has never branched on a PHP or WordPress version.
    
    The one test that asserted the classes must NOT be prefixed with WP_ now
    asserts the opposite, and that each one lives in the file its name implies.
  • 29/07/2026 11:01

    Step 2: adopt the shared config files from _standards/templates/

    Step 2: adopt the shared config files from _standards/templates/
    
    .editorconfig, .gitignore, composer.json, phpcs.xml, both phpunit configs,
    bin/test.sh, bin/test-multisite.sh, every index.php and ci.yml now come from
    the templates verbatim, with only the placeholders substituted. The eslint job
    goes with them: this plugin ships no JavaScript, so there is nothing to lint
    and no package.json, eslint.config.mjs or vitest.config.mjs to add.
    
    .wp-env.json gains the ports assigned in STANDARDS.md 10, 8906 and 8907, so
    this plugin's environment can no longer collide with a sibling's.
    
    The template's /tests/* exclusion list is closed, which cost four fixes here
    rather than four new exclusions:
    
    * The fixtures write their plugin files through WP_Filesystem instead of
      mkdir()/file_put_contents()/unlink()/rmdir(), with the transport pinned to
      "direct" in the bootstrap so it never negotiates credentials. The two tests
      that built a fixture inline now share the same pair of helpers.
    * The DOM inspection reads DOMAttr's lowercase name/value and describes the
      node with getNodePath(), rather than reaching for camelCase DOMNode
      properties WPCS cannot let through.
    * Five docblocks opening on a lowercase function name were reworded.
  • 26/07/2026 13:27

    Move template-tags.php and deprecated.php under includes/

    Move template-tags.php and deprecated.php under includes/
    
    They were the only loose PHP left in the plugin root, which is inconsistent
    with the rest of the collection: wp-useronline keeps both under includes/
    (638001f), and wp-pagenavi and wp-polls do the same with template-tags.php.
    Only the three files WordPress requires to be there stay at the root -- the
    main plugin file, uninstall.php, and the index.php silence guard.
    
    Adds a test pinning that layout, so the next file cannot quietly land in the
    root again.
    
    Verified after the move: 115 tests single-site and multisite, the Playground
    harness 84/84 on PHP 7.4 / WP 6.0 and PHP 8.3 / WP latest, and a deploy dry-run
    confirming the shipped tree is the same 12 files in their new places.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
  • 26/07/2026 13:20

    Cover multisite in CI and bin/test.sh

    Cover multisite in CI and bin/test.sh
    
    The suite passed as multisite already; nothing was pinning it that way, so
    nothing would have caught a regression. Two behaviours only exist on a network:
    uninstall.php branches on is_multisite(), and network-activated plugins are
    read from active_sitewide_plugins, a site option the single-site run never
    touches. Running as a network also makes the uninstall test execute the
    get_sites() loop and the switch_to_blog()/restore_current_blog() pair for real
    rather than only asserting on the source.
    
    WP_MULTISITE is read by the WordPress test bootstrap inside the container, so
    it is forwarded in rather than exported around the npx process.
    
    114 tests / 220 assertions green single-site and multisite.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
  • 26/07/2026 13:17

    Run CI on Node 24

    Run CI on Node 24
    
    Node 20 went end-of-life in April 2026. This standardises every plugin on 24,
    the current active LTS (Krypton), which wp-polls was already using.
    
    This is the Node that runs npx @wordpress/env for the test suite, not the
    Actions runtime -- that was the separate checkout/setup-node v7 bump -- so it
    had to move on its own.
  • 26/07/2026 13:11

    Bump actions/checkout and actions/setup-node to v7

    Bump actions/checkout and actions/setup-node to v7
    
    GitHub is deprecating Node 20 on Actions runners, so checkout@v4 and
    setup-node@v4 were being force-run on Node 24 and emitting a warning on every
    job. v5 moved these actions to Node 24; v7 is the current major.
    
    Neither breaking change affects this workflow: setup-node v6 limited automatic
    caching to npm, and no cache input is passed here; checkout v7 blocks checking
    out fork PRs for pull_request_target and workflow_run, and this triggers on
    push and pull_request.
    
    shivammathur/setup-php stays at v2 -- that is still its current major and it
    was not in the deprecation warning.
  • 26/07/2026 9:37

    Expand the test suite from 67 to 114 tests

    Expand the test suite from 67 to 114 tests
    
    New coverage:
    
    - test-bootstrap.php: constants agree with the plugin header and the readme
      Stable tag, every code file carries its ABSPATH guard, the classes avoid the
      WP_ prefix core reserves, and the admin hooks are absent on the front end.
    - test-stats.php: the summary sentence pluralises through three independent
      _n() calls, so singular, plural and zero are three code paths. None were
      covered; the singular form had never been rendered by a test.
    - Uninstall is now executed, not just read: it deletes the option for real on
      the single-site branch. Added checks that it references no plugin class or
      function (it runs with the plugin inactive) and that the plugin writes no
      option row other than the one uninstall names.
    - Template: hiding matches whole names rather than substrings, no trailing
      space when the version is suppressed, absent URIs emit no anchor,
      descriptions are still texturized, the per-request cache holds and
      reset_cache() clears it, icons carry role/aria-label and differ by shape,
      and nothing enqueues a script or stylesheet.
    - Settings: a plugin name containing quotes round-trips through the form and
      still matches what it hides, unknown keys are dropped, ticked boxes render
      as checked.
    - Deprecated: the render path must never call a shim, or every page load would
      emit deprecation notices under WP_DEBUG.
    
    Two harness bugs the new tests exposed, both fixed:
    
    - GLOB_BRACE is a GNU extension and is undefined in the container's PHP, so
      three tests errored. Source files are now collected with two glob() calls.
    - Searching raw source matched comments, not code: the docblock in
      class-pluginsused-template.php explaining that pluginsused_format_display()
      forwards to it counted as a call site. Source inspection now goes through
      php_strip_whitespace(), the same fix the uninstall test already needed.
    
    phpcs.xml needed no new exemptions. helper-source.php is split out of
    helper-fixtures.php because a file may declare either functions or a class.
    
    114 tests / 220 assertions green on PHP 7.4 / WP 6.0 and PHP 8.3 / WP latest.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
  • 26/07/2026 9:18

    Add PHPUnit suite, CI, and update the readme for 2.0.0

    Add PHPUnit suite, CI, and update the readme for 2.0.0
    
    67 tests / 122 assertions against a real WordPress database via wp-env, run
    on both ends of the support matrix (PHP 7.4 / WP 6.0 and PHP 8.3 / WP latest).
    Nothing here ships: plugin_deploy.sh already excludes tests/, bin/,
    phpunit.xml*, composer.*, .wp-env.json and .git*.
    
    The tests drive the plugin's own entry points against fixture plugins written
    to disk, because every defect worth catching here is integration, not logic:
    
    - test-escaping.php parses the output with DOMDocument and asserts no
      event-handler attribute and no javascript: URL survives into the DOM, while
      the hostile name is still displayed as text. Substring matching would be
      wrong: once escaped, "onmouseover=&quot;" still occurs inside an attribute
      value, where it is inert.
    - test-uninstall.php asserts on php_strip_whitespace() output rather than the
      raw file. Matching the raw source passes on the comment that documents
      'number' => 0 and keeps passing after the argument itself is deleted --
      confirmed by deleting it.
    - test-template.php covers the multisite active_sitewide_plugins fix, ordering,
      and that hidden plugins leave the counts as well as the listings.
    
    Every assertion was verified to fail against deliberately broken code.
    
    phpcs.xml relaxes docblock, file-naming and DOM-property rules under tests/
    only, so CI can fail on warnings everywhere else.
    
    Adds .gitignore: wp-env installs dev dependencies into the mounted plugin
    directory, so vendor/ appears on the host and must not be committed.
    
    Readme: documents the settings screen, the three filters and the template tag,
    trims Tags to the five-tag maximum, drops the dead dev.wp-plugins.org
    translations link and the FamFamFam credit for the removed GIFs.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
  • 26/07/2026 8:54

    Restructure into includes/, add a settings screen, drop the GIF icons

    Restructure into includes/, add a settings screen, drop the GIF icons
    
    Replaces the hand-rolled plugin scanner and the edit-the-source config with
    current WordPress APIs. Behaviour is unchanged: the three shortcodes, the
    display_pluginsused() template tag and the rendered markup all keep working.
    
    - Scanning: get_plugins() replaces the bespoke directory walk and the
      "|Plugin Name:(.*)|i" header regexes, which read whole files and could
      match a "Version:" occurring anywhere in one. Core sorts identically
      (_sort_uname_callback is the same strnatcasecmp on name), so ordering is
      byte-for-byte what it was.
    - Config: settings now live in one option row, pluginsused_options, behind
      Settings -> WP-PluginsUsed. Previously the only way to hide a plugin or a
      version number was to edit wp-pluginsused.php, which every plugin update
      silently reverted. The PLUGINSUSED_SHOW_VERSION constant and the
      $pluginsused_hidden_plugins global are still honoured, and two new filters
      (pluginsused_show_version, pluginsused_hidden_plugins) are applied last.
    - Icons: inline SVG replaces images/plugin_{active,inactive}.gif. They stay
      crisp at any pixel density, inherit the theme's text colour, cost no HTTP
      request, and distinguish state by shape as well as by an aria-label.
    - Multisite: network-activated plugins were listed as inactive, because only
      active_plugins was consulted. active_sitewide_plugins is now merged in.
    - Empty Plugin URI / Author URI no longer emit <a href="">.
    - Drops load_plugin_textdomain(); WordPress has loaded plugin translations
      automatically since 4.6, and since 6.7 an early call triggers _doing_it_wrong.
    - Old procedural functions remain in deprecated.php as working shims.
    
    Layout follows the Plugin Handbook: includes/class-*.php, index.php silence
    guards, an uninstall.php whose multisite loop passes 'number' => 0 and
    restores inside the loop.
    
    Verified with a WordPress Playground harness, 84 assertions, on both PHP 7.4 /
    WP 6.0 and PHP 8.3 / WP latest.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
  • 26/07/2026 8:40

    Apply WordPress Coding Standards (phpcbf)

    Apply WordPress Coding Standards (phpcbf)
    
    Formatting only, no logic change: 96 violations fixed automatically.
    Adds phpcs.xml so the ruleset is pinned to PHP files (WPCS has had no
    JavaScript sniffs since 3.0) and CI uses the same config as local runs.
    
    Verified against the Playground harness: 33/33 assertions unchanged.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo
  • 26/07/2026 8:39

    Fix stored XSS in plugin listing output

    Fix stored XSS in plugin listing output
    
    pluginsused_format_display() interpolated plugin header values straight
    into href/src/alt/title attributes. strip_tags() was the only filtering
    applied, and it removes tags but leaves quotes, so a plugin whose header
    contains a double quote broke out of the attribute:
    
      Plugin Name: Evil" onmouseover="alert(1)
      Plugin URI:  javascript:alert(2)
    
    rendered as a live onmouseover handler on both the <img> and the <a>,
    plus href="javascript:alert(2)". Escape at each sink with esc_url(),
    esc_attr() and esc_html() instead.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01TFsMxKw9RRmE5Kk41hAxYo

Log in