Releases: lunaluna/wp-maintenance-audit-reporter
Releases · lunaluna/wp-maintenance-audit-reporter
Release list
1.6.0
利用者から見た機能変更はありません。リリース基盤(配布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
Fixed
--no-snapshotnever worked on eitherwp maintenance-audit runorwp wpmar audit run— WP-CLI's argument parser rejected it withError: unknown --snapshot parameterbecause 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 astruebyisset( $assoc_flags[...] ), so--no-revertwithout--dry-runactually reverted storage instead of migrating it. All CLI bool/int flag reads now go through the newWPMAR_CLI_Flagshelper, 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 runcommand and the no-longer-required--syncflag.
Changed
- Consolidated the two WP-CLI command namespaces into one:
wp wpmar audit(run/test),wp wpmar report(list/delete/export), andwp wpmar storage(migrate). The legacywp maintenance-auditnamespace is removed. wp wpmar audit runis synchronous by default;--syncis now a backward-compatible no-op and--asyncopt-in enqueues the run on the Action Scheduler queue instead.--same-settingand--id=<blog_id>(previously legacy-only) and thetestsubcommand are now available underwp wpmar audit.
1.5.0
Changed
- Self-update mechanism now runs on the shared
l2d-wp-github-update-liblibrary (1.1.0, vendored intolib/l2d-updater/viagit subtree) instead of the plugin-localWPMAR_GitHub_Updaterimplementation. 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.ymlnow publishes as a GitHub Release draft; a maintainer must rungh release edit <tag> --draft=falseafter verifying the assets before any site can pick up the release via auto-update. Added to close off a real risk: sites runningforced-auto-update-controlleralongside this plugin would otherwise pick up a freshly published release unattended.bin/build-zip.shnow hard-fails iflib/l2d-updater/loader.phporclass-l2d-github-updater.phpis missing from the staged tree (the main plugin filerequires the loader, so a missing copy would be a fatal error on every site).phpcs.xml.distnow excludeslib/*(the vendored library copy is not linted against this plugin's coding standards).
Added
wpmar_github_updater_enabledfilter (kill switch): returningfalsedisables the update checker entirely, e.g. as an emergency rollback lever.
Fixed
- The Action Scheduler bundling step in
bin/build-zip.shno longer ships a stray.claude/directory that had been accidentally left inside the localvendor/woocommerce/action-scheduler/copy (unrelated to this migration, found while verifying the distributed-zip file list).
1.4.1
Fixed
- GitHub-Releases update checker no longer falls back to the auto-generated zipball —
WPMAR_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 isowner-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_VERSIONconstant alone —check_for_update()compared the latest GitHub release against theWPMAR_VERSIONconstant, which can drift from the plugin header'sVersion:(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 — withWPMAR_VERSIONkept only as a fallback and for the User-Agent string. Also added anis_object( $transient )guard, and the tag-name normalization now usespreg_replace( '/^v/i', ... )instead ofltrim( $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) usedget_transient()/set_transient()(per-blog), while theupdate_pluginstransient 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 toget_site_transient()/set_site_transient()/delete_site_transient()throughout;uninstall.phpalready covered the site-transient key pattern, so no change was needed there. - "Requires PHP" / "Tested up to" in the update details modal no longer hardcoded —
plugin_info()andbuild_plugin_update_object()each hardcoded'6.0'/'7.4'/''instead of reading the plugin header, so they could silently drift from the realRequires at least:/Tested up to:/Requires PHP:values (and the emptytestedvalue meant the "Compatible up to" line never rendered in the modal). Both now read from the header via a sharedplugin_requirements()helper usingget_file_data(). - Distributed plugin zip no longer risks bundling dev/build files —
.github/workflows/release.ymlandbin/build-zip.sheach maintained their own exclusion list, and the two had already drifted: CI's list omittedbin/(so the release zip bundled the build scripts themselves), while the local script's list omittedfonts/andvendor-pdf.zip.sha256(so a local build run after generating the bundled PDF fonts could ship them in the main zip). Introduced.distignoreas the single source of truth for both;release.yml's "Assemble plugin zip" step now callsbash bin/build-zip.shdirectly instead of duplicating its logic.
Changed
- CI version check now covers 5 locations instead of 1 —
release.ymlpreviously verified only the release tag against the plugin header'sVersion:. It now also verifies theWPMAR_VERSIONconstant,readme.txt's andreadme-ja.txt'sStable tag:, andcomposer.json's"version", failing the release job if any one of them drifts from the tag.
1.4.0
Added
- wp.org plugin/theme metadata now cached (
wpmar_wporg_cache_ttlfilter, default 12h) —WPMAR_WPOrg_Client::fetch_plugin_information()/fetch_theme_information()previously calledwp_remote_get()unconditionally on every run. Results are now cached viaset_site_transient()/get_site_transient()underwpmar_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 thegather:inventory-donestep 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 generation —
WPMAR_Runner::run()andrun_site_segment()now drop the heaviest parts of$dataset(checksums,plugins.org,themes.org) via a sharedrelease_heavy_dataset_memory()helper and callgc_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 line —
WPMAR_Logger::step()now checksmemory_get_usage(true)against 80% ofmemory_limiton every step and logs oneWARNline 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 onequeuedrow per target blog in a new{$wpdb->prefix}wpmar_network_segmentstable (queued→running→done/failed, anattemptscounter for the retry work below, and only the renderedclient_body/admin_body— never the raw per-site dataset), queues onewpmar/run_network_site_segmentaction per blog (each runningWPMAR_Runner::run_site_segment()for exactly one site, in its own process), then onewpmar/run_network_aggregateaction that waits for every segment to reachdone/failedbefore 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. Afailed(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_jobsalso gains anattemptscolumn 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()andWPMAR_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 throughmark_failed()per row instead of a bulkUPDATEso 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, whileattemptsis belowwpmar_job_max_attempts(default 2 — one retry, shared by both scopes), reset the row toqueuedandas_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 toqueuedand re-fires the whole thing from scratch (for a network job, this re-dispatches — segments from the failed attempt are cleared first viadelete_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 above —
wpmar_network_segmentsrows 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 newsegment-history.login the plugin's private logs directory viaWPMAR_Logger::log_segment_outcome(), and the single-site path (WPMAR_Runner::run()) gets the same treatment in a parallelrun-history.logviaWPMAR_Logger::log_run_outcome()— that one also records the site'smemory_limitnext 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-jobrun-*.logfiles, 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 tuningwpmar_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
attemptscolumn on the existing diagnostics log — Both reuse the plugin's existingmanage_options/manage_network_optionscapability constants (no new capability introduced) and the existing admin-post + nonce pattern (newwpmar_admin_actionbranches inWPMAR_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'swpmar_run_lockstate (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 — itswpmar_network_segmentsstatus 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 targetWPMAR_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 error —WPMAR_Logger::handle_shutdown()only ever deleted the single-sitewpmar_run_locktransient; a network rollup killed by a fatal error (OOM, forced process termination) leftwpmar_network_run_lockto expire on its own 20-minute TTL instead of being cleared immediately. The shutdown handler now checks the failed job's ownscopecolumn and releases the matching lock (wpmar_run_lockforsingle,wpmar_network_run_lockfornetwork) — 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 thewp-content/prefix that the Markdown checkbox directly above it already spelled out. Text-only fix; the actual storage location is unchanged (and iswp-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...
1.3.1
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.phpprotection 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 towp-content/wpmar-private/by default (reports/,pdf/,logs/,tmp/subdirectories), overridable via theWPMAR_PRIVATE_STORAGE_DIRconstant orwpmar_private_storage_dirfilter to move storage outside the document root entirely. Every directory gets an auto-generated.htaccess(Require all denied/Deny from all) plusindex.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 bysite-{blog_id}/, sincewp-content(unlikewp_upload_dir()) is shared network-wide. When the private directory isn't writable, the plugin falls back to an equally token-protectedwp-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, andwp wpmar storage migrate --revertreverses it (for downgrading back to a pre-1.3.1 release) while keeping the existing filename/token and adding the.htaccess/index.phpprotection v1.3.0 never had. - PDF rendering: Parsedown safe mode unified across the PDF and HTML-email paths —
WPMAR_PDF_Writer::write_pdf_from_markdown()instantiated\Parsedowndirectly, 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 singlemarkdown_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 setsallow_local_file_accesstofalse. vendor-pdf.zipchecksum 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 theWPMAR_PDF_VENDOR_ZIP_SHA256constant orwpmar_pdf_vendor_zip_sha256filter; 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 URIheader 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: nosniffon every download response — Previously only the diagnostics-log download set this header; Markdown, client-Markdown, PDF, and ZIP downloads now all go through one sharedWPMAR_Download_Headershelper so the header can't be forgotten on a new download endpoint.- PDF-installer Ajax handlers: capability checked before nonce —
handle_ajax(),handle_preflight_ajax(), andhandle_manual_upload_ajax()checked the nonce before theinstall_pluginscapability, the reverse of the order used everywhere else in the plugin since the 1.0.0 hardening; all three now check capability first. SECURITY.mdadded — 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), pinsactions/checkoutandshivammathur/setup-phpto a commit SHA instead of a floating major-version tag, and runscomposer auditagainst dev dependencies in addition to the existing--no-devcheck.
1.3.0
Added
- Glob patterns in checksum exclude lists —
WPMAR_Check_Checksums::build_exclude_set()/is_excluded()now recognize entries containing*,?, or[asfnmatch()glob patterns (matched against the lowercased, normalized relative path), in addition to the existing exact-match and directory-prefix (/,/*suffix) forms. A pattern likewordfence:*/.htaccessexcludes that repeating filename at any nesting depth in one line, instead of requiring one entry per directory. Falls back to apreg_match()-based equivalent (*→.*,?→.) on environments withoutfnmatch().
1.2.0
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
queuedforever. A new loopback detector (WPMAR_Loopback_Detector) probesadmin-ajax.phpthe 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_blockedcolumn 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 viawpmar_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 --syncinstead. - 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
queuedfor ~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 --syncsetup with a multisite--urlexample.
1.1.1
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
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 synchronouswp wpmar audit run --syncCLI path. A shutdown handler captures fatal errors (includingE_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
runningbecause its process was killed hard enough that no handler ever ran (e.g.SIGKILL, OOM killer) is now automatically flipped tofailedonce its heartbeat goes stale (25+ minutes), checked opportunistically whenever the job-status REST endpoint or Reports screen is accessed. The{prefix}wpmar_jobstable gainsstepandlog_pathcolumns. - 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.