== Bulwarden changelog: 1.27.3 and earlier ==

Releases from 1.28.0 onward are listed in readme.txt. wordpress.org shows at most 5,000 words of a plugin's changelog, so older entries live here.

= 1.27.3 =
* Fixed: **a restore of a large backup no longer leaves the site stuck showing
  "Briefly unavailable for scheduled maintenance".** Downloading and decrypting
  a backup take as long as they take, and a round of work that had spent its
  whole time doing that went on to start restoring the database anyway — which
  begins by switching the site into maintenance mode. It then managed a single
  instruction before stopping for the round, so the maintenance mode bought
  nothing; and because WordPress checks for maintenance mode before it loads
  any plugin, a round that did not live long enough to switch it back off took
  the site's own timer down with it — the timer being the only thing that could
  start the next round. The site sat unavailable, and the restore sat still,
  until WordPress itself gave up on the maintenance flag ten minutes later. A
  round that is out of time now stops cleanly between phases, and maintenance
  mode is switched off the moment the database work pauses rather than at the
  very end of the round.

= 1.27.2 =
* Fixed: **restoring a backup onto a different site now rewrites its addresses
  to *this* site's.** With "rewrite addresses" ticked, the restore replaced the
  backup's web address with the address it read from the site — but it read it
  after the backup's database was already in place, so it was reading the
  backup's own address back. A backup moved to a new domain either had nothing
  rewritten at all, leaving the restored site pointing at the domain it was
  copied from, or had every address in the database rewritten from `http://` to
  `https://` on that old domain. The restore reported success in both cases.
  This site's addresses are now recorded before the restore starts and used as
  the target; if they could not be recorded, the addresses are left alone and
  the restore says so instead of guessing.
* Fixed: a backup now records the site's own stored web address rather than the
  address of the internal request that happened to start it, so the restore
  searches for the address that is really in the backup.

= 1.27.1 =
* Fixed: **a long restore of a backup that predates this plugin now finishes.**
  Restoring the database replaces the list of active plugins with the backup's,
  and a backup taken before this plugin was installed does not list it. On a
  site big enough to need several rounds of work, the round after the swap
  loaded WordPress without the plugin, so nothing was left to carry on and the
  restore sat half-done until the hub gave up. The plugin now puts itself back
  the moment the table is swapped in, in the same round.
* Changed: **a restore never writes WordPress' drop-in files**
  (`object-cache.php`, `db.php`, `advanced-cache.php` and the rest of that
  family in `wp-content`). They are loaded before any plugin and are tied to
  the server they were written on — a cache server address, database hosts,
  absolute paths — so a backup's copy restored onto another server took the
  whole site down before anything could repair it. The copy already on the site
  is kept, and the restore log says which files were left alone.

= 1.27.0 =
* Fixed: **a large restore no longer stops part-way through the database.**
  Restoring the database replaces the WordPress options table, and the
  restore's own progress record and its "carry on" timer were rows in that
  table — so on a site big enough to need several rounds of work, the round
  that swapped the table in was the last one that ever ran. The site sat
  half-restored, the dashboard showed the last progress it had heard of, and
  if the backup came from another site, the hub could no longer reach this one
  at all. The restore now puts its own state and this site's own credentials
  back the moment the table is swapped in, and carries on.
* Fixed: each round of a long restore now starts the next one immediately
  instead of waiting for a timer, which roughly halves the wall-clock time of a
  restore that spans many rounds — and no longer depends on a visitor arriving
  to trigger it.
* Improved: **restoring a large database is many times faster.** Backups now
  write rows in batches rather than one statement per row, and the restore
  replays them inside transactions, so a database that used to trickle in at
  about a thousand rows a second now goes back at the speed of the disk.
  Backups made by earlier versions restore exactly as before.
* Improved: a restore interrupted by the hosting provider (a killed process, a
  dropped database connection) now resumes exactly where its last saved
  progress left off. Previously the rows written since the last save were
  replayed on top of themselves, which on a bad day was enough errors to
  abandon the restore.
* Improved: a restore that resumes after a pause keeps the database session
  settings the backup asked for (character set, foreign-key checks, the rule
  that keeps a row with id 0 from being renumbered).
* Improved: two rounds of one restore can no longer run at the same time.
* Improved: the hub's restore page now says when it could not ask the site for
  progress, and why, rather than showing the last answer as if it were live.
* Developer note: `BULW_RESTORE_TICK_BUDGET` (seconds) shortens a restore's
  per-round budget so that a multi-round restore can be reproduced on a small
  test site.

= 1.26.0 =
* New: **the scan now reports program files stored on your site.** A malware scan
  reads code — PHP, JavaScript, page templates — so an installer or a program
  sitting in your media library was invisible to it. That is the commonest thing
  a compromised WordPress site is actually used for: not defacing the site, but
  quietly hosting a file that is mailed out to other people, until the domain is
  flagged by Google Safe Browsing or the host suspends the account. These are now
  listed on the security tab.
* New: a program stored under a name that says otherwise — a Windows or Linux
  program saved as `.jpg`, `.pdf` or `.mp4` in the uploads folder — is reported as
  a critical finding. There is no innocent reason for one, and it is exactly how
  such a file is kept out of sight.
* New: a program file found inside `wp-admin` or `wp-includes` is reported as
  critical. WordPress itself ships none, in any language.
* New: every one of these findings carries a SHA-256 checksum of the file, shown
  next to it in your provider's dashboard. That is the part your provider can act
  on: it identifies the file itself, so it can be checked against a malware
  database and recognised if the same file turns up on another site.
* Note: an ordinary program file elsewhere on the site is reported for
  information only. It never marks the site as infected and never triggers an
  alert — a business that distributes its own software has every right to store
  one, and the scan says so rather than crying wolf.

= 1.25.1 =
* Fix: **a restore no longer hands this site another site's identity.** WordPress
  keeps this plugin's settings in the database, so restoring a backup made on a
  *different* site replaced this site's connection credentials with that site's —
  and from that moment your provider's hub could not reach this site at all: no
  health checks, no cron, no scans, no backups. Putting it right meant logging in
  and pairing the site again by hand. The credentials, the hub address and the
  backup passphrase are now carried across a restore and put back the instant the
  database is in place. Everything else in the backup's settings is restored as
  before.
* Fix: the backup passphrase was replaced the same way, so the next backup was
  encrypted with a passphrase recorded for a different site — something nobody
  would discover until the day they needed it.
* Fix: a backup no longer carries its own "restore in progress" marker, which
  could drop one run's state onto a site part-way through another.

= 1.25.0 =
* New: **a backup can now be restored onto a site whose database tables are named
  differently.** WordPress puts a short prefix in front of every one of its table
  names, and it is rarely the same on two installs &mdash; so restoring a backup onto
  a staging copy, or onto a site being rebuilt after a break-in, used to be refused
  outright. Your provider can now ask for the tables to be renamed as they are
  restored, which leaves the site reading its own names and needs no change to
  wp-config.php at all. Everyone's role and personal settings are moved across with
  them, so nobody loses their login.
* Change: **the message shown when the names do not match no longer recommends
  something harmful.** It used to suggest editing wp-config.php to match the backup.
  On a site that is up, that stops WordPress finding its own database, takes the
  site down, and stops this plugin running &mdash; which removes the only thing able to
  perform the restore. The message now explains the two safe ways forward instead.

= 1.24.1 =
* Fix: **a restore that stopped explained itself in punctuation codes.** The messages
  this site sends your provider when a restore cannot go ahead &mdash; the reason it
  stopped, and the database errors behind it &mdash; were encoded for a web page here
  before being sent, and then encoded a second time by the page that displayed them.
  Every apostrophe and quotation mark in the one line meant to explain the failure
  arrived as a run of digits and semicolons, including the table names in a database
  error, which left the explanation unreadable exactly when it was needed. The
  messages are now sent as written.

= 1.24.0 =
* New: **a restore can now be watched while it runs.** Putting a site back can
  take an hour on a large one, and until now your provider's hub learned nothing
  at all until it had finished &mdash; a spinner, and then an answer. This site now
  reports every step of a restore as it goes: checking, downloading, decrypting,
  the database, the files, the addresses, and the finish, each with how far
  through it is and how long it has taken, alongside the run log as it is
  written.
* Change: a step that will not run says so from the start, and says why &mdash; a
  database-only restore is not quietly "about to" restore your files.

= 1.23.0 =
* New: **the malware scan knows eighteen more kinds of backdoor.** They were
  written against real ones: the plugins compromised on wordpress.org in June
  2024 and April 2026, the two backdoored plugins found there in 2026, and the
  fake plugins that plant a hidden administrator, rewrite `wp-cron.php`, take a
  role from the request, or unpack code fetched from a server nobody published.
  Each check pairs the dangerous action with the thing that makes it
  illegitimate, so an honest plugin doing the same work is not reported.
* Fix: **eight existing checks reported honest code as malware.** A code comment
  mentioning a shell command, a full-width video frame, a compressed setting, an
  embedded font, a temporary-login plugin creating its account from variables,
  and a plugin's own test files were each being flagged. Measured against the
  250 most popular plugins and WordPress core, those checks now report nothing
  there. Your provider's hub refuses to install an update it considers infected,
  so this also unblocks updates it was wrongly refusing.

= 1.22.2 =
* Change: **nothing on your site changes in this release.** It exists because the
  plugin's description here had fallen behind what the plugin actually does, and
  the directory shows that text from the released version rather than from the
  latest one &mdash; so correcting it takes a release.
* Change: three features that shipped in 1.20.0, 1.21.0 and 1.22.0 were described
  only in this changelog and not in the description anybody reads first: the
  activity log on your own site, the scan the site starts for itself when a plugin
  nobody published appears, and what a restore now does when it cannot finish.
* Change: the security section described how requests between this site and your
  provider are signed, and had been describing the form used before 1.11.0. The
  signature has covered a single-use value per request since then, which is what
  makes a captured request useless rather than merely short-lived.
* Change: the screenshots in the directory listing were taken nine releases ago,
  before the support card had a *Request help* button or a telephone number. They
  are re-taken from this version.

= 1.22.1 =
* Fix: **backups of sites larger than 4 GB could not be completed.** Past that size the
  archive was written in a form no unpacker can read, so the check that runs on every
  finished archive rejected it and the backup was reported as failed with "the finished zip
  could not be re-opened (code 21)". Smaller sites were never affected. On a site whose
  hosting is missing PHP's `zip` extension that check cannot run, so an affected backup may
  have been stored instead of failing &mdash; those cannot be restored, and should be
  replaced with a fresh backup after updating.

= 1.22.0 =
* Improve: **a restore that cannot finish no longer leaves your site half-restored.** Each
  table is now rebuilt alongside the live one and swapped in only once it is complete, so a
  restore interrupted by a timeout, a server limit or an error leaves your database exactly
  as it was instead of partly replaced. Long restores also pick themselves back up where
  they stopped rather than starting over, and your site is put into maintenance mode only
  while the database is actually being written.
* Improve: **a restore now says when something went wrong.** Failed database statements are
  counted and reported instead of being discarded, and a restore that could not create or
  replace your tables stops immediately and explains why rather than finishing and
  reporting success. Your provider's dashboard shows the difference.
* Improve: **checks before a restore starts, so it fails before it can do damage.** Free
  disk space, the database permissions the restore needs, whether the downloaded backup
  matches what was stored, and whether the backup's tables match this site's are all
  verified first. An incomplete download is now reported as an incomplete download rather
  than as a wrong passphrase.
* Improve: **restores can optionally remove files the backup does not contain**, for
  recovering from a hack rather than from a mistake. Off unless your provider asks for it,
  and never applied to your uploads.
* Improve: **after a restore, the site is put back in working order**: caches are cleared
  (so you see the restored site and not the old one), plugins and themes the restored
  database names but that are no longer installed are deactivated rather than breaking the
  site, and a pending WordPress database upgrade is completed.
* Improve: backups and restores can no longer run at the same time. A backup started while
  a restore was in progress could previously store a copy of a half-restored site.
* Improve: web addresses are rewritten more completely on a restore &mdash; the WordPress
  address and the site's folder path as well as the home address &mdash; and the rewrite can
  no longer skip rows on large tables or be stopped by unusual stored data.
* Improve: database triggers and stored routines are now included in backups and restored
  with them.
* Fix: the number of files a restore wrote is shown again on your provider's dashboard.
* Fix: files whose names contain two dots in a row (`photo..2019.jpg`) are no longer
  skipped by a restore.

= 1.21.0 =
* Improve: **the Activity log now records the things it was missing.** Updates installed on
  this site by your provider are written down &mdash; what was updated, and anything that
  failed &mdash; and so are a file being read off this server, a change to the provider
  details shown on your dashboard, and a change to the address this site reports to.
  Between them those were the actions with the biggest effect on the site and the least
  trace of who asked for them. Nothing about how the plugin works changes; the log simply
  answers more of the questions people bring to it.

= 1.20.0 =
* Add: **a plugin that appears out of nowhere is now scanned immediately.** When a plugin
  turns up on your site that WordPress.org does not publish &mdash; a premium plugin you
  installed by hand, or something nobody installed at all &mdash; the site starts a malware
  scan itself, within seconds, and tells your provider what set it off. Fake plugins are
  how most WordPress break-ins arrive, and until now the first look at one waited for the
  next scheduled scan. Nothing is blocked, quarantined or deleted: the site looks, and your
  provider decides. Anything appearing in the *must-use plugins* folder counts too, since a
  file put there runs on every request with nothing to activate it and no way to switch it
  off.

= 1.19.0 =
* Add: **German**. Every screen this plugin adds &mdash; the settings page, the dashboard
  support card, every status message, every scan finding and every error &mdash; is
  translated, in both *Deutsch* and *Deutsch (Sie)*. Nothing to configure: WordPress uses
  your site's own language setting. The wording is formal throughout, because these
  screens are read by the person whose site it is rather than by the agency maintaining
  it.
* Add: a page trace now lists the page's resource hints &mdash; the
  `<link rel="preconnect">` and `<link rel="dns-prefetch">` tags &mdash; and says which
  plugin asked for each one. That tag is the reason a page reaches out to a third-party
  server before it has drawn anything, it is what a webfont privacy check flags, and it
  was the one thing on a traced page the trace could not name: nothing enqueues it, and
  WordPress prints it itself, so tracing the bytes only ever answered "WordPress". Hints
  added through WordPress' own filter now carry the callback that added them; hints
  printed straight into a theme template are found in the page and labelled with the file
  that printed them.
* Add: **server-configuration checks**. The `.htaccess`, `php.ini` and `.user.ini` files
  inside your WordPress folder are now read and reported to your provider, with a history
  of every time one of them changed. These are the one part of a WordPress install that
  changes what the *server* does rather than what WordPress does &mdash; nothing in the
  WordPress admin shows them, and a single line in one can outlive every other trace of a
  break-in. Flagged: a file set to run before every request, a rule that makes uploaded
  images execute as PHP, a redirect that only fires for search engines or for visitors
  arriving from one, and a `php.ini` sitting in the media library, which has no legitimate
  reason to exist.
* Note: nothing is ever changed on your site. A good share of these directives are written
  deliberately by a host or a security plugin, so the point is that you get to see them and
  decide &mdash; not that they are removed for you. A folder correctly set to refuse PHP is
  reported too, as good news rather than as a problem.
* Add: the scan's signature list can now be updated by your provider without updating this
  plugin. A new detection reaches your site within hours instead of waiting for a plugin
  release to travel a whole fleet &mdash; and a signature that turns out to fire on a
  legitimate plugin can be switched off just as quickly. A site that has never been paired,
  or whose provider is unreachable, keeps scanning with the list this plugin ships.
* Add: **email delivery is now monitored**. WordPress keeps no record of the mail a site
  sends: when a contact form stops delivering, or an order confirmation never arrives, the
  error is raised and thrown away and the site goes on looking perfectly healthy. This now
  counts what the site tries to send and what fails, keeps the reason the mail server gave,
  and reports which transport is doing it &mdash; PHP's `mail()`, an SMTP server, or the
  plugin that configured one. Your provider sees it alongside everything else.
* Note: no part of any message is recorded. Not the recipient, not the subject, not the
  body &mdash; only how many were attempted, how many failed, and why. And "this site has
  sent no email" is reported as exactly that rather than as a fault: a site with no forms
  and no shop legitimately sends nothing for months.
* Add: a malware scan can now be **cancelled** from your provider's hub. A full scan reads
  every file on the site across several scheduled runs, so on a large or slow one it can
  sit half-finished for a long time &mdash; and until now nothing could stop it, including
  the next scan, which was refused because a run was already going. Cancelling stops the
  walk on the site and lets a new scan start straight away.
* Add: the backup passphrase field now shows how strong the passphrase is as you type.
  Nothing is enforced &mdash; a backup passphrase is not a login, and nobody, including
  your provider, can recover a backup without it, so being locked out by a rule you did
  not expect would be far worse than a weak one. The meter simply says what helps:
  **length beats punctuation**, and four unrelated words reach the top of the scale where
  a short password with symbols in it does not.
* Fix: a remote update could be killed part-way through by the host's PHP time limit. A
  core update or a batch of plugin updates is downloaded, unpacked and installed inside a
  single request, and thirty seconds &mdash; the default on a great many shared hosts
  &mdash; is not long enough. The update is now taken off that clock and detached from the
  connection that asked for it, so it always runs to completion even if the hub stops
  waiting; the hub, for its part, now re-checks the site rather than reporting a slow
  update as a failed one.

= 1.18.0 =
* Add: the malware scan now recognises the backdoors that arrive dressed as an ordinary plugin, not just the classic obfuscated webshells. A plugin absent from WordPress.org has no published files to check it against, so until now one like this surfaced only as *unverifiable* &mdash; never as malware &mdash; however plainly hostile its code. The scan now flags, as critical, a plugin that forges a login as an administrator it picked for the caller, that gates a privileged action on a secret key baked into the file, that creates an administrator from hard-coded credentials, that hides accounts from the users list, or that runs request data through a dynamic function call.
* Note: each of these is matched by the thing that makes it illegitimate, not by an API a healthy plugin also uses &mdash; a login plugin that sets a session after checking a password is untouched, and WordPress' own installer, which creates the first administrator on every site, is not mistaken for a planted one. The false positives it must not raise are pinned by a test alongside the detections themselves.

= 1.17.1 =
* Fix: an untouched WordPress could be reported as having a missing or modified core file. A non-English WordPress installs the readme and licence in its own language too &mdash; `liesmich.html` on a German site, `licenza.html` on an Italian one, eight more names across the languages &mdash; and only the English `readme.html` and `license.txt` were treated as the documentation they are. So deleting them, which most hardening guides tell you to do and a good many hosts already have, was reported as a missing core file, and a copy edited to hide the version number or rewritten in transit was reported as a critical modification. Neither could be cleared: the file's own language package stopped shipping it years ago.
* Change: the release's documentation is now recognised by what it is &mdash; a readme or licence sitting in the site root &mdash; rather than by a list of the English names, so the same is true of every language and of any name a future one uses. Files inside `wp-admin` and `wp-includes` are unaffected: nothing legitimately rewrites one of those, whatever its extension, and a modified one is still a critical finding. The skipped files are still pattern-scanned for malware exactly as before.

= 1.17.0 =
* Add: **page tracing**. Your provider can now ask this site, from their hub, to render one of its own public pages and report which file produced each line of the HTML that came back &mdash; core, a plugin, the theme, a must-use plugin, a drop-in. The question it exists to answer is the one nothing in WordPress answers: *which* plugin adds the Google Fonts tag, or the tracking script, or the markup nobody can find a setting for. Styles and scripts are reported separately with the callback that enqueued each one, because those tags are printed by WordPress itself and naming WordPress helps nobody.
* Note: the trace never alters the page. Nothing is injected into the output, no marker and no comment; the attribution travels beside the HTML as byte offsets, so what is measured is exactly the page a visitor would have received. Two things it reports honestly rather than papering over: a page served straight from a page cache never reaches this plugin at all, and a plugin that captures whole sections of the page into its own output buffer collapses everything inside it to one line. Both are said out loud rather than guessed around.
* Note: a trace runs only for a request carrying a single-use token this site minted seconds earlier for one specific page of its own, and the token is spent before the page renders. Only paths on this site can be traced, no file contents are ever read or sent, and every trace is written to the activity log.
* Add: `POST /jgm/v1/trace` for the above. Additive: a hub too old to know about it never calls it, and a hub that calls it on an older release of this plugin is told the route does not exist rather than handed an empty answer.

= 1.16.0 =
* Add: the dashboard support card can show a telephone number for whoever maintains this site, alongside the email address. It is set by your provider in their hub and pushed here with the rest of the card, and the line is shown only when they have set one &mdash; a support card advertising a blank telephone number is worse than one without. On a phone it is a `tel:` link, because that is where a good share of WordPress dashboards are actually read.
* Note: additive in both directions. A hub too old to send a number leaves the card exactly as it is today, and a number that arrives on a site running an older release of this plugin is simply ignored.

= 1.15.0 =
* Add: the malware scan now counts the plugins in `wp-content/plugins` itself and compares that against the list WordPress reports. Malware that hides a plugin does it by answering the question before WordPress can &mdash; the Plugins screen, this plugin's inventory and, worst of all, its file checks then all agree the plugin is not installed, so not one of its files is ever hashed against the published original. A plugin on disk that the list leaves out, or an active plugin file that declares no plugin at all so nothing can list it, is now reported as a critical finding.
* Change: a plugin found this way is checked against WordPress.org along with the ones WordPress named, rather than being skipped for not appearing on a list it had removed itself from.
* Note: a discrepancy is only reported when WordPress itself, asked again with its cached answer thrown away, disagrees with the answer it first gave. A plugin this plugin reads a header for and WordPress does not is treated as this plugin being wrong, and is not reported.
* Add: must-use plugins are now reported. WordPress runs every `.php` file in `wp-content/mu-plugins` on every single request &mdash; nothing has to activate it, and the admin offers no way to switch one off &mdash; so dropping a file there is the tidiest way to install a backdoor. The scan now lists what is in there, which nothing in this plugin has ever done, and flags two cases outright: a file WordPress runs but leaves off its own must-use list (WordPress hides `index.php` from that list purely for being under 30 bytes, and `&lt;?php include"x";` is eighteen), and a file named to stay out of a directory listing, which WordPress runs anyway.
* Add: the scan also reports when the Plugins screen is not showing an administrator the whole truth &mdash; a plugin filtered out of the list, or a whole tab of it hidden. Both are ordinary WordPress features that white-label plugins and managed hosts use on purpose, so both are reported as information rather than as a problem, naming whatever is being concealed so you can decide which it is.

= 1.14.0 =
* Add: a **Request help** button on the dashboard support card, next to Contact support. It asks for confirmation, then tells everyone on your provider's team &mdash; through the hub, already attached to this site &mdash; that this site needs attention, with an optional note. The mailto: link reaches one person's inbox and arrives without saying which site it is about; this reaches the whole team and does. Hidden on a site that is not connected to a hub.
* Add: a **leftover-file check**. The hub can now ask this site for the artifacts an interrupted backup, a finished restore or a migration plugin left behind &mdash; a `wp-config.php.bak` the web server hands out as plain text, a `database.sql` in the web root, a Duplicator `installer.php`, another backup plugin's local archive folder, an unfinished working folder holding an unencrypted copy of the site &mdash; and delete the ones you pick. Only paths this site itself reported are ever deletable, and WordPress' own files never are.
* Add: `POST /jgm/v1/residue` for the above, and `POST /api/support/request` in the reverse direction for the help button. Both are additive: an older hub never calls either.
* Add: your provider can now switch off either button on the support card, or the card itself, from their hub. Turning off **Request help** closes the route as well as hiding the button, so nothing can be submitted through it. A hub too old to have an opinion leaves all three showing, exactly as before &mdash; the card is only ever removed because somebody chose to remove it. When it is, the Bulwarden screen says so, so the setting does not read as a bug.

= 1.13.7 =
* No change to the plugin. Like 1.13.6 it exists for the release itself: publishing to the plugin directory failed on the very first attempt, in the step that tells the directory which files a version has dropped &mdash; there was no previous version for this one to have dropped any, and the step could not cope with having nothing to do.

= 1.13.6 =
* No change to the plugin itself &mdash; it is 1.13.5 under a new number. The screenshots on the plugin directory name the version they were taken from, so publishing them alongside a matching release is what keeps the listing and the download describing the same thing.

= 1.13.5 =
* Fix: opening a running backup's log squeezed it into the half of the panel beside the Cancel button, leaving the button stranded in the middle of a column of log lines. The log now gets the full width of the panel.
* Fix: the progress bar for the encryption and upload phases sat empty at 0% for a few seconds after each page load, before the first update reached it &mdash; directly under a line already saying how much had been transferred. It now appears only once there is progress to draw.
* Change: the encryption passphrase field no longer stretches the whole width of the panel.

= 1.13.4 =
* Change: the Bulwarden screen has been rebuilt around the question it never answered &mdash; is this site actually protected right now. It opens with the connection, the encryption passphrase, the last backup and the last scan, each with its own state, and a site that has not been set up yet is walked through the three steps that finish it instead of being shown a disabled button and told to look elsewhere.
* Add: a running backup now shows which phase it is in &mdash; database, files, encryption, upload &mdash; and its run log, which the site has been recording all along and never displayed. A failed or cancelled run stays on screen until something replaces it.
* Add: the last scan is summarised with what it actually found, grouped by kind, and says plainly how much of the site could be checked against published originals and how much publishes none.
* Add: a panel for what this server can do &mdash; PHP, the encryption extension, the archive backend, and whether WP-Cron is running &mdash; because a backup that never starts on one site is otherwise invisible from the admin.
* Add: your provider's support details, as pushed by the hub, are now visible on the screen rather than only on the dashboard.
* Add: the connect string and API key can be copied with a button, and the secret is masked until you ask to see it.
* Fix: the progress bar for the encryption and upload phases never appeared, so a long upload looked identical to a stalled one.
* Fix: the button that removes a backup exclusion did nothing when the click landed on its icon.

= 1.13.3 =
* Change: Bulwarden now has its own entry in the WordPress admin menu instead of sitting under Settings. It is a screen you come back to &mdash; to pair the site, set the backup passphrase, start a backup or read the activity log &mdash; not something configured once and forgotten.
* Add: a **Settings** link on the plugin's row on the Plugins screen, so the way to it is where you look right after activating.

= 1.13.2 =
* Fix: an untouched WordPress could be reported as having a modified core file. WordPress publishes a separate package per language, and they differ in exactly one file it checks: `wp-includes/version.php`. The package a site installed is not the language it is displayed in &mdash; switching the admin language installs translation files and rewrites no core file at all &mdash; so a site whose two disagree (an English install later switched to German, or a German one switched to "Deutsch (Sie)") had that one file compared against a package it never installed. It was reported as a critical modification on every scan, and nothing an operator could do would clear it. The package on disk now decides which checksums are fetched, and if WordPress.org publishes none for it, that one file is left unchecked rather than judged against another package's.
* Fix: core findings now tell the hub which package they were measured against, so the diff view can say why WordPress.org's copy of `version.php` differs from a localized site's rather than presenting it as an unexplained mismatch.

= 1.13.1 =
* Fix: some untouched plugins were reported as modified. WordPress.org publishes more than one accepted copy of a file whenever a release's repository copy and its downloadable zip drift apart &mdash; both are genuine, and a site can legitimately have installed either. Every published copy is now accepted, so a file matching any of them is clean. Affected files were reported as critical findings on plugins nothing had touched.
* Fix: findings on those files now tell the hub every hash the file was measured against, so it can show you the diff instead of reporting that WordPress.org has no copy of the file.

= 1.13.0 =
* New: your hub can now see &mdash; and finish &mdash; an update that changed this site's files but never completed. The two that strand a site are a pending WordPress database upgrade (the "Database Update Required" screen every administrator is sent to, which the public site hides completely) and a leftover `.maintenance` lock from an update that died half-way. Both are reported to the hub, and both can be finished from it: the repair runs exactly what WordPress itself would have run, and nothing else.
* New: a plugin waiting on its own database migration (WooCommerce, most commonly) is reported too. It is reported only &mdash; that migration belongs to its author and runs from their own screen.
* New: a restore can now put back **only the database**, leaving every file on disk untouched. A bad content edit or a plugin that mangled its own tables is a database problem, and restoring the files alongside it would also undo every plugin, theme and core update made since the backup &mdash; including the security ones. Restores made by an older hub are unaffected and still restore everything.
* Add: the health report now includes this plugin's own version, so your hub can tell which sites are new enough for a feature before offering it rather than after failing.

= 1.12.0 =
* New: when a file no longer matches the version published on WordPress.org, your hub can now show you the actual diff — the published file beside the one on your site, with the changed lines marked. A checksum mismatch on its own can't tell a caching plugin rewriting an asset apart from a backdoor appended to a core file; this is what tells them apart. Findings now carry the release they were compared against and the hash they should have had, so the hub can prove the copy it fetched really is the published one.
* New: `POST /jgm/v1/file` returns the current contents of a file, and **only** of a file the site's own last scan flagged. It is not a general file read: anything not named by a finding is refused, as are wp-config.php, symlinks, paths outside the site, and files over 1 MB.

= 1.11.1 =
* Fix: the malware scan called a site infected over files that cannot run. A stylesheet, image or font inside a plugin or theme that no longer matches its published release is now reported as a warning rather than as a critical finding — the usual author of a rewritten `style.min.css` is a caching or optimisation plugin, not an attacker, and the file cannot execute either way. It is still checked, still reported, and still scanned for signatures; code files (PHP, JavaScript, HTML, SVG) and WordPress core are unaffected.
* Change: findings now say what was actually observed — "no longer matches the version published on WordPress.org" — instead of implying a verdict. A checksum mismatch means a file changed after it was installed, which is a fact about the file, not a diagnosis of the site.

= 1.11.0 =
* Fix: sites intermittently reported themselves as unreachable to their hub, clearing again on the next check. Every signed request now carries a single-use nonce, so two legitimate requests that arrive in the same second are no longer mistaken for one being replayed and refused.
* **This release must be installed alongside the matching hub update** — the two sign requests to each other, and the signing form changed on both sides at once.

= 1.10.4 =
* Fix: the malware scan flagged a plugin's readme file as "does not match the released version". wordpress.org generates a plugin's checksums once, at release, but authors keep editing the tagged readme afterwards (a "Tested up to" bump, a changelog typo), so this fired on most healthy sites. Readme, changelog and licence files inside a plugin or theme are now reported as informational instead of as a modified plugin — they are still checked, and code files are unaffected.
* Fix: when file-integrity findings from several plugins were listed together, the section was titled after whichever plugin happened to sort first, so it named one plugin above a list that included others. Findings are now titled by what they are, with the affected plugin shown on each file.

= 1.10.3 =
* Fix: a missing or broken component at boot (e.g. an interrupted plugin update that leaves the site with a mix of old and new files) no longer takes down every page on the site. Each boot component now initializes in isolation, the same way a single failing Site Health test already can't break the rest of the health report.

= 1.10.2 =
* Fix: the uploads-folder hidden-PHP check still had false positives after 1.10.1 on images whose embedded color profile or gamma curve happened to be readable-looking binary (a narrow, slowly-drifting run of bytes that lands in printable ASCII without being text — and, being copied verbatim into every thumbnail WordPress generates, showed up identically across a whole set of derivative image sizes). It now also requires the readable run to contain actual letters, which genuine injected PHP always has (a function name, a keyword, at least a superglobal like `$_GET`) and this kind of structured binary noise essentially never does.

= 1.10.1 =
* Fix: the malware scan's uploads-folder check for hidden PHP could flag a legitimate image as a false positive — JPEG/PNG data is effectively random binary, and a large enough image has a real chance of containing a bare `<?=` by pure coincidence. It now also requires readable text to follow the tag, which genuine injected PHP always has but random image data essentially never does.

= 1.10.0 =
* Add: the malware scan's file-integrity check now also covers your **active theme**, the same byte-for-byte verification core files and wordpress.org-hosted plugins already get. A custom or premium theme with no published release to compare against is reported as unverifiable rather than treated as a finding, exactly like a premium plugin.
* Fix: the malware scan's file-integrity check could wrongly report `wp-includes/version.php` as modified on a completely untouched WordPress install, on hosts where PHP's own cached view of that file can briefly disagree with what's actually on disk (e.g. after a core auto-update completes). The check now reads the version straight off disk instead of trusting that cache, and any other checksum mismatch is double-checked before being reported, in case it was caught mid-write.

= 1.9.0 =
* Remove: **white-labelling is gone.** The plugin always appears as "Bulwarden" with its real author in the plugins list, and can no longer be renamed, re-attributed or hidden from users who don't administer the site. Your provider's name, logo and support address still appear on the dashboard support card — that part is unchanged.
* Change: all of this plugin's stored data moved to a `bulw_` prefix, as the Plugin Directory guidelines require. **There is no automatic migration**: a site updated from an earlier release finds no stored settings, and has to be re-paired with its hub (Settings → Bulwarden → Regenerate credentials, then paste the new connect string into the hub). Note the backup passphrase has to be re-entered too, and existing backups still need the *old* passphrase to decrypt — keep it before you change it.
* Change: the settings screen's styles and scripts are now loaded as proper enqueued files instead of being written inline into the page, and two unnecessary loads of WordPress core files were removed.
* Add: readme now documents every external service the plugin contacts, and what is sent to each.

= 1.8.0 =
* Add: when your provider restores a backup to this site, it can now choose whether to also rewrite every URL/domain found in the database to this site's own — on by default (unchanged), with the option to leave the restored content exactly as backed up, which matters when a backup is restored onto a different domain than the one it came from.

= 1.6.0 =
* Add: every backup keeps a detailed **run log** — each phase with timings, per-table row counts, anything skipped or pruned and why, and the exact operation that failed. It shows live on the Settings screen while a backup runs, travels to your provider with every finished backup, and is attached to failure reports, so a failed backup can be diagnosed remotely without server access.
* Add: **chunked, resumable uploads**. Large backups are now sent in small, individually retried pieces that resume mid-file after a network error, instead of one huge request that a proxy body-size limit or dropped connection could doom outright. Falls back automatically when the hub doesn't support it yet. The old hard one-hour upload limit is gone — a genuinely stalled transfer still aborts within about 2 minutes, but a slow, healthy one is never cut off again.
* Add: **end-to-end integrity checks**. The finished archive is verified on disk before it is encrypted (exact size, plus a full read-back of the archive index), and a SHA-256 of the encrypted blob travels with the upload so the hub can prove it stored exactly the bytes this site produced.
* Add: a **disk-space preflight** refuses to start a backup that clearly cannot fit (based on the previous backup's size), failing in seconds with a clear message instead of after an hour of work — or filling the disk under the live site.
* Add: a new streaming archive writer builds the backup in a single pass with no rewrites — dramatically less disk I/O on large sites (a 400 MB test tree dropped from ~24 GB of rewrite traffic to zero) — and already-compressed media (images, video, archives) is stored without recompression, cutting CPU time on media-heavy sites. Requires only zlib; ZipArchive remains as a fallback.
* Fix: several ways a database dump could silently come out incomplete are now hard failures instead — a dropped database connection mid-dump no longer masquerades as "table finished", and a dump missing the options table is refused outright. Dumps are also taken as a single point-in-time snapshot, binary data is written in a corruption-proof form, database views are handled as views (previously their rows were duplicated as table data), and large rows can no longer exhaust memory.
* Fix: an unreadable directory anywhere in the site (a root-owned folder, a mounted volume's lost+found) no longer aborts the entire backup — it is skipped, recorded, and reported.
* Fix: sites whose plugins or themes are installed as symlinks now get an explicit record that those directories were not archived, instead of silently missing data.
* Fix: a backup run killed by the host no longer leaves its temporary working directory — including an unencrypted copy of the site — behind forever; leftovers are cleaned up at the start of the next run.
* Fix: a restored backup no longer carries the "backup in progress" state of the run that produced it, which could block new backups on the restored site for up to an hour.

= 1.4.1 =
* Fix: two `phpcs:ignore` comments in the 1.4.0 working-directory cleanup referenced sniff codes that don't exist, so they didn't actually suppress anything — the WordPress Plugin Check tool correctly flagged direct `rmdir()`/`scandir()` use. No functional change.

= 1.4.0 =
* Add: a "Cancel backup" button on the Settings screen, shown while a backup is running. It works even if the backup process is wedged, not just when it's responsive: it cleans up the working directory and notifies your provider immediately, rather than waiting for the process to notice.
* Fix: a symlinked directory anywhere in the site (pointing at an ancestor, another site sharing storage, or a loop between two directories) could send the archiving step into unbounded recursion with no error — indistinguishable from a hang. Symlinked directories are no longer followed.
* Fix: a stray Unix socket or FIFO under the site (e.g. a cache/queue backend's socket file) could make the archiver hang indefinitely trying to read it. Only regular files are ever archived now.
* Fix: an upload that stalls completely (a proxy or load balancer silently dropping an idle transfer) now fails within about 2 minutes with a clear error, instead of hanging for up to an hour.
* Fix: every backup's temporary working directory is now fully removed when it finishes — previously an empty folder was left behind after every single run, successful or not.

= 1.3.0 =
* Add: live backup status — the Settings screen now shows which step a running backup is on (dumping the database, archiving, encrypting, uploading), its size, and upload speed, refreshed automatically while it runs. Your provider can now also query this status directly, so a backup that crashes without reporting back no longer leaves its record stuck at "running" forever.
* Add: only one backup can run at a time; starting a second one while another is in progress is refused with a clear reason instead of the two colliding.

= 1.2.1 =
* Fix: a phpcs:ignore comment for the backup-exclusions form field no longer covered the line it was meant to (Settings.php), and the repository now ships a `.gitattributes` pinning source files to LF line endings — no functional change.

= 1.2.0 =
* Add: backup exclusions are now a plain editable list on the Settings screen (add/remove a directory row) instead of a freeform textarea, pre-filled with every exclusion currently in effect — including the defaults, which used to be invisible and hardcoded.
* Add: new default exclusions for the folders the most common alternative backup plugins (UpdraftPlus, All-in-One WP Migration, Duplicator, BackupBuddy, Total Upkeep, WP Clone) use for their own local archives, so they no longer bloat every backup by default.
* Add: a `.git` directory is now excluded wherever it occurs in the site tree (not just at the site root), and is pruned from the walk entirely rather than iterated and discarded — much faster on a site with a large repository history.
* Fix: sites that already had a custom backup-exclusions list keep the protection that used to be hardcoded (the cache/upgrade exclusion) via a one-time migration, so upgrading can't silently shrink what gets excluded.

= 1.1.1 =
* Fix: the plugin's own pre-branding default (Settings menu label, admin plugins-list name) is now `Bulwarden` instead of the generic `Site Maintenance` placeholder left over from before the rebrand — matches the hub's own agency branding default, which already shipped as `Bulwarden`. An agency's white-label branding still overrides it after a site is paired.

= 1.1.0 =
* Add: the malware scan's signature walk (Layer B) now skips any file already proven byte-identical to its published release by the checksum verifier (Layer A) — a pattern match inside code the vendor actually shipped isn't evidence of compromise on this site, so it no longer gets reported.
* Fix: the scan no longer flags its own installation folder. It isn't hosted on wordpress.org so it was never checksum-verified, and its own pattern documentation quotes the very strings the signature scanner looks for — which the scanner was matching against itself.

= 1.0.2 =
* Fix: text domain corrected to `bulwarden` (header and every translatable string). The 1.0.0 revert to `jg-maintenance` was itself a mistake: it matched this plugin's source-repo folder name rather than its actual installed slug, which has shipped as `bulwarden` since 0.3.2.
* Fix: the restore process's self-protection check (skip this plugin's own folder mid-restore, so it can't overwrite the code that's running it) was still checking `wp-content/plugins/jg-maintenance` and so silently failed to protect the real `wp-content/plugins/bulwarden` install.

= 1.0.1 =
* Fix: resolved the remaining WordPress Plugin Check findings — the header/translatable-string text domain regression from 1.0.0 (see below), a stale `Tested up to`, and several false-positive warnings (direct file I/O in the backup/restore subsystem, direct `$wpdb` calls, `set_time_limit()` in background jobs, and core-filter lookups) now carry documented justification so they no longer flag.

= 1.0.0 =
* Fix: text domain reverted to `jg-maintenance` everywhere (header and every translatable string). It must match the plugin's slug/folder name, not the `Bulwarden` brand — the earlier switch to `bulwarden` broke WordPress.org's text-domain convention despite matching the new name.
* Add: a Privacy section documenting exactly what data is sent to a paired hub, and when.
* Harden: encrypted backup uploads now honor a site's configured HTTP proxy and verify TLS certificates the way the WordPress HTTP API does, rather than relying on raw curl defaults.

= 0.3.3 =
* No plugin changes. Version bump only, to re-cut a release after a suite CI fix (release:maintenance/deploy:plugin were still looking up the pre-rebrand plugin slug).

= 0.3.2 =
* Fix: main plugin file renamed to bulwarden.php to match the Bulwarden rebrand (the CI build looks up the file by plugin slug).

= 0.3.1 =
* Rebrand: the plugin is now Bulwarden (user-facing strings, plugin header, and default branding).

= 0.3.0 =
* Rebrand: the plugin is now Bulwark, the site agent for Bulwarden (user-facing strings and plugin header).

= 0.2.3 =
* Backups: the archiver now tolerates files that are deleted or replaced mid-backup (e.g. another backup plugin rotating its own files) — the affected files are skipped and logged instead of aborting the whole backup.

= 0.2.2 =
* Backups: admin-configurable exclusion paths (Settings → JG Maintenance → Backup exclusions) so directories the plugin can't know about on its own — e.g. a legacy backup plugin's folder — can be left out of the archive instead of bloating every future backup.

= 0.1.0 =
* Initial scaffold: client agent with signed REST API, health + security collectors, remote updates, white-label settings. (Hub is a separate webapp.)
