199 lines
11 KiB
Markdown
199 lines
11 KiB
Markdown
# The surface Nolimit Send exposes
|
|
|
|
<!-- nlsnd:version 0.1.1 -->
|
|
|
|
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.
|
|
|
|
| Hook | This plugin's relationship | Where |
|
|
| --- | --- | --- |
|
|
| `wpmgr_dashboard_cards` | **answers** — supplies this plugin's card | `includes/class-admin.php` |
|
|
| `wpmgr_dashboard_cards` | applied by the shared file this plugin ships | `includes/shared-menu.php` |
|
|
| `wpmgr_menu_title` | applied by the shared file this plugin ships | `includes/shared-menu.php` |
|
|
| `wpmgr_menu_glyph` | applied by the shared file this plugin ships | `includes/shared-menu.php` |
|
|
| `admin_head` | listened to by the shared file this plugin ships | `includes/shared-menu.php` |
|
|
|
|
<!-- nlsnd:hooks:end -->
|
|
|
|
### 🔴 Shipping a file is not owning its hooks
|
|
|
|
`includes/shared-menu.php` is byte-identical across the Nolimit plugins.
|
|
Whichever copy loads **first** declares the functions; every later copy sees
|
|
them already defined and does nothing. So the three `wpmgr_*`
|
|
filters above are applied by *a* copy of that file, which on any real site is
|
|
probably another plugin's. Anyone depending on those filters should depend on
|
|
the family, not on this plugin being installed.
|
|
|
|
The card this plugin answers with carries `title`, `status`, `headline`,
|
|
`lines`, `url`, `action` and `colour`. `colour` is `NLSND_COLOUR`, which the
|
|
shared file paints as a card header with white text.
|
|
|
|
## The provider table — shared with Nolimit Forms
|
|
|
|
`NLSND_Mailer::providers()` is **hand-copied** from `NLFRM_Mailer::providers()`
|
|
in Nolimit Forms, which has an SMTP mailer of its own. Nothing shares the code
|
|
across the two plugins, so a difference between the copies is a defect that
|
|
neither plugin can see. The values that must agree, by id:
|
|
|
|
| Id | Host | Port | Encryption |
|
|
| --- | --- | --- | --- |
|
|
| `brevo` | `smtp-relay.brevo.com` | 587 | tls |
|
|
| `smtp2go` | `mail.smtp2go.com` | **2525** | tls |
|
|
| `mailgun` | `smtp.mailgun.org` | 587 | tls |
|
|
| `sendgrid` | `smtp.sendgrid.net` | 587 | tls |
|
|
| `postmark` | `smtp.postmarkapp.com` | 587 | tls |
|
|
| `ses` | *(none — region-specific, typed in)* | 587 | tls |
|
|
| `google` | `smtp.gmail.com` | 587 | tls |
|
|
| `microsoft` | `smtp.office365.com` | 587 | tls |
|
|
| `custom` | *(none — typed in)* | 587 | tls |
|
|
|
|
Cautions such as the Microsoft one are kept out of the table on purpose
|
|
(`NLSND_Mailer::warning()`), so one can change without touching the contract.
|
|
|
|
**Both plugins hook `phpmailer_init`.** With both switched on, the later
|
|
callback wins. Nolimit Forms is told this plugin is present by the constant
|
|
`NLSND_VERSION`, which is defined at include time for exactly that reason.
|
|
Nolimit Send reports Forms' SMTP on its own screen as a notice rather than
|
|
standing down for it.
|
|
|
|
## Stored data
|
|
|
|
Every row here is removed by `uninstall.php` on delete — not on deactivate,
|
|
because switching a plugin off for ten seconds to test something must not
|
|
destroy the shop's mail settings.
|
|
|
|
| Key | Kind | Holds |
|
|
| --- | --- | --- |
|
|
| `nlsnd_settings` | option, not autoloaded | Every setting, including the SMTP password as typed. |
|
|
| `nlsnd_last_failure` | option | The last delivery failure: a time and a message capped at 400 characters. |
|
|
| `nlsnd_release_feed` | transient, 15 min | The cached update feed. |
|
|
| `nlsnd_auto_update_seeded` | option | A marker, so the auto-update default is offered once and never re-applied. |
|
|
|
|
The password is stored in clear because WordPress has nowhere to keep a secret;
|
|
anyone who can read the options table can read it. It is never rendered back
|
|
into a form. Mail content — recipient, subject, body — is **never** stored, and
|
|
there is no send log.
|
|
|
|
## Capabilities
|
|
|
|
| Capability | Gates |
|
|
| --- | --- |
|
|
| `manage_options` | Seeing the screen, saving settings, sending a test, clearing the last failure. |
|
|
| `update_plugins` | The auto-update toggle only. WordPress' own capability for the same decision. |
|
|
|
|
## Constants
|
|
|
|
`NLSND_VERSION`, `NLSND_PLUGIN_FILE`, `NLSND_PLUGIN_DIR`, `NLSND_PLUGIN_URL`,
|
|
`NLSND_PLUGIN_BASENAME`, `NLSND_COLOUR`.
|
|
|
|
`NLSND_VERSION` has a reader outside this repository: Nolimit Forms asks whether
|
|
it is defined. It must stay defined at include time, not from a hook.
|
|
|
|
`NLSND_COLOUR` (`#556b0f`) is the other one with a reader outside this
|
|
plugin: the shared menu paints it as a dashboard card background with
|
|
hardcoded white text, which it measures at 6.01:1 against white, over the 4.5:1
|
|
the 14px label needs. It exists in three places that must stay the same string —
|
|
the `define()`, the stylesheet's `--nlsnd-accent`, and `assets/icon.svg`.
|
|
|
|
## What is deliberately not here
|
|
|
|
Stated because an absent surface is invisible, and the next person to want one
|
|
should find the reason rather than the silence.
|
|
|
|
* **No WP-CLI commands, no REST routes, no AJAX endpoints.** The screen is a
|
|
form post handled on `admin_init`.
|
|
* **No JavaScript.** One stylesheet, no script is enqueued.
|
|
* **No cron, no scheduled events.** Nothing runs on a timer; the update feed is
|
|
fetched lazily behind a transient.
|
|
* **No queue, throttle or retry.** A failed message is recorded and not re-sent.
|
|
* **No send log, and no bounce or complaint handling.** SMTP cannot report them.
|
|
* **No OAuth.** Google needs an App Password; Microsoft's password sign-in is
|
|
being retired and the screen says so.
|
|
* **No hook on `wp_mail`, and no filter over the SMTP credentials.**
|
|
* **No user meta, post meta or term meta.**
|
|
* **No telemetry of any kind.** The only network traffic is the mail itself, to
|
|
the server the owner named, and the update check against the public release
|
|
repository, which sends no credential.
|