Bodholdt Backup for OneDrive
WordPress backups to Microsoft OneDrive
Freeforever
One edition, free forever, on as many sites as you like. Full and incremental backups on a schedule, full and selective restore, multisite, an email after every run, and up to 500 backups kept. No license key, no account, and updates arrive in your dashboard.
- Streaming restore that stays FK-safe and BLOB-safe
- Full and incremental backups, scheduled or on demand
- Microsoft Graph API and Azure app integration
- Complete multisite-network backup
- Selective restore of the database, plugins, themes, uploads, and core, free on every install
- Resumable chunked upload with auto-retry
- Guided setup wizard with an OAuth walkthrough
- An email after every backup, success or failure, to whichever address you choose
- A warning email before your Azure client secret expires, once you paste the expiry date Azure shows you into Settings
- Updates in your WordPress dashboard, with no license key to enter
- The Long Keep arcade, with a protection shield and a clean-backup streak that rewards every scheduled run, free and off until you switch it on
No account and no email address needed. Updates arrive in your WordPress dashboard.
Get the same news in Slack or Discord
This plugin already emails you after every backup attempt, success or failure. Bodholdt Backup Pro is a separate paid companion that puts those same events where your team already works, and adds a scheduled summary of how the whole site is doing.
- Five backup events posted to your own Slack or Discord webhook, each with its own toggle
- A backup-health digest by email, webhook, or both, weekly or daily
- One license covers both clouds, and it works with the free editions of both
This plugin stays free either way, and nothing in it is locked behind the companion.
Real screens from Bodholdt Backup for OneDrive
These are real admin screens, not mockups. Click any one to view it full size.
The same backup engine, pointed at Microsoft OneDrive. Same admin UI, same security hardening, same selective restore. Different cloud.
Streaming restore that stays FK-safe and BLOB-safe
Full and incremental backups, scheduled or on demand
Microsoft Graph API and Azure app integration
Complete multisite-network backup
Selective restore of the database, plugins, themes, uploads, and core, free on every install
Resumable chunked upload with auto-retry
Guided setup wizard with an OAuth walkthrough
An email after every backup, success or failure, to whichever address you choose
A warning email before your Azure client secret expires, once you paste the expiry date Azure shows you into Settings
Updates in your WordPress dashboard, with no license key to enter
The Long Keep arcade, with a protection shield and a clean-backup streak that rewards every scheduled run, free and off until you switch it on
- Streaming restore that stays FK-safe and BLOB-safe
- Full and incremental backups, scheduled or on demand
- Microsoft Graph API and Azure app integration
- Complete multisite-network backup
- Selective restore of the database, plugins, themes, uploads, and core, free on every install
- Resumable chunked upload with auto-retry
- Guided setup wizard with an OAuth walkthrough
- An email after every backup, success or failure, to whichever address you choose
- A warning email before your Azure client secret expires, once you paste the expiry date Azure shows you into Settings
- Updates in your WordPress dashboard, with no license key to enter
- The Long Keep arcade, with a protection shield and a clean-backup streak that rewards every scheduled run, free and off until you switch it on
- Deleting the plugin could interrupt a running backup or restore on hosts that set WP_TEMP_DIR. The plugin writes its staging folders and its operation locks under WordPress's own temporary directory, which honours that constant. The uninstaller looked under the system temporary directory instead. Where the two differ, the busy check that is supposed to stop Delete while a backup or restore is in flight looked in the wrong place, found no lock, and reported the plugin idle. On the same hosts, staging holding a full database export was left behind after Delete. Uninstall now resolves the directory exactly as the plugin does, a regression test proves the two can never part again, and cleanup also sweeps the old location so anything already orphaned is collected.
- Core directory locations. The wp-includes folder is now read through WordPress's own WPINC constant, the plugins and must-use plugins folders through WP_PLUGIN_DIR and WPMU_PLUGIN_DIR, and the plugin's own folder through plugin_dir_path(). Twenty-nine defensive checks for constants WordPress always defines before a plugin loads have been removed, along with the hardcoded folder names they fell back to. What a backup reads and the names it records inside the archive are unchanged, and existing backups are unaffected.
- The readme fix in 5.50.1 landed in the wrong file. These plugins ship one edition, the free one, and its readme is built from readme-wporg.txt. That file still carried every sentence the 5.50.1 entry said had gone: a verify button on every backup row, a one-click full restore, and log status pills reading success, error and running. So the note describing the correction shipped without the correction. The Screenshots section you are reading now matches what the plugin actually shows.
- The Screenshots list in this readme described a few screens that are no longer here. The Logs page has counted chips for complete, error and warning rather than a filter and sort pair, and there was never a "running" or "success" status in the log itself. The backup list shows verify only where an archive can actually be checked. And the restore section now says the two things that matter most: you choose exactly what comes back, and there is no undo. Nothing about the plugin changed in this release. It only stops describing itself wrongly.
- The check that asks your own site whether it came back up is now careful about who it will trust. After a restore, and again before your first backup, this plugin asks your own site for one page and reads nothing but the answer code. That request goes to the very address you are being served from, so it has always accepted that address's certificate without checking it, which is what lets the check work on a staging site with a self signed certificate. The list of addresses it asks for can be redirected by a filter, and until now anything the filter supplied was accepted the same way. From this release only the address you are being served from right now is accepted unchecked. Every other address is verified exactly like every other request this plugin makes.
- This still cannot change the outcome of a restore. It never rolls anything back and it never turns a finished restore into a failed one. When it cannot get an answer, for any reason at all, it says nothing rather than guessing. A certificate your server cannot verify is now one of those reasons.
- One thing to know if you use that filter. If you point the check at an address over https whose certificate does not validate on your server, you will get no verdict instead of a wrong one. Point it at an http address, or at a name the certificate is valid for.
- Held in place by tests. All four answers this check can give, healthy, server error, maintenance and no answer, are now pinned for an address on your own site and for one the filter supplied, so this cannot quietly drift back.
- The list of requests that never leave your own site was one short. That section said it was complete and named two: cron starting a backup, and the check after a restore. There is a third. Before your first backup, and again each time you start a backup by hand, the plugin asks your own admin screen whether your site can answer itself, which is how the setup check knows to warn you that backups may not start on their own. It has worked that way since 5.46.10, it never leaves your server, and nothing about it has changed. It is now described alongside the other two.
- The wording no longer depends on counting. "Two more requests" is a promise that has to be rechecked by hand every time anything is added, which is exactly how it went out of date. The Privacy section and the External services section now describe every request this plugin makes to your own site, which stays true whatever the number is.
- The note for 5.49.4 counted them too, and no longer does.
- Waking cron is described more accurately. It happens when you start a restore by hand as well as a backup.
- Nothing in the plugin changed. This release is readme wording only.
- The privacy wording is now generated from one description of what this copy sends, and where. The Privacy section, the External services section and the FAQ answer about the log table were each written at a different time and had drifted apart from one another. All three now come from the same statement, so a claim cannot be true in one of them and false in another three screens down. Nothing in the plugin changed, and nothing about what it sends changed.
- The answer about the log table was wrong, and is now right. It said none of the backup history rows are ever sent anywhere. They are never sent to us and never to anyone else, but a database backup exports every table in your database, so those rows do travel inside `database.sql` to your own OneDrive like every other table. The same answer now also lists the four arcade tables this copy creates, which it had never mentioned at all.
- Two sentences in the note for 5.49.3 were wrong about themselves, and are gone. It said the External services section had described the update check in full since 5.49.0. That section did not say what happens when you accept an update until 5.49.3, so it had not described it in full. It also gave a cadence of about twice a day without saying that a failed check is retried far more often than that.
- The update check is described honestly. Twelve hours is the cache on a good answer, so about twice a day; a failure is cached for one hour, so while our server is unreachable the check can ask up to once an hour. "You can switch it off" meant a one line PHP filter and not a setting, and the section now says so. It also now says what happens when you accept an update, and that we count the download.
- The 5.21.0 note below no longer calls the Microsoft calls this plugin's only outbound requests. Some requests go to your own site and were never counted: cron starting a backup, and the check after a restore that asks your own site whether it came back up. Both are now described under External services.
- One more thing that was never written down now is. The requests this plugin makes to Microsoft carry the user agent WordPress itself sets, which names your WordPress version and your site address, so Microsoft sees your address on every backup call.
- The install step no longer offers a plugin directory listing that does not exist. It tells you to install the file you downloaded from bodholdtlabs.com and keep it, because this is the copy that offers you new versions in your dashboard.
- Two sentences in these notes were wrong about this copy, and are now right. Nothing in the plugin changed. Under 5.43.2 and 5.21.0 below, this changelog said this copy reports nothing back to us, and that nothing about your site is ever sent to us. Neither was true. This copy was downloaded from bodholdtlabs.com, so it asks one address of ours whether a newer version exists, and that request carries the version it is running and this site's address in its user agent. Both entries now say so, and the External services section above describes the check. What the plugin sends is unchanged in this release. There is still no telemetry, no analytics and no tracking, and no site content or backup data ever reaches us.
- A correction to the release notes, not to the plugin. The note for 5.49.1 was written about the WordPress.org copy of this plugin, and it was wrong about the copy you are holding. It said nothing here had changed, and that this copy updates from WordPress.org. Neither was ever true of it. The corrected note is below, and nothing in the plugin itself changed here.
- This copy now says in its own header where it updates from. You downloaded this plugin from bodholdtlabs.com, and it installs into the same folder name a WordPress.org listing would use. WordPress matches an installed plugin to WordPress.org by that folder name, so if these plugins are ever listed there, WordPress could have replaced this copy with the WordPress.org build without anybody asking, and that build carries no update channel of its own. This copy now states in its own header that it updates from bodholdtlabs.com, which is WordPress's own way of saying leave this one where it is. It goes on updating from bodholdtlabs.com, the way it has since 5.49.0, and the check it makes is described under External services above.
- The card advertising a paid edition is gone. A small card sat at the top of every screen of this plugin, pointing at a paid edition of it. That edition has been retired, so the card was advertising something nobody can buy. It has been removed and nothing replaced it: there is now no upsell of any kind anywhere in this plugin.
- The description now says what is actually true. One edition, free, no tiers, nothing switched off: full and incremental backups, scheduling, retention, full restore, selective restore, multisite, unlimited sites, and email notifications after every backup whether it succeeded or failed. Nothing on that list will ever move behind a payment.
- A backup that cannot reach OneDrive now ends as a failure, and tells you. If your Microsoft connection had expired or been withdrawn, the backup got as far as signing in and then simply stopped. Nothing was recorded as a failure, no "Backup Failed" email arrived, and the dashboard was left saying "Authenticating with OneDrive..." at 10 per cent until that status aged out an hour later. The run now ends the way every other failure ends, so the log entry, the email, and anything else listening all happen once, and the message names the expired connection and the control that fixes it. A connection you removed on purpose, and a site that has never connected, stay quiet exactly as before, and the dashboard no longer claims a backup is running after one of those.
- A failed backup now carries a one line diagnosis with it. The plugin already works out a short cure for the failures it recognises, such as "Re-authenticate OneDrive in Settings.", and until now only the paid Slack and Discord alerts could see it. That sentence now travels with the failure itself, so anything listening for a failed backup can lead with the cure instead of the raw error text. When there is no honest short answer, such as a brief outage at Microsoft's end, the sentence is deliberately left empty and the full message is always there behind it. What the paid alerts send is unchanged.
- Free build packaging cleanup. No functional changes in either edition.
- Developers can now listen for what the plugin does. The backup engine announces five moments as ordinary WordPress actions: `bodholdt_od_backup_success`, `bodholdt_od_backup_failure`, `bodholdt_od_restore_initiated`, `bodholdt_od_watchdog_stale`, and `bodholdt_od_preflight_blocked`. Each one hands over a single array of plain facts, such as the archive name and size. A sixth action, `bodholdt_od_settings_sections`, lets other code add a section to the end of the Settings screen. If you do not write code against WordPress, nothing changes for you.
- Tidier packaging for the free edition. The free download no longer carries fragments of paid-only code it could never run. This is housekeeping in how the package is put together. No feature was added or removed in either edition.
- Clearer notes for translators. Three of the delete confirmation messages now carry a note explaining what their placeholders stand for, so a translator can see that one is a count of backups and another is the word you have to type. Nothing about how the plugin works has changed.
- The setup wizard now checks what you pasted, and says what looks wrong. In Azure a client secret has both a Secret ID and a Value, they sit side by side, and only the Value works. Pasting the Secret ID is the easiest mistake to make, and it is now recognised on sight and explained, including the awkward part: the Value is shown once, so if you have navigated away you need a new secret. It never stops you saving.
Show the full release history (82 older releases)
- Plainer writing. A few messages used long dashes where a full stop or a separator reads more easily. Nothing functional changed.
- A correction to the setup check added in 5.46.10. It said a private staging folder was ready after looking at only one of the two places a backup has to be able to write. A backup also needs somewhere to put a short-lived lock file, in your system temp folder, and on a host where that is not available the check reported everything was fine while every backup, restore, and settings change quietly refused to start. It now answers about both, so a site where nothing can run is told so.
- You can now delete a backup from OneDrive without leaving WordPress. The plugin could create backups but never remove one, so clearing out a test archive or an old one meant opening OneDrive and hunting for it yourself. Every backup in Your Backups now has a Delete button. Before anything happens, the plugin tells you exactly what would go, including any newer incremental backups that were built on top of the one you picked, because those cannot be used without it. You then type the word delete to confirm. Deleting is refused while a backup, restore, or settings change is running, and if the backup changed in OneDrive since the list loaded, the plugin stops and asks you to refresh instead of removing something you did not mean to.
- Two corrections to the plugin description. The Screenshots section called the backup list sortable, which it has never been, and described the buttons on each row inaccurately. It now says what is actually there.
- A new install now points you at setup instead of leaving you to find it. Activating the plugin added a menu item near the bottom of the sidebar and said nothing else. The bolded "Set up" link on the Plugins list helped anyone who happened to look at that row, but it is on one screen and it disappears the moment you click anything, and bulk activation and WP-CLI never really show it at all. The next admin screen you open now offers one clear door into the setup wizard, once. It never appears if you have already connected your account or finished with the wizard, it is not shown on the plugin's own screens where the wizard opens by itself, and dismissing it retires it for good.
- Your first backup now tells you when it cannot start. WordPress runs backups on a scheduled task, and on some hosts that scheduler is switched off or blocked. When that happened, the backup screen showed a spinner and the word Initializing for a full hour before giving up, with nothing to explain why. Until your first backup succeeds, the dashboard now runs a short setup check and says in plain language whether your site can reach itself and whether there is a private folder to build the backup in. If a backup you started has not moved after ninety seconds, the screen stops spinning and tells you what to look at. A check we cannot complete is reported as exactly that, because on a hardened host an unanswerable test is normal and does not mean anything is wrong.
- The staging folder warning is quicker. The check behind it touches your file system and only changes when your host changes, so the answer is now reused for five minutes instead of being recalculated on every admin page you open.
- The plugin now warns you before your Microsoft app secret expires. Microsoft caps that secret at two years and offers no permanent option, and when it lapses backups stop. Until now the only thing standing between you and that was a line of setup text asking you to set your own calendar reminder, so the failure arrived silently and you usually found out at the worst possible moment. Record the expiry date Azure shows you when you create the secret, in the setup wizard or on the Settings tab, and the dashboard counts down while the plugin emails you at thirty days, at seven, and once if it lapses. The date is optional, and leaving it blank behaves exactly as before.
- The setup wizard now tells you the truth about how long connecting OneDrive takes. It promised no technical experience and a three-minute setup, then asked you to register an app with Microsoft, set a redirect URI, create a client secret and grant Graph permissions. The wizard and the install guide now say to plan on about 15 minutes to start, and they say plainly that Microsoft expires the secret you create within two years, so there is one short renewal to do later. Nothing about the setup itself changed.
- Packaging only, no functional change. This release exists so the code we build from and the package you receive are byte for byte the same. 5.46.5 shipped moments earlier with one comment line different in a shared file. Nothing you can see or use has changed.
- A backup stopped by a missing PHP extension now says exactly why, and what to do. On a server without the PHP intl extension, a site with even one accented or non-Latin filename cannot be backed up safely, and the plugin rightly stops rather than write an archive it could not restore there. But the message named neither the file nor a remedy. It now names the consequence, the cause, the offending file, and the fix: ask your hosting provider to enable the PHP intl extension. The same clear sentence reaches the activity log, the dashboard status, the notification email, and the webhook.
- A dashboard notice that clears itself. After such a run, wp-admin shows a warning naming the file involved. The notice re-checks the extension live and clears on its own once intl is enabled, and uninstalling the plugin removes its stored evidence.
- Nothing about what is backed up or restored changes. Sites without accented filenames, and servers with intl enabled, see no difference at all.
- Restore works again. Please update before you need a restore. Since 5.44.0 every backup has included a small health report, signed into the archive like everything else. The safety check that runs before a restore verifies that every signed file in the archive is listed in the backup's own inventory, and it did not know to expect the health report, so it refused every restore of a recent backup with "An authenticated archive member is absent from or disagrees with the signed exact-file inventory." The check now recognizes the plugin's own report. Nothing was ever lost: the refusal happened before any data was touched, every archive you have is intact, and all of them restore correctly once this update is installed.
- Verify & Trust accepts recent backups again. Adopting a backup on a fresh site ran the same check plus a second one with the same blind spot, so it refused too. Both now recognize the health report.
- Backups themselves were never affected. Only the checks that read the archive back were wrong, and only for backups made on 5.44.0 or newer.
- American spellings throughout. The plugin and this changelog used British spellings in places (licence, colour, behaviour, cancelled). They now match the rest of Bodholdt Labs. Wording only, nothing behaves differently.
- Housekeeping only. Nothing in this edition changed. The paid edition corrected a changelog line in this release, and both editions are versioned together so they never drift apart.
- Incremental backups work in this edition. Version 5.45.0 briefly removed them from this build. That change is reversed: full and incremental both run here, and nothing in this plugin is switched off.
- Superseded by 5.45.1, which restores incremental backups to this edition.
- Setup guide fix: the staging directory example no longer breaks backups. The FAQ told you to pin the staging directory to a path inside your WordPress install. The plugin refuses any staging path inside the install, on purpose, so anyone who followed that example had every backup and every restore stop with a security error instead of running. The example is now an absolute path outside the web root, which is what the plugin actually accepts, and the answer says plainly that a path inside the install is rejected.
- The same answer described a third fallback location that does not exist. Staging resolves in two steps, not three: the prepared directory under `/var/lib/`, then a private directory in the system temp folder. Both sit outside the web document root. The instructions for denying web access to a third location are gone, because the plugin never creates it.
- Nothing changes in how backups are taken, uploaded, or restored.
- When a view stops the backup, the message names it. Some views use SQL this plugin cannot analyze well enough to guarantee it can recreate them in the right order, so it stops rather than write a backup it might not be able to restore. That refusal was right, but it named nothing, giving you no way to find the view. Every one of these messages now names the view, and where relevant the object it reads.
- The most common trigger is a WITH clause (a common table expression), and the second is a window function's PARTITION BY. The message names both as likely causes.
- On MariaDB this is the only code path, because MariaDB does not provide the view-dependency metadata the plugin prefers, so a single view added by any plugin can stop the whole backup.
- Views that read tables shared across a multisite network are now explained by name instead of reported as an unspecified scope error.
- Known limitation, unchanged: such a view still stops the backup rather than being skipped.
- A daily check now warns you before a view can stop a backup. The plugin rehearses the view plan once a day, using the same planner the backup uses, and raises a warning naming the view if the next backup would stop. Without it the first sign of trouble is a failed backup at whatever hour yours runs.
- The warning appears in the log and on the dashboard, and is sent to your notification webhook on the first day of a problem. It clears itself when the view is fixed or removed.
- It runs on the daily schedule only, so browsing wp-admin never pays for it.
- The plugin now emails you when nothing has backed up for too long. Previously a stale backup only produced a dashboard banner, a log entry, and a webhook for Pro users, all of which need somebody to come looking. The quiet failure, where backups simply stop, reached nobody.
- The free edition gets this too - it is not a licensed feature, and it is the only out-of-band alert the free edition can receive.
- One email when the problem starts, then at most one a week while it lasts, stopping on its own once a backup succeeds. A site with no schedule configured is never called stale.
- Views using a WITH clause or a window function are now backed up rather than refused. Previously either one stopped the whole backup within seconds, so a single view added by any plugin could leave a site with no backup at all.
- Nothing about what gets captured has been relaxed. A view still cannot read a table that is not part of the backup, and every source it reads is still checked against the signed list of what the archive contains.
- Recursive WITH clauses are still refused; the daily pre-flight names the view if you have one.
- Backups no longer stop because another plugin changed one of its own tables. Wordfence Login Security keeps `wfls_role_counts` in memory and rebuilds it from scratch, so this plugin has always saved that table's structure and skipped its contents rather than refusing the backup. Recognising it relied on matching Wordfence's exact column list. Wordfence 9.0.0 added a passkey column and widened the key, the match stopped, and every backup on an affected site failed within seconds before any file was written. The table is now recognized by which table it is, so Wordfence can reshape it freely and backups keep running.
- Restores were blocked by the same cause. The restore validator carried its own copy of that column list, and it also runs while a backup is being written. Both now check only the structure this plugin itself writes, never a third party's column list. Archives created by earlier versions still restore.
- `wfls_passkeys` is now treated as network-wide, alongside the other Wordfence Login Security tables, so multisite installs backing up one site at a time are not stopped by an unclassified table.
- Regression tests were asserting the failure was correct and have been inverted, now covering the real Wordfence 9.0.0 structure plus one no release has seen.
- A blocked backup now names the files that blocked it. When an archive would contain two paths that differ only in capitalization, the backup stops, because restoring onto macOS or Windows would silently keep only one of them. That refusal is right, but it used to name nothing. It now names both paths and says what to do.
- A scheduled backup no longer claims another backup is running when none is. If the plugin cannot claim its run lock at all - most often because the lock file belongs to a different system user, which is what happens when backups are triggered from WP-CLI - the scheduled run reported a busy site. Both manual entry points have said the accurate thing since 5.38.0; the scheduled path now does too.
- The same collision refusal is fixed in both places OneDrive checks it, and a failed Unicode normalization is no longer reported as if it were a collision.
- Known limitation: an archive taken by this version on a Wordfence 9.0.0 site cannot be restored by 5.44.0 or earlier. It only matters if you downgrade and then restore; a site on the older version cannot make a backup at that point anyway. Archives from earlier versions restore on this version normally.
- Backup health check: your backups now show their work. Every backup already proved two things and told you neither. It checked that the database export can be read back by the same strict reader a restore uses, and it checked that every file in the archive matches a signed record. Both results were thrown away. The dashboard now shows a Restore Readiness result for your most recent backup, with the checks behind it, and a copy of the report travels inside the archive so it survives even if the site does not. These checks confirm the archive is complete and readable. They do not restore the site.
- It also tells you when a backup is not the whole site. If uploads, themes, plugins or core were left out, the result says so and names what is missing. Excluding things is a perfectly good choice, but it should not be a surprise on the day you need the backup. Incremental backups are flagged too, because restoring one also needs the full backup it builds on.
- Translation fix. Four admin messages changed wording in 5.43.4 but never reached the translation catalog, so they could not be translated. The catalog is now rebuilt and checked on every release.
- Nothing about how backups are taken, uploaded or restored changes in this release. The health check only records what was already being checked, and it can never stop a backup: if the report itself fails, the backup continues without it.
- Corrected what the paid tiers actually add. The description said paid tiers add selective restore, licenses for extra sites, and Slack and Discord alerts. Selective restore has been free on every install since August 2026, the free edition has no activation limit at all, and Slack and Discord start at Solo. The Pro tier section said it adds 10 sites; every paid tier now covers up to 127, and the tiers differ by support depth instead.
- Fixed two admin messages that told paying Solo customers a feature was not theirs. The webhook settings notice and the webhook error both described notifications as requiring Pro or a Bundle. Solo qualifies, and the Bundle is a withdrawn product. Nothing about backups or restore changes in this release.
- Documentation accuracy. Corrected tier descriptions in the paid edition's documentation. No functional change to this free edition.
- The licensed edition no longer transmits Slack or Discord webhook bearer URLs off the site. Remote alerting in that edition reports only that email delivery is enabled; local site-originated webhooks remain separate. This copy sends us no telemetry, no analytics and no tracking, and no site content or backup data. Since 5.49.0 it does ask bodholdtlabs.com whether a newer version exists, sending the version it is running and this site's address, as described under External services above.
- About the standalone migration installer. Some entries below, from earlier releases, describe a standalone `installer.php` and a Migration Mode that produced migration keys. Neither is part of this release. `installer.php` is excluded from every distribution build, verified on the built zips, no runtime code can package it, and there is no Migration Mode setting. Restoring to a different server is done by installing WordPress there, connecting this plugin to the same cloud account, and restoring from the Your Backups tab.
- Fixed: the file path in a stopped backup message is readable again. When a backup stops and tells you which file or folder it stopped on, that path is now shown exactly the way it looks on your site. In the previous release, characters such as an ampersand, an apostrophe or an angle bracket were turned into web codes first, so a folder named `R&D <draft>` arrived as `R&D <draft>` in the progress line on the Backup tab. That is the one piece of information you need in order to act, so it should always be plain. Nothing else about how backups or restores work has changed.
- Quieter error logs during backups. On hosting that restricts which folders PHP may look at, the plugin's check for a private staging folder was writing an "open_basedir restriction in effect" warning to your error log every time it looked at a folder it was not allowed to see. Backups were never affected: the check simply moved on to the next folder and used that one. It now asks whether it is allowed to look before looking, so those warnings are gone. A single backup run on one test site went from 60 of these warnings to none.
- Slack and Discord notifications now start at the Solo plan instead of Pro. Pro and Agency keep them exactly as before; Solo gains them. The free edition does not include webhook notifications.
- Translations now load on the `init` hook instead of `plugins_loaded`, matching WordPress 6.7 guidance. Nothing visible changes.
- You can now choose exactly which parts of your site to restore, on every install. The restore panel has always had a chooser for Database, Plugins, Themes, Uploads and WordPress system files. Until now it was shown locked unless you held a paid license, and the server put the choice back to a full restore regardless. Both of those are gone: the chooser is live for everyone and the parts you tick are the parts restored.
- Leaving "Everything" ticked behaves exactly as before, restoring every part your chosen backup actually contains. A full restore remains the default and the recommendation.
- No changes to backups.
- A restore that worked is no longer thrown away over folder permissions. If any of your files or folders belong to a different system user than your web server, which is ordinary on shared hosting whenever something was uploaded over FTP next to a plugin installed through WordPress, the restore could put everything back correctly and then refuse at the last step because it was not allowed to change those permission settings. The whole restore was rolled back, leaving your site exactly as broken as before you asked. The rollback worked and your site stayed online, so nothing was lost, but the restore you needed did not happen and retrying failed the same way.
- What happens now. Your content is checked exactly as strictly as before, and a backup whose bytes do not match is still refused and rolled back. If your content is correct and only permission settings could not be reapplied, the restore completes and tells you so plainly.
- No other changes. Backups are untouched.
- Choosing only part of your site to back up now works everywhere the engine reads that choice, with your database always included exactly as the settings screen has always said. The settings screen says Database (always included) and the save button has always enforced it, but a backup scope stored without the database by any other route, such as wp-cli or a direct option write, failed every single run with an error naming no cause, on every site layout including a completely ordinary one. Those runs now produce the same archive the settings screen promises, and this matches what the Google Drive edition has always done. Your restore choices are untouched: restoring only your files still restores only your files.
- A backup of part of your site now completes on a site whose uploads folder sits inside a folder the backup already walks. With UPLOADS pointed at somewhere under wp-content/plugins, a backup that selected your plugins without your uploads, for example database and plugins, failed with an error naming no cause and wrote no archive. The backup writer was refusing its own correct archive: it carries those files at their real path, and the check comparing the finished archive against its own signed inventory classified them under a part of your site the run had not selected. The two now read one answer, every one of these backups completes, and restoring only your plugins from such a backup still leaves your media alone. Every archive that verified before verifies byte for byte, and an ordinary site is untouched.
- The restore health check now asks the outcome question. It used to pass a part of your site the moment the backup held a folder for it. It now also asks what you would: did the folder your site loads that part from end the restore holding the restored content. A correct restore already satisfies both, so nothing changes on a passing run; a restore that finished without delivering what you selected now fails its health check by name instead of reporting success.
- A folder your site loads from that has been moved on its own is now refused by name, instead of being left out in silence. WordPress lets you move your plugins, must-use plugins, uploads, translations or themes folder on its own, with WP_PLUGIN_DIR, WPMU_PLUGIN_DIR, UPLOADS, WP_LANG_DIR, or the uploads folder box on Settings, Media. Your themes folder counts here only when the theme your site is actually running loads from somewhere other than wp-content/themes; a second themes folder that a plugin has registered alongside your normal one does not stop a backup, and is covered below. Every version before this one recorded those files at their usual place under wp-content and never asked WordPress where they really are. On such a site the backup reported Full Backup Success with none of that folder in it, and the restore reported itself complete with permissions verified and put none of it back. Measured on 5.39.0 and on this release before the fix: a site with WP_PLUGIN_DIR pointed somewhere else got a successful backup with zero of its plugins in the archive, then a successful restore that brought none of them back. A site with WPMU_PLUGIN_DIR pointed somewhere else had its must-use plugins written into a folder the site does not load from, under a message saying the restore was complete. From this release the run stops before anything is uploaded, and the message names the folder, where a backup records it, which wp-config.php line or setting moved it, and what to change. The size estimate answers the same way, so you are not shown a file count on a site that will be refused. Database-only backups keep working. And a backup taken by an older version on such a site now stops before it changes anything, for the same reason and with the same message. One thing this does not refuse: a moved folder that holds nothing a backup would have taken. An empty folder, or one holding only files your Excluded Files and Folders setting leaves out anyway, has nothing to lose, so it is treated the same way as a folder that is not there at all and your backups keep running. That is about backups. Restoring is a different question, and this release answers it by looking at the backup rather than at the folder. A restore is about to write, and it writes to the usual place under wp-content, so the question it asks is whether anything it writes reaches the folder your site really reads. Where nothing does, it stops and names the folder instead of putting your files somewhere your site does not look. Where your files do reach that folder, because the backup carries them at their real path or because the folder already holds exactly those files and this restore leaves them alone, it goes ahead. If the backup holds nothing for that folder, the restore goes ahead as normal, which is the common case on a rebuilt server and on any site with an empty moved folder. A restore also stops if the moved folder sits inside a part of your site a full restore clears out and the backup carries none of it, because a full restore would delete what is in it. When a restore does stop, you can move the folder back and remove the line from wp-config.php, or run the restore again with that part of your site left out and everything else is restored as usual, and a database-only restore always works. One more thing about a moved folder that sits inside a folder the backup already walks, for example an uploads folder you have pointed at somewhere under wp-content/plugins. The backup takes every one of those files at their real path and a restore puts them back at that same path, so the backup is not stopped. The restore is judged on its own, against the backup you are restoring. If that backup was taken before you moved the folder it holds nothing for the folder, and a full restore of the part of your site the folder now sits in would empty it, so the restore stops and names it. Run the restore again with that part left out, or restore a backup taken after the move. Your must-use plugins folder is the one folder that behaves differently when you move it: a backup records it at wp-content/mu-plugins wherever you have put it, so the backup runs and says so on the run that succeeds. What a restore does there changed in this release, and this is the one line in this note that used to say the opposite of what the build did. A restore used to write that spare copy into wp-content/mu-plugins over whatever it found, so a file of yours in that folder with the same name as one in your real folder was replaced with a copy of the other one, and nothing in the backup could put yours back. A restore now leaves your file exactly where it is and does not write the spare copy over it. If the restore is also clearing out what the backup does not carry, leaving the file alone would not save it, so the restore stops before it changes anything and names both folders and what would have happened to each. And choosing one part of your site to restore now follows the folder your site loads that part from, not the folder the backup files it under. If your uploads folder sits somewhere under wp-content/plugins, choosing uploads restores your media, and choosing plugins leaves your media alone. Before this release those two were the wrong way round: choosing uploads reported a complete restore and put back none of your media, and choosing plugins replaced and removed media the backup was never asked to touch. And when your layout will stop a restore, the backup says so on the run that succeeds, in the same words the restore will use, instead of leaving you to find out on the day you need it.
- Which site layouts are supported, plainly. Supported: the ordinary layout, unchanged and proven unchanged, with the same files selected byte for byte. Supported: a content folder inside your WordPress folder under any name and at any depth, for example app or sites/app, including one reached through a link whose target is also inside your WordPress folder, and including Fast Backups on those sites. Supported: translations at wp-includes/languages, which is where WordPress keeps them when wp-content/languages does not exist. Supported, measured end to end on this release: choosing one part of your site to restore on a site whose uploads folder sits inside a folder the backup already walks, for example under wp-content/plugins. Restoring uploads puts your media back in the folder your site really loads it from, restoring plugins leaves that media alone, and the health check that judges the run asks about the folder your site loads from, so a correct restore on such a site reports success. Not supported and refused by name at backup: a content folder outside your WordPress folder, and that includes one reached through a link whose target points outside it. A linked content folder counts by where its target really is, which matters because a link that points outside is the ordinary shape on container and NAS hosts. Not supported and refused by name at backup: your plugins, must-use plugins, uploads or translations folder moved on its own to somewhere the backup does not already walk, and the same for a themes folder when the theme your site is running loads from it. Two things that do not stop a BACKUP: a moved folder that sits inside a folder the backup already walks, for example an uploads folder pointed at somewhere under wp-content/plugins, because those files are in the backup at their real path and a restore puts them back there; and a moved folder that holds nothing a backup would have taken. A restore of such a backup is judged on its own and stops by name when the backup holds nothing for that folder while a full restore would clear out the part of your site it sits in. Not backed up, and this one is not refused: themes kept in an extra themes folder that a plugin has registered alongside your normal one. Your normal themes folder is backed up and restored as usual, and a backup that leaves an extra folder out says so on the run, naming the folder and each theme in it, so a successful backup never quietly stands in for a missing theme. If you keep a theme only in an extra folder, keep your own copy of it or move it into wp-content/themes. Not supported and refused by name at restore: a backup from a site whose content folder had a different name from this site's. Refused by name at restore, and only when it really would go wrong: a backup whose files for a folder you have moved reach none of the folder your site really reads, or a backup that holds none of a moved folder that a full restore would clear out. In both cases the rest of your site can still be restored by leaving that part out, a database-only restore always works, and the backup that will hit this says so on the run that succeeds. Not supported and refused by name: a per-site backup on a multisite network whose content folder is not named wp-content. On every refused layout nothing is uploaded, nothing is changed, and database-only backups still work.
- A restore no longer throws itself away over one file it knows where to put. If a backup was taken on a site that had registered an extra startup file with the bodholdt_od_extra_root_files filter, and you restore it onto a site where that filter is not set up, which is exactly what moving to a new server looks like, this plugin used to refuse the whole restore over that one file, and the message it gave told you to take a fresh backup, which on a move to a new server is an instruction for the machine you just left. A restore that would have recovered your entire site recovered nothing. The restore now writes the file back exactly as the backup holds it, checks it byte for byte like every other file, and tells you on the result screen and in the activity log that it did, so you can add the name to this site's filter if you want your next backup to carry it. Drilled on the shipping bytes with a firewall file loaded from .htaccess: before this change the restore refused and the site stayed at HTTP 500, after it the restore completes and the site answers 200.
- A backup this version genuinely cannot read stops before it changes anything, and says what it could not read. If a backup was made by a newer version and holds files in a place this version does not know about, the restore now stops before touching your site and names the files, the version that wrote the backup, and the version running here. It never tells you to take a fresh backup, because on a restore to a new server that is an instruction for the machine you just left.
- Verify and Trust accepts a backup the restore would accept. A backup carrying an extra startup file your current site does not list used to be refused by Verify and Trust and accepted by the restore. One backup now gets one answer.
- The plugin no longer claims .user.ini loads a file when your server does not read .user.ini. PHP reads .user.ini only when your host runs PHP as CGI or FastCGI. On a server running PHP as an Apache module, a .user.ini left behind by a previous host does nothing, and every backup used to state as fact that it loads a file on every request, and after a server error the restore pointed you at the wrong file. The plugin now looks at how PHP is running. Where .user.ini is genuinely inert it says nothing about it, where it is genuinely read the sentence is unchanged, and where the backup was started from the command line and cannot tell, it says which mode the claim depends on.
- Backups in your OneDrive folder that this site cannot prove it created are now mentioned. Retention only deletes what this site can prove is its own, which is what keeps it from deleting a recovery point the restore list is offering you. The cost is that such files stay forever and keep using storage, and nothing said so. The activity log now says how many there are and what to do about them, at most once a day.
- A restore that cannot finish no longer leaves your site with fewer files than it started with. Before this release, a restore that met one folder the web server was not allowed to change discovered it halfway through, after it had already removed files, and the recovery step then met the same folder and gave up before putting anything back. Two things changed. A restore now proves, before it changes anything, that the web server can add, replace and remove files in every folder it needs to touch, and it stops with that folder named if it cannot. And if a restore does fail partway anyway, the recovery step now tries every file it is holding instead of stopping at the first one it cannot place. Drilled with a folder the web server genuinely cannot write into: five files that were on the site and not in the backup were all still there, byte for byte, after the failed restore.
- Backups now include the files your site needs in order to start. Some sites load a file on every request through .htaccess or .user.ini, a firewall file for example. Those files are now in the backup, along with robots.txt, ads.txt and php.ini. Before this release a restored site could return an error on every page because a file it loads at startup was not in the backup. After a restore the plugin now asks your site for a page and tells you if the answer is a server error. A site that is deliberately in maintenance mode, which answers 503, is not reported as broken.
- If your backup uses a startup file this list does not cover, the backup log names it and tells you how to add it with the bodholdt_od_extra_root_files filter.
- Restoring an older backup no longer guesses why a startup file is missing. The message used to say the backup was taken before this feature existed and to take a fresh backup, which was untrue if you had simply excluded that file. It now says only what it measured: the backup does not contain the file, the copy already on your site was left exactly as it was, and either a fresh backup will contain it or your Excluded Files & Folders setting will leave it out again.
- A restore that stops now says so once, not twice, and the activity log says the same thing the screen does. A refusal used to name the exact folder and the fix, then repeat itself in vaguer words, while the log line kept only the vague half. The specific sentence, with the folder and the fix, now appears once on screen and once in the log.
- A backup that skips something now names it. The warning about symbolic links, and the one about sockets, named pipes and device files, used to give a count and nothing else. Both now list the paths, up to five, then "and N more".
- The backup list is more honest. A Fast Backup whose full base is no longer in your OneDrive folder is marked as not restorable, with the reason, instead of appearing ready. Every restore failure now carries a support reference you can read on screen.
- Schedules. An overdue scheduled run shows as overdue instead of as a future run, and the dashboard tile says so in words. If a schedule has been armed and no backup has ever completed, you now get a warning.
- Take a fresh backup after updating. This one is not optional. Backups made before this release do not contain the startup files. A restore from one of them works, and it says which files it could not bring, but your first current restore point only exists after your first backup on this version.
- Restoring between sites: update every site first. A backup made by 5.40.0 contains startup files that 5.39.0 does not recognize. A site still running 5.39.0 refuses to restore it, for every restore option including database only. Nothing on that site is changed, but the message it shows says the backup is too old, which is the wrong way round. Update every site to 5.40.0 or later before restoring a backup from one site onto another. From this release forward, a refusal like that names both version numbers and tells you to update, rather than blaming the backup's age.
- Leaving a folder out of your backups no longer makes those backups unrestorable. Excluding a whole area, for example putting wp-content/uploads or themes in Excluded Files & Folders, made every backup taken afterwards fail its final health check and roll itself back. The check decided an area had restored nothing purely by looking for that folder in the backup, and never asked the backup what it had deliberately left out. It now reads the exclusion policy the backup itself recorded and signed, which is the same policy the backup used to skip the folder in the first place. A folder the backup deliberately left out is expected to be missing, not a fault. This fixes backups you already have: a backup taken with 5.38.0 or earlier with an area excluded restores under this version, proven on real backups taken on 5.38.0.
- A restore that really is missing an area now says which one and where it looked. For example: nothing was restored for uploads, because this backup contains no files under wp-content/uploads. A backup taken before exclusion policies were recorded has nothing to prove a choice was made, so it keeps failing loudly. And when a restore succeeds and the backup did leave an area out on purpose, the result says so plainly.
- Broad exclusion patterns no longer save in silence. There was no themes probe in the disclosure check, so themes and wp-content/themes emptied that area with no warning, and every probe was a deep file path, so a pattern matching only a folder, such as `*/uploads` or `wp-content/?ploads`, was never disclosed either. Each area is now checked at the folder itself as well as at a file inside it, so all of these are disclosed, and wp-content/* now names themes alongside plugins and uploads. Narrow patterns such as wp-content/cache/* still save quietly. The notice also tells you what a restore does with those files: it leaves them as they are on this site.
- Unchanged on purpose. Which files a backup selects, the archive format, the patterns that are refused outright, and restores of backups with no excluded area, verified by restoring the same backup under both versions and comparing the result byte for byte.
- The check that catches a file changed during a backup now works on container hosting. Just before a backup is sealed, every file is read again and compared with what was captured when it was added, to prove nothing changed while the run was underway. A change to a file's contents was always caught. What could slip through was a swap that replaced the file while keeping its contents, a change of owner, or a timestamp change made to look untouched, and on container based hosting, which includes Docker and many managed platforms, the filesystem hands a recreated file the same identity numbers the check compared. The check now also compares the change stamp the operating system keeps on every file. That stamp moves on each of these changes and ordinary software cannot set it back, so they are all caught now, on container hosting and everywhere else.
- A backup stops and tells you when the saved exclusions would leave it nearly empty, even when no single line is at fault. The previous release stopped a backup when one stored line matched the whole site. Several narrower lines can add up to the same mistake, for example wp-admin, wp-includes and wp-content/* on three lines. The saved list is now checked as a whole at the start of every backup, and when its lines together would exclude files under wp-admin, wp-includes, wp-content/uploads and wp-content/plugins, the backup stops before anything is uploaded and the message names the exact lines to fix. Saving such a list is refused the same way, naming the lines and confirming that no settings were changed. These messages claim only what the check proves: files under those four core areas are excluded. And because leaving a whole area out can also be a deliberate choice, every one of these messages now points at the What to Back Up checkboxes as the supported way to do that on purpose.
- A stopped backup names the file that changed and what changed about it. When the final check finds a file changed while the backup was being written, the message now names the file and the changed fields, for example its permissions or the change stamp that a nightly permissions fix rewrites, and suggests scheduling the backup away from such maintenance jobs. The safety-copy check that can stop a restore before any change is made now reports the same way. Before, both refusals said only that something changed, naming nothing.
- A refused restore now tells you its real cause. When a restore stops because the signed folder records inside a backup cannot be verified, the message used to give only a general verdict. The specific reason now travels with it to the restore screen and the activity log, so you can see what was wrong instead of guessing.
- Small message and translation fixes. The note about a special file appearing during a restore now leads with the count whether there is one or several, so it reads consistently and translates cleanly. A refusal that names a file path now carries that path exactly as it is on disk into the activity log and the failure email, instead of turning a character such as & or < into an HTML code that did not match your file. A backup or restore that could not start because this site could not create its lock file used to report that a run was already in progress, so you waited for a run that had never started; it now says the lock could not be claimed and where that file lives. And while a restore replaces the database, the progress screen could briefly read Ready at zero per cent while the restore was still running; it now says progress is temporarily unavailable and keeps the progress bar where it was.
- No fresh backup is needed after this update. Nothing in this release changes what a backup contains. Every backup that verifies and restores today keeps verifying and restoring, byte for byte, after you update.
- A restore no longer stalls when a special file appears while it is running. The previous release handled a socket, a named pipe, or a device file that was already on the site when a restore began. One created after that moment, by a caching plugin or a mail queue for example, could still make the restore hang quietly partway through. The restore now checks again at the instant it would touch each spot and never opens such a file. When it can safely finish, it does, and the restore screen names every file it left in place. When the backup needs to put a file exactly where the special file sits, the restore stops without changing your site, and the message names both the file it wanted to write and the object in the way, so you know exactly what to move. On a site holding hundreds of these objects, the safety check now covers every one of them, where before it stopped counting at 200. And if the restore cannot verify the signed folder records inside a backup, it now stops before any change and names the cause, instead of quietly carrying on without them.
- A backup that would contain no files now stops and tells you. If the Excluded Files and Folders list would exclude every file on the site, the backup now stops before anything is uploaded, with a message naming the line to fix and suggesting a narrower pattern such as `wp-content/cache/*`. Saving such a line is already refused, but a list saved by an older version could still hold one, and until now every backup it produced was empty while the dashboard stayed green.
- The settings screen speaks up about broad patterns. A pattern such as `wp-content/*` still saves, and the screen now adds a note naming the core areas it excludes files under, so a quiet gap in your backups cannot hide behind a green save message.
- A refused older backup explains the cure. When a backup made before a plugin update cannot be used because the update changed which files are excluded, the message now says so plainly and tells you what to do: take a fresh full backup to get a current restore point.
- No fresh backup is needed after this update. Nothing in this release changes what a backup contains. Every backup that verifies and restores today keeps verifying and restoring, byte for byte, after you update.
- A file restore no longer stops when your site holds a special file. A socket, a named pipe, or a device node left behind by a caching plugin or a mail queue could stop a full file restore before it began, with a message that did not say what was in the way. The previous release fixed this for backups and said the restore side was next. This is that fix. A restore now skips the special file and reports it on the restore screen, exactly as a backup already does, and the file itself is never touched or deleted. There is one case where a restore still stops: when the backup needs to place a file or folder exactly where the special file sits, the restore refuses before changing anything, and the message now names the exact file so you know what to move. A restore stopped by a symbolic link now names the link as well.
- An exclusion line that would silently do nothing is now refused when you save it. Two new checks in Excluded Files and Folders, both naming the line to fix. A line whose folder name contains a character this version cannot store safely yet is refused, instead of being saved and then quietly failing to exclude that folder. And a pattern so broad it would exclude the entire site, such as a bare `*`, is refused, instead of quietly leaving you with backups that contain no files at all. The message suggests a narrower pattern such as `wp-content/cache/*`. The one-time tidy-up of saved exclusion lists from the previous release now leaves a line containing such a character exactly as you saved it, instead of storing it wrongly.
- Refused saves point at the right line. If the Excluded Files and Folders box starts with blank lines, the line number in the message now matches the row you actually see in the box. And every refusal now ends by confirming that no settings were changed.
- No fresh backup is needed after this update. Nothing in this release changes what a backup contains. Every backup that verifies and restores today keeps verifying and restoring, byte for byte, after you update.
- An excluded folder could be deleted from your live site during an exact restore. If a line in Excluded Files and Folders had a stray space at the start or the end of it, the backup did leave that folder out of the archive, but the restore did not recognize it as one you had asked to keep. An exact restore then saw a folder on your site that was not in the archive and removed it. The folders people exclude tend to be the private ones, so this took exactly the wrong thing. Exclusion lines are now tidied when you save them, and a list that was already saved with stray spaces is corrected for you, once, the first time the updated plugin loads. Your lines stay in the same order and blank lines stay where they are. Backups that were already made with a stray space in that list keep it, so restoring one of those is handled differently: every file in the backup is restored exactly, and anything on your site that is not in the backup is left alone, which means the excluded folder cannot be removed. The restore screen tells you when it has done that.
- Please take a fresh full backup right after updating, and use it in place of your older restore points. Two separate changes in this release alter which files the plugin treats as restorable content, and either one on its own is enough to make an older backup unusable. First, macOS `.DS_Store` files are no longer put into the archive. Second, wildcard exclusion lines now match, where before they matched nothing at all, so files you believed were excluded were in fact being archived every night. A backup taken before this update still lists those files as content it is responsible for, so the updated plugin refuses it rather than carry out a restore it can no longer call complete. That covers any site that has ever been worked on from a Mac, and any site whose Excluded Files and Folders list has ever held a line such as `*.log` or `*.sql`. To make sure you are not left without a working restore point, the update clears the local change record that Fast Backup keeps, so your very next run is a full backup and you get a usable restore point without doing anything. That run will say "Fast Backup switched to Full". Please run it by hand now rather than waiting for the schedule if you can.
- Excluded Files and Folders now understands patterns. Lines like `*.log`, `*.sql`, and `wp-content/cache/*` now match. Until now this plugin compared every line literally, so a pattern matched nothing at all and gave no warning, and the files you thought you had left out were uploaded every night. Version 5.33.0 announced this for both backup plugins, but only the Google Drive one actually had it. Plain paths behave exactly as they always did.
- macOS `.DS_Store` files are no longer backed up. These are hidden Finder files with nothing in them worth restoring, and a site that has been handled from a Mac can carry thousands of them.
- An excluded path that this plugin cannot use is now refused when you save it. A line consisting of a bare dot, or a line pasted with a tab or another invisible control character in it, used to save with a success message and then break every backup from that moment on, with an error that did not say which line was at fault. The Settings screen now checks each line against the same rule the backup itself uses and names the line number if one of them cannot be used.
- Must-use plugins are found when their folder has been moved. If your site points `WPMU_PLUGIN_DIR` somewhere outside `wp-content`, which some hosts and custom setups do, your must-use plugins were missing from every backup. They are now included. One thing to know about getting them back: a restore writes a spare copy to `wp-content/mu-plugins` as well as putting your files back where the backup read them, so on a site with a relocated folder check both after a restore. From 5.40.0 that spare copy is never written over a file of yours that is already there, and where a restore would remove such a file instead it stops first and names the folder.
- A backup no longer stops when it meets a special file. A socket, a named pipe, or a device node left behind by a caching plugin or a mail queue used to end the whole run, so you got no backup that night or any night after. These cannot be archived or restored, so they are now skipped, the rest of your site is backed up as normal, and the run tells you exactly what it left out, both in the run details and in the activity log. One thing to know: a full file restore still stops when it meets one of these, so a backup can now succeed where a restore does not. Closing that gap is next on our list.
- Scheduled database backups no longer fail when a security plugin rebuilds its temporary table mid backup. Some security tools create and then drop a temporary scratch table while your site is running. If that happened partway through the database export, the backup stopped with a "table definition has changed" message and only finished on the next scheduled run, so you saw a failure followed by a success. That scratch table holds nothing worth restoring, so it is now left out of the backup entirely, and any brief database hiccup during a backup now retries on its own up to three times instead of ending the run. Nothing for you to change.
- Clearer restore confirmation before anything is overwritten. The restore warning now says plainly that the automatic safety copy covers your database only and there is no one-click undo, replacing wording that implied a restore could simply be undone. It also adds a heads-up that your site briefly shows a maintenance page while a restore runs and reopens on its own within about ten minutes if the restore is interrupted, so you are not left wondering. The confirmation checkbox and modal are unchanged. No change to how restore itself works.
- Backups now restore on sites running both Bodholdt backup plugins. If you have both the Google Drive and OneDrive backup plugins installed, every archive included the other plugin's files, and restore refused those archives outright because it is designed never to overwrite a backup plugin mid restore. The result was backups that always reported success and could never be restored. Archives taken from this version on restore normally. If you run both plugins, please take a fresh backup after updating so you have an archive that restores.
- Please take a fresh backup after updating if your site runs MySQL 8. Columns whose default is the current timestamp were being left out of the database export. Many plugin tables use these for dates like "created" and "last updated", including WooCommerce order analytics. Backups still reported success, and on restore those dates were refilled with the time of the restore rather than the original values, so the data looked plausible but was wrong. Archives created before this update are affected on MySQL 8. Sites on MariaDB were never affected. One new backup after updating replaces the gap.
- Backups no longer stop when your site contains a symbolic link. One linked theme, plugin folder, or single linked file inside uploads used to end the entire run, and the failure email suggested checking your server logs or internet connection, which had nothing to do with the real cause. Links are now skipped, the rest of your site is backed up normally, and the run tells you exactly which links were left out. This is common on managed hosting, on container based setups, and on any site using a linked child theme or a shared media mount.
- An interrupted restore can no longer leave your site stuck offline. If a restore was killed partway through, by a memory limit, a timeout, or a server restart, WordPress was left in maintenance mode permanently. Both the site and the WordPress admin returned "Briefly unavailable for scheduled maintenance" forever, and the only way out was deleting a file over SSH or FTP. A running restore still closes the site as it should, but an abandoned one now reopens on its own after ten minutes so you can get back into your admin and try again.
- A stalled restore no longer blocks every future backup with no way to clear it. The Force Unlock button only ever appeared for stalled backups, never for stalled restores, so a restore that stopped unexpectedly left backups permanently blocked with the fix hidden from view.
- Clearer, more honest failure messages. Running out of local disk space is no longer reported as your OneDrive being full, and no longer suggests deleting your backups, which would not have helped. The size estimate on the dashboard now tells you what actually went wrong instead of only "Could not calculate estimate." Internal log tags no longer appear in messages meant for you.
- File exclusions now support wildcards. Patterns like `*.log` and `wp-content/cache/*` previously matched nothing at all and gave no warning, so files you thought you had excluded were still being uploaded.
- Backup notification emails are now translatable and no longer arrive in English on a translated site. They also fall back to plain text if the HTML message cannot be delivered, and a delivery failure is recorded instead of passing silently.
- Retention problems are now visible. If old backups could not be removed from your OneDrive, the run said nothing and storage kept growing. It now tells you.
- A warning when scheduled backups cannot actually run. If WP-Cron is switched off on your host, Settings used to show an active schedule while nothing ever ran. You are now told, with instructions for setting up a real cron job.
- Also in this release. A working admin menu when the plugin is activated on a single site of a multisite network. Large downloads no longer time out on a blank page, and a download that was merely interrupted is no longer reported as a corrupt archive. The setup wizard no longer says setup is complete when the server rejected the save, and it no longer overwrites a backup time you already chose. The progress bar no longer freezes forever if a status check fails.
- Clearer badges and error text (WCAG 2.1 AA). The plan badge in the header, the status badges, and the red error text in the activity log now meet AA contrast against their dark backgrounds. On some screens they could render a little too dim to read comfortably. No change to how backups or restores work.
- Restore works, including full disaster recovery. Several internal checks disagreed with each other about the exact bytes of your archive, so a restore could stop before changing anything. Backups, restores, incremental restores, and fresh-site recovery (reconnect a new site to your existing OneDrive folder and restore) are now all verified working end to end. If you were unable to restore a backup on an earlier version, take a fresh backup and use that one.
- Backups keep their exact identity. The backup's stored name and the name it is signed under are now always the same string, so a cloud-side rename can no longer make an archive unrestorable.
- More accessible admin. The dashboard now meets WCAG 2.1 AA: stronger error-text and focus-outline contrast, and the optional celebration pop-ups are announced, dismissible, and pause on hover or focus.
- Backups to a personal OneDrive account now work. The plugin writes an ownership marker into its backup folder and reads it back to confirm the folder is really yours. Microsoft answers that read with a redirect to a separate content host, which earlier versions treated as a failure, so the check never passed and every backup stopped before it started. The marker is now read the same way the plugin already downloads archives, so backups run normally.
- Your backups are recorded, restorable, and pruned again. Each upload is recorded in a local list that proves the plugin owns that archive. A missing field in one Microsoft request made every entry fail its ownership check and get dropped, so backups uploaded and reported success while becoming unrestorable, retention never deleted anything, and the plugin treated its own archives as if they belonged to someone else. If you have unrestorable archives from an earlier version, take a fresh backup and use that one.
- Your cloud backups no longer contain your credentials on some sites. If your database table prefix has no trailing underscore, such as `wp` instead of `wp_`, the filter that keeps live credentials out of uploaded archives did not match your options table. Your Microsoft client secret, refresh token, and WordPress security salts were written into the backup. Redaction now works with any valid prefix. If your site uses a prefix like this, please delete your existing cloud backups and create a fresh one.
- Binary and case-sensitive collations survive a restore exactly. Backups previously rewrote collations such as `utf8mb4_0900_bin` and `utf8mb4_ja_0900_as_cs` to a case-insensitive one, which silently changed sorting, uniqueness, and case-sensitive lookups on the restored site. They are now preserved exactly, in every place the plugin reads or writes a collation.
- Backups record your real database schema. Collation rewriting at backup time is now off by default, so an archive is a faithful copy of your source schema. If a restore lands on a server that cannot support one of your collations, the plugin adapts at restore time instead, where it can see what the destination actually supports. If you restore a dump by hand with the `mysql` command onto a different database engine, switch the setting back on before taking that backup.
- Backups no longer fail on valid database schemas. A site with a view or table that uses `REPLACE()`, `TRUNCATE()`, or `INSERT()` as a function was blocked from backing up at all. Those are now read as functions rather than as dangerous statements, and genuinely unsafe statements are still rejected.
- Restore no longer writes credentials to your error log. If a statement failed while a restore was rolling back, WordPress logged the whole statement, including options rows holding your live credentials. Failures are now recorded without the statement text, and errors are still detected and reported to you.
- A stuck backup can no longer leave the plugin unusable. If a backup process was killed, the operation lease stayed held, listing and restore stopped working, and after an hour the Force Unlock control disappeared, leaving no way to recover without server access. The stale-lock warning and its Force Unlock button now follow the real lock state, and the message explains what actually happened.
- The settings screen always opens. A corrupted saved option could stop the page mid-render, so the form and its save button never appeared and there was no way to repair it from the admin.
- Exact OAuth continuity after restore. Encrypted credential and token rotations now preserve an existing option row's raw autoload metadata while changing only its encrypted value, avoiding a false restore failure on legacy WordPress rows.
- Self-contained Fast Backup restore. Removed an unused restore-base receipt. Fast Backup restore already downloads, authenticates, hash-links, and privately composes its exact Full base before maintenance mode or customer-data mutation.
- Credential changes stay atomic. Hookable cache cleanup now happens before the final encrypted credential commit, so a throwing object-cache integration cannot leave OAuth authority partly rolled back.
- Mobile and keyboard-safe admin. The HUD wraps on narrow screens, wizard focus follows each visible step, Escape dismisses cleanly, inline restore controls remain in normal page focus, and the destructive-restore dialog restores focus to its trigger.
- Reliable downloads and copy controls. AJAX download links no longer depend on a popup, clipboard success requires a confirmed copy, and failures are reported clearly.
- Accessible, accurate setup. Credential and schedule controls have explicit labels; Microsoft authorization and expired-secret guidance now match the actual same-tab flow and dashboard/log error reporting.
- Safe Dashboard metadata. Legacy object- or array-valued log fields are ignored before the Dashboard renders them, matching the hardened Logs table.
- Signed file permissions. On POSIX hosts, new recovery points authenticate every managed file's SHA-256 digest and POSIX mode. Other hosts authenticate file bytes plus an explicit unavailable mode capability; filesystem restore stays fail-closed unless exact rollback permissions can be guaranteed. Restore proves bytes and file identity, applies authenticated file modes before directory modes where supported, and automatically rolls back if chmod or verification fails.
- Exact Fast Backup chains. Hashes, modes, and tombstones compose in lockstep. A base without exact POSIX mode authority forces a self-contained Full backup instead of creating a mixed-fidelity chain.
- Honest older/platform restores. On POSIX destinations, older authenticated archives preserve existing target modes, use restrictive modes for recreated files, and leave destination-only files in place because degraded archives cannot authorize exact scope deletion; source-mode-unavailable archives show a separate warning while bytes, type, and single-link identity remain verified. Filesystem restore on a non-POSIX destination is refused because exact automatic permission rollback cannot be guaranteed; database-only restore remains available.
- Bound required recovery authority. The caller-validated database dump and finalized recovery manifest must match their exact size and SHA-256 records inside the signed archive inventory. Stable bounded manifest reads, archive-identity continuity, and strict unique-key OneDrive ownership markers stop source swaps, oversized records, and duplicate-key authority collapse.
- No partial failed exports. An aborted, truncated, empty, or storage-failed database export removes its incomplete SQL image before reporting the failure, preventing stale recovery data from being reused accidentally.
- Client Secrets stay out of page HTML. Wizard and Settings password fields remain blank after a secret is saved and show only that a stored value exists. Leaving the field blank keeps the saved secret; changing credentials still requires a complete matching pair.
- Safe partial legacy log rows. Older metadata that contains only a backup size or file count now renders with a harmless placeholder instead of a PHP warning; nonscalar display fields are ignored safely.
- Clean legacy Logs screens on modern PHP. Older log rows with empty or malformed metadata are displayed safely without PHP 8.5 deprecation notices.
- Correct database recovery guidance. A nontransactional material table now always produces the InnoDB conversion advice in logs and email, even when its name contains misleading cloud or storage keywords.
- Accurate settings confirmation. The green settings-saved feedback now appears only after WordPress verifies the complete settings transaction. Invalid input, a busy backup or restore, and failed writes or rollbacks no longer show a contradictory success message.
- Wordfence no longer blocks safe backups. The rebuildable `wp_wfls_role_counts` MEMORY cache is preserved as an empty InnoDB table for Wordfence to repopulate; any other non-transactional material table still stops the backup clearly.
- Multisite recovery boundaries are explicit. Every primary-site table must have one unambiguous owner, and a Network backup records the one network and all of its sites. Ambiguous tables, multi-network databases, and older archives without the new signed recovery authority are refused before anything is changed.
- Action after upgrading: Create a new Full backup in Network mode before relying on multisite restore; older network recovery points do not contain the authority required by this release.
- Cleaner safe uninstall. Successful removal clears the legacy uploads staging folder, while an uninstall attempted during active or queued recovery leaves that evidence untouched.
- Safer setup and connections. Settings and setup-wizard saves are transactional, multi-administrator OAuth state is bounded and invalidated safely, and concurrent legacy credential/token upgrades cannot overwrite newer values.
- Restore rollback and storage protection. Exact no-follow, digest-bound rollback snapshots are fully verified before mutation and preserve a 64 MiB emergency disk reserve.
- Restore progress reconnects safely. Temporary maintenance-mode responses keep polling with a reconnecting notice; final failures expose only safe diagnostic codes, not customer or provider details.
- Recoverable database exports. MySQL and MariaDB views are emitted in dependency order; incomplete or ambiguous graphs and unsupported executable schema objects stop clearly instead of producing an incomplete archive.
- Hardened packages. Builds reject unsafe content before publication, uninstall removes only OneDrive-owned state, operator notices require permission, and diagnostics omit customer/provider identifiers and paths.
- Database integrity hotfix. Percent signs and serialized data round-trip correctly, exports use one consistent snapshot, and incomplete database metadata or non-transactional tables fail clearly.
- Safer restore. Imports cannot succeed without schema and data, multisite topology is preserved, config output is verified, and ZIP extraction checks paths, types, size, and CRC.
- Reliable operation and cleanup. Owner-token leases prevent overlap, requested-file omissions and missing recovery members fail before upload, weekly schedules retain their weekday, and uninstall removes OneDrive rows from every legacy subsite log table without removing sibling-provider rows.
- Restore you can trust. Restores now tell you the truth: if any part of the database or files could not be applied, you get a clear "finished with problems" message that points you to the pre-restore snapshot, instead of a false "Restore complete!". Cross-version moves are smoother too, so a backup taken on a newer MySQL restores cleanly onto an older MySQL or MariaDB.
- More reliable moves between servers. Restore keeps your real site configuration and table prefix (custom prefixes and multisite restore properly now), and imports stored procedures and triggers without dropping them.
- Backups that hold up on big sites. Long backups on large sites no longer risk colliding with another run, and generated columns and special column types (BIT, spatial) back up and restore correctly.
- Clearer and safer everywhere else. Honest schedule status on a fresh install, an accurate description of per-site multisite mode, important warnings now visible to multisite network admins, saved credentials no longer loaded on every page, a cleaner reconnect prompt when OneDrive access expires, and a range of smaller polish and cleanup fixes.
- Backup safety and robustness hardening. Backups and restores can no longer run at the same moment, so a scheduled backup, a manual backup, and a restore never overlap. We also made the Settings save shrug off unusual form input instead of erroring, finished the cleanup of network settings when you remove the plugin from a multisite network, and made activation safer around an old backup folder. No change to how your backups run.
- Security and cleanup hardening. Removing the plugin now fully cleans up after itself: settings, stats, and scheduled tasks all clear. Also quieted a repeated staging-folder log line on some hosts and corrected a few translation labels so the whole interface localizes. No change to how your backups run.
- Connection card leads with status. Once connected, Settings shows your connection status first, with Test Connection and Disconnect beside it; the credentials tuck behind "Change credentials".
- Backup Size Estimate is now a table with a proportion bar per component and a totals row, instead of a plain text list.
- Your Backups rows now lead with the date and size, with the file name below plus a one-click Copy. Restore is styled distinctly from Download so the two are easy to tell apart.
- Activity log filters are one-click chips (All, Success, Errors) with live counts, and long paths get a Copy button.
- Reset to Defaults moved into a clearly marked danger zone. Accessibility and contrast improvements throughout, and emoji removed from admin messages.
- Admin redesign and polish. Refreshed, accessible dark admin UI: blue status pills, gradient action buttons, a white "Start Backup Now" icon, a themed "Your Backups" table, and a tidied Logs toolbar (gradient "Apply", clearly-marked-red "Clear History"). Cosmetic only, with no change to backup, restore, or scheduling behavior.
- This edition is fully functional and free, with no feature locks. No account and no key: configurable retention is not capped, restore performs a full restore (database, plugins, themes, and uploads), and no telemetry, analytics or tracking of any kind goes to us. Since 5.49.0 this copy does ask bodholdtlabs.com whether a newer version exists, sending the version it is running and this site's address, and no site content or backup data ever reaches us. Email backup notifications are unchanged.
- Added an "External services" section to this readme documenting the Microsoft Graph and Microsoft OAuth calls. They were the only requests this copy made to anywhere other than your own site until 5.49.0 added the update check, which that section describes too.
- Housekeeping for WordPress.org guidelines: removed an admin-email footer credit line and lowered the admin menu position so it no longer sits among core items.
- Fixed: footer line missing on installs that run third-party plugins which globally blank the WP admin footer. Some plugins (e.g. Taxonomy CSV Import/Export) register `__return_empty_string` against `admin_footer_text` at priority 11 across every admin page, which silently wiped our v5.20.0 footer line (Bodholdt Backup for OneDrive v5.X.X · Documentation · Support). Our filter now runs at priority 99, so it executes last and the footer renders as designed regardless of any other plugin's behavior.
- UI: version moved out of the page title. The big "BODHOLDT BACKUP FOR ONEDRIVE V5.X.X" plugin-page heading is now just the product name. The version still appears, but now in the WordPress admin footer alongside Documentation and Support links, discreet and out of the way. Aligned with how WordPress core, WooCommerce, Yoast, and most established plugins handle this.
- Brand consistency follow-up. A leftover short-form "Bodholdt OneDrive" label in the WP admin sidebar menu entry wasn't updated during the v5.19.0 rebrand. It now reads "Bodholdt Backup for OneDrive" to match the rest of the plugin. No behavior change.
- Fixed: Setup Wizard never appeared on fresh installs. A function-scope bug in the dashboard renderer left the wizard's display gate (`$show_wizard`) undefined, which PHP 8+ surfaces as a Warning and silently coerces to null, so on a fresh install the wizard quietly didn't render, leaving new users with no guided onboarding (just an empty dashboard asking them to enter OAuth credentials with no setup hand-holding). The wizard now renders correctly as designed.
- Renamed to "Bodholdt Backup for OneDrive." Adopts the trademark-safe "for [destination]" naming pattern across all customer-facing surfaces (plugin header, admin menus, dashboard, email subjects, logs). Functionally identical, with no settings or backups affected.
- Setup wizard: improved "Testing on a local site?" callout. The advisory now recommends Local's "Live Link" feature (HTTPS public URL) as a fallback if Microsoft still rejects a `.local` redirect URI after enabling HTTPS, so testers have a clear path forward.
- Tested up to: WordPress 7.0. Verified compatibility with the current stable release.
- Honest claims. Softened a few storefront descriptions to be evergreen rather than time-specific ("accessibility-minded" rather than a literal compliance grade; "security-audited each release" rather than a specific score) so they stay accurate without periodic updates.
- wp.org Contributors handle updated to `bodholdtlabs` in preparation for wp.org plugin directory submission.
- Per-site backup folders. Each site now backs up to its own uniquely-named folder in your OneDrive (identified by a per-site ID), so multiple WordPress sites sharing one Microsoft account no longer write into the same folder. If a shared folder from an earlier version is detected, the plugin moves this site to its own folder and shows a one-time notice. Nothing is deleted and your existing backups stay untouched.
- Brand consolidation. Author byline, support email, and documentation links updated to Bodholdt Labs / bodholdtlabs.com. No behavior change.
- Fun + accessibility pass. Small delight and polish touches, matched to the Bodholdt Backup for Google Drive sibling.
- New: A running "backups protected" tally. The Backup Health card now shows how many backups you've made and the total size kept safe (e.g. "47 backups protected · 182 GB kept safe"). This counts off dedicated counters, so clearing your on-screen log history doesn't reset it.
- New: First-backup moment. Your very first successful backup now gets a bigger one-time celebration and a "Your first backup is done. Your site is officially protected!" message.
- New: Restore celebration. Finishing a restore now greets you with a gentle celebration and a "Site restored. Welcome back." message.
- Clearer destructive confirms. The "Disconnect OneDrive" and "Delete all backup log history" confirmations now spell out exactly what happens and what is NOT affected (your cloud backups are never touched).
- Warmer copy on the empty "Your Backups" state and the backup-complete message.
- Cross-plugin parity follow-ups.
- "Reset to Defaults" now restores a sensible Daily 3:00 AM backup (was previously "Manual only"; both backup plugins now reset to the same default). And a scheduled backup on a site that hasn't connected OneDrive yet now skips quietly, instead of emailing a daily "backup failed" alert.
- Internal consistency: the default backup schedule is now Daily for fresh installs (matching Bodholdt Backup for Google Drive). The one-click backup flow is unchanged.
- Steve Jobs / Grandma Test Pass: Rounds 2-5 (error states, settings parity, micro-interactions, visual polish). Brings the plugin to full feature/UX parity with Bodholdt Backup for Google Drive. Bundles three internal batches:
- *Error handling (R2):* clearer test-connection and restore-failure messages; the failed-backup notice now links straight to Settings and Logs; "Disconnect" now tells you how to fully revoke this site's access in your Microsoft account; a mistyped notification email is no longer saved silently; cancelling at Microsoft's consent screen is handled gracefully; backup-download errors are now friendly with a back link.
- *Settings (R3):* added an Hourly-schedule performance warning, hid the time picker when scheduling is set to Manual, added a server-side "Settings saved" confirmation, and clearer "Backups to Keep" and file-exclusion copy.
- *Polish (R4/R5):* reliable copy-to-clipboard (with fallback), a proper "Refreshing…" working state on the size estimate, and all admin brand colors moved onto the shared design-token system (so the warning color is now consistent across both backup plugins). Build pipeline excludes editor/OS cruft from the distributed zip.
- Fixed: Staging directory now has a graceful fallback when `/var/lib/bodholdt-staging/` isn't writable. Pre-v5.15.0 the plugin defaulted `BODHOLDT_OD_DIR` to `/var/lib/bodholdt-staging/onedrive/<random-suffix>/` with no fallback, a path that only exists on operator-prepared hosts. On any host without root access (most managed WordPress hosts, every local development environment, every WP.org reviewer install), the `mkdir()` call returned false, the next `fopen()` failed silently, and the backup or restore died mid-flight with the AJAX progress indicator simply disappearing at "Fetching Download link…", with no error to the user. v5.15.0 adds a two-tier staging resolution at plugin-load time: (1) `/var/lib/bodholdt-staging/onedrive/<suffix>/` if writable (preserves the post-2026-05-05 security model on operator-prepared hosts), and (2) a private `bodholdt-staging-onedrive-<suffix>/` directory in the system temp folder if `/var/lib/` is not writable (ephemeral, and also outside the web document root). Each fallback transition is logged via `error_log()` so operators on hardened hosts can see why the plugin moved off the primary. Both candidates sit outside the web root, and a staging path inside the WordPress install is rejected. Override with an absolute out-of-web-root path via `define('BODHOLDT_OD_DIR', '/var/lib/bodholdt-staging/onedrive/')` in wp-config.php to bypass the resolution entirely. Symmetric fix to the Bodholdt Backup for Google Drive plugin's v6.15.0 change (also shipped today). Plan §4.5.13.
- Fixed: Restore engine was silently dropping `wp-content/mu-plugins/`, `wp-content/languages/`, and WordPress drop-ins (object-cache.php / advanced-cache.php / db.php / maintenance.php). OneDrive's backup engine already picked up mu-plugins via the wider `core` scope walk (no v5.14.1 backup-side hotfix needed), but the restore engine only copied `themes/`, `plugins/`, and `uploads/` from the extracted backup to the destination's `wp-content/`. Result: any disaster-recovery restore landed a site without its must-use plugins (custom auth handlers, debug-log filters, multisite-shared utilities, etc.) and without its drop-ins (Redis object cache, full-page cache, custom DB layer, maintenance-mode override). The restore engine now treats `mu-plugins/` and `languages/` like the database (always copied, no user-facing scope toggle) and copies the four standard wp-content drop-ins when they're present in the backup. Surfaced 2026-05-17 night during the §4.5.10 GDrive end-to-end retest, applied symmetrically to OneDrive. Plan §4.5.15. (v5.14.1 was never shipped. That version number was reserved as the GDrive companion to this fix's mirror, and v5.14.2 is the first shipped restore-side fix on the OneDrive side.)
- Fixed: Restore engine no longer unconditionally drops your other Bodholdt backup plugin. Prior versions hard-skipped both `bodholdt-onedrive-backup` AND `bodholdt-google-drive-backup` directories during restore. The self-skip is necessary (a plugin can't safely overwrite itself mid-execution), but the sibling-skip meant customers running both plugins as belt-and-suspenders backup would lose one on every restore. The restore engine now skips the sibling only when it's currently network-active (i.e. running and could collide); installed-but-inactive siblings are restored normally. Plan §4.5.18.
- New: Unified 4-step setup wizard. The setup wizard has been rebuilt to a 4-step shape that mirrors the Bodholdt Backup for Google Drive wizard exactly: Welcome → Connect OneDrive (credentials inline, with a collapsible "How do I get these?" guide) → Schedule → Run Your First Backup with size estimate. The previous 3-step wizard ended on a "go elsewhere" link to the Settings tab, and new users routinely got lost there. The new wizard keeps everything in one flow, with the full 5-step OAuth setup guide reachable from the credentials step via a `<details>` panel. State persists across the OAuth round-trip (a new `bodholdt_od_wizard_step` option), so refreshing the page or returning from Microsoft mid-flow picks up where the user left off. Closes Steve Jobs pass Round 1. P1-C.
- New: Wizard returns to the dashboard after OAuth. Previously OneDrive's OAuth success redirected to the Settings tab, which meant the user had to find their way back to the dashboard before the wizard could continue. Now, when a user is mid-wizard, the OAuth callback redirects directly to the dashboard tab so the wizard resumes at the Schedule step automatically.
- New: New AJAX endpoint `bodholdt_od_wizard_save_creds`. Saves Client ID + Client Secret from the wizard's Step 2 and returns the Microsoft authorization URL with a CSRF state nonce. Byte-for-byte compatible with the existing "Authorize OneDrive Access" button on the Settings tab.
- New: "Set up" link on the Plugins page. After activating, a "Set up" / "Backups" quick-link now appears next to Activate/Deactivate in the WordPress Plugins list, giving you one-click access to the dashboard. Standard WP pattern; we should have shipped this in v3. Steve Jobs pass Round 1, P1-A.
- New: Friendlier setup-guide language. Wizard welcome copy and the OAuth Setup Guide on Settings have been rewritten to drop "Microsoft Azure app" / "app registration" jargon in favor of consumer-friendly framing ("free Microsoft developer account", "Microsoft Developer Portal"). Technical terms ("Client ID", "Client Secret", "Redirect URI") stay where they're necessary, but the welcome copy no longer reads like enterprise IT. P1-D.
- New: OAuth Setup Guide Step 5 is split into Save + Authorize. The old single Step 5 packed three actions ("copy, paste+save, authorize") into one line, and new users routinely missed the second click. Now Step 5 saves the credentials and Step 6 authorizes the connection. P2-D.
- New: Backup destination renders as a breadcrumb. The Settings tab's "Backup Destination" line now shows `Your OneDrive → Apps → Bodholdt Backup → <site>` instead of the raw `/Apps/Bodholdt Backup/<site>/` path. Easier for non-developers to map to their OneDrive web UI. P2-F.
- New: Backup Size Estimate auto-loads on dashboard. Mirror of GDrive's behavior: the estimate card now populates on dashboard visit instead of waiting for a manual "Calculate Size" click. P2-E.
- Fixed: Multisite retention manifest + retention setting were per-blog (architectural). The retention manifest (`bodholdt_od_backup_ids`) and the configured backup-retention count (`bodholdt_od_retention`) were stored as per-blog WordPress options, but the OneDrive folder they track is per-account and shared across every blog in the network. On multisite installs, a customer who activated the plugin from a non-main blog would hit a manifest scoped to that blog only; subsequent backup runs under a different blog context (e.g., from network admin, or scheduled cron under blog 1) saw an empty manifest, skipped the cap enforcement, and let the cloud grow silently past the configured retention. Both values are now stored as network-wide `site_option`s. A one-time on-upgrade helper merges every existing blog's manifest into a single network manifest (de-duped by Graph item ID, so no data loss) and promotes the main blog's retention setting to network scope. Customer impact: multisite users will see retention enforce correctly on the next scheduled backup; single-site users are unaffected (`site_option` transparently falls back to `option` there). Plan §4.2 v5.12.
- Fixed: Manifest seed could only run once per install (Bug C). The function that reconciles the manifest against the actual cloud folder was gated by `if ( ! empty( manifest_get() ) ) return;`, meaning seed only ran ONCE in the install's lifetime, the first time the manifest was empty. After that, no source of cloud↔manifest desync (lookup_item_id silent skip after upload, manual operator file ops, pre-v5.1.1 backups inherited at upgrade time, etc.) could ever self-heal. The function is now a reconciliation pass that runs every enforcer call: it lists the folder once, diffs against the known manifest, and appends only previously-unknown canonical-named cloud files. Healthy installs pay zero option writes per pass; leaky installs self-recover on the next backup. Plan §0a 2026-05-16 evening.
- Improved: Uninstall now cleans up the network-scoped options + transients introduced in v5.12 (manifest + retention). Previously these were left behind on plugin uninstall on multisite, harmless but untidy.
- Fixed: Retention seed-migration silently broken since v5.1.1. The `bodholdt_od_manifest_seed_from_folder()` function used `$orderby=createdDateTime` in its Microsoft Graph query, which Graph rejects on the `:/children` endpoint with HTTP 400 "Operation not supported." The function silently fell through and returned with an empty manifest, so installs with desynced manifests (caused by occasional `lookup_item_id` failures after upload) never self-recovered, and cloud backup counts could grow unbounded past the user-configured retention. Removed the unsupported `$orderby` parameter; the enforcer already sorts the manifest in PHP. Customer impact: customers whose backup count had drifted above their configured retention will see the cap re-enforce on the next scheduled backup run.
- New: HTTP-status logging in the manifest seed. Non-2xx Graph responses (token expired, throttled, unsupported parameter, etc.) are now logged to `error_log()` so future Graph-side regressions surface in WP_DEBUG instead of vanishing silently.
- New: Cross-engine collation portability. Backups taken on MariaDB 10.10+ (which defaults to the `utf8mb4_uca1400_*` collation family) now restore cleanly on MySQL 5.7 / 8.x and older MariaDB. The dump rewrites every `*_uca1400_*` collation to the closest portable equivalent (`utf8mb4_unicode_520_ci`, `utf8mb3_unicode_ci`, etc.) at emit time across CREATE TABLE / VIEW / TRIGGER / PROCEDURE / FUNCTION / EVENT statements. Set option `bodholdt_od_translate_collations` to 0 if you need byte-for-byte fidelity (e.g., restoring back to the same MariaDB version).
- New: Progress bar resumes on page reload. Refresh the WP admin tab while a backup is mid-run and the progress UI now picks up where it was: bar, status text, and poll loop all re-attach automatically from the server-side job-status transient. No more "did my backup die?" anxiety.
- New: Default-exclusion patterns for dev artifacts. `apply-bodholdt-*-v*.py`, `__pycache__/`, `*.pyc`, and `*.source.php` are now excluded by default. Pairs with the v5.10.0 readability probe. The probe defends against batch-poisoning regardless, but pre-excluding keeps the manifest tidy and the zip leaner.
- Fixed: Backup-engine drops Bodholdt-prefixed plugins/theme on symlinked-deploy patterns. Common deploy pattern (a separate clone of in-house plugins symlinked into `wp-content/plugins/`) used to silently drop those plugins from backup zips. Three independent root causes contributed; all three close in this release. Plan §4.5.6. 1. The relpath calc used `getRealPath()` + `substr($path, strlen(ABSPATH))`, which on symlinked subtrees returned paths outside ABSPATH and produced corrupted relpath entries (e.g., `oldt/bodholdt-wordpress/plugins/...`). Replaced with `RecursiveIteratorIterator::getSubPathname()` (iteration-depth-tracked, symlink-safe) plus a per-scope `relpath_prefix` (`wp-content/plugins/` etc.). 2. PHP's `RecursiveDirectoryIterator::hasChildren()` defaults to `$allowLinks = false`, so the iterator emits symlinked DIR entries but never descends into them. The entire symlinked subtree gets skipped silently. Fixed via a small RDI subclass (`Bodholdt_OD_FollowSymlink_RDI`) that overrides `hasChildren()` to pass `$allowLinks=true`. File-level symlinks (e.g. mu-plugins) were always picked up correctly because `isDir()` returns false for them. 3. `ZipArchive::close()` silently drops the entire ~500-file staged batch when even one staged file is unreadable (e.g., owner-only 0600 perms; `close()` returns false with a `Permission denied` warning, but PHP discards every legitimate sibling file in the batch alongside the bad one). Pre-validate readability with `@fopen($path, 'rb')` + `fclose()` before `addFile()`; unreadable files are skipped cleanly with optional `WP_DEBUG` logging. `addFile()` and `close()` return values are also checked + logged.
- Fixed: File-collector path corruption for files reached through symlinks. Same root cause as the symlink-relpath bug above. Files reached via a symlinked subtree (e.g., `wp-content/mu-plugins/bodholdt-*.php` on the same deploy pattern) landed in the zip at `oldt/bodholdt-wordpress/mu-plugins/` instead of the canonical `wp-content/mu-plugins/`. The `getSubPathname()` approach produces correct canonical paths regardless of symlink chains. Plan §4.5.7.
- Fixed: VIEW emission order regression in multisite-network mode. v5.9.0 emitted each blog's views at the end of that blog's iteration, BEFORE the network-shared group ran. Views that JOIN `wp_users` (network-shared) failed at CREATE time on restore because wp_users hadn't been created yet. All non-table schema objects (views, triggers) are now deferred to the absolute tail of the dump, after every CREATE TABLE across every blog + the network-shared group. Plan §4.5.8.
- New: Proper self-exclusion. The backup zip no longer includes the Bodholdt Backup for OneDrive plugin's own directory (Russian-doll prevention). Sibling Bodholdt plugins and the bodholdt-labs theme are deliberately INCLUDED so customers with multiple Bodholdt plugins get a complete backup. Self-exclusion uses canonical-relpath equality, not name-prefix substring matching.
- New: mu-plugins and other non-scoped wp-content children are now backed up. Previously the "core" scan-and-skip rule excluded everything under wp-content. Relaxed so only the three scope-mapped subtrees (`wp-content/plugins/`, `wp-content/themes/`, `wp-content/uploads/`) are skipped during the core scan; `wp-content/mu-plugins/`, `wp-content/languages/`, etc. now land in the backup at their canonical paths and restore to where they belong.
- New: Unreadable files are skipped cleanly instead of poisoning a 500-file batch. Previously, ZipArchive's deferred-read behavior meant a single 0600/0700 file (e.g., a forgotten dev artifact) could silently drop dozens of unrelated legitimate backups. The pre-`addFile` readability probe defends against this regardless of what's on disk.
- New: Multisite-network backup mode. The backup engine now auto-detects single-site vs multisite and defaults to a complete-network backup on multisite installs. Network mode walks every blog via `switch_to_blog()`, dumps each blog's content tables, then dumps the network-shared tables (`wp_users`, `wp_usermeta`, `wp_blogs`, `wp_blogmeta`, `wp_site`, `wp_sitemeta`, `wp_registration_log`, `wp_signups`) exactly once. Restoring such a backup recreates the full multisite layout.
- New: Per-blog mode (opt-in). Multisite admins who want per-customer subsite isolation can switch to per-blog mode in the network admin's Settings tab. Per-blog backups contain only that blog's content tables: no shared tables, no other blogs.
- New: Backup manifest (`bodholdt-manifest.json`). Every zip now includes a manifest at the zip root describing mode, plugin version, WP version, blog topology, tables dumped, and shared-table inclusion. The restore engine reads it first to verify compatibility.
- New: Restore-side mode compatibility check. The engine refuses to restore a multisite backup onto a single-site install (the wp_N_* tables don't belong) and warns when a single-site backup is being restored onto a multisite (data lands on current blog only).
- Single-site installs continue to behave exactly as before: no UI changes, no feature flag, no migration required.
- Database export entry function now accepts an optional `$mode` parameter for callers that want to force a specific mode regardless of saved settings.
- New: Full backup and restore with no account and no key. Every capability is available out of the box; there are no locks on the admin UI, on manual backups, or on scheduled backups.
- New: Retention is bounded only by a per-install sanity limit, so you can keep as many recent backups as your OneDrive has room for. The save handler, the settings input, and the retention enforcer all share that same bound.
- Fixed: Rename-bypass in the retention enforcer. Previous folder-listing + regex enforcer let a renamed backup file slip past the cap entirely. Replaced with an ID-manifest written at upload time, so renames can no longer bypass retention.
- New: One-time seed migration on upgrade.
- Last-success-timestamp watchdog with stale-alert.
- Out-of-web-root staging directory (fixes a 2026-05 SQL-dump exposure risk).
- Retention pagination + ordering fix, so folders with more than 200 backups now prune correctly.
- Settings save reliability fix.
- Email-delivery failure detection. Plain-text email fallback.
- Setup wizard, pre-restore snapshot, ZIP integrity verification, atomic locking, contextual help tabs.
- Initial Bodholdt-Engine rewrite. OAuth 2.0. Resumable Graph API uploads. AES-256-CBC + Sodium encryption.
Related reading: How WordPress backups to your own OneDrive work