=== RS Remote Site Manager === Contributors: mslepko Tags: management, dashboard, multisite, monitoring Requires at least: 6.2 Tested up to: 6.9 Stable tag: 2.7.6 Requires PHP: 8.0 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html Connect your WordPress site to wp-admin.online remote Site Manager for centralized management. == Description == RS Remote Site Manager allows you to manage multiple WordPress sites from a single dashboard as https://wp-admin.online. Monitor your WordPress versions, plugins, themes, and log in with one click. **Features:** * Centralized dashboard for all your WordPress sites * Track WordPress core version * Monitor installed plugins and themes * Get notified about available updates * One-click login to wp-admin * Automatic daily sync == Installation == 1. Upload the plugin files to the `/wp-content/plugins/rootscope-remote-site-manager` directory, or install the plugin through the WordPress plugins screen directly. 2. Activate the plugin through the 'Plugins' screen in WordPress 3. Go to Tools -> Rootscope Remote Site Manager to configure the plugin 4. Enter your API Key and API Secret from your Rootscope Remote Site Manager dashboard and choose the Login Administrator account 5. Click "Verify Connection" to confirm the connection == Screenshots == 1. Central dashboard showing all connected WordPress sites and their health score 2. View all plugins and themes across your sites and track available updates 3. Monitor WordPress and PHP versions across all connected sites 4. Manage multiple WordPress sites from a single central dashboard == Third-Party Service == This plugin connects to the [Rootscope Remote Site Manager](https://wp-admin.online) service to provide centralized WordPress site management. The following data is sent to the Rootscope Remote Site Manager API (`https://wp-admin.online/api/v1`): * WordPress version and PHP version * List of installed plugins and themes (names, versions, active status) * Administrator usernames and display names (for one-click login) Data is transmitted over HTTPS and authenticated using HMAC signatures. No data is sent until you configure your API credentials. * [Terms of Service](https://wp-admin.online/terms) * [Privacy Policy](https://wp-admin.online/privacy) == Frequently Asked Questions == = Where do I get my API credentials? = Log in to your Rootscope Remote Site Manager dashboard at https://wp-admin.online, add a new site, and you'll receive your API Key and API Secret. = How often does the plugin sync data? = The plugin automatically syncs data once daily. You can also trigger a manual sync from the settings page. = Is my data secure? = Yes. All communication between your site and Rootscope Remote Site Manager is encrypted via HTTPS and authenticated using HMAC signatures. = How does the plugin resolve client IPs behind a reverse proxy or CDN? = 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. == Changelog == = 2.7.6 = * **Bug fix:** dashboard sync now returns accurate plugin/theme update lists immediately after updates apply. The data collector was reading `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 (eyecan.je, 2026-05-19). The collector now resets `last_checked` and calls `wp_update_plugins()` / `wp_update_themes()` before reading the transient. * Throttled to one wp.org round-trip per 5 minutes per site so rapid Sync clicks from the dashboard don't hammer api.wordpress.org from customer servers. Successful in-plugin updates call `wp_clean_plugins_cache(true)` which deletes the transient outright, so post-update syncs always bypass the throttle. = 2.7.5 = * **Bug fix:** `/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 (incident 2026-05-19, site 15, 2,428 B growth). Both loops are now bounded by the initial size, matching the pattern already used by `handle_file_chunk`. = 2.7.4 = * **Bug fix:** sites with a renamed `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. = 2.7.3 = * **Bug fix:** path-traversal guards rejected legitimate filenames containing two or more consecutive dots (e.g. `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. * The handler now passes entries straight to the batcher (light-touch normalization only); the batcher counts every drop as "skipped" so accounting stays correct end-to-end. Previously dropped-at-handler entries were invisible to the dashboard. = 2.7.2 = * **Critical follow-up to 2.7.1:** the batched-files tar streamer (`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. site 54: 65,495 of 68,670 files skipped). 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. * As a side effect, the `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. = 2.7.1 = * **Critical follow-up to 2.7.0:** the file-read endpoints (`/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. = 2.7.0 = * **Critical fix:** file backup scanner now walks `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). * Symlink handling rewritten: previously skipped ALL symlinks, which discarded legitimate WPE-managed plugin symlinks (`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. * Defence in depth: post-scan sanity check compares per-category byte totals against what the environment collector reports. If filesystem says the site has 500 MB of plugins but the scan found <10%, the backup fails loudly with a clear error message instead of producing another silently-empty archive. = 2.6.0 = * Backup dump-chunk endpoint now supports keyset/cursor pagination for tables with non-integer primary keys (e.g. `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" fallback = 2.5.0 = * Tolerate files that vanish mid-backup: the batch endpoint now reports skipped paths in a new `X-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 file = 2.4.0 = * Add site audit-scan endpoint with phased scanner: hook-integration detection, plugin source classification (wp.org / commercial / unknown), object-cache and page-cache signals, multisite detection, and capped mu-plugin scan * Backup preflight now emits `binary_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`) * wpdb chunk-dumper fallback caps batch size against a 2 MB byte budget before the first query, preventing fat-row pages from blowing memory mid-range (filterable via `rsadmin_backup_wpdb_batch_byte_budget`) * Raise WP memory limit to the admin threshold at `dump-chunk` entry so single wide LONGBLOB rows fit during INSERT assembly on hosts with sub-256MB defaults = 2.3.0 = * Add bulk-quick update endpoint enabling the dashboard's Quick update mode (one HTTP call per chunk of plugin/theme updates, with per-slug success/skipped/failed results) * Harden login IP resolution: gate `CF-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 = 2.2.0 = * Fix plugin updates and rollbacks failing with "Could not copy file" on WP Engine sites * /update, /rollback, and rollback chunked endpoints now route through admin-ajax with the wpe-auth cookie = 1.1.3 = * Invalidate caches when API secret is saved = 1.1.2 = * Fixed api secret double-encryption when option didn't exist * Preselect first admin account by default = 1.1.1 = * Use correct API URL constant, add dashboard link to registration notice * Rename wpadmin_account_token to rsadmin_account_token for consistency * Encrypt account token at rest, clean up on uninstall, fix changelog formatting * Token registration = 1.1.0 = * Full site backups * Uptime Checks * Remote updates * Bulk install using a token = 1.0.0 = * Initial release