Bodholdt Backup for OneDrive
WordPress backups to Microsoft OneDrive
Get Bodholdt Backup for OneDrive
Lifetime licenses available · 14-day money-back · Cancel anytime
Free
Free forever:
- 1 site
- Full plugin functionality
- Full restore + unlimited backup retention
- Community support
Solo
Everything in Free, plus:
- A paid license for 1 site
- Selective restore, so you choose what to restore (database, plugins, themes, uploads, core)
- Lifetime license available
- Email support while active
Pro
Everything in Solo, plus:
- 5 sites on a single license
- Slack & Discord notifications on backup success, failure, restore, and stale-alert events
Agency
Everything in Pro, plus:
- Up to 127 sites
- Priority email support while active
See full feature comparison →
| Feature | Free | Solo | Pro | Agency |
|---|---|---|---|---|
| Sites | 1 | 1 | 5 | 127 |
| Full plugin functionality | ✓ | ✓ | ✓ | ✓ |
| Full restore | ✓ | ✓ | ✓ | ✓ |
| Backup retention | Unlimited | Unlimited | Unlimited | Unlimited |
| Selective restore | — | ✓ | ✓ | ✓ |
| Slack & Discord alerts | — | — | ✓ | ✓ |
| Lifetime license option | — | ✓ | ✓ | ✓ |
| Support | Documentation | Email while active | Email while active | Priority email |
Start free with the Lite edition
Full automated backups and full restore, with unlimited retention, free forever. Upgrade to Solo for selective restore, or Pro for multi-site and Slack/Discord alerts.
Get Bodholdt Backup for OneDrive (free)
The free version is yours. No card, no license key. Enter your email and we will send you a download link.
Enter the 6-digit code sent to your email:
Didn't receive it? Check your spam folder, or click Resend Code.
Real screens from Bodholdt Backup for OneDrive
These are real admin screens, not mockups. Click any one to view it full size.
Same bulletproof backup system, powered by Microsoft OneDrive. Same beautiful 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 (Solo and up)
Cross-server migration installer
Resumable chunked upload with auto-retry
Guided setup wizard with an OAuth walkthrough
Vault Watch arcade, with a protection shield and a clean-backup streak that rewards every scheduled run
- 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 (Solo and up)
- Cross-server migration installer
- Resumable chunked upload with auto-retry
- Guided setup wizard with an OAuth walkthrough
- Vault Watch arcade, with a protection shield and a clean-backup streak that rewards every scheduled run
- 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, license key, 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.
- Your license is no longer removed when you upgrade. On single-site installs a one-time migration deleted the stored license key, status, and tier, quietly dropping paid installs to the free feature set. It now does nothing outside multisite, and it can be run repeatedly without harm.
- 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 on the Dashboard or Logs tab.
- Correct database recovery guidance. A nontransactional material table now always produces the InnoDB conversion advice in logs, webhooks, and email, even when its table name contains words such as token, quota, space, zip, or disk.
- 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 changes now commit and verify as one transaction or roll back exactly. Concurrent administrators can connect safely, stale OAuth callbacks are bounded and invalidated, and legacy credentials, tokens, and licenses cannot overwrite a newer valid change during upgrade.
- Restore rollback and storage protection. Pre-restore snapshots bind exact no-follow file copies to content digests, reject filesystem drift and link/type swaps, verify the complete private rollback image before mutation, and preserve a 64 MiB emergency disk reserve.
- Restore progress reconnects safely. A temporary maintenance-mode response no longer turns the progress screen red or stops polling. The page reconnects automatically, and final health failures expose only safe diagnostic codes rather than site or provider details.
- Recoverable database exports on MySQL and MariaDB. Dumps preserve the disk reserve, create dependent views in a safe global order, reject incomplete or ambiguous view graphs, and stop clearly when triggers, routines, or events cannot be represented instead of silently omitting them.
- Hardened packages, Free edition, cleanup, and privacy. Release builds reject unsafe source/ZIP content before atomic publication, and the Free build retains its required restore helper instead of fatalling on first load. Uninstall removes only OneDrive-owned state, operator notices require the backup capability, and diagnostics omit provider/customer identifiers and paths.
- Database recovery points are trustworthy again. Literal percent signs and serialized data round-trip byte-for-byte. Exports use one isolated InnoDB consistent snapshot, and non-transactional tables or metadata omissions stop the backup clearly.
- Migration and restore fail safely. Imports require usable schema and data, multisite domain/path mappings are preserved, generated configuration is verified, and SQL/manifest secrets live in a protected key-bound hidden directory. Large ZIP members receive streamed size/CRC and path/type checks.
- Operations and settings are honest. Owner-token leases survive options-table restore, mandatory members and requested files are verified before upload, all webhook triggers can be disabled, weekly schedules retain their weekday, multisite reset is complete, and uninstall removes plugin transients plus OneDrive rows from every legacy subsite log table without removing Google Drive rows. Missing subsite arcade tables are skipped without database errors.
- Restores you can trust, right on the screen. Restoring an incremental backup over your existing site no longer reports a false "nothing was restored", and any compatibility caution now appears on the restore screen itself, not only in the log.
- Migrations that finish the job. Moving a multisite network keeps every subsite on its correct address, saved nested settings survive a domain change, and backups taken on newer MySQL versions restore cleanly onto older database servers.
- Protected recovery evidence. A successful migration removes its database file and manifest. A failed migration retains them for retry and diagnosis; 5.32.1 moves them into a protected random directory from extraction onward. Removing the plugin from a large multisite network now cleans every site, not just the first hundred.
- Notifications and scheduling polish. The "Send Test" button now tests the webhook address you just typed, "Reset to Defaults" truly resets everything, a weekly schedule no longer triggers a daily false "backups are stale" alert, and your backup keeps its chosen time through daylight saving changes. Plus a long list of smaller reliability, safety, and translation fixes. Nothing about how your everyday backups run changes.
- Migrations you can trust on any host. Moving a site to a new server now keeps every table even when your host uses a custom database prefix (common on managed hosting), so a migration no longer lands on a blank WordPress setup screen. Multisite network settings carry across too, and the one click site mover deletes its database file the moment it finishes instead of leaving it in your web root.
- Backups and restores that tell the truth. A backup now stops and warns you if the database export was cut short, instead of quietly saving an incomplete copy. A restore that had nothing to put back tells you which parts were empty rather than showing a cheerful "all done". And an incremental backup is clearly labeled, so restoring one on its own can never rebuild a broken site by surprise.
- Settings you can save any time. You can now save your schedule and other settings before you connect OneDrive, and saving before you enter a license no longer clears your Slack or Discord alert choices. Activating a valid key while our server is briefly unreachable now says exactly that, rather than claiming your key is wrong, and a slow licensing server no longer makes your Plugins screen hang.
- Calmer by default, and much more. The arcade now has its own settings and confetti is off unless you turn it on, the nightly watchdog reliably warns you if backups go stale, and there is a long list of smaller fixes and tidy ups. Nothing about how your everyday backups run changes.
- Reliability and safety hardening (QA round). Saving Settings now reliably persists every change, restores report their status honestly, the database export stops rather than truncating on an error, and automatic backups re arm correctly after the plugin is switched off and on. Plus a range of smaller migration, retention, and cleanup fixes. No change to how your everyday backups run.
- 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 migrations. The standalone migration installer now works correctly on modern PHP (8.1 and newer), 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, generated columns and special column types (BIT, spatial) back up and restore correctly, and every row is captured even in unusual tables.
- 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, tidier file-exclusion matching, 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 and leave you with an inconsistent archive. We also tightened how backup file names are shown in your Backups list, 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 day to day.
- Security and cleanup hardening. Tightened the Slack/Discord webhook so notifications only ever reach real Slack and Discord addresses. Hardened the bundled one-click migration installer so its dormant copy can never run in place. Removing the plugin now fully cleans up after itself: settings, stats, gamification data, and scheduled tasks all clear. Also quieted a repeated staging-folder log line on some hosts, corrected a few translation labels so the whole interface localizes, and refreshed some onboarding wording. No change to how your backups run.
- Arcade engine polish (shared). Protection XP now counts correctly when several events land at the same moment, the on-screen HUD labels are fully translatable, and celebration cards always stay within the screen. No change to how your backups run.
- The Vault Watch. Your backups now earn their keep. A calm HUD shows your protection level and an Unbroken Watch streak that grows every night a scheduled backup runs clean, with a live Shield gauge for how protected you are right now. Trophies mark the milestones. It stays quiet by design: sound is off, the mood is steady rather than loud, and one switch turns it off. Nothing about how your backups run changes.
- 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.
- Privacy & transparency. Added an "External services" section to this readme, and self-hosted the admin font so no request is sent to Google Fonts.
- Renewal UX. If a license lapses, the Plugins screen now shows a "renew to install this update" note instead of a failed update.
- Brand-string cleanup (author/URI, translation template). No functional or backup-behaviour change.
- Corrected support and upgrade links. The catch-all backup-failure hint and the contextual-help "Email Support" link now point to [email protected], and the in-admin upgrade prompts link to the product page instead of the account portal.
- Accurate tier copy. Upgrade hints and the readme now match what each tier actually includes: retention is unlimited on every tier, including Free, and selective restore is a Solo feature (not Pro). Documentation and labelling only, with no functional or backup-behaviour change.
- More cyan polish. "Start Backup Now" cloud icon is now pure white; the "Your Backups" table (column headers, file/Download/Restore icons, row hover) now uses the OneDrive blue instead of grey/purple; in-app "Upgrade to Pro" notices and the troubleshooting path now use the blue accent.
- Logs toolbar. The "Apply" filter button is now a gradient button (Clear History was already red).
- Admin polish. Dashboard "Success" and "Connected" status pills now use the OneDrive blue (matching the theme); "Calculate Size", "Test Connection", "Send Test Email", and "Send Test Notification" are now gradient buttons; and "Reset to Defaults" is now clearly marked red as a destructive action.
- Distinct cyan admin + consistent buttons. The OneDrive admin is now explicitly themed in its signature cyan (matching its product page, so it reads as distinct from the green Google Drive admin), and the "Start Backup Now" button uses the same gradient as "Save Settings."
- Admin styling now sourced from the shared Bodholdt Labs design system. The admin UI consumes the canonical `bod-admin.css` (the same dark-admin stylesheet shipped across Bodholdt Labs plugins) instead of a plugin-local copy, so the look stays consistent and future polish lands everywhere at once. Visually unchanged save for one accessibility fix (purple text colour bumped to a WCAG-AA contrast). No functional or behavioural change.
- Pro edition with licensed auto-updates. This is the paid, off-directory build of Bodholdt Backup for OneDrive, distributed by Bodholdt Labs. It enables in-dashboard automatic updates delivered from the Bodholdt Labs licensing server when a valid license key is active (license key + site URL + product identifier only; never site content or backup data).
- Selective restore available on every paid tier, including Solo. Pick exactly which components (database, system files, themes, plugins, uploads) to restore. Retention is uncapped on all tiers (bounded only by a per-install sanity limit).
- Slack/Discord webhook notifications for backup success/failure, restore-initiated, and stale-backup watchdog events (Pro and Bundle tiers).
- Hardening (parity with the directory edition): first-party download streaming now uses the WordPress HTTP API; admin-notice scripts moved to `wp_add_inline_script`; staging/uninstall paths resolved via `wp_get_upload_dir()`; installer CSRF/lockfiles relocated to the system temp directory with a table-prefix validation guard; all request inputs run through `wp_unslash()` before sanitization.
- Added an "External services" section to this readme documenting the Microsoft Graph / Microsoft OAuth endpoints, the Bodholdt Labs licensing/update server, and the WordPress.org secret-key (salt) request used during cross-server migration.
- 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 tier badge ("FREE" / "Pro" / etc.). 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 and most established plugins handle this.
- Brand consistency follow-up. Two leftover short-form "Bodholdt OneDrive" labels weren't updated during the v5.19.0 rebrand: the WP admin sidebar menu entry, and the License sub-page heading. Both now read "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.
- Licensing fix + brand consolidation. License activation, validation, and auto-updates now point at the correct server (bodholdtlabs.com), so license keys validate reliably (the previous host redirected and dropped the request). Author byline, support email, and account links updated to Bodholdt Labs / bodholdtlabs.com.
- Retention tier fix. Solo and Agency licenses now get their correct backup-retention limits (Solo 50, Agency unlimited up to the system maximum), and Agency now includes selective restore and Slack/Discord notifications. Previously these tiers fell back to the Free 10-backup limit because the entry tier is issued as "default" and the top tier as "agency". Both are now recognized. Pro and Bundle are unchanged.
- 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.
- Accessibility: Celebrations respect "reduced motion." All confetti animations now honor your operating system's "reduce motion" setting (the JS canvas confetti previously ignored it).
- 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" / "Migration Tools" / 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 three-tier fallback hierarchy resolved 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), (2) `sys_get_temp_dir() . '/bodholdt-staging-onedrive/<suffix>/'` if `/var/lib/` isn't writable (system temp, ephemeral but still isolated from the web document root), (3) `wp-content/uploads/.bodholdt-staging-onedrive/<suffix>/` as a last resort with three hardening layers (an `.htaccess` deny-all rule for Apache, an `index.php` returning HTTP 403 as defense-in-depth, and a docs-recommended nginx `location` deny rule in the FAQ). Each fallback transition is logged via `error_log()` so operators on hardened hosts can see why the plugin moved off the primary. Override via `define('BODHOLDT_OD_DIR', __DIR__ . '/your/custom/path/')` 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 with 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. The version number was reserved as the GDrive companion to this fix's mirror; 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. 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, 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, mirroring the v5.8.0 license-storage migration. 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;`, which meant 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.8 (license keys) and 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, so 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 (Free-tier-included). 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, with no shared tables and 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, with no UI changes, no feature flag, and 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: Multisite-network compatible. License state (key, tier, status, last-seen-valid timestamp) is now stored as a network-wide site option. A customer who activates a Solo license on blog 1 of a multisite network has the license recognized automatically on every other blog. One-time migration runs on upgrade. Existing per-blog license data is copied to network scope.
- Single-site installs are unaffected (site_option falls back to option there).
- New: Slack and Discord webhook notifications (Pro+). One URL field, auto-detected provider. Per-event toggles for backup success, backup failure, restore initiated, and watchdog stale-alert. Send-Test-Notification button in Settings.
- New: Helper `bodholdt_od_notifications_allowed()` returns true only for Pro/Bundle. Filterable via `apply_filters( 'bodholdt_od_notifications_allowed', $allowed, $tier )` for one-off overrides.
- Failure notifications include the same auto-diagnosis hint as the email notification (token / quota / disk / network).
- New: Free tier is now the default experience. No license required for full backup + restore at the Free cap (10 retained).
- New: Tier badge inline in the plugin header (Free / Solo / Pro / Bundle).
- New: Lapse banner. When a previously-active license becomes invalid, a dismissible banner reminds you to renew while backups continue at Free-tier limits.
- New: Soft-degrade on lapse. Backups keep running even with an invalid license. Retention auto-clamps to 10; selective restore disables. Manual + scheduled backups continue.
- Removed: Three hard-locks that previously blocked the admin UI, manual backups, and scheduled backups on non-valid licenses.
- New: Tier-gated selective restore. Pro and Bundle customers can choose what to restore (database, plugins, themes, uploads). Free + Solo restore everything.
- New: Inline "Upgrade to Pro" hint shown next to disabled scope chooser on Free + Solo.
- Server-side defense: restore handler coerces submitted scope to full on tiers without selective access.
- New: Tier-aware retention cap helper (`bodholdt_od_retention_cap`). Free=10 / Solo=50 / Pro=100 / Bundle=100. Save handler clamps; settings input cap is dynamic.
- New: Over-cap inline notice when a previously-saved retention value exceeds the current tier cap (e.g., after a downgrade).
- New: Pre-flight `force_refresh()` before retention enforcement so tier downgrades take effect within one backup, not 12 hours later.
- Licensing client now captures `tier_key` from server responses.
- 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. Folders with more than 200 backups now prune correctly.
- Settings save reliability fix.
- Email-delivery failure detection. Plain-text email fallback. Migration key now included in success emails.
- Setup wizard, selective restore, 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. Migration installer.