Since 1.6.0 "Show" both whitelisted a method for its group and hid it from everybody else. A group shown ordinary methods next to a free one therefore took the ordinary ones away from every stranger, who was left with only the free method. "Only" now carries the reservation; "Show" is just the group's whitelist. Existing show rules are converted to only once on update so the checkout does not change until the shop chooses. Co-Authored-By: Claude Code <noreply@anthropic.com>
112 lines
6.2 KiB
Markdown
112 lines
6.2 KiB
Markdown
# Contract
|
|
|
|
Every hook this plugin offers, and every family hook it answers. Version 1.7.0.
|
|
|
|
A contract is a name somebody else has to know. Core hooks this plugin merely
|
|
listens to — `init`, `admin_menu`, `wp_enqueue_scripts` — are not here;
|
|
WordPress documents those, and listening to `init` is not a promise to anyone.
|
|
The exception is at the bottom, and it is there because it is shared with every
|
|
sibling plugin on the same update host.
|
|
|
|
The tables and the source are compared in both directions by
|
|
`ci/check-docs.php`, which runs in CI. A hook added without a row here fails
|
|
the build, and so does a row whose name no longer exists in the source.
|
|
|
|
## Hooks this plugin provides
|
|
|
|
All six are filters, all read a setting that has a working default, and all are
|
|
answered from `apply_filters()` in shipped code. A shop that adds no code gets
|
|
the default and never needs this table.
|
|
|
|
| hook | signature | default | if you are absent |
|
|
| --- | --- | --- | --- |
|
|
| `wc_edm_audit_log_enabled` | `bool $enabled` | `true` | the audit log records admin actions |
|
|
| `wc_edm_audit_log_retention_days` | `int $days` | `30` | rows older than 30 days are pruned daily; the value is floored at 1, so `0` means 1 day, not "keep forever" |
|
|
| `wc_edm_client_ip` | `string $ip` | the resolved address | the plugin's own resolution is used — override this behind a proxy |
|
|
| `wc_edm_rate_limit_enabled` | `bool $enabled` | `true` | REST routes are rate limited |
|
|
| `wc_edm_rate_limit_max_requests` | `int $default, string $method` | per method | the built-in allowance applies; floored at 1 |
|
|
| `wc_edm_rate_limit_window` | `int $seconds, string $method` | `MINUTE_IN_SECONDS` | a 60-second window; floored at 1 |
|
|
|
|
**What a consumer must not do.** Do not return a non-scalar: each of these is
|
|
cast (`(bool)`, `(int)`, `(string)`) and a bad type becomes a wrong value rather
|
|
than an error. Do not assume `wc_edm_rate_limit_*` can switch limiting off by
|
|
returning `0` — they are floored at 1; use `wc_edm_rate_limit_enabled`.
|
|
Returning `false` from `wc_edm_audit_log_enabled` on a shop that has a
|
|
compliance reason to keep the log is a decision, not a performance tweak.
|
|
|
|
## The family's shared hooks
|
|
|
|
These cross a repository boundary, which is the class of value nobody's checks
|
|
cover, so both directions are written out.
|
|
|
|
| hook | this plugin | what that means |
|
|
| --- | --- | --- |
|
|
| `wpmgr_menu_title` | **answers** it (main file) and **provides** it (our copy of `shared-menu.php`) | returns `Nolimit`, so the shared parent menu is labelled the same whichever sibling registered it |
|
|
| `wpmgr_dashboard_cards` | **answers** it (`class-admin.php`) and **provides** it (our copy of `shared-menu.php`) | adds this plugin's card — colour `#1a7a52`, two counts, cached 5 minutes |
|
|
| `wpmgr_menu_glyph` | **provides** it only, via our copy of `shared-menu.php` | this plugin does not override the glyph; it answers with the shared default `∞` |
|
|
|
|
**Why "provides" and "answers" are both true.** `includes/shared-menu.php` is
|
|
byte-identical in all eight family plugins and frozen at a SHA in
|
|
`ci/plugin.conf`. Whichever copy loads first is the one that runs, so this
|
|
plugin is simultaneously one of the eight possible providers of these three
|
|
hooks and a consumer of whichever copy won. That is why the label is set with a
|
|
filter and not by editing the shared file: editing it would make a site with
|
|
three of eight plugins updated show a different menu title depending on
|
|
activation order, and change its mind when a plugin is deactivated.
|
|
|
|
**What a consumer must not do.** Do not edit `shared-menu.php` to change any of
|
|
these — it reaches every site in activation order and the frozen SHA will fail
|
|
the build in all eight trees. Answer the filter from your own plugin.
|
|
`wpmgr_menu_title` must be idempotent: every sibling adds a callback and the
|
|
order is not defined.
|
|
|
|
## One core hook, because it is shared
|
|
|
|
| hook | this plugin | why it is in a contract document |
|
|
| --- | --- | --- |
|
|
| `update_plugins_{$hostname}` | **answers** it, `add_filter( …, 10, 3 )` | 🔴 the filter name is built from the *hostname*, not the repository, so every plugin updating from `gitea.nolimit.dk` lands on the same hook and WordPress runs every callback for every one of them |
|
|
|
|
Answer it unconditionally and you offer your release to your neighbour. On a
|
|
live shop in September 2026 the subscriptions plugin was told version 1.4.4 was
|
|
an update for itself; 1.4.4 was this plugin's. Load order decides who wins, so
|
|
a rename is enough to flip which plugin gets corrupted — and flipped toward one
|
|
with auto-updates on, it installs a stranger's zip over itself unattended.
|
|
|
|
The guard is two halves and both are required:
|
|
|
|
```php
|
|
add_filter( "update_plugins_{$host}", array( $this, 'offer_update' ), 10, 3 );
|
|
|
|
// and inside offer_update():
|
|
if ( '' !== $plugin_file && plugin_basename( WC_EDM_PLUGIN_FILE ) !== $plugin_file ) {
|
|
return $update;
|
|
}
|
|
```
|
|
|
|
Leave the argument count at 2 and `$plugin_file` never arrives, so the guard
|
|
silently does nothing — the same shape of bug it fixes. **No integration test
|
|
can catch this**, because a test site installs one plugin from that host and the
|
|
collision cannot arise; the assertion is the defence.
|
|
|
|
`plugins_api` is also answered and is safe, because it guards on `$args->slug`.
|
|
|
|
## What the checker cannot see, and is therefore listed by hand
|
|
|
|
`ci/check-docs.php` reads hook names out of the shipped source as string
|
|
literals. Two names in this tree are not literals at the call site:
|
|
|
|
| hook | how it is written | why it is missed |
|
|
| --- | --- | --- |
|
|
| `wc_edm_prune_audit_log` | `add_action( self::CRON_HOOK, … )` | a class constant, resolved at runtime |
|
|
| `update_plugins_{$hostname}` | `add_filter( "update_plugins_{$host}", … )` | interpolated from a variable |
|
|
|
|
Both rows above are real and both would survive a rename of the constant or the
|
|
variable without the checker noticing. `wc_edm_prune_audit_log` is the daily
|
|
audit-log prune event; it is internal wiring rather than an extension point,
|
|
but it is a name that appears in `wp_next_scheduled()` output and in any cron
|
|
viewer, so it is written down.
|
|
|
|
This section exists because the instrument's blind spot is worth stating next
|
|
to its results. A generated list is better than a remembered one, and it is
|
|
still a list of what the generator can see.
|