| 开发者 | mslepko |
|---|---|
| 更新时间 | 2026年10月20日 06:42 |
| PHP版本: | 8.0 及以上 |
| WordPress版本: | 7.1 |
| 版权: | GPLv2 or later |
| 版权网址: | 版权信息 |
/wp-content/plugins/rootscope-remote-site-manager directory, or install the plugin through the WordPress plugins screen directly.Log in to your Remote Site Manager dashboard at https://wp-admin.online, add a new site, and you'll receive your API Key and API Secret.
The plugin automatically syncs data once daily. You can also trigger a manual sync from the settings page.
Yes. All communication between your site and https://wp-admin.online is encrypted via HTTPS and authenticated using HMAC signatures.
Login and failed-login events record the originating client IP. By default, the plugin uses REMOTE_ADDR directly, and only consults forwarded headers (X-Forwarded-For, X-Real-IP) when REMOTE_ADDR is a private or reserved address (i.e. the request reached PHP from a local reverse proxy).
If your site sits behind a public reverse proxy (load balancer, CDN, etc.), define one or both of the following constants in wp-config.php so the plugin knows which upstream addresses are trusted to set forwarded headers:
define('RSADMIN_TRUSTED_PROXY_IPS', '203.0.113.0/24, 198.51.100.10');
Accepts a comma-separated string or an array of IPs/CIDR ranges (IPv4 and IPv6). When REMOTE_ADDR matches an entry, the plugin walks X-Forwarded-For right-to-left and returns the first IP that is not itself a trusted proxy — this prevents an attacker from spoofing the leftmost value.
define('RSADMIN_TRUSTED_CF_IPS', '173.245.48.0/20, 103.21.244.0/22');
Required if you rely on Cloudflare's CF-Connecting-IP header. The plugin only honors that header when REMOTE_ADDR is in one of the configured Cloudflare edge ranges — without it, a non-Cloudflare proxy could forward a client-supplied CF-Connecting-IP value and falsify attribution. Use the official list at https://www.cloudflare.com/ips/. If you use the official Cloudflare WordPress plugin (which rewrites REMOTE_ADDR directly), you do not need to set this constant.
rsadmin_collect_errors is no longer scheduled and is removed from existing sites on the next page load. Error collection is unchanged.UPDATE queries on one options row (about 14 seconds on one site's busiest page). Errors are now counted in memory during the request and saved with a single write at the end of it, before the object cache closes. Nothing is written when a request raises no errors. Stored data, deduplication, the 1,000-error cap and PII redaction are unchanged.open_basedir. Checking whether mysqldump was executable touched /usr/bin outside the allowed paths, adding a warning to the error log on every backup. The check now skips paths open_basedir doesn't allow. The result is unchanged: on those hosts backups keep using the built-in database export.wp rsadmin connect <token> stores your account token and registers the site, the same as the Connect Site button under Tools → RS Remote Site Manager. The site then waits for approval on the Sites page of your dashboard. A site that is already connected is left alone. Until now the dashboard suggested wp option update rsadmin_account_token, which saved the token but never registered the site.s3.us-west-002.backblazeb2.com but the US backup bucket presigns as s3.us-west-001.backblazeb2.com, so any core rollback whose snapshot lived on the US disk was refused as unsafe_presigned_url. Both region hosts are now allowed, on the restore and backup allowlists alike.bulk_upgrade() returns the upgrader's result-info array for an item it actually updated, but the response classifier only recognised true (WordPress's up-to-date branch), WP_Error, and null/false. A genuinely applied update fell through to the disk-state fallback — where the installed version now equals the target, so every applied update was reported as skipped. The dashboard then recorded the batch as failed and ran no post-update visual verification even though the site had just changed. Result arrays now go through the same trust-the-disk verification as the up-to-date branch: at or past target is success, still below target is version_unchanged.wp_login_failed and wp_login callbacks declared strict parameter types, but those hooks are re-fired by login and membership plugins with looser arguments than WordPress core uses — Ultimate Member fires wp_login_failed with null, which raised an uncaught TypeError on every failed login through its form. Both callbacks now accept whatever the firing plugin passes, normalise it, and record the event; a login recorded without a resolvable user is stored with an empty user id rather than dropped.rsadmin_bulk_quick with a matching nopriv hook) as well as the REST API. WP Engine denies wp-content writes to requests that do not arrive over admin-ajax with the platform's auth cookie, so Quick updates on WPE sites failed at the unpack step with "Could not copy file.". Other hosts are unaffected. The handler itself landed between 3.4.0 and 3.4.1 without a release of its own, and 3.4.0 builds without it exist in the wild — 3.4.2 gives the capability an unambiguous version for the dashboard to gate on. No other changes.update_package_available. WordPress's update transient distinguishes an offered update the site may download (a populated package URL) from one it may not (new_version set, package empty) — the ordinary shape of a lapsed premium licence. That state was previously discarded, so the dashboard could only discover it by taking a backup, attempting the install, and failing. Null means no update is offered, so the question does not apply.plugins_hash and themes_hash now include what the update transient offers for each item, so a licence lapsing or being renewed perturbs the manifest and triggers a full sync. Previously neither event changed an installed version or the active list, so the change stayed invisible until something unrelated moved the hash.run_update() has always computed an error_code, but the REST layer dropped it, leaving the dashboard to pattern-match WordPress's error prose — which WordPress writes in the site's language and wraps in wp-admin markup. Failed /update responses now include error_code, and the two outcomes that abort before anything is written to disk are named rather than flattened into update_failed: already_current (no entry in the update transient) and licence_blocked (an entry with no package URL)./update no longer echoes the requested version back as to_version. It now reports the version actually on disk after the attempt, with the requested target moved to requested_version. The old response made a no-op indistinguishable from a failed install.backup/core-plan endpoint returns the allowlist-scoped core file list, the dashboard pulls those files in short bounded requests via the existing backup/file-batch-chunk/file-stream endpoints and assembles the rollback tar itself, then backup/core-finalize writes the op-keyed restore manifest. Each site request now stays well under Cloudflare's ~100s edge timeout, so large cores behind Cloudflare back up reliably instead of failing with HTTP 524. The single-shot backup/export-core endpoint is retained for older dashboards and will be removed after fleet rollout. Rollback (rollback/core/apply) is unchanged.wp-content/plugins and mu-plugins (taken with scandir(), so plugins that hide themselves from WordPress listings are still seen), a bounded scan for .php files under uploads, configuration canaries (administrator set, application-password counts, siteurl/home, flags over the tail of wp-config.php), and exposure markers for watch-listed vulnerable plugins — including whether WPMU DEV Dashboard is in its exploitable disconnected state (reported as a boolean; option values never leave the site). A hash of the snapshot is embedded in the sync manifest so any change triggers an immediate full sync.plugin_installed/theme_installed activity events — the provenance record the dashboard's backdoor detector checks against.plugin_activated event carries had_install (files arrived through WordPress) and recent_admin_login (an administrator logged in within 5 minutes) so a programmatic activation of never-installed code is identifiable from the event alone./disconnect call; every subsequent API request then fails closed.wp_options.autoload values yes/no with on/off/auto-on/auto-off, and the database health collector still matched only autoload = 'yes' — a site installed after 6.6 reported 0 bytes of autoloaded options, and migrated sites under-counted by every row core had rewritten. The collector now matches the same value set core itself loads (wp_autoload_values_to_autoload()), with a hardcoded fallback for pre-6.6 WordPress./manifest fingerprint the dashboard polls is a GET, and an HTTP cache in front of the site (a page-cache plugin, a host edge cache, or a CDN) could serve a stale copy keyed on the URL — pinning the dashboard to the pre-update state. The manifest response now sends Cache-Control: no-store so caches skip it. (The dashboard also cache-busts the request, so already-updated sites recover on the next sync even before this plugin update is installed.)wp-content/upgrade, the same directory WordPress empties at the start of a core update — so if a core update needed to roll back (for example after a visual-regression check flagged the updated site), the manifest was already gone and the rollback failed, leaving the site on the new core. The manifest is now stored in a dedicated wp-content/rsadmin-core-state directory that the WordPress updater never touches; the restore side reads the new location first and still falls back to the old one for any update captured by a previous plugin version.define('WP_CONTENT_DIR', ABSPATH.'/content')). The scanner's cycle-detection set was shared across walk roots, so the ABSPATH walk marked the whole content tree as visited before the content root's own walk ran — the scan then saw every subdirectory as a cycle and shipped zero plugins/themes/uploads. Caught in production by the scan integrity check (which refused the empty backup); skip-listed directories are now pruned at traversal time.node_modules/.git/cache directories. The integrity baseline now applies the same directory exclusions as the scan itself instead of comparing against a raw directory size./themes) no longer log a spurious "realpath() failed" warning on every scan.$wp_version and $wp_db_version actually match the requested target before declaring success.WP_CONTENT_DIR and theme roots, managed-mode read-only filesystems, and split-content layouts./backup/file-chunk, /backup/file-stream) used to fail with a bare "Failed to open file" whenever fopen() failed after filesize() had already succeeded, giving the dashboard no way to tell a permissions issue from a missing file from a host restriction. This is the WP Engine wp-content/mysql.sql failure mode — reproducible across plugin 2.7.1–2.7.6, so not a one-off. The read path now (a) drops PHP's stat cache and retries the open once, to absorb the transient case where filesize() reads a cached inode that open() can't, and (b) on a persistent failure reports exists/readable/size plus the underlying PHP error in both the response and the error log, so the real cause is recorded. This surfaces why a read fails; on a host that is genuinely blocking the read it does not by itself make the backup succeed.get_theme_root() . '/' . $slug. get_theme_root() with no stylesheet argument returns only the default theme root, so on sites that register more than one theme root (Bedrock/Sage, where the active theme lives in a relocated/nested root) every bundled theme in a non-default root was reported Theme "twentyeleven" not found. — even though wp_get_themes() lists it and the dashboard inventory shows it with a pending update. /backup/export-item returned 404 for those themes, and because the dashboard requires a snapshot before updating, a single un-backupable theme failed the whole batch and discarded the backups already taken for every plugin in it (e.g. 8 themes blocking 10 healthy plugin updates in a single batch). The update handler's get_item_path() had the same bug, so even a backed-up theme would have extracted into the wrong root. Both now resolve through wp_get_theme($slug)->get_stylesheet_directory() (falling back to get_theme_root($slug)), honoring every registered theme root — the same multi-root handling the file scanner and restore-staging code already use. Plugin paths (WP_PLUGIN_DIR) were always absolute and unaffected.update_plugins / update_themes site transients directly; WP core throttles those refreshes to once every 12 hours and only repopulates them on wp-admin/plugins.php / update-core.php pageviews. After a dashboard-applied update, the dashboard kept showing already-installed updates as still pending until someone loaded the WP admin. The collector now resets last_checked and calls wp_update_plugins() / wp_update_themes() before reading the transient.wp_clean_plugins_cache(true) which deletes the transient outright, so post-update syncs always bypass the throttle./backup/file-stream had a TOCTOU race against actively-written files. The handler captured the file size once via filesize(), advertised that size in Content-Length, but then both the sha256 pre-pass and the streaming loop ran to feof with no cap. If the file grew between the stat and the stream (iThemes Security event logs, Wordfence live-traffic logs, simple-history rows), the plugin emitted more bytes than Content-Length claimed and the sha256 was computed over a different byte range than the one sent. Dashboard backups failed with File size mismatch: expected N bytes, got N+delta and aborted on the first such file, killing multi-GB backups over a few KB of log churn (~2.4 KB of mid-stream growth was enough). Both loops are now bounded by the initial size, matching the pattern already used by handle_file_chunk.WP_CONTENT_DIR (e.g. define('WP_CONTENT_DIR', ABSPATH.'/content') instead of the default wp-content/) failed the post-scan sanity check with "plugins: 0 B, themes: 0 B, uploads: 0 B (100% missing)" even though the scanner had walked the right files. The walker fell through to the ABSPATH walk and emitted paths like content/plugins/foo which category_for() couldn't map to the plugins bucket. Now promotes WP_CONTENT_DIR to a separate walk root whenever its basename isn't wp-content, so archive-relative paths come out as wp-content/plugins/foo regardless of the on-disk directory name. Defends against vestigial wp-content/ directories alongside the renamed real one too.wp-content/uploads/2018/08/218231..png from typo'd uploads). Backups failed with "Batch accounting mismatch" because the file-batch-chunk handler's pre-sanitization silently dropped those entries before they reached the batcher, leaving the dashboard's tarred + skipped == expected check unable to balance. Switched all three call sites (handler sanitization, scanner resolver, restore validator) to segment-based detection — only reject paths where .. is a complete component between / separators, never reject filenames that just happen to contain consecutive dots. realpath() boundary check inside the resolver still catches symlink escapes, so security posture is unchanged.RSAdmin_Backup_File_Batcher::stream(), used by /backup/file-batch-chunk to bundle small files into tar archives) had its own copy of the ABSPATH-only path resolution and was missed by the 2.7.1 unification. On Pressable / WPE / Bedrock the batcher silently skipped 95-100% of files per batch (e.g. 65,495 of 68,670 files skipped in one batch). The dashboard's "tolerate vanished files" feature treated this as a stream of legitimate race conditions, so the backup looked like it succeeded until the final manifest-count check caught the mismatch. Fixed by delegating to the shared scanner resolver. All other file-read endpoints already went through that path in 2.7.1.X-RSAdmin-Batch-Skipped-Paths header would truncate at ~150 paths under the 8 KiB cap, so most of the skipped paths weren't even reported back — the dashboard had ~95% silent loss with only ~5% accounted for. Sanity checking the header count vs. the count header (X-RSAdmin-Batch-Skipped) revealed the gap./backup/file-batch-chunk, /backup/file-stream, /backup/file-chunk) still resolved every relative_path under ABSPATH. After 2.7.0 the scanner correctly emits wp-content/... paths from WP_CONTENT_DIR even on split-core hosts, but the resolver couldn't find them back on disk, so backups failed on the first file outside ABSPATH (Invalid or unreadable relative_path). Resolution now mirrors the scanner's multi-root prefix logic (most-specific first: mu-plugins → plugins → themes → wp-content → root) and verifies the realpath lands inside one of the configured WP roots before serving the file.WP_CONTENT_DIR, WP_PLUGIN_DIR, WPMU_PLUGIN_DIR, and all theme roots when they live outside ABSPATH. Previous versions walked only ABSPATH, which on hosts with split-core layouts (Pressable, WP Engine, Bedrock) silently captured only the default WordPress-shipped wp-content (akismet, hello.php, twentytwenty* themes) and missed every user-installed plugin, theme, upload, and mu-plugin. Backups completed "successfully" but were unusable for restore. Also picks up wp-config.php when it lives above ABSPATH (Bedrock).wp-content/plugins/akismet → /wordpress/plugins/akismet/latest). Now follows symlinks whose target stays within a configured WP root, with cycle detection via visited-realpath set. Targets outside the WP tree (the original /usr/bin/python3 dev-artifact case) are still skipped.wp_pmxi_hash with a binary hash column). The dashboard sends an opaque base64 cursor parameter instead of id_start/id_end, and the plugin returns the next cursor via a new X-RSAdmin-Next-Cursor response header. Fixes backups failing with HTTP 406 on sites where the upstream WAF rejected requests carrying raw binary PK bytes in the JSON body, and gives non-int PK tables real per-chunk checkpointing instead of the previous "one giant request" fallbackX-RSAdmin-Batch-Skipped-Paths header so the dashboard can record what isn't in the archive (typical on active sites with upload churn, log rotation, or cache eviction) instead of failing the entire backup on the first vanished filebinary_bytes_ratio per table so the dashboard planner can size chunk ranges by SQL output bytes rather than row count (fixes OOM on BLOB-heavy tables like itsec_distributed_storage)rsadmin_backup_wpdb_batch_byte_budget)dump-chunk entry so single wide LONGBLOB rows fit during INSERT assembly on hosts with sub-256MB defaultsCF-Connecting-IP to Cloudflare edges via new RSADMIN_TRUSTED_CF_IPS constant, walk X-Forwarded-For right-to-left skipping trusted proxies, support Cloudflare Pseudo IPv4 (CF-Connecting-IPv6), and prefer X-Forwarded-For over X-Real-IP in multi-hop setups