Skip to content

1.4.0

Choose a tag to compare

@github-actions github-actions released this 12 Aug 01:30
· 95 commits to main since this release

Added

  • wp.org plugin/theme metadata now cached (wpmar_wporg_cache_ttl filter, default 12h)WPMAR_WPOrg_Client::fetch_plugin_information()/fetch_theme_information() previously called wp_remote_get() unconditionally on every run. Results are now cached via set_site_transient()/get_site_transient() under wpmar_wporg_plugin_{slug}/wpmar_wporg_theme_{slug} keys — a network-wide cache (not per-blog), so on a multisite audit the first site to ask for a given slug primes it and every other site's segment hits cache instead of re-querying wp.org. Cache hit/miss counts are logged on the gather:inventory-done step for visibility. The 12h default TTL is an unmeasured estimate of wp.org metadata churn, not a benchmarked value — adjust via the filter if the logged hit rate suggests otherwise.
  • Explicit memory release before PDF generationWPMAR_Runner::run() and run_site_segment() now drop the heaviest parts of $dataset (checksums, plugins.org, themes.org) via a shared release_heavy_dataset_memory() helper and call gc_collect_cycles() once the rendered Markdown bodies no longer need the raw payload — most notably right before mPDF (memory-hungry) runs, and before a multisite site-segment result is handed back to accumulate in $segments[].
  • Memory-usage WARN log lineWPMAR_Logger::step() now checks memory_get_usage(true) against 80% of memory_limit on every step and logs one WARN line if crossed, so a run trending toward OOM is visible from whichever phase happened to be running (not only the handful of steps that already logged memory explicitly).
  • Network rollups now split into one independent async job per site — A real (non-dry) network-scope audit job no longer loops every target site inside one Action Scheduler action. WPMAR_Job_Dispatcher::run_audit_job() now dispatches: it creates one queued row per target blog in a new {$wpdb->prefix}wpmar_network_segments table (queuedrunningdone/failed, an attempts counter for the retry work below, and only the rendered client_body/admin_body — never the raw per-site dataset), queues one wpmar/run_network_site_segment action per blog (each running WPMAR_Runner::run_site_segment() for exactly one site, in its own process), then one wpmar/run_network_aggregate action that waits for every segment to reach done/failed before finalizing the report. This is the fix for the structural memory ceiling of the old design: peak memory used to grow with site count and one site's OOM took the whole run down; now peak memory per action stays flat regardless of network size, and an OOM in one site's segment only fails that one segment. A failed (or timed-out) segment gets a "このサイトはエラーのため取得できませんでした" note in the merged report instead of its body, so a report still goes out for every site that finished. wpmar_jobs also gains an attempts column for the retry work below. Dry network runs (preview only) are unaffected — they still run synchronously, same as before. The old single-action fallback path (WPMAR_Network_Runner::run() called directly, used when Action Scheduler itself is unavailable or from WP-Cron/WP-CLI) is unchanged and unaffected — it never goes through the dispatcher.
  • Automatic retry for transient job/segment failures, capped at a fixed attempt count — Targets recoverable failures (a passing server-load spike, a momentary wp.org timeout), not the structural "this site's data volume always OOMs" case the per-site split above already solves. WPMAR_Jobs_Repository::mark_failed() and WPMAR_Network_Segments_Repository::mark_failed() now each fire an action (wpmar_job_marked_failed / wpmar_network_segment_marked_failed) on success — the one point every caller of either method (this dispatcher's catch blocks, WPMAR_Logger::handle_shutdown(), both repositories' own stale-heartbeat sweeps, now refactored to go through mark_failed() per row instead of a bulk UPDATE so the action fires for those too) converges on, so retry policy lives in one listener per scope instead of being duplicated at each call site. WPMAR_Job_Dispatcher::maybe_retry_job()/maybe_retry_segment() listen for these and, while attempts is below wpmar_job_max_attempts (default 2 — one retry, shared by both scopes), reset the row to queued and as_schedule_single_action() a re-run after a backoff (wpmar_job_retry_delay, default 15 min, for jobs; wpmar_segment_retry_delay, default 5 min, for segments — both unmeasured defaults). Retrying a job resets it to queued and re-fires the whole thing from scratch (for a network job, this re-dispatches — segments from the failed attempt are cleared first via delete_by_run()); retrying a segment only touches that one (run_id, blog_id) row, with zero effect on sibling sites. A segment force-failed by the run's own 90-minute global wait timeout is marked non-retryable (mark_failed(..., false)) since finalize runs immediately after and the segment row is deleted once it does. Action-Scheduler-unavailable synchronous paths (dry-run, direct fallback calls) never create the rows these hooks fire from, so they're unaffected — same "wait for the next cycle" behaviour as before.
  • Persistent duration + peak-memory history for calibrating the timing filters abovewpmar_network_segments rows are deleted once their run finishes, so nothing previously recorded how long a segment actually took, or how much memory it needed. WPMAR_Network_Segments_Repository::mark_done()/mark_failed() now both log one line (run id, blog id, done/failed, dispatch-to-completion duration, memory_get_peak_usage(true), retry count) to a new segment-history.log in the plugin's private logs directory via WPMAR_Logger::log_segment_outcome(), and the single-site path (WPMAR_Runner::run()) gets the same treatment in a parallel run-history.log via WPMAR_Logger::log_run_outcome() — that one also records the site's memory_limit next to peak usage, so a run can be judged against the site's overall headroom rather than only against this plugin's own footprint. Unlike the per-job run-*.log files, these two files accumulate indefinitely (growth is inherently slow — one line per site per monthly run, not per request) and are the intended source of real data for eventually tuning wpmar_network_segment_stale_minutes/wpmar_network_aggregate_max_wait/the retry delays above away from their unmeasured defaults. Both histories are also rendered (newest entry first) on the matching システム機能 screen below, so reading them does not require file access.
  • New "システム機能" screen (site + network), and an attempts column on the existing diagnostics log — Both reuse the plugin's existing manage_options/manage_network_options capability constants (no new capability introduced) and the existing admin-post + nonce pattern (new wpmar_admin_action branches in WPMAR_Admin_Menu::handle_post()/WPMAR_Network_Admin_Menu::handle_post(), not a new form-handling mechanism). Site-level (WPMAR_System_Status_Page, under the existing top-level menu): shows the shared wp.org cache's entry count with a "キャッシュをクリア" button, and this blog's wpmar_run_lock state (active/TTL) with a "強制解除" button for manually recovering a stuck single-site run. Network-level (WPMAR_Network_System_Status_Page, under Network Admin): the same wp.org cache clear button (the cache is shared, not per-blog), wpmar_network_run_lock's own state/force-unlock, and — for any network run currently in flight — its wpmar_network_segments status counts (queued/running/done/failed) plus a table of failed sites with blog ID, site name, attempts, and error message (a completed run's segment rows are already deleted by the time this renders; see the persistent history above for looking back past that point). WPMAR_Log_Viewer::render_section()'s diagnostics table also gains an "試行回数" column so a job's current retry count is visible without querying the DB directly. The whole "診断ログ" section moved off the Reports screen to the top of the site-level システム機能 screen (WPMAR_Log_Viewer's page gate and its view/download URLs now target WPMAR_SYSTEM_STATUS_PAGE_SLUG, and the admin CSS/JS loads there too), so per-job logs and the persistent run/segment history above are read from one place. Both screens are named "システム機能" rather than "システム状態", which read as too close to the plugin's own status output; the run-lock headings carry a one-line explanation of what the lock does and when force-unlocking is appropriate.

Fixed

  • Network run lock (wpmar_network_run_lock) never released on a fatal errorWPMAR_Logger::handle_shutdown() only ever deleted the single-site wpmar_run_lock transient; a network rollup killed by a fatal error (OOM, forced process termination) left wpmar_network_run_lock to expire on its own 20-minute TTL instead of being cleared immediately. The shutdown handler now checks the failed job's own scope column and releases the matching lock (wpmar_run_lock for single, wpmar_network_run_lock for network) — deliberately not both, so a fatal error on one scope can't clobber a legitimately in-progress run of the other scope.
  • Settings screens: the PDF attachment checkbox described the wrong storage path — Site and network settings both said PDFs are written to uploads/wpmar/pdf/, dropping the wp-content/ prefix that the Markdown checkbox directly above it already spelled out. Text-only fix; the actual storage location is unchanged (and is wp-content/wpmar-private/pdf/ by default since 1.3.1 anyway).
  • uninstall.php: the private storage directory (wp-content/wpmar-private/) was never removed — Uninstall only ever deleted the pre-1.3.1 fallback location (wp_upload_dir()/wpmar), so every report, client PDF, and diagnostics log written since 1.3.1 — the exact files the 1.3.1 storage move classified as sensitive — survived removing the plugin. uninstall.php now resolves the private base directory the same way WPMAR_Private_Storage::configured_base_dir() does (the WPMAR_PRIVATE_STORAGE_DIR constant, then the wpmar_private_storage_dir filter — both still in effect at uninstall time, unlike the plugin's own classes, which core never loads for uninstall.php) and deletes it. On multisite each blog's site-{blog_id}/ subdirectory is removed inside the existing get_sites() + switch_to_blog() loop, so a filter whose value depends on the current blog resolves correctly, and the shared parent is then removed only if it is empty — never recursively, since an operator-chosen path may hold unrelated files. A relocated install also gets the well-known default location (wp-content/wpmar-private) cleaned, because moving the base directory only changes where new files are written and WPMAR_Private_Storage::resolve() keeps reading from the old default. Relatedly, wpmar_uninstall_delete_uploads() (the fallback location) ran once outside the multisite loop and therefore only ever cleaned the blog the uninstall was triggered from; it now runs per blog as well.
  • uninstall.php: multisite sub-site tables and network-wide (sitemeta) settings were never removedwpmar_uninstall_drop_tables() only ever dropped the current blog's tables, even though WPMAR_Activator::activate_network() creates wpmar_reports/wpmar_snapshots/wpmar_jobs on every blog via get_sites() + switch_to_blog() — sub-site tables (and their wpmar_% options) survived uninstall on a network. Separately, wpmar_network_settings/wpmar_last_network_audit_completed_at/wpmar_wp_cron_last_fired_at are written with update_site_option() into $wpdb->sitemeta, a table the existing $wpdb->options LIKE cleanup never reached. uninstall.php now mirrors the activator's per-blog loop on is_multisite() and adds an explicit $wpdb->sitemeta LIKE wpmar_% sweep plus delete_site_option() calls for the three known keys. The new wpmar_network_segments table (added for the network site-segment work below) is now part of the same drop list.

Changed

  • A network rollup's parent job now stays running for longer, until every site's independent segment finishes — Previously the parent job flipped to done as soon as the (synchronous) full site loop returned. Now it waits on every per-site action plus the aggregate action described above, capped by wpmar_network_aggregate_max_wait (default 90 min). The admin polling UI's contract (queuedrunningdone/failed on one job id) is unchanged, and the final report is identical in shape — only the wall-clock time to done increases, in exchange for the memory-ceiling fix above. If your monitoring alerts on a network job's running duration, its expected upper bound just changed.
  • Dev dependency: WordPress Coding Standards bumped to 3.4.1 (CVE-2026-45293) — WPCS 0.14.1–3.4.0 evaluate the $ver argument of wp_enqueue_script() and friends through eval() in WordPress.WP.EnqueuedResourceParameters::is_falsy(), so running PHPCS over untrusted PHP could execute arbitrary code (GHSA-3pwp-g2mj-5p3v, CVSS 8.6). This repository's phpcs.xml.dist uses the affected WordPress ruleset. require-dev now floors wp-coding-standards/wpcs at ^3.4.1 and squizlabs/php_codesniffer at ^3.13.5 (PHPCS 4.x is not supported by WPCS 3.x). No impact on the distributed plugin: vendor/ is gitignored and excluded from the release zip, which is built with composer install --no-dev — WPCS never ships to users. The composer audit step in CI was failing on this advisory and is green again.
  • Dev dependency: PHP_CodeSniffer floored at 3.13.6 (CVE-2026-67434) — PHP_CodeSniffer below 3.13.6 (and 4.0.0–4.0.1) passes unescaped input to a shell command, so running PHPCS could execute arbitrary OS commands (GHSA-hmqg-cxww-wqhq, high severity, published 2026-08-05). require-dev now floors squizlabs/php_codesniffer at ^3.13.6 (still 3.x — PHPCS 4.x is not supported by WPCS 3.x) and composer.lock moves 3.13.5 → 3.13.6. No impact on the distributed plugin, for the same reason as the WPCS advisory above: vendor/ is gitignored and the release zip is built with composer install --no-dev, so PHPCS never ships to users. The composer audit step in CI was failing on this advisory and is green again.