Everything another plugin, a theme or a site owner can bind to, store against,
or break by changing. It describes what the installed plugin actually does, not
what it might one day do: a table written from intent lists what a plugin means
to provide rather than what it provides.
The contract covers the files inside the installed plugin. Nothing else is part
of it.
## Hooks this plugin provides
**None.** This plugin fires no `do_action()` and applies no `apply_filters()`
of its own, so there is nothing here for anyone to hook.
That is a deliberate state rather than an oversight, and it is worth one line
so nobody adds one casually: every extension point is a promise that outlives
the reason it was added, and nothing has yet asked to extend this. In
particular there is no filter that lets another plugin change the SMTP
settings — that is exactly the door a credential leaks through. The moment
something does ask, the filter goes in this table in the same commit.
<!-- nlsnd:hooks:start -->
## Hooks this plugin registers on
🔴 Deliberately not headed "consumes". `add_filter()` is how you **answer** a
filter — you supply the value somebody else asked for — and reading it as
consumption gets the direction of the promise backwards. For an action the
word is *listens*; for a filter it is *answers*. The Kind column below says
which.
Priority and accepted-argument count are given where they are not WordPress'
defaults of `10` and `1`, because those two are what break silently — see the
`update_plugins_` row.
| Hook | Kind | Registered in | Why |
| --- | --- | --- | --- |
| `phpmailer_init` | action | `includes/class-mailer.php` | The whole job: points PHPMailer at the chosen server. Does nothing unless enabled, with a host, and no other mail plugin present. |
| `wp_mail_failed` | action | `includes/class-mailer.php` | Keeps the reason a message failed — time and message only — when this plugin was the one sending. |
| `wp_mail_from` | filter, priority 99 | `includes/class-mailer.php` | The sender address. 99 so WooCommerce's own callback at 10 has already had its say. |
| `wp_mail_from_name` | filter, priority 99 | `includes/class-mailer.php` | The sender name, on the same rule. |
| `plugins_loaded` | action | `includes/class-plugin.php` | Everything else is registered from here. |
| `admin_menu` | action | `includes/class-admin.php` | Registers the screen under the shared Nolimit menu. |
| `admin_init` | action | `includes/class-admin.php` | Handles the save, test and clear form posts, after the nonce and capability checks. |
| `admin_init` | action | `includes/class-updater.php` | Drops the cached feed when the user presses "Check again". |
| `admin_enqueue_scripts` | action | `includes/class-admin.php` | The one stylesheet, on this plugin's screen only. |
| `admin_post_nlsnd_auto_update` | action | `includes/class-auto-update.php` | The auto-update toggle's form target. Name is this plugin's; the hook is WordPress'. |
| `plugin_action_links_<basename>` | filter | `includes/class-admin.php` | The "Settings" link on the Plugins screen. Hook name is built from `NLSND_PLUGIN_BASENAME`. |
| `plugins_api` | filter, 3 args | `includes/class-updater.php` | The "View details" panel. Guarded on `$args->slug`. |
| `update_plugins_<hostname>` | filter, 3 args | `includes/class-updater.php` | Offers this plugin's own updates. |
| `upgrader_process_complete` | action, 0 args | `includes/class-updater.php` | Forgets the cached feed after any update runs. |
`send_test()` also adds and removes a `wp_mail_failed` listener around one
`wp_mail()` call, so the test button can say why a message did not go out. It
is removed again on the way out.
**Nothing hooks `wp_mail` itself.** No filter here can alter a message's
recipient, subject, body, headers or attachments, and the plugin's own checks
assert that `wp_mail` has no callback from it. That is the property that lets
the plugin be site-wide: it configures the transport and leaves the message
alone.
### 🔴 `update_plugins_<hostname>` is shared, and the argument count is load-bearing
The hook name is built from the **host**, not the repository, so every plugin
updating from `gitea.nolimit.dk` lands on the same filter and WordPress runs
every callback on it for every one of them. Answering unconditionally offers
this plugin's release to a sibling — which has happened on a live shop, and
installs a stranger's zip over a plugin with auto-updates on.
The defence is two halves and neither works alone: `add_filter( …, 10, 3 )`
**and** a guard returning `$update` untouched when `$plugin_file` is not this
plugin's basename. Leave the count at `2` and the third argument never
arrives, so the guard compares against an empty string and silently does
nothing — the same bug it was written to prevent, wearing a fix's clothes.
A test on a site with one plugin from that host cannot see this, because the
collision cannot arise there. The way to check it is to fire the real filter in
both directions: the neighbour is left alone, **and** this plugin still answers
for itself. The second half is not redundant. A guard that bails on everything
also leaves the neighbour alone.
## The family's shared hooks
These are the Nolimit-family surface, not this plugin's.