=== EffortLess Backup And Restore ===
Contributors: domclic
Donate link: https://id7.dev/donate/
Tags: backup, restore, database, migration, multisite
Requires at least: 6.2
Tested up to: 7.1
Stable tag: 1.0.25
Requires PHP: 7.4
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html

Automatic daily backups of your database and files, kept for 7 days, plus an on-demand instant backup and one-click restore.

== Description ==

A straightforward backup and restore plugin:

* One backup automatically every day, taken through WordPress's own scheduler (WP-Cron), with a fallback check on admin page loads so a low-traffic site doesn't silently miss a day.
* An "Instant Backup" button on the plugin's Backups page for an on-demand backup at any time.
* Every backup — automatic or instant — is kept for 7 days, then automatically deleted to save disk space.
* Restoring a backup replaces the current database and `wp-content` files with that backup's contents. A safety backup of the current state is taken automatically immediately before any restore.
* Each backup can also be downloaded (database dump and files archive separately) for an offline copy.
* No shell access or external service required — the database dump is generated entirely in PHP, and files are archived with PHP's own zip extension.
* On WordPress Multisite, one backup and restore covers the entire network at once (see the Multisite FAQ entry below) — managed from Network Admin, Super Admin only.

== Installation ==

**Single site:**

1. Upload the plugin folder to `/wp-content/plugins/`
2. Activate the plugin through the "Plugins" menu in WordPress
3. Go to the new **Backups** menu item in the admin sidebar
4. Click **Backup Now** to take your first backup immediately, or simply wait — one is taken automatically within 24 hours
5. Under **Settings** on the same page, automatic daily backups are on by default; untick the checkbox and save if you only want manual, on-demand backups

**Multisite:**

1. Upload the plugin folder to `/wp-content/plugins/`
2. From the Network Admin's **Plugins** page, **Network Activate** the plugin — do not activate it on an individual site (see the Multisite FAQ entry below for why)
3. Go to the new **Backups** menu item under **Network Admin**
4. The rest is identical to the single-site steps above — one backup, one restore, covering every site on the network at once

== Frequently Asked Questions ==

= Where are my backups stored? =

Directly inside `wp-content/`, in a folder with a randomly generated name, protected by a `.htaccess` rule that denies all direct web access on servers that honor it. Backups are never linked to directly from the admin page — downloading one always goes through a password-protected (nonce and login) handler.

= What exactly gets backed up? =

The entire database (every table, not just WordPress's own — any custom tables from other plugins are included), and the entire `wp-content` directory (themes, plugins, uploads, and anything else placed there), excluding this plugin's own backup storage folder.

The database is dumped table by table, not as a single all-or-nothing snapshot — on a very active site, a write landing in one table right as its neighbor was already dumped (for example, a new post saved between the `wp_posts` and `wp_postmeta` tables being dumped) could in principle leave a backup with the two very slightly out of step. This is normal for a lightweight, dependency-free backup tool and not something to worry about on an ordinary site.

= What happens when I restore a backup? =

The files backup is extracted back over `wp-content`, overwriting the current files, and the database dump is replayed, replacing every table's structure and data with what was captured at backup time. This is a significant, hard-to-partially-undo action — a fresh backup of your site's current state is taken automatically right before a restore begins (unless the most recent backup is only a few minutes old already, in which case that one serves the same purpose), so you can restore back to "just before this restore" if needed. If that safety backup itself can't be taken (for example, the disk is full), the restore is aborted before touching anything, rather than proceeding with no way back.

Only one backup or restore can run at a time — the Backups page shows a notice and disables "Backup Now" and "Restore" while one is in progress, and automatically re-enables them (refreshing the page) once it finishes.

A restore runs in the background in small steps rather than one single request, so it can complete even on a host that limits how long a request may run — the Backups page shows its progress and updates on its own when it finishes; there is no need to keep a browser tab open or stay logged in for it to continue. It isn't wrapped in one all-or-nothing database transaction (structural changes like `CREATE TABLE` commit immediately in MySQL regardless of any transaction). While a restore is running, the site's public pages (and REST API) show a brief "back shortly" message instead of staying live — this prevents a visitor's activity (an order, a comment, a form) from being silently lost when the restore replaces the database moments later. If the backup file being restored turns out to be truncated or corrupted, the restore stops with a clear error instead of silently skipping the missing part.

= If I restore a backup, does it remove files that didn't exist when the backup was taken? =

No. Restoring extracts the backup's files archive back over `wp-content`, which overwrites any file the backup contains and adds back any file it deleted — but a file created *after* the backup (for example, a new plugin installed since then) is left in place, since it simply isn't part of what's being restored. The database is fully replaced either way, so a plugin added after the backup point would show as installed on disk but not activated in the (restored) database.

= Can I use this to move my site to a new server? =

The files archive and database dump can both be downloaded from the Backups page. Restoring on a different WordPress install isn't a built-in one-click feature in this version — this plugin is built for keeping (and restoring) a site's own recent history, not general-purpose site migration.

= Does this work on Multisite? =

Yes, and it always covers the whole network at once — a Multisite network's sites all share one database (so the database dump already includes every site's tables, not just one) and one `wp-content` directory, so there is no meaningful way to back up or restore "just one site" separately from the rest.

Because of that, this plugin must be **Network Activated**, never activated on an individual site. Network Activating moves its admin page to Network Admin → Backups, and requires a Super Admin (`manage_network_options`) rather than an ordinary site Administrator — restoring here overwrites every site on the network, which a single site's own Administrator must never be able to trigger. If it's ever activated on just one site by mistake, a notice on that site explains this and points you to Network Activate it properly instead; it doesn't do anything from that per-site activation.

= I configured everything and no backups are appearing. What should I check? =

WordPress's scheduler (WP-Cron) only runs when a page on your site is visited, so a very low-traffic site can take a little over a day for its first automatic backup to appear — click **Backup Now** in the meantime for an immediate one. Also confirm your host has the PHP zip extension enabled; without it, the Backups page shows a notice and the files portion of any backup will fail.

== External services ==

This plugin does not call any external service. Every backup is generated and stored entirely on your own server.

== Changelog ==

= 1.0.25 =
* Internal: cleared the two remaining coding-standard warnings in the codebase (both about how WordPress hook names are invoked — one is this plugin's own hook name held in a variable, the other is a WordPress core hook this plugin correctly participates in rather than owning). No behavior change.

= 1.0.24 =
* Fix: hardened the database backup format against a table or column with an unusual name (containing a backtick character) — such a name is now correctly escaped everywhere it's written into the backup file, instead of potentially producing an invalid or corrupted database dump.

= 1.0.23 =
* Fix: the files archive's entries did not mark themselves as using UTF-8 filenames, which the ZIP format otherwise assumes means an old, limited text encoding from the 1980s. This plugin's own restore process was not affected, but a backup downloaded and opened with some other, stricter unzip tools could show non-English filenames (Chinese, Japanese, accented European names, etc.) as garbled text instead of extracting them correctly. Every file the plugin now archives is explicitly marked as UTF-8, matching how modern ZIP tools already work.

= 1.0.22 =
* Enhancement: the "in progress" message on the Backups page and the notice shown when you try to start something while another operation is already running now say exactly what's happening — "a backup", "a restore", or, when you click Restore and a fresh safety backup is taken first, "a backup, to be followed automatically by a restore" — instead of the vaguer "a backup or restore" used for every case before.

= 1.0.21 =
* Fix: found the actual, root cause of the long-running "Permission denied" failure while backing up files, thanks to a clearer error message the previous release's rewrite happened to surface. It was never a problem writing the backup archive at all — it was one single file inside another installed plugin's own bundled dependencies that this site's server would not let the backup process read. That one unreadable file is now skipped, with a note in the Activity Log identifying it, instead of stopping the whole backup.

= 1.0.20 =
* Fix: on the same host as the previous release's fix, the "Permission denied" failure while writing the files archive turned out to follow the write location it was moved to as well — even a folder completely outside the site's own files, on the very first archive ever created there. That rules out the security-scanner theory the previous fix was based on and points instead at something in how the server's ZIP-writing software finishes an archive on that particular host's storage, which isn't something a plugin can work around from the outside. The files archive is now built with its own dedicated, self-contained code instead of relying on that ZIP-writing software at all, using the same simple, reliable read/write approach that has never had a problem writing the database part of a backup on any host, including this one.

= 1.0.19 =
* Fix: identified the actual cause of the recurring "Permission denied" failure on one host, from real diagnostic data rather than a further guess — every standard check (ownership, permissions, free disk space) came back completely normal, yet only the files archive specifically was ever affected; the database part of the same backup, right next to it, never failed once. That pattern points to something inspecting archive files in particular, most likely a security scanner watching the site's own folder, and the fix is to no longer build the archive there in the first place: it's now assembled in the server's general temporary storage (outside the site's own folder entirely on most hosts) and only moved into its final, permanent location once it's completely finished.

= 1.0.18 =
* Enhancement: for the file-writing error that has recurred on one particular host across several previous releases despite retrying — that host's exact cause still isn't confirmed, and continuing to guess at fixes without more information isn't productive. The Activity Log now records the concrete facts needed to diagnose it for good if it happens again: the affected file and folder's permissions and ownership, whether they're actually writable, how much disk space is free, and the server's PHP details — no code behavior changes otherwise.

= 1.0.17 =
* Fix: on the same host that had been hitting the transient file-permission error, the retry added in 1.0.15/1.0.16 gave up on the first occurrence specifically at the finishing step, instead of getting the fair retry it was meant to. The whole writing step is now retried as one unit rather than only part of it, so it can actually recover from this the way it was designed to.

= 1.0.16 =
* Fix: the retry added in 1.0.15 for a transient file-permission error could itself crash a backup outright with a fatal PHP error, on PHP 8. Reusing the same internal zip object across a failed-then-retried attempt is not safe on PHP 8 — reported and confirmed from a live site. Fixed by using a completely fresh attempt each time instead of reusing one that already failed.

= 1.0.15 =
* Fix: reported from a live site — a backup could fail immediately with "Could not finish writing the files backup archive", even though the earlier steps that just ran had already written to disk without any problem, which pointed away from disk space despite that being the message shown. The site's own server log showed the real cause: "Permission denied" on a file that had just been created moments earlier in the same request — the signature of a security scanner briefly locking a new file to inspect it, common on shared hosting. Writing to the backup archive is now retried a few times before giving up, and every failure message now includes the exact underlying error instead of a guess, so the real cause is visible without needing separate access to the server's own error log.

= 1.0.14 =
* Feature: the site's front end (and REST API) now shows a plain "back shortly" message for the duration of a restore, instead of staying live. A restore replaces the database and files while running, sometimes across several minutes on a slower host — without this, a visitor could place an order, leave a comment, or load a half-updated page during that window, and any such activity would simply be lost the moment the restore finishes. This does not apply to backups, which never touch the live site's content and continue to run with the site fully accessible.

= 1.0.13 =
* Fix: backups and restores now run in resumable pieces, so they complete on hosts that cap how long a single request may run. Previously a large backup simply could not finish on such a host — confirmed on a live site whose limit was 60 seconds, with the setting that would normally raise it disabled by the host, so no amount of scheduling could have helped. Each piece now saves its exact position and the next one continues from there, for as many pieces as it takes.
* Fix: a restore could previously be left half-applied and unable to continue, in two separate ways — it could stop at a moment when WordPress itself could not start (so nothing was left running that could resume it), and it logged out the browser whose page was driving it forward. Restores now only ever pause at points where the site still works, and can be carried to completion by the page that started them even after being logged out.
* Fix: a failure while writing a backup (a full disk, most commonly) is now detected and reported instead of producing a truncated archive that was listed as a usable backup.
* Fix: the plugin's own working state (whether a backup is running, and how far along) is now kept in a file rather than in the database, so a restore — which replaces the entire database — can no longer corrupt or erase the record of itself while running. This also fixes a successful restore sometimes reporting itself as partially failed.
* Fix: the Activity Log section always appears on the Backups page now, showing "nothing logged yet" until there is something to show, instead of being invisible until the first backup ran.
* Enhancement: progress is now reported in much finer detail — per database table and row count, and per batch of files — both on the page and in the Activity Log.
* Note: a restore no longer overwrites this plugin's own files. The installed version is kept, since replacing the code that is running the restore part-way through is unsafe.

= 1.0.12 =
* Fix: reported from a live site — a backup's database and files could finish writing to disk (visible via FTP) while the Backups page kept showing "running" indefinitely, with the backup never appearing as available. This happens when the server itself cuts off a backup or restore partway through finishing up, which previously wasn't detected until an internal 2-hour safety valve. Backups and restores now report their progress in much smaller steps (per database table, per batch of files, per batch of SQL statements during a restore), so an interruption like this is now detected within a few minutes instead, and is reported as a clear error — including in the Activity Log — instead of just leaving the page stuck.
* Enhancement: the busy notice now shows real progress within each stage (e.g. "Dumping database — table 12 of 40", "Compressing files… (300 so far)"), not just which stage it's on.

= 1.0.11 =
* Fix: the 1.0.10 Activity Log section didn't appear on the Backups page at all until at least one backup or restore had run since upgrading — it now always appears, showing a plain "nothing logged yet" message until then, so it's clear the feature is there rather than looking like it never shipped.

= 1.0.10 =
* Feature: an Activity Log now appears on the Backups page, showing exactly what this plugin has done — when a backup or restore started, each stage it reached, and the full error message if one failed. This is visible right on the page even on hosts where the server's own PHP error log isn't accessible to the site owner.
* Feature: the busy notice now shows what stage a running backup or restore is currently at (e.g. "Dumping database…", "Compressing files…"), instead of just "running", so a genuinely stalled step is visible rather than looking identical to normal progress.
* Fix: the safety net that automatically clears a stuck status after a PHP fatal error was being registered in the wrong request (the one that starts a backup/restore, not the one that actually runs it) ever since backups/restores started running in the background in 1.0.7 — so it never actually caught anything. It's now registered in the request that does the real work, in both the normal background path and the 1.0.9 fallback path, and a caught fatal error is now also written to the new Activity Log with its full message.

= 1.0.9 =
* Fix: on some hosting setups, the request WordPress uses to start a backup/restore running in the background never actually reaches the site at all (a firewall or CDN blocking the site from calling itself, or a security setting some hosts apply) — this behaves identically for every WordPress plugin that relies on WP-Cron, not something specific to how this plugin schedules it, and previously left the busy status stuck indefinitely on hosts affected by it. The Backups page now detects this after a short, fair wait and runs the operation directly as a guaranteed fallback, so a backup/restore always completes regardless of whether that background request is able to reach the site.

= 1.0.8 =
* Fix: 1.0.7's background dispatch could still leave the Backups page showing "running" for several minutes with nothing actually happening on some hosting setups. The request that starts the background job now also retries automatically every few seconds while the busy status is showing, so a dropped first attempt corrects itself quickly instead of waiting on unrelated site traffic. (See 1.0.9 for the fuller fix — this release narrowed the problem but didn't fully solve it for every host.)

= 1.0.7 =
* Fix: clicking "Backup Now" or "Restore" ran the entire backup/restore inside that very click's own request — long enough on a real site to exceed a host's own request time limit, which could surface as the click itself failing outright (for example, an Apache 503 "Service Unavailable") well before the operation could finish. Backups and restores (including the automatic safety backup taken before a restore) now start in the background instead: the click returns immediately, and the Backups page updates on its own once the operation finishes. Replaces 1.0.6's manual "Clear stuck status" button, which is no longer needed.

= 1.0.6 =
* Fix: on some hosts, a large backup or restore could be killed outright by a server-side wall-clock time limit (rather than by PHP itself), which skipped this plugin's own cleanup and left the Backups page showing "a backup or restore is currently running" indefinitely, with no error, until an internal 2-hour safety valve finally cleared it. A PHP-level fatal error (for example, running out of memory) is now also caught and cleared immediately via a shutdown handler, and the Backups page now offers a manual "Clear stuck status" action after 10 minutes for the cases a server-side kill can still cause, so an admin never has to wait out the full 2 hours.

= 1.0.5 =
* Fix: the Backups page could trigger a PHP "Array to string conversion" warning (visible in the server error log) while a backup or restore was running. The busy indicator added in 1.0.4 passed its status value straight into WordPress's `disabled()` — which is only meant to receive true/false — instead of the plain yes/no it actually needed. Reported from a live site; reproduced and confirmed fixed with the exact same warning text and line number before shipping.

= 1.0.4 =
* Feature: a running backup or restore is now shown on the Backups page (with a busy indicator), and "Backup Now"/"Restore" are disabled until it finishes — and a second backup or restore can no longer be started while one is already running, including the daily automatic backup overlapping a manual one.
* Fix: if the automatic safety backup taken right before a restore fails, the restore is now aborted instead of proceeding without it — proceeding would have silently removed the one undo path this plugin provides.
* Fix: the database dump's table name and row queries now use `$wpdb->prepare()`'s identifier placeholder instead of manual escaping.
* Fix: a truncated or corrupted backup file is now detected and reported during restore instead of silently skipping its unfinished final statement.
* Fix: a backup that fails partway through (after writing its database dump or files archive, but before finishing) no longer leaves orphaned files behind indefinitely — they're now cleaned up automatically.
* Fix: the low-traffic-site missed-backup check is now correctly shared network-wide on Multisite, matching this plugin's other shared settings.
* Hardening: the Download and Delete links are now bound to the specific backup they act on.

= 1.0.3 =
* Fix: two `set_time_limit()` calls (in the backup and restore engines) now also suppress PHP_CodeSniffer's `Squiz.PHP.DiscouragedFunctions` warning, alongside the WordPress-specific codes already suppressed there.
* Reviewed and documented a WordPress.org Plugin Check warning (`UnescapedDBParameter`) on the restore engine's SQL-replay query: replaying a complete, previously-generated backup statement isn't expressible through `$wpdb->prepare()` or `esc_sql()` without corrupting the restored data, so this is an accepted, justified finding rather than a bug — see the code comment at the call site for the full reasoning.

= 1.0.2 =
* Fix: the "missed automatic backup" self-heal check (for low-traffic sites where WordPress's own scheduler might not fire on time) was hooked to fire on every single admin request, not just when actually viewing this plugin's own Backups page. In practice this meant clicking "Backup Now" created an extra, unwanted "Automatic" backup alongside the instant one every time (since `admin-post.php`, which is what "Backup Now" submits to, itself also triggers this same check before running your action) — and two of those could appear at once right after first activation if two admin requests landed close together before the throttle kicked in. It now only ever runs when the Backups page itself is loaded.

= 1.0.1 =
* Renamed from "EffortLess Backup & Restore" to "EffortLess Backup And Restore" (matching the elbar prefix more clearly).
* Feature: full Multisite support. Network Activate the plugin and manage it from Network Admin → Backups — one backup and one restore now covers the entire network (every site's database tables, since they all share one database, and the whole shared wp-content directory) in a single operation. Restricted to Super Admins (manage_network_options); a per-site Administrator can no longer access it, and activating it on an individual site (instead of network-wide) shows a notice instead of any functional menu. The backup storage location no longer depends on which site's context happens to be active when it's resolved, closing a possible inconsistency on networks with per-site upload paths.

= 1.0.0 =
* Initial release: automatic daily backups with a 7-day rolling retention, an instant on-demand backup button, one-click restore (with an automatic safety backup taken first), and per-backup database/files downloads.
