Files
nolimit-discounts/includes/shared-menu.php
T
kristianwindandClaude Opus 5 3020338378 Take shared-menu.php 1.1.0 and keep its hash in one place
The family froze a new copy of includes/shared-menu.php: 184cebdc, contract
1.1.0. Two changes in it for this plugin, which was still on f4862d5c (1.0.0).

The menu icon is now an infinity sign, painted from admin_head behind a
wpmgr_menu_glyph filter that defaults to it — Kristian asked for it because the
family is called Nolimit. And the file's own header said 1.0.0 while its
constant said 1.1.0, and it counted five plugins where there are six; Nolimit
SEO found both.

Taken today rather than when convenient, and the reason is measured rather than
tidiness: two plugins were on 1.1.0 and three on 1.0.0 at the same time, so a
shop got the glyph or the cog depending on activation order — while every
repository's own hash check was green on its own side. A frozen file is a
family-wide fact, and a green local check says nothing about the shop.

The API change is additive — a new filter and a new function, nothing removed —
so this plugin's use of wpmgr_ensure_menu() and wpmgr_menu_title is unaffected,
and the default glyph means there is nothing to opt into.

The hash now lives once, as WPMGR_MENU_SHA in ci/plugin.conf, checked by
ci/static.sh and required by ci_load_conf so an unset value cannot turn the
comparison into a no-op. It was previously a literal inside tests/smoke.php as
well; two copies of one constant drift, and the copy that drifts is the one
nobody remembers. The runtime assertion that remains proves a different fact —
that the file is loaded at all — because wpmgr_ensure_menu() only exists if it
was.

Proven red by appending one line to the shared file: the check names both
hashes and says not to edit it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-20 09:31:37 +02:00

290 lines
12 KiB
PHP

<?php
/**
* The one admin menu the manager plugins share.
*
* ┌──────────────────────────────────────────────────────────────────────────┐
* │ THIS FILE IS BYTE-IDENTICAL IN EVERY MANAGER PLUGIN. │
* │ Do not edit it to add something for one plugin. Extend it through the │
* │ `wpmgr_dashboard_cards` filter, which is what it is for. │
* │ Contract version 1.1.0. See "Why it is frozen" below — and note that │
* │ the two changes it has had were both paid for in load-order confusion. │
* └──────────────────────────────────────────────────────────────────────────┘
*
* The problem it solves: a family of plugins each adding a top-level menu is
* a handful of entries in a sidebar that already has fifteen. So they share
* one parent and each adds a submenu under it. Six of them as of this
* writing, which is why the count is not stated anywhere it would have to be
* maintained.
*
* Three things about how it does that are load-bearing, and none is obvious.
*
* **1. Every declaration is guarded.** Each plugin `require_once`s its own
* copy of this file, in one request. `require_once` only deduplicates by
* resolved path, and these are different paths — so without the
* `function_exists()` guard, activating the second plugin is
* `Fatal error: Cannot redeclare wpmgr_ensure_menu()`. A white screen on a
* shop, caused by nothing except owning two of these plugins. It is the same
* shape as the update-hook collision: a fault that exists only when there is
* more than one, which means no repository's own tests can find it.
*
* **2. Whichever copy loads first wins, and that order is arbitrary** — it is
* plugin activation order, effectively alphabetical by directory. PHP cannot
* un-declare a function, so there is no honest way for a newer copy to replace
* an older one that has already been loaded. The only stable answer is that
* the file never changes. Hence: **frozen**. The parent slug, the capability,
* the position and the filter name are a contract between every repository in
* the family, and anything new arrives through the filter.
*
* 🔴 It has moved twice, and both times the cost was the same: a half-adopted
* change means the menu is whatever the alphabetically-first plugin's copy
* says, and every repository's own hash check goes green on its own side
* while the family disagrees. Nolimit SEO measured exactly that today — two
* plugins on 1.1.0 and three on 1.0.0, so a shop got the infinity glyph or
* the old cog depending on activation order. If this file has to change,
* every plugin takes it in the same hour or the change is not made.
*
* **3. The dashboard is built from a filter, not owned by a plugin.** If the
* plugin that happened to register the menu is deactivated, the menu is
* re-registered by the next one on the following request and the page still
* renders — because the page's content comes from whoever is installed, not
* from whoever created it. Registering a callback that lives in one plugin
* would mean deactivating that plugin takes the other four's screens with it.
*
* Each plugin calls `wpmgr_ensure_menu()` at the top of its own `admin_menu`
* callback, before its own `add_submenu_page( 'wpmgr', … )`. Not on some other
* hook: `add_submenu_page` against a parent that does not exist yet silently
* fails, and whose `admin_menu` runs first is, again, arbitrary.
*
* The package below is the family, not any one plugin: this file is
* byte-identical in five repositories, and a tag naming whichever of them it
* was first written in reads as a mistake in the other four — which invites
* somebody to "fix" it, in one repository, which is the exact edit the freeze
* exists to prevent.
*
* @package WordPress_Manager_Family
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! defined( 'WPMGR_MENU_VERSION' ) ) {
define( 'WPMGR_MENU_VERSION', '1.1.0' );
}
if ( ! defined( 'WPMGR_MENU_SLUG' ) ) {
define( 'WPMGR_MENU_SLUG', 'wpmgr' );
}
if ( ! function_exists( 'wpmgr_ensure_menu' ) ) {
/**
* Register the shared parent menu, unless somebody already has.
*
* Call this first thing in your own `admin_menu` callback.
*/
function wpmgr_ensure_menu() {
global $admin_page_hooks;
if ( isset( $admin_page_hooks[ WPMGR_MENU_SLUG ] ) ) {
return;
}
/**
* The label on the shared parent menu.
*
* Left untranslated here on purpose: this file is byte-identical in
* five plugins, so it cannot carry any one of their text domains, and
* a string with the wrong domain is a string that is silently never
* translated. Any plugin may translate it in its own domain through
* this filter.
*
* @param string $title The menu title.
*/
$title = apply_filters( 'wpmgr_menu_title', 'Manager' );
/**
* The glyph on the shared parent menu.
*
* An infinity sign, because the family is called Nolimit. A cog said
* nothing, and there is no infinity dashicon.
*
* Drawn as TEXT rather than as an SVG passed to add_menu_page(), and
* the reason is colour. WordPress renders a data-URI SVG as a CSS
* background-image, and a background-image cannot inherit a colour —
* so it stays one fixed grey while every other icon lightens on hover
* and turns white on the current item. It looks subtly broken under
* every admin colour scheme, including the default. A `::before`
* glyph is text: it takes the colour it is given and follows all of
* those states for free, with no image request.
*
* Filterable so that changing it later needs no edit to this file.
* This file is byte-identical across the family and hash-frozen in
* each plugin's CI, so an edit here is a coordinated release for all
* of them; a filter is one line in one plugin.
*
* @param string $glyph A single character, or '' for the old cog.
*/
$glyph = apply_filters( 'wpmgr_menu_glyph', '∞' );
add_menu_page(
$title,
$title,
'manage_options',
WPMGR_MENU_SLUG,
'wpmgr_render_dashboard',
// 'none' makes WordPress emit the icon wrapper with no dashicon
// class on it, which is the hook the style below needs. A dashicon
// name here would win instead, because its class carries its own
// ::before content.
'' === $glyph ? 'dashicons-admin-generic' : 'none',
// Just below WooCommerce (55.x) and above Appearance (60). The
// fractional position is not decoration: an integer collides with
// whatever core or another plugin put there, and the loser is
// simply dropped.
58.9
);
if ( '' !== $glyph ) {
add_action( 'admin_head', 'wpmgr_menu_glyph_style' );
}
}
/**
* The one style rule the glyph needs.
*
* Printed in the head on every admin page, because the menu is on every
* admin page — a stylesheet enqueued only on this family's own screens
* would leave a blank square everywhere else.
*
* `font-family` has to be overridden: WordPress sets the icon wrapper to
* the Dashicons font, and Dashicons has no infinity glyph, so inheriting
* it renders nothing at all.
*/
function wpmgr_menu_glyph_style() {
$glyph = apply_filters( 'wpmgr_menu_glyph', '∞' );
if ( '' === $glyph ) {
return;
}
printf(
'<style>#adminmenu .toplevel_page_%1$s .wp-menu-image::before{content:"%2$s";font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:20px;font-weight:400;line-height:1.4}</style>',
esc_attr( WPMGR_MENU_SLUG ),
esc_attr( $glyph )
);
}
/**
* The shared landing page.
*
* Nothing here knows which plugins exist. Every card comes from the
* filter, so the page is correct whichever of them registered the menu and
* whichever of them have since been deactivated.
*/
function wpmgr_render_dashboard() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html( 'You do not have permission to view this page.' ) );
}
/**
* Cards on the shared dashboard, one per installed manager plugin.
*
* Each entry:
* title string Plugin name, already translated by its owner.
* status string 'ok' | 'warn' | 'fail' — what the check said.
* headline string One short line: the number that matters.
* lines array label => value, a handful at most.
* url string Where "Open" goes.
* colour string The plugin's own colour, as #rrggbb.
*
* Implementations must not measure anything here. This filter runs on
* an admin page load; a card that walks the disk or makes an HTTP
* request turns somebody else's dashboard into a slow page, and they
* will not know why.
*
* @param array $cards The cards so far.
*/
$cards = apply_filters( 'wpmgr_dashboard_cards', array() );
echo '<div class="wrap wpmgr-dashboard">';
echo '<h1>' . esc_html( apply_filters( 'wpmgr_menu_title', 'Manager' ) ) . '</h1>';
if ( empty( $cards ) ) {
echo '<p>Nothing to report.</p></div>';
return;
}
echo '<style>'
. '.wpmgr-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;margin-top:20px}'
. '.wpmgr-card{background:#fff;border:1px solid #dcdcde;border-radius:8px;padding:0;overflow:hidden}'
. '.wpmgr-card header{padding:14px 18px;color:#fff;display:flex;justify-content:space-between;align-items:center}'
. '.wpmgr-card header h2{margin:0;font-size:14px;color:inherit}'
. '.wpmgr-card .wpmgr-body{padding:16px 18px}'
. '.wpmgr-headline{font-size:22px;font-weight:600;margin:0 0 10px}'
. '.wpmgr-card dl{display:grid;grid-template-columns:1fr auto;gap:4px 12px;margin:0;font-size:13px}'
. '.wpmgr-card dt{color:#646970;margin:0}.wpmgr-card dd{margin:0;text-align:right}'
. '.wpmgr-state{font-size:11px;text-transform:uppercase;letter-spacing:.04em;'
. 'background:rgba(255,255,255,.22);padding:2px 8px;border-radius:10px}'
. '.wpmgr-card footer{padding:12px 18px;border-top:1px solid #f0f0f1}'
. '</style>';
echo '<div class="wpmgr-cards">';
$states = array(
'ok' => 'OK',
'warn' => 'Attention',
'fail' => 'Action needed',
);
foreach ( $cards as $card ) {
if ( empty( $card['title'] ) ) {
continue;
}
$colour = isset( $card['colour'] ) && preg_match( '/^#[0-9a-f]{6}$/i', $card['colour'] )
? $card['colour']
: '#50575e';
$status = isset( $card['status'] ) && isset( $states[ $card['status'] ] ) ? $card['status'] : 'ok';
echo '<section class="wpmgr-card">';
printf(
'<header style="background:%s"><h2>%s</h2><span class="wpmgr-state">%s</span></header>',
esc_attr( $colour ),
esc_html( $card['title'] ),
esc_html( $states[ $status ] )
);
echo '<div class="wpmgr-body">';
if ( ! empty( $card['headline'] ) ) {
echo '<p class="wpmgr-headline">' . esc_html( $card['headline'] ) . '</p>';
}
if ( ! empty( $card['lines'] ) && is_array( $card['lines'] ) ) {
echo '<dl>';
foreach ( $card['lines'] as $label => $value ) {
echo '<dt>' . esc_html( $label ) . '</dt><dd>' . esc_html( $value ) . '</dd>';
}
echo '</dl>';
}
echo '</div>';
if ( ! empty( $card['url'] ) ) {
printf(
'<footer><a class="button" href="%s">%s</a></footer>',
esc_url( $card['url'] ),
esc_html( isset( $card['action'] ) ? $card['action'] : 'Open' )
);
}
echo '</section>';
}
echo '</div></div>';
}
}