Skip to content

Releases: lunaluna/wp-maintenance-audit-reporter

1.6.0

Choose a tag to compare

@github-actions github-actions released this 03 Sep 03:07
4deb71f

利用者から見た機能変更はありません。リリース基盤(配布ZIPの作り方・GitHub
Releaseの作り方)を共有ライブラリ l2d-wp-github-update-lib(1.2.0)の
reusable workflow へ寄せました。PHPコードは1行も変わっておらず、配布ZIPの
中身は1.5.6と同一です(vendor-pdf.sha256 を除く。フォントの取得元
google/fonts の更新によりビルド時点でハッシュが変わり得るため)。

Changed

  • bin/build-zip.sh を、同梱ライブラリの汎用ビルダーへ委譲する薄いラッパーに変更。プラグイン固有の前処理(composer install、Action Scheduler の同梱)は bin/build-zip.pre.sh へ分離した。
  • .github/workflows/release.yml(184行の独自実装)を、ライブラリの plugin-release.yml@1.2.0 を呼び出す約25行の構成に置き換えた。フォント生成・vendor-pdf.zip 作成・チェックサムの3ステップは bin/release.pre.sh フックへ移設した(内容は変更なし)。
  • ローカルビルドの出力先が「プラグインルートの1つ上」から「プラグインルート」へ変わった(.gitignore で無視)。

1.5.1

Choose a tag to compare

@github-actions github-actions released this 21 Aug 13:58
bec5f32

Fixed

  • --no-snapshot never worked on either wp maintenance-audit run or wp wpmar audit run — WP-CLI's argument parser rejected it with Error: unknown --snapshot parameter because only the negative form was declared. Renamed to a positive flag, --skip-snapshot.
  • wp wpmar storage migrate --no-revert (and --no-dry-run, --no-network) were silently read as true by isset( $assoc_flags[...] ), so --no-revert without --dry-run actually reverted storage instead of migrating it. All CLI bool/int flag reads now go through the new WPMAR_CLI_Flags helper, which honours WP-CLI's --no-<flag> negation.
  • Admin-facing WP-CLI hints (loopback notice, settings pages, network admin) still referenced the removed wp maintenance-audit run command and the no-longer-required --sync flag.

Changed

  • Consolidated the two WP-CLI command namespaces into one: wp wpmar audit (run/test), wp wpmar report (list/delete/export), and wp wpmar storage (migrate). The legacy wp maintenance-audit namespace is removed.
  • wp wpmar audit run is synchronous by default; --sync is now a backward-compatible no-op and --async opt-in enqueues the run on the Action Scheduler queue instead.
  • --same-setting and --id=<blog_id> (previously legacy-only) and the test subcommand are now available under wp wpmar audit.

1.5.0

Choose a tag to compare

@github-actions github-actions released this 21 Aug 03:03
275a801

Changed

  • Self-update mechanism now runs on the shared l2d-wp-github-update-lib library (1.1.0, vendored into lib/l2d-updater/ via git subtree) instead of the plugin-local WPMAR_GitHub_Updater implementation. The documented cache transient key (wpmar_github_release_cache) and filter names (wpmar_github_updater_cache_ttl, wpmar_github_updater_backoff_ttl) are passed explicitly to the library and continue to work unchanged.
  • The "Description" section in the update details modal now comes from this plugin's own header Description: line (via the library's default behaviour) instead of a hardcoded string.
  • release.yml now publishes as a GitHub Release draft; a maintainer must run gh release edit <tag> --draft=false after verifying the assets before any site can pick up the release via auto-update. Added to close off a real risk: sites running forced-auto-update-controller alongside this plugin would otherwise pick up a freshly published release unattended.
  • bin/build-zip.sh now hard-fails if lib/l2d-updater/loader.php or class-l2d-github-updater.php is missing from the staged tree (the main plugin file requires the loader, so a missing copy would be a fatal error on every site).
  • phpcs.xml.dist now excludes lib/* (the vendored library copy is not linted against this plugin's coding standards).

Added

  • wpmar_github_updater_enabled filter (kill switch): returning false disables the update checker entirely, e.g. as an emergency rollback lever.

Fixed

  • The Action Scheduler bundling step in bin/build-zip.sh no longer ships a stray .claude/ directory that had been accidentally left inside the local vendor/woocommerce/action-scheduler/ copy (unrelated to this migration, found while verifying the distributed-zip file list).

1.4.1

Choose a tag to compare

@github-actions github-actions released this 18 Aug 14:17
baec59a

Fixed

  • GitHub-Releases update checker no longer falls back to the auto-generated zipballWPMAR_GitHub_Updater::extract_zip_url() fell back to $body['zipball_url'] when no release asset matched the plugin slug by name. That zipball's inner directory is owner-repo-<sha>/, not the plugin's own directory name, so installing it would rename the plugin directory and deactivate the plugin. The fallback is removed; a release with no matching asset now correctly reports "no update available" (fail closed) instead of risking a broken install.
  • Update-availability comparison no longer trusts the WPMAR_VERSION constant alonecheck_for_update() compared the latest GitHub release against the WPMAR_VERSION constant, which can drift from the plugin header's Version: (the CI version check only ever covered the header, not this constant). A drifted constant could leave a stale "update available" notice in place after updating, or suppress a real update notice. The comparison now uses $transient->checked[WPMAR_PLUGIN_BASENAME] — the version WordPress core itself read from the plugin header — with WPMAR_VERSION kept only as a fallback and for the User-Agent string. Also added an is_object( $transient ) guard, and the tag-name normalization now uses preg_replace( '/^v/i', ... ) instead of ltrim( $tag, 'v' ), which trims by character set rather than by prefix.
  • Update-checker cache is now a site-wide transient, matching Network: true — The cache (wpmar_github_release_cache) used get_transient()/set_transient() (per-blog), while the update_plugins transient it feeds is itself network-wide. On a multisite network this meant one GitHub API request per sub-site instead of one per network, unnecessarily consuming the unauthenticated rate limit (60 req/h). Switched to get_site_transient()/set_site_transient()/delete_site_transient() throughout; uninstall.php already covered the site-transient key pattern, so no change was needed there.
  • "Requires PHP" / "Tested up to" in the update details modal no longer hardcodedplugin_info() and build_plugin_update_object() each hardcoded '6.0' / '7.4' / '' instead of reading the plugin header, so they could silently drift from the real Requires at least: / Tested up to: / Requires PHP: values (and the empty tested value meant the "Compatible up to" line never rendered in the modal). Both now read from the header via a shared plugin_requirements() helper using get_file_data().
  • Distributed plugin zip no longer risks bundling dev/build files.github/workflows/release.yml and bin/build-zip.sh each maintained their own exclusion list, and the two had already drifted: CI's list omitted bin/ (so the release zip bundled the build scripts themselves), while the local script's list omitted fonts/ and vendor-pdf.zip.sha256 (so a local build run after generating the bundled PDF fonts could ship them in the main zip). Introduced .distignore as the single source of truth for both; release.yml's "Assemble plugin zip" step now calls bash bin/build-zip.sh directly instead of duplicating its logic.

Changed

  • CI version check now covers 5 locations instead of 1release.yml previously verified only the release tag against the plugin header's Version:. It now also verifies the WPMAR_VERSION constant, readme.txt's and readme-ja.txt's Stable tag:, and composer.json's "version", failing the release job if any one of them drifts from the tag.

1.4.0

Choose a tag to compare

@github-actions github-actions released this 12 Aug 01:30

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. `un...
Read more

1.3.1

Choose a tag to compare

@github-actions github-actions released this 28 Jul 02:09
8ec6179

Security

  • Report/PDF/log storage moved to a protected private directory (unauthenticated disclosure fix) — Generated reports, client PDFs, and diagnostics logs lived under wp_upload_dir()/wpmar/ with no .htaccess/index.php protection outside the logs subdirectory, and filenames built from little more than a domain and timestamp — a few dozen to a few hundred requests could locate and download a report containing administrator email addresses, a full core/theme/plugin version inventory (i.e. a map of unpatched known vulnerabilities), checksum-mismatch findings, and server path/permission details. New artefacts now write to wp-content/wpmar-private/ by default (reports/, pdf/, logs/, tmp/ subdirectories), overridable via the WPMAR_PRIVATE_STORAGE_DIR constant or wpmar_private_storage_dir filter to move storage outside the document root entirely. Every directory gets an auto-generated .htaccess (Require all denied / Deny from all) plus index.php, and every filename carries a 20-character random token — the one defense that does not depend on the web server type. Multisite splits the base directory by site-{blog_id}/, since wp-content (unlike wp_upload_dir()) is shared network-wide. When the private directory isn't writable, the plugin falls back to an equally token-protected wp-content/uploads/wpmar/ and shows an admin notice. Existing v1.3.0 files are migrated automatically in the background after upgrading (WPMAR_Storage_Migrator; batched, idempotent, resumable, with per-file rollback if a paired DB update fails); progress shows on the plugin's admin screens, and old files are not deleted until migration completes. wp wpmar storage migrate [--dry-run] [--network] [--batch=<n>] drives or previews it manually, and wp wpmar storage migrate --revert reverses it (for downgrading back to a pre-1.3.1 release) while keeping the existing filename/token and adding the .htaccess/index.php protection v1.3.0 never had.
  • PDF rendering: Parsedown safe mode unified across the PDF and HTML-email pathsWPMAR_PDF_Writer::write_pdf_from_markdown() instantiated \Parsedown directly, skipping the safe mode already applied to the HTML-email path, so raw HTML (<script>, <annotation>, a remote <img src>) embedded in an attacker-controlled plugin/theme/display-name string reached mPDF unsanitized. Both paths now go through a single markdown_to_safe_html_fragment(), which also strips <img> tags after conversion (Markdown's own ![]() syntax still emits one even under safe mode), and the mPDF config now explicitly sets allow_local_file_access to false.
  • vendor-pdf.zip checksum verification enabled by default — Previously the installer only verified the downloaded/uploaded PDF-library archive's SHA-256 if an operator manually pinned one via the WPMAR_PDF_VENDOR_ZIP_SHA256 constant or wpmar_pdf_vendor_zip_sha256 filter; unset by default, a ~30 MB executable PHP library was extracted into the writable plugin directory over TLS with no other integrity check. The release workflow now embeds the digest for that exact release inside the plugin package itself, and the installer checks it automatically — the constant/filter still take precedence for a custom build, and a source checkout (no bundled digest) keeps today's no-op behaviour.
  • Update URI header added — Prevents a same-slug plugin on WordPress.org from silently overriding this plugin's own GitHub-Releases-based updater, per the header WordPress 5.8 introduced for exactly this case.
  • X-Content-Type-Options: nosniff on every download response — Previously only the diagnostics-log download set this header; Markdown, client-Markdown, PDF, and ZIP downloads now all go through one shared WPMAR_Download_Headers helper so the header can't be forgotten on a new download endpoint.
  • PDF-installer Ajax handlers: capability checked before noncehandle_ajax(), handle_preflight_ajax(), and handle_manual_upload_ajax() checked the nonce before the install_plugins capability, the reverse of the order used everywhere else in the plugin since the 1.0.0 hardening; all three now check capability first.
  • SECURITY.md added — Documents the supported-version policy, the private GitHub Security Advisories reporting channel, and response-time targets.
  • CI hardening — The CI workflow now declares permissions: contents: read (previously unset), pins actions/checkout and shivammathur/setup-php to a commit SHA instead of a floating major-version tag, and runs composer audit against dev dependencies in addition to the existing --no-dev check.

1.3.0

Choose a tag to compare

@github-actions github-actions released this 14 Jul 04:29
37e0716

Added

  • Glob patterns in checksum exclude listsWPMAR_Check_Checksums::build_exclude_set() / is_excluded() now recognize entries containing *, ?, or [ as fnmatch() glob patterns (matched against the lowercased, normalized relative path), in addition to the existing exact-match and directory-prefix (/, /* suffix) forms. A pattern like wordfence:*/.htaccess excludes that repeating filename at any nesting depth in one line, instead of requiring one entry per directory. Falls back to a preg_match()-based equivalent (*.*, ?.) on environments without fnmatch().

1.2.0

Choose a tag to compare

@github-actions github-actions released this 13 Jul 23:49
0789707

Added

  • Basic auth (blocked-loopback) support for manual runs — Sites behind HTTP Basic authentication reject the loopback requests WP-Cron / Action Scheduler depend on, so async audit jobs previously sat in queued forever. A new loopback detector (WPMAR_Loopback_Detector) probes admin-ajax.php the same way core's Site Health does and caches the verdict for 12 hours (per-site transient, with a re-check button). Jobs enqueued while blocked are flagged (loopback_blocked column on {prefix}wpmar_jobs, applied via the existing dbDelta upgrade), and the job-status REST endpoint (GET /wpmar/v1/jobs/{id}) then drains the Action Scheduler queue in-process while the admin page polls — batch size pinned to 1, 15-second budget per poll (filterable via wpmar_inline_runner_time_limit), transient mutex against concurrent pollers. Manual report generation therefore completes as long as the admin page stays open; environments with working loopbacks are entirely unaffected. Scheduled monthly generation under Basic auth is explicitly not supported — use server cron + wp wpmar audit run --sync instead.
  • Loopback-blocked admin warnings — The plugin's screens (single-site and network admin) show a warning notice with a re-check button when loopback is blocked; the schedule settings sections (single-site and network) gain an inline note that monthly auto-reports cannot run, recommending server cron + WP-CLI; the job polling panel tells the operator to keep the page open while a blocked job progresses and warns when a job stays queued for ~2 minutes without progress.

Documentation

  • README (4 variants): Basic auth section — New "Sites behind HTTP Basic authentication / Basic 認証環境での利用について" section in README.md / README-ja.md / readme.txt / readme-ja.txt covering what does not work (scheduled generation), what works (manual generation while the page polls), and the recommended server-cron + wp wpmar audit run --sync setup with a multisite --url example.

1.1.1

Choose a tag to compare

@github-actions github-actions released this 09 Jul 02:47
23cd4e5

Changed

  • Report user-information section rendered as a table — The 【ユーザー情報】 section listed privileged users as tab-separated lines, which the client PDF (Markdown → Parsedown → mPDF) collapsed into hard-to-read unaligned text. Both the client and operator report bodies now emit the list as a GFM pipe table (ID / ユーザー名 / 表示名 / メールアドレス / 権限 / 登録日), which the existing PDF stylesheet renders as a bordered table. Literal | characters in user fields are escaped so free-text display names cannot break the table layout. No data collection or PDF-writer changes.

Documentation

  • README 4種に診断ログ(動作ログ)の使い方を追記 — 「今すぐ実行」パネルのステップ表示・失敗時のログDLリンクの説明、およびログ行フォーマット・ステップの流れ・「最後の行が停止箇所」という読み方・ログの取得方法(DL手順・保存場所・保持件数20件・25分無応答ジョブの自動失敗化)を README.md / README-ja.md / readme.txt / readme-ja.txt の使い方セクションに追加。コード変更なし。

1.1.0

Choose a tag to compare

@github-actions github-actions released this 09 Jul 00:18
1f1616a

Added

  • Diagnostics: per-job step logging — Audit runs now write an unbuffered, per-job log (wp-content/uploads/wpmar/logs/, one line flushed per phase) so a run that stalls or dies mid-execution (OOM, host timeout) can be diagnosed from its last recorded step instead of leaving no trace. Covers the async job dispatcher, the full run pipeline (gather/diff/persist/render/mail/PDF/retention), the slowest sub-phases (checksums, security ops), multisite per-site segments, and the synchronous wp wpmar audit run --sync CLI path. A shutdown handler captures fatal errors (including E_USER_ERROR) that bypass the normal try/catch, logging the failure and releasing the run lock. Log files are capacity-limited to the 20 most recent runs.
  • Diagnostics: stale-job auto-recovery — A job stuck in running because its process was killed hard enough that no handler ever ran (e.g. SIGKILL, OOM killer) is now automatically flipped to failed once its heartbeat goes stale (25+ minutes), checked opportunistically whenever the job-status REST endpoint or Reports screen is accessed. The {prefix}wpmar_jobs table gains step and log_path columns.
  • Reports screen — 診断ログ (Diagnostics) section — Lists recent jobs that have a log file (status, last step, updated time) with an on-screen tail preview (last ~200 lines) and a capability + per-job-nonce-gated download link. The job-status polling panel (settings screen) now also shows the current step and, on failure, a log download link.