diff --git a/CONTEXT.md b/CONTEXT.md index dd652cac..3a265fe3 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -47,6 +47,18 @@ defined in time for their hooks. follow-up, separate from the relocation. Newly authored `inc/` files remain fully linted. +## Config write path + +`src/config/class-config.php` (`Automattic\WPSC\Config`) is the single owner of +the on-disk `wp-cache-config.php` format. All config-file writes flow through +`Config::set()` / `Config::write_line()`. The global functions `wp_cache_setting()` +and `wp_cache_replace_line()` in `wp-cache-phase2.php` are delegating shims kept +for third-party compatibility; they lazy-load the class so engine-only loads +remain safe. `$GLOBALS` variables remain the canonical, supported read API — +reads are not migrated. See `docs/adr/0002-config-module-write-path.md` for the +full decision, constraints, and the catalog of writes that deliberately bypass +`Config::set()`. + ## Tests Two tiers (see `tests/php/README.md`): diff --git a/docs/adr/0002-config-module-write-path.md b/docs/adr/0002-config-module-write-path.md new file mode 100644 index 00000000..b054db7d --- /dev/null +++ b/docs/adr/0002-config-module-write-path.md @@ -0,0 +1,126 @@ +# 2. Config module write-path + +Date: 2026-06-22 + +## Status + +Accepted + +## Context + +After splitting `wp-cache.php` into focused `inc/` files (ADR 0001), config-file +writes were still scattered across those files as direct calls to the +`wp_cache_setting()` and `wp_cache_replace_line()` global functions. Neither +function was tested, their on-disk format rules lived only in `wp_cache_setting()` +at line ~1400 of `wp-cache-phase2.php`, and there was no clear owner of the +`wp-cache-config.php` file format. Issue #1062 called for consolidating the write +path into a testable class. + +The constraint the config file is operating under is fundamental: `wp-cache-phase1.php` +`@include`s it before WordPress boots — no database, no `wp_options` — so it must +remain a flat, pre-WordPress PHP file that sets global variables. + +## Decision + +### `Automattic\WPSC\Config` owns the config-file write path + +`src/config/class-config.php` (`Automattic\WPSC\Config`) is the single owner of +the on-disk `wp-cache-config.php` format. All config-file writes are expected to +flow through it. Its public surface: + +- **`Config::set( $field, $value, $file = null )`** — updates `$GLOBALS[$field]`, + formats the value, and writes the line. The `$GLOBALS` update is unconditional: + the runtime state and the file are always kept in sync. +- **`Config::format_value( $value )`** — encodes the PHP literal written to the + file: numeric → unquoted; bool → `true`/`false`; array/object → whitespace- + collapsed `var_export`; else → single-quoted string. +- **`Config::write_line( $old, $new, $file )`** — the atomic line rewriter: reads + the file, replaces the line matching `$old` (a bare regex), writes to a + `tempnam`-generated temp file, `rename`s it into place, `chmod`s it to match + the original permissions, and calls `opcache_invalidate`. + +### `wp_cache_setting()` and `wp_cache_replace_line()` remain as delegating shims + +Both functions stay in `wp-cache-phase2.php` with their original signatures. They +are part of the surface that third-party cache plugins call, so they are not +removed. Each shim lazy-loads the class via: + +```php +if ( ! class_exists( 'Automattic\WPSC\Config' ) ) { + if ( file_exists( WPCACHEHOME . 'src/config/class-config.php' ) ) { + require_once WPCACHEHOME . 'src/config/class-config.php'; + } +} +``` + +The `file_exists` guard means the debug-log write path keeps working when only the +engine (`wp-cache-phase2.php`) is loaded and the admin tree is absent. + +### Globals remain the canonical read API + +`Config::set()` always writes to `$GLOBALS[$field]`. Reads (`global $foo;` / +`$GLOBALS['foo']` / settings-map dynamic lookups) are intentionally not migrated — +that is a separate, larger effort and out of scope here. Any future `Config::get()` +must be a typed read-through over `$GLOBALS` (the single source of truth, populated +from the flat file before WordPress boots), not a value served from a private copy +the class parsed separately. If `Config` maintained its own parsed copy, a +third-party plugin writing a global directly would be invisible to `get()` and the +two would drift — the same symmetry as the write side, which keeps `$GLOBALS` +authoritative. + +### No new locking or concurrency + +The existing atomic `tempnam`+`rename` sequence is preserved unchanged. File +locking is explicitly out of scope: admin config writes are effectively +single-human, and the rename is already atomic at the OS level. + +### No runtime autoloader + +The class is loaded by explicit `require_once` — from `wp-cache.php` on the admin +path and lazily from the two phase2 shims. The Composer classmap (`composer.json` +`autoload.classmap`) is dev/test-only and not loaded at runtime. + +### Exception catalog: writes that deliberately stay on `wp_cache_replace_line()` + +Most direct `wp_cache_replace_line()` callers in the `inc/` files were migrated to +`Config::set()`. The following writes remain on the raw function because their +on-disk format is not reproducible by `Config::format_value()` or they are not +simple `$field = value;` key/value assignments. These are intentional, not misses: + +| Exception | Reason | +|-----------|--------| +| `define( ... )` rewrites (`WP_CACHE`, `WPCACHEHOME`, `WPLOCKDOWN`) | Not `$var =` assignments; some target the site `wp-config.php`, not the config file | +| Line removals (`$new = ''`) | Removal is not a value write; `Config::set()` has no delete path | +| `cache_path` | Written via `var_export()`, which escapes backslashes and quotes differently from `format_value`'s string branch | +| `cached_direct_pages` | Written as a hand-built `array( ... )` literal with no integer keys; `var_export` emits `array ( 0 => ... )` | +| `wp_cache_mobile_groups` | Written as an imploded comma-string while the runtime global is kept as an array; the on-disk format diverges from the value type | +| `cache_time_interval` | Always written quoted despite a numeric value; `format_value` would emit it unquoted, changing the on-disk bytes | +| `cache_max_time` | Written unquoted in `inc/settings-forms.php` but quoted in `inc/admin-ui.php` — same field, inconsistent legacy formatting across callers; migrating would resolve the inconsistency but would change the on-disk bytes from at least one path | +| `cache_page_secret` | An `md5()` value written quoted unconditionally; an all-digit md5 would be classified as numeric by `format_value` and emitted unquoted | +| `$wp_cache_pages[ "..." ]` | Array sub-key writes, not a simple `$field = value;` line | +| `cache_rejected_user_agent` | The `___`↔space round-trip leaves the in-memory array holding `___` placeholders at write time while the written string has spaces restored, so `Config::set()` (which formats the array) would write different bytes | +| `sem_id` write in `inc/lifecycle.php` | Uses a bare unanchored regex (`'sem_id'`), not the `'^ *\$sem_id'` anchored form `Config::set()` generates; migrating would change which lines the regex matches | +| Debug-log writes in `wp-cache-phase2.php` | Stay on `wp_cache_setting()` for engine-only load safety (the lazy-load guard); the shim handles this correctly | +| `plugins/*` direct calls | Third-party-facing plugin integrations shipped with the plugin; out of scope for this refactor | +| `set_ossdlcdn` in `rest/class.wp-super-cache-rest-update-settings.php` | Raw integer-in-string write (`"\$ossdlcdn = $ossdlcdn;"`); matches `format_value`'s unquoted-int output but was left on the raw call. The handler's other `wp_cache_setting()` calls (already routed through the same `is_numeric` logic) were migrated to `Config::set()` | + +## Consequences + +- Config-file writes have a single testable owner with documented format rules. + The `format_value` and `write_line` methods can be unit-tested in isolation + without a WordPress runtime. +- Direct `wp_cache_replace_line()` callers that were migrated to `Config::set()` + now also write `$GLOBALS[$field]` (the raw rewriter never did). At every + migrated site the on-disk bytes are unchanged; the added global write keeps the + runtime state and the file consistent and was verified to be inert (no site + re-reads the global later in the same request in a way that would observe a + difference). This is intentional, not a behavioural regression. +- The public shim surface (`wp_cache_setting` / `wp_cache_replace_line`) is + unchanged, so third-party plugins are unaffected. +- The exception catalog above is the authoritative list of writes that are not + routed through `Config::set()`. Future contributors should add to it rather than + silently adding direct `wp_cache_replace_line()` calls without explanation. +- A future `Config::get()` is explicitly pre-constrained: it must read through + `$GLOBALS`, not a private parsed copy, to preserve symmetry with the write side. +- The config file's flat-PHP format is locked in as a first-class constraint (not + accidental), because the phase1 drop-in loads it before WordPress boots. diff --git a/docs/superpowers/plans/2026-06-22-config-module-write-path.md b/docs/superpowers/plans/2026-06-22-config-module-write-path.md new file mode 100644 index 00000000..fb09ed30 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-config-module-write-path.md @@ -0,0 +1,736 @@ +# Config Module (write path) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Introduce a single `Automattic\WPSC\Config` module that owns reading/writing the on-disk `wp-cache-config.php` file, route the two public write functions through it, and migrate the in-plugin direct callers to a typed `Config::set()` API — without changing any runtime behaviour or any read site. + +**Architecture:** Additive abstraction. `wp_cache_setting()` and `wp_cache_replace_line()` (both in `wp-cache-phase2.php`) stay as public global functions but become thin delegations to `Config`. The dangerous regex file-rewriter moves verbatim into `Config::write_line()`; the per-type value formatting moves into `Config::format_value()`; `Config::set()` reproduces `wp_cache_setting()` exactly (update `$GLOBALS[$field]`, format, write). Globals remain the canonical store and supported read API. In-plugin direct `wp_cache_replace_line()` callers that are simple key/value writes migrate to `Config::set()`; non-key/value callers (line removal, `define` rewrites) and all third-party/`plugins/` callers stay on the raw (now-delegating) function. + +**Tech Stack:** PHP 7.x+, WordPress plugin (no runtime autoloader — explicit `require_once`; Composer classmap is dev/test-only), PHPUnit 9.6, wp-env/Docker integration tier. + +## Global Constraints + +- **No runtime autoloader.** Load the class with explicit `require_once`, mirroring `plugins/jetpack.php` loading `src/device-detection/class-device-detection.php`. Composer `classmap: ["src/"]` covers it under test only. +- **`src/` convention:** namespace `Automattic\WPSC`, filename `class-config.php`, located at `src/config/class-config.php`. +- **Byte-identical write behaviour.** Every Phase B commit must leave the resulting config-file contents and `$GLOBALS[$field]` value byte-for-byte identical to today. The Phase A characterization suite is the gate. +- **Public surface is frozen.** `wp_cache_setting( $field, $value )` and `wp_cache_replace_line( $old, $new, $my_file )` keep identical signatures and return semantics (third-party cache plugins call them). +- **Write path only.** Do NOT touch any read site (`global $foo;`, `$GLOBALS['foo']`, settings-map dynamic reads). No `Config::get()` in this plan. +- **No new locking/concurrency.** Preserve the existing `tempnam` → `rename` → `chmod` → `opcache_invalidate` sequence exactly. +- **Stay-on-raw callers:** all `plugins/*` callers, the 4 `wp-cache-phase2.php` internal (debug-log) callers, and `rest/*` direct `wp_cache_replace_line()` callers are NOT migrated. Third-party + non-key/value. +- **Tiering:** Config write tests run in the **integration tier** (`tests/php/integration/`, `make test-integration`) — the rewriter calls `wp_rand()` and `set_transient()` unconditionally/optionally, which the no-WordPress smoke bootstrap does not provide. CI stays lint + smoke. +- **Verification commands:** `composer lint` (changed-lines PHPCS — check full output, not just the tail), `composer test-php` (smoke), `make test-integration` (local WP+DB, runs the FULL integration suite — it takes no args), `composer test-e2e` (settings specs — essential for Phase C). +- **Filtered integration runs:** `make test-integration` cannot filter. Where a step says `make test-integration -- --filter NAME`, run this verified form instead: + ```bash + docker exec -w /var/www/html/wp-content/plugins/wp-super-cache \ + -e WP_PHPUNIT__TESTS_CONFIG=/var/www/html/wp-content/plugins/wp-super-cache/tests/php/wp-tests-config.php \ + wp-super-cache-tests-cli-1 \ + vendor/bin/phpunit -c phpunit-integration.9.xml.dist --filter NAME --colors=never + ``` + (Container `wp-super-cache-tests-cli-1` must be up; confirmed working this session.) +- **No `make pre-build`/`publish`/tag/SVN** steps anywhere. + +--- + +## File Structure + +- **Create** `src/config/class-config.php` — the `Automattic\WPSC\Config` class. Sole owner of the config-file format: `set()`, `format_value()`, `write_line()`. ~120 lines. +- **Create** `tests/php/integration/ConfigWritePathTest.php` — Phase A characterization tests against the existing public functions (`wp_cache_setting` / `wp_cache_replace_line`). This is the regression gate for the delegations. +- **Create** `tests/php/integration/ConfigClassTest.php` — Phase B direct unit tests against `Config::set()` / `Config::format_value()` / `Config::write_line()`, mirroring Phase A. +- **Modify** `wp-cache-phase2.php:1445-1565` — replace `wp_cache_setting()` and `wp_cache_replace_line()` bodies with class-guarded delegations. +- **Modify** `wp-cache.php` (after the `inc/` requires, ~line 43) — explicit `require_once` of the Config class for the admin write path. +- **Modify (Phase C)** `inc/lifecycle.php`, `inc/settings-forms.php`, `inc/admin-ui.php`, `inc/preload.php`, `inc/htaccess.php`, `inc/plugins-cookies.php`, `ossdl-cdn.php` — migrate simple key/value writes to `Config::set()`. +- **Modify (Phase D)** `CONTEXT.md`, add `docs/adr/0002-config-module-write-path.md`. + +--- + +## Phase A — Lock down current write behaviour first + +### Task 1: Characterization tests for the existing write path + +**Files:** +- Test: `tests/php/integration/ConfigWritePathTest.php` (create) + +**Interfaces:** +- Consumes: existing globals `wp_cache_setting( $field, $value )`, `wp_cache_replace_line( $old, $new, $my_file )`; globals `$GLOBALS['wp_cache_config_file']`, `$GLOBALS['cache_path']`. +- Produces: a green characterization suite that later tasks re-run unchanged. + +The temp-config helper mirrors the proven pattern in `SettingsFormUpdatersTest.php` (sets both `wp_cache_config_file` and `cache_path`, the latter used by the rewriter's `tempnam`). + +- [ ] **Step 1: Write the failing test file** + +```php +temp_dirs as $dir ) { + $this->rrmdir( $dir ); + } + $this->temp_dirs = array(); + parent::tear_down(); + } + + /** + * Seed a writable temp config file and point the write-path globals at it. + * + * @param string[] $lines Config lines (without the opening PHP tag). + * @return string Absolute path to the config file. + */ + private function make_config_file( array $lines ) { + $dir = trailingslashit( get_temp_dir() ) . 'wpsc-cfg-' . uniqid(); + mkdir( $dir, 0700, true ); + $this->temp_dirs[] = $dir; + + $config = trailingslashit( $dir ) . 'wp-cache-config.php'; + file_put_contents( $config, "rrmdir( $path ) : unlink( $path ); + } + rmdir( $dir ); + } + + public function test_setting_numeric_writes_unquoted_and_sets_global() { + $config = $this->make_config_file( array( '$wp_cache_mod_rewrite = 0;' ) ); + + $this->assertTrue( wp_cache_setting( 'wp_cache_mod_rewrite', 1 ) ); + $this->assertSame( 1, $GLOBALS['wp_cache_mod_rewrite'] ); + $this->assertStringContainsString( '$wp_cache_mod_rewrite = 1;', file_get_contents( $config ) ); + } + + public function test_setting_boolean_writes_true_false_literal() { + $config = $this->make_config_file( array( '$wp_supercache_304 = false;' ) ); + + wp_cache_setting( 'wp_supercache_304', true ); + $this->assertSame( true, $GLOBALS['wp_supercache_304'] ); + $this->assertStringContainsString( '$wp_supercache_304 = true;', file_get_contents( $config ) ); + } + + public function test_setting_array_writes_collapsed_var_export() { + $config = $this->make_config_file( array( '$cache_rejected_uri = array();' ) ); + + wp_cache_setting( 'cache_rejected_uri', array( 'wp-admin', 'feed' ) ); + $this->assertSame( array( 'wp-admin', 'feed' ), $GLOBALS['cache_rejected_uri'] ); + $this->assertStringContainsString( + "\$cache_rejected_uri = array ( 0 => 'wp-admin', 1 => 'feed', );", + file_get_contents( $config ) + ); + } + + public function test_setting_string_writes_single_quoted() { + $config = $this->make_config_file( array( "\$wp_cache_debug_ip = '';" ) ); + + wp_cache_setting( 'wp_cache_debug_ip', '203.0.113.5' ); + $this->assertSame( '203.0.113.5', $GLOBALS['wp_cache_debug_ip'] ); + $this->assertStringContainsString( "\$wp_cache_debug_ip = '203.0.113.5';", file_get_contents( $config ) ); + } + + public function test_replace_line_replaces_matching_line() { + $config = $this->make_config_file( array( '$wp_cache_front_page_checks = 0;' ) ); + + $this->assertTrue( + wp_cache_replace_line( '^ *\$wp_cache_front_page_checks', '$wp_cache_front_page_checks = 1;', $config ) + ); + $this->assertStringContainsString( '$wp_cache_front_page_checks = 1;', file_get_contents( $config ) ); + $this->assertStringNotContainsString( '$wp_cache_front_page_checks = 0;', file_get_contents( $config ) ); + } + + public function test_replace_line_unchanged_is_noop_early_return() { + $config = $this->make_config_file( array( '$wp_cache_mobile = 1;' ) ); + $before = file_get_contents( $config ); + + $this->assertTrue( wp_cache_replace_line( '^ *\$wp_cache_mobile', '$wp_cache_mobile = 1;', $config ) ); + $this->assertSame( $before, file_get_contents( $config ) ); + } + + public function test_replace_line_not_found_appends_after_assignments() { + $config = $this->make_config_file( array( '$existing = 1;' ) ); + + $this->assertTrue( wp_cache_replace_line( '^ *\$brand_new_key', '$brand_new_key = 5;', $config ) ); + $this->assertStringContainsString( '$brand_new_key = 5;', file_get_contents( $config ) ); + $this->assertStringContainsString( '$existing = 1;', file_get_contents( $config ) ); + } + + public function test_replace_line_missing_file_returns_false() { + $this->assertFalse( wp_cache_replace_line( '^ *\$x', '$x = 1;', '/no/such/wp-cache-config.php' ) ); + } +} +``` + +- [ ] **Step 2: Run the suite to verify it PASSES against current code** + +Run: `make test-integration -- --filter ConfigWritePathTest` (or the raw `docker exec ... vendor/bin/phpunit -c phpunit-integration.9.xml.dist --filter ConfigWritePathTest` fallback from the handoff). +Expected: all 8 tests PASS. (This task characterizes existing behaviour, so the suite is green from the start — it is the regression net, not a red-then-green TDD cycle.) + +- [ ] **Step 3: Confirm it runs under the smoke tier guard (must SKIP/ABSENT, not fatal)** + +Run: `composer test-php` +Expected: PASS — `ConfigWritePathTest` is in `tests/php/integration/` and is not collected by the smoke config. Confirm no fatal from `wp_rand`. + +- [ ] **Step 4: Commit** + +```bash +git add tests/php/integration/ConfigWritePathTest.php +git commit -m "test: characterize config-file write path before Config module (#1062)" +``` + +--- + +## Phase B — Introduce the owner (no caller changes yet) + +### Task 2: Add the `Config` class + mirrored unit tests + +**Files:** +- Create: `src/config/class-config.php` +- Create: `tests/php/integration/ConfigClassTest.php` + +**Interfaces:** +- Produces: + - `Automattic\WPSC\Config::set( string $field, mixed $value, ?string $file = null ): bool` — updates `$GLOBALS[$field]`, formats by type, writes; returns the writer result. `$file` defaults to `$GLOBALS['wp_cache_config_file']`. + - `Automattic\WPSC\Config::format_value( mixed $value ): string` — returns the PHP literal exactly as `wp_cache_setting()` builds it today (numeric → `(string)`, bool → `true`/`false`, array/object → whitespace-collapsed `var_export`, else → single-quoted). + - `Automattic\WPSC\Config::write_line( string $old, string $new, string $file ): bool` — the verbatim body of today's `wp_cache_replace_line()`. +- Consumes: WordPress runtime `wp_rand()`, `set_transient()`, `wp_cache_debug()`, `__()`, `is_writeable_ACLSafe()` (all already used by the original function). + +- [ ] **Step 1: Write the failing test file** + +```php +temp_dirs as $dir ) { + $this->rrmdir( $dir ); + } + $this->temp_dirs = array(); + parent::tear_down(); + } + + private function make_config_file( array $lines ) { + $dir = trailingslashit( get_temp_dir() ) . 'wpsc-cfg-' . uniqid(); + mkdir( $dir, 0700, true ); + $this->temp_dirs[] = $dir; + $config = trailingslashit( $dir ) . 'wp-cache-config.php'; + file_put_contents( $config, "rrmdir( $path ) : unlink( $path ); + } + rmdir( $dir ); + } + + public function test_format_value_numeric() { + $this->assertSame( '5', \Automattic\WPSC\Config::format_value( 5 ) ); + $this->assertSame( '0', \Automattic\WPSC\Config::format_value( 0 ) ); + } + + public function test_format_value_boolean() { + $this->assertSame( 'true', \Automattic\WPSC\Config::format_value( true ) ); + $this->assertSame( 'false', \Automattic\WPSC\Config::format_value( false ) ); + } + + public function test_format_value_array_is_whitespace_collapsed() { + $this->assertSame( + "array ( 0 => 'a', 1 => 'b', )", + \Automattic\WPSC\Config::format_value( array( 'a', 'b' ) ) + ); + } + + public function test_format_value_string_single_quoted() { + $this->assertSame( "'hello'", \Automattic\WPSC\Config::format_value( 'hello' ) ); + } + + public function test_set_updates_global_and_file() { + $config = $this->make_config_file( array( '$wp_cache_mobile = 0;' ) ); + $this->assertTrue( \Automattic\WPSC\Config::set( 'wp_cache_mobile', 1 ) ); + $this->assertSame( 1, $GLOBALS['wp_cache_mobile'] ); + $this->assertStringContainsString( '$wp_cache_mobile = 1;', file_get_contents( $config ) ); + } + + public function test_set_array_matches_legacy_format() { + $config = $this->make_config_file( array( '$cache_acceptable_files = array();' ) ); + \Automattic\WPSC\Config::set( 'cache_acceptable_files', array( 'wp-comments-popup.php' ) ); + $this->assertStringContainsString( + "\$cache_acceptable_files = array ( 0 => 'wp-comments-popup.php', );", + file_get_contents( $config ) + ); + } + + public function test_write_line_replaces() { + $config = $this->make_config_file( array( '$x = 0;' ) ); + $this->assertTrue( \Automattic\WPSC\Config::write_line( '^ *\$x', '$x = 9;', $config ) ); + $this->assertStringContainsString( '$x = 9;', file_get_contents( $config ) ); + } + + public function test_write_line_missing_file_returns_false() { + $this->assertFalse( \Automattic\WPSC\Config::write_line( '^ *\$x', '$x = 1;', '/no/such/file.php' ) ); + } +} +``` + +- [ ] **Step 2: Run to verify it FAILS** + +Run: `make test-integration -- --filter ConfigClassTest` +Expected: FAIL — `Error: Class "Automattic\WPSC\Config" not found`. + +- [ ] **Step 3: Write the class** + +Create `src/config/class-config.php`. `write_line()` is the verbatim body of `wp_cache_replace_line()` (`wp-cache-phase2.php:1463-1565`) with the parameter renamed `$my_file` → `$file`. `format_value()` and `set()` reproduce `wp_cache_setting()` exactly. + +```php + write -> rename -> chmod -> opcache_invalidate sequence is + * preserved exactly. + * + * @param string $old Regex (without delimiters) matching the line to replace. + * @param string $new Replacement line (no trailing newline). + * @param string $file Config file path. + * @return bool True on success/unchanged, false on failure. + */ + public static function write_line( $old, $new, $file ) { + // PASTE the verbatim body of wp_cache_replace_line() here, renaming the + // $my_file parameter to $file throughout. Do not alter any logic. + } +} +``` + +When pasting `write_line()`, copy `wp-cache-phase2.php:1464-1564` exactly and rename `$my_file` → `$file`. Do not "improve" it — byte-identical output is the contract. + +- [ ] **Step 4: Wire runtime loading from `wp-cache.php`** + +In `wp-cache.php`, immediately after the block of `inc/` requires (after line 43, `require_once __DIR__ . '/inc/admin-ui.php';`), add: + +```php +require_once __DIR__ . '/src/config/class-config.php'; +``` + +- [ ] **Step 5: Run the class tests to verify they PASS** + +Run: `make test-integration -- --filter ConfigClassTest` +Expected: all tests PASS. + +- [ ] **Step 6: Run the Phase A suite — still green (class loadable, unused)** + +Run: `make test-integration -- --filter ConfigWritePathTest` +Expected: PASS unchanged. + +- [ ] **Step 7: Commit** + +```bash +git add src/config/class-config.php tests/php/integration/ConfigClassTest.php wp-cache.php +git commit -m "feat: add Automattic\\WPSC\\Config write-path module (#1062)" +``` + +### Task 3: Delegate `wp_cache_replace_line()` to the module + +**Files:** +- Modify: `wp-cache-phase2.php:1463-1565` + +**Interfaces:** +- Consumes: `Automattic\WPSC\Config::write_line()`. +- Produces: unchanged `wp_cache_replace_line( $old, $new, $my_file )` signature; all 74 callers route through `Config` transparently. + +- [ ] **Step 1: Replace the function body with a class-guarded delegation** + +Replace the entire `wp_cache_replace_line()` function (`wp-cache-phase2.php:1463-1565`) with: + +```php +function wp_cache_replace_line( $old, $new, $my_file ) { + if ( ! class_exists( 'Automattic\WPSC\Config' ) ) { + if ( file_exists( WPCACHEHOME . 'src/config/class-config.php' ) ) { + require_once WPCACHEHOME . 'src/config/class-config.php'; + } + } + + return \Automattic\WPSC\Config::write_line( $old, $new, $my_file ); +} +``` + +The lazy `class_exists` guard keeps the phase2/debug-log write path working when only the engine is loaded (e.g. `advanced-cache.php` defines `WPCACHEHOME`). + +- [ ] **Step 2: Run both suites — must be byte-identical** + +Run: `make test-integration -- --filter ConfigWritePathTest && make test-integration -- --filter ConfigClassTest` +Expected: all PASS, no changes. + +- [ ] **Step 3: Lint** + +Run: `composer lint` +Expected: pass. (Note: `wp-cache-phase2.php` is NOT in the `inc/` exclusion list, so changed lines are checked. Check full output.) + +- [ ] **Step 4: Commit** + +```bash +git add wp-cache-phase2.php +git commit -m "refactor: delegate wp_cache_replace_line() to Config::write_line() (#1062)" +``` + +### Task 4: Delegate `wp_cache_setting()` to the module + +**Files:** +- Modify: `wp-cache-phase2.php:1445-1461` + +**Interfaces:** +- Consumes: `Automattic\WPSC\Config::set()`. +- Produces: unchanged `wp_cache_setting( $field, $value )` signature; after this commit there is exactly one owner of the file format and all 117+ write sites flow through `Config` via the two shims. + +- [ ] **Step 1: Replace the function body with a class-guarded delegation** + +Replace the entire `wp_cache_setting()` function (`wp-cache-phase2.php:1445-1461`) with: + +```php +function wp_cache_setting( $field, $value ) { + global $wp_cache_config_file; + + if ( ! class_exists( 'Automattic\WPSC\Config' ) ) { + if ( file_exists( WPCACHEHOME . 'src/config/class-config.php' ) ) { + require_once WPCACHEHOME . 'src/config/class-config.php'; + } + } + + return \Automattic\WPSC\Config::set( $field, $value, $wp_cache_config_file ); +} +``` + +Passing `$wp_cache_config_file` explicitly preserves the original's `global $wp_cache_config_file` source byte-for-byte (rather than relying on `Config::set`'s `$GLOBALS` default). + +- [ ] **Step 2: Run both suites — byte-identical** + +Run: `make test-integration -- --filter ConfigWritePathTest && make test-integration -- --filter ConfigClassTest` +Expected: all PASS. + +- [ ] **Step 3: Run the existing settings-form net (no regression)** + +Run: `make test-integration -- --filter SettingsFormUpdatersTest` +Expected: PASS unchanged — these go through `wp_cache_setting` / `wp_cache_replace_line`. + +- [ ] **Step 4: Lint + commit** + +Run: `composer lint` + +```bash +git add wp-cache-phase2.php +git commit -m "refactor: delegate wp_cache_setting() to Config::set() (#1062)" +``` + +--- + +## Phase C — Migrate direct callers to the typed API + +**Migration rule (applies to every Phase C task).** A call is migratable ONLY if it is a simple key/value write of the exact form: + +```php +wp_cache_replace_line( '^ *\$FIELD', "\$FIELD = ;", $file ); +``` + +where `` is what `Config::format_value()` produces for the value. Convert it to: + +```php +\Automattic\WPSC\Config::set( 'FIELD', $value, $file ); +``` + +**Leave on the raw function (do NOT migrate)** any call that: +- removes a line (replacement is `''`), +- rewrites a `define(...)` or `if ( ! defined ... )` line, +- writes a hand-built literal that `format_value()` would not reproduce (e.g. embedded expressions, multi-line, manual quoting that differs), +- is in `plugins/*`, `rest/*`, or is one of the 4 internal `wp-cache-phase2.php` debug-log calls. + +For every migrated call, confirm `Config::format_value($value)` reproduces the original replacement string's value portion. When in doubt, leave it on the raw function and list it in the commit body as an explicit exception. + +**Per-task workflow (same for Tasks 5–9):** +1. List the cluster's calls: `grep -n "wp_cache_replace_line(" inc/.php`. +2. For each, classify migratable vs exception per the rule above. +3. Convert the migratable ones to `Config::set()`. +4. Run the relevant integration suite + e2e settings specs. +5. Commit; list exceptions in the commit body. + +### Task 5: Lifecycle writes (`inc/lifecycle.php`) + +**Files:** +- Modify: `inc/lifecycle.php` (8 `wp_cache_replace_line` + 10 `wp_cache_setting` sites) + +The `wp_cache_setting` sites already delegate (Task 4) — leave them unless converting for consistency is trivial; the deliverable is migrating the **direct `wp_cache_replace_line`** calls. + +- [ ] **Step 1: List and classify** + +Run: `grep -n "wp_cache_replace_line(" inc/lifecycle.php` +For each line, apply the migration rule. Activation/deactivation/enable-disable toggles (e.g. `wp_cache_mod_rewrite`, `wp_cache_enabled`) are typically simple numeric/bool sets → migratable. + +- [ ] **Step 2: Convert each migratable call** + +For each migratable site, replace +`wp_cache_replace_line( '^ *\$FIELD', "\$FIELD = $value;", $file )` +with +`\Automattic\WPSC\Config::set( 'FIELD', $value, $file )`. +Preserve the surrounding control flow and the `$file`/config-file variable used at that site. + +- [ ] **Step 3: Run integration + e2e** + +Run: `make test-integration` then `composer test-e2e` +Expected: PASS. e2e exercises activation/lifecycle — essential here. + +- [ ] **Step 4: Lint + commit** + +Run: `composer lint` +(`inc/lifecycle.php` is in the `.phpcs.xml.dist` exclusion list per #1066, so full-file PHPCS is excluded; `composer lint` still checks changed lines via the branch-diff sub-check — read full output.) + +```bash +git add inc/lifecycle.php +git commit -m "refactor: migrate lifecycle config writes to Config::set() (#1062)" +``` + +### Task 6: Settings-form writes (`inc/settings-forms.php`) + +**Files:** +- Modify: `inc/settings-forms.php` (20 `wp_cache_replace_line` sites) + +- [ ] **Step 1: List and classify** + +Run: `grep -n "wp_cache_replace_line(" inc/settings-forms.php` +Covers the rejected/accepted-list, tracking-parameter, and debug-settings updaters. Watch for any non-key/value rewrites (leave on raw). + +- [ ] **Step 2: Convert migratable calls** (per the migration rule). + +- [ ] **Step 3: Run the strongest net for this cluster** + +Run: `make test-integration -- --filter SettingsFormUpdatersTest` then `make test-integration` then `composer test-e2e` +Expected: PASS. `SettingsFormUpdatersTest` directly pins this cluster's parse→persist contract. + +- [ ] **Step 4: Lint + commit** + +```bash +git add inc/settings-forms.php +git commit -m "refactor: migrate settings-form config writes to Config::set() (#1062)" +``` + +### Task 7: Admin-UI / manager writes (`inc/admin-ui.php`) + +**Files:** +- Modify: `inc/admin-ui.php` (28 `wp_cache_replace_line` sites — the largest cluster, the `wp_cache_manager_updates` save path) + +This cluster is the highest-risk: most sites, most likely to contain subtle non-key/value rewrites. Classify each carefully; leave anything non-trivial on the raw function and list it. + +- [ ] **Step 1: List and classify** — `grep -n "wp_cache_replace_line(" inc/admin-ui.php` + +- [ ] **Step 2: Convert migratable calls** (per the migration rule). Leave exceptions on raw. + +- [ ] **Step 3: Run integration + e2e** + +Run: `make test-integration` then `composer test-e2e` +Expected: PASS. The settings save path is exercised by the e2e settings specs. + +- [ ] **Step 4: Lint + commit** (list any exceptions in the body) + +```bash +git add inc/admin-ui.php +git commit -m "refactor: migrate admin-UI config writes to Config::set() (#1062)" +``` + +### Task 8: Preload writes (`inc/preload.php`) + +**Files:** +- Modify: `inc/preload.php` (8 `wp_cache_setting` sites; 0 direct `wp_cache_replace_line`) + +Since `inc/preload.php` has no direct `wp_cache_replace_line` calls, this task is the optional consistency conversion of its `wp_cache_setting` calls to `Config::set()`. If the maintainer skips the cosmetic Task 10, **skip this task too** — the preload writes already delegate. + +- [ ] **Step 1: List** — `grep -n "wp_cache_setting(" inc/preload.php` + +- [ ] **Step 2: (Optional) Convert** `wp_cache_setting( 'FIELD', $value )` → `\Automattic\WPSC\Config::set( 'FIELD', $value )`. + +- [ ] **Step 3: Run** — `make test-integration -- --filter PreloadStatusTest` then `composer test-e2e` + +- [ ] **Step 4: Lint + commit** + +```bash +git add inc/preload.php +git commit -m "refactor: route preload config writes through Config::set() (#1062)" +``` + +### Task 9: Remaining scattered writes + +**Files:** +- Modify: `inc/htaccess.php` (1 `wp_cache_replace_line` + 2 `wp_cache_setting`), `inc/plugins-cookies.php` (5 `wp_cache_setting`), `ossdl-cdn.php` (1 `wp_cache_setting`) + +htaccess/mod-rewrite flags are the likeliest to contain `define` rewrites — classify carefully. + +- [ ] **Step 1: List per file** + +Run: `grep -n "wp_cache_replace_line(\|wp_cache_setting(" inc/htaccess.php inc/plugins-cookies.php ossdl-cdn.php` + +- [ ] **Step 2: Convert migratable calls**; leave `define`/conditional rewrites on raw. + +- [ ] **Step 3: Run integration + e2e** + +Run: `make test-integration -- --filter HtaccessRulesTest` then `make test-integration` then `composer test-e2e` + +- [ ] **Step 4: Lint + commit** (list exceptions) + +```bash +git add inc/htaccess.php inc/plugins-cookies.php ossdl-cdn.php +git commit -m "refactor: migrate remaining scattered config writes to Config::set() (#1062)" +``` + +### Task 10 (optional, cosmetic): Retire internal `wp_cache_setting()` usage + +Convert the remaining internal `wp_cache_setting()` calls to `Config::set()` for consistency. `wp_cache_setting()` stays as a public back-compat alias. Drop this task if churn outweighs value — the maintainer's call. + +- [ ] **Step 1:** `grep -rn "wp_cache_setting(" inc/ rest/ ossdl-cdn.php` (exclude the definition). +- [ ] **Step 2:** Convert each to `\Automattic\WPSC\Config::set(...)`. +- [ ] **Step 3:** `make test-integration && composer test-e2e`. +- [ ] **Step 4:** `composer lint` + commit `refactor: use Config::set() for internal config writes (#1062)`. + +--- + +## Phase D — Document + +### Task 11: Document the module and the contract + +**Files:** +- Modify: `CONTEXT.md` +- Create: `docs/adr/0002-config-module-write-path.md` + +- [ ] **Step 1: Add an ADR** + +Create `docs/adr/0002-config-module-write-path.md` (follow the format of `docs/adr/0001-split-wp-cache-into-inc-files.md`) stating: +- All config-file writes go through `Automattic\WPSC\Config`. +- `wp_cache_setting()` / `wp_cache_replace_line()` remain as public delegating shims (third-party surface). +- The globals / `$GLOBALS` remain a permanent, supported read API; reads are intentionally NOT migrated. +- The config file stays a flat pre-WordPress PHP file (drop-in `@include`s it before WP boots; cannot move to `wp_options`). +- No locking added; the existing atomic `tempnam`+`rename` is preserved (single-human admin writes). +- Constraint recorded for a future `Config::get()`: it must be a typed read-through over `$GLOBALS` (single source of truth), not a private parsed copy. + +- [ ] **Step 2: Update `CONTEXT.md`** + +Add a short "Config write path" entry pointing at `src/config/class-config.php` as the owner and linking the ADR. + +- [ ] **Step 3: Commit** + +```bash +git add CONTEXT.md docs/adr/0002-config-module-write-path.md +git commit -m "docs: record Config write-path module and contract (#1062)" +``` + +--- + +## Self-Review + +- **Spec coverage:** Phase A (commit 1) → Task 1. Phase B (commits 2–4) → Tasks 2–4. Phase C (commits 5–10) → Tasks 5–10. Phase D (commit 11) → Task 11. Decision Document points (globals permanent, flat file, no locking, shims stay, no autoloader) → Global Constraints + Task 11 ADR. Out-of-scope items (read migration, `Config::get()`, locking) → excluded and recorded. +- **Type consistency:** `Config::set($field, $value, $file=null)`, `Config::format_value($value)`, `Config::write_line($old, $new, $file)` used identically in Tasks 2, 3, 4, and the Phase C migration rule. +- **Drift noted:** issue says 64 direct `wp_cache_replace_line` callers; actual is 74 (28 admin-ui + 20 settings-forms + 8 lifecycle + 1 htaccess in-plugin migratable; 11 plugins/phase2/rest stay on raw). Clusters unchanged. +- **Placeholders:** the only deliberate "fill at execution" point is the verbatim paste of the rewriter body into `write_line()` (Task 2 Step 3) — sourced exactly from `wp-cache-phase2.php:1464-1564` — and the per-site classification in Phase C, which is inherent to the work (the issue itself flags that subtle non-key/value cases hide in admin-ui/settings-forms and must be inspected, not blind-converted). diff --git a/inc/admin-ui.php b/inc/admin-ui.php index 1706e033..325a372d 100644 --- a/inc/admin-ui.php +++ b/inc/admin-ui.php @@ -128,7 +128,7 @@ function wp_cache_manager_error_checks() { ' . __( "Warning! You attempted to enable compression but zlib.output_compression is enabled. See #21 in the Troubleshooting section of the readme file.", 'wp-super-cache' ) . ''; } elseif ( $new_cache_compression !== (int) $cache_compression ) { $cache_compression = $new_cache_compression; - wp_cache_replace_line( '^ *\$cache_compression', "\$cache_compression = $cache_compression;", $wp_cache_config_file ); + \Automattic\WPSC\Config::set( 'cache_compression', $cache_compression, $wp_cache_config_file ); if ( function_exists( 'prune_super_cache' ) ) { prune_super_cache( $cache_path, true ); } @@ -728,18 +728,18 @@ function toggleLayer( whichLayer ) { } if ( substr( get_option( 'permalink_structure' ), -1 ) == '/' ) { - wp_cache_replace_line( '^ *\$wp_cache_slash_check', "\$wp_cache_slash_check = 1;", $wp_cache_config_file ); + \Automattic\WPSC\Config::set( 'wp_cache_slash_check', 1, $wp_cache_config_file ); } else { - wp_cache_replace_line( '^ *\$wp_cache_slash_check', "\$wp_cache_slash_check = 0;", $wp_cache_config_file ); + \Automattic\WPSC\Config::set( 'wp_cache_slash_check', 0, $wp_cache_config_file ); } $home_path = parse_url( site_url() ); $home_path = trailingslashit( array_key_exists( 'path', $home_path ) ? $home_path['path'] : '' ); if ( ! isset( $wp_cache_home_path ) ) { $wp_cache_home_path = '/'; - wp_cache_setting( 'wp_cache_home_path', '/' ); + \Automattic\WPSC\Config::set( 'wp_cache_home_path', '/' ); } if ( "$home_path" != "$wp_cache_home_path" ) { - wp_cache_setting( 'wp_cache_home_path', $home_path ); + \Automattic\WPSC\Config::set( 'wp_cache_home_path', $home_path ); } if ( $wp_cache_mobile_enabled == 1 ) { diff --git a/inc/htaccess.php b/inc/htaccess.php index b71b59e5..cbfdeeba 100644 --- a/inc/htaccess.php +++ b/inc/htaccess.php @@ -41,8 +41,8 @@ function wpsc_remove_marker( $filename, $marker ) { function update_cached_mobile_ua_list( $mobile_browsers, $mobile_prefixes = 0, $mobile_groups = 0 ) { global $wp_cache_config_file, $wp_cache_mobile_browsers, $wp_cache_mobile_prefixes, $wp_cache_mobile_groups; - wp_cache_setting( 'wp_cache_mobile_browsers', $mobile_browsers ); - wp_cache_setting( 'wp_cache_mobile_prefixes', $mobile_prefixes ); + \Automattic\WPSC\Config::set( 'wp_cache_mobile_browsers', $mobile_browsers ); + \Automattic\WPSC\Config::set( 'wp_cache_mobile_prefixes', $mobile_prefixes ); if ( is_array( $mobile_groups ) ) { $wp_cache_mobile_groups = $mobile_groups; wp_cache_replace_line('^ *\$wp_cache_mobile_groups', "\$wp_cache_mobile_groups = '" . implode( ', ', $mobile_groups ) . "';", $wp_cache_config_file); diff --git a/inc/lifecycle.php b/inc/lifecycle.php index 03b07651..bbf7a835 100644 --- a/inc/lifecycle.php +++ b/inc/lifecycle.php @@ -81,7 +81,7 @@ function wpsupercache_deactivate() { wp_clear_scheduled_hook( 'wp_cache_check_site_hook' ); wp_clear_scheduled_hook( 'wp_cache_gc' ); wp_clear_scheduled_hook( 'wp_cache_gc_watcher' ); - wp_cache_replace_line('^ *\$cache_enabled', '$cache_enabled = false;', $wp_cache_config_file); + \Automattic\WPSC\Config::set( 'cache_enabled', false, $wp_cache_config_file ); wp_cache_disable_plugin( false ); // don't delete configuration file delete_user_option( get_current_user_id(), 'wpsc_dismissed_boost_banner' ); } @@ -140,7 +140,7 @@ function wp_cache_enable() { return true; } - wp_cache_setting( 'cache_enabled', true ); + \Automattic\WPSC\Config::set( 'cache_enabled', true ); wp_cache_debug( 'wp_cache_enable: enable cache' ); $cache_enabled = true; @@ -162,7 +162,7 @@ function wp_cache_disable() { return true; } - wp_cache_setting( 'cache_enabled', false ); + \Automattic\WPSC\Config::set( 'cache_enabled', false ); wp_cache_debug( 'wp_cache_disable: disable cache' ); $cache_enabled = false; @@ -180,7 +180,7 @@ function wp_super_cache_enable() { return true; } - wp_cache_setting( 'super_cache_enabled', true ); + \Automattic\WPSC\Config::set( 'super_cache_enabled', true ); wp_cache_debug( 'wp_super_cache_enable: enable cache' ); $super_cache_enabled = true; @@ -207,7 +207,7 @@ function wp_super_cache_disable() { return true; } - wp_cache_setting( 'super_cache_enabled', false ); + \Automattic\WPSC\Config::set( 'super_cache_enabled', false ); wp_cache_debug( 'wp_super_cache_disable: disable cache' ); $super_cache_enabled = false; @@ -727,11 +727,11 @@ function wpsc_set_default_gc( $force = false ) { $cache_max_time = 1800; $cache_schedule_interval = 'hourly'; $cache_gc_email_me = 0; - wp_cache_setting( 'cache_schedule_type', $cache_schedule_type ); - wp_cache_setting( 'cache_time_interval', $cache_time_interval ); - wp_cache_setting( 'cache_max_time', $cache_max_time ); - wp_cache_setting( 'cache_schedule_interval', $cache_schedule_interval ); - wp_cache_setting( 'cache_gc_email_me', $cache_gc_email_me ); + \Automattic\WPSC\Config::set( 'cache_schedule_type', $cache_schedule_type ); + \Automattic\WPSC\Config::set( 'cache_time_interval', $cache_time_interval ); + \Automattic\WPSC\Config::set( 'cache_max_time', $cache_max_time ); + \Automattic\WPSC\Config::set( 'cache_schedule_interval', $cache_schedule_interval ); + \Automattic\WPSC\Config::set( 'cache_gc_email_me', $cache_gc_email_me ); wp_schedule_single_event( time() + 600, 'wp_cache_gc' ); } @@ -746,7 +746,7 @@ function wpsc_update_check() { ! isset( $wpsc_version ) || $wpsc_version != 169 ) { - wp_cache_setting( 'wpsc_version', 169 ); + \Automattic\WPSC\Config::set( 'wpsc_version', 169 ); global $wp_cache_debug_log, $cache_path; $log_file = $cache_path . str_replace('/', '', str_replace('..', '', $wp_cache_debug_log)); if ( ! file_exists( $log_file ) ) { diff --git a/inc/plugins-cookies.php b/inc/plugins-cookies.php index f603bc55..c0bc8cdb 100644 --- a/inc/plugins-cookies.php +++ b/inc/plugins-cookies.php @@ -44,7 +44,7 @@ function wpsc_update_plugin_list( $update ) { foreach( $update as $key => $enabled ) { $plugin_toggle = "cache_{$key}"; if ( isset( $GLOBALS[ $plugin_toggle ] ) || isset( $list[ $key ] ) ) { - wp_cache_setting( $plugin_toggle, (int)$enabled ); + \Automattic\WPSC\Config::set( $plugin_toggle, (int)$enabled ); } } } @@ -60,7 +60,7 @@ function wpsc_add_plugin( $file ) { ! in_array( $file, $wpsc_plugins ) ) { $wpsc_plugins[] = $file; - wp_cache_setting( 'wpsc_plugins', $wpsc_plugins ); + \Automattic\WPSC\Config::set( 'wpsc_plugins', $wpsc_plugins ); } return $file; } @@ -77,7 +77,7 @@ function wpsc_delete_plugin( $file ) { in_array( $file, $wpsc_plugins ) ) { unset( $wpsc_plugins[ array_search( $file, $wpsc_plugins ) ] ); - wp_cache_setting( 'wpsc_plugins', $wpsc_plugins ); + \Automattic\WPSC\Config::set( 'wpsc_plugins', $wpsc_plugins ); } return $file; } @@ -96,7 +96,7 @@ function wpsc_add_cookie( $name ) { ! in_array( $name, $wpsc_cookies ) ) { $wpsc_cookies[] = $name; - wp_cache_setting( 'wpsc_cookies', $wpsc_cookies ); + \Automattic\WPSC\Config::set( 'wpsc_cookies', $wpsc_cookies ); } return $name; } @@ -110,7 +110,7 @@ function wpsc_delete_cookie( $name ) { in_array( $name, $wpsc_cookies ) ) { unset( $wpsc_cookies[ array_search( $name, $wpsc_cookies ) ] ); - wp_cache_setting( 'wpsc_cookies', $wpsc_cookies ); + \Automattic\WPSC\Config::set( 'wpsc_cookies', $wpsc_cookies ); } return $name; } diff --git a/inc/preload.php b/inc/preload.php index 80e75b3b..68771768 100644 --- a/inc/preload.php +++ b/inc/preload.php @@ -161,7 +161,7 @@ function wp_cron_preload_cache() { if ( $wp_cache_preload_email_volume == 'none' && $wp_cache_preload_email_me == 1 ) { $wp_cache_preload_email_me = 0; - wp_cache_setting( 'wp_cache_preload_email_me', 0 ); + \Automattic\WPSC\Config::set( 'wp_cache_preload_email_me', 0 ); } $just_started_preloading = false; @@ -656,7 +656,7 @@ function wpsc_preload_settings() { // phpcs:ignore WordPress.Security.NonceVerification.Missing $wp_cache_preload_interval = (int) $_POST['wp_cache_preload_interval']; - wp_cache_setting( 'wp_cache_preload_interval', $wp_cache_preload_interval ); + \Automattic\WPSC\Config::set( 'wp_cache_preload_interval', $wp_cache_preload_interval ); } if ( $_POST[ 'wp_cache_preload_posts' ] == 'all' ) { @@ -664,33 +664,33 @@ function wpsc_preload_settings() { } else { $wp_cache_preload_posts = (int)$_POST[ 'wp_cache_preload_posts' ]; } - wp_cache_setting( 'wp_cache_preload_posts', $wp_cache_preload_posts ); + \Automattic\WPSC\Config::set( 'wp_cache_preload_posts', $wp_cache_preload_posts ); if ( isset( $_POST[ 'wp_cache_preload_email_volume' ] ) && in_array( $_POST[ 'wp_cache_preload_email_volume' ], array( 'none', 'less', 'medium', 'many' ) ) ) { $wp_cache_preload_email_volume = $_POST[ 'wp_cache_preload_email_volume' ]; } else { $wp_cache_preload_email_volume = 'none'; } - wp_cache_setting( 'wp_cache_preload_email_volume', $wp_cache_preload_email_volume ); + \Automattic\WPSC\Config::set( 'wp_cache_preload_email_volume', $wp_cache_preload_email_volume ); if ( $wp_cache_preload_email_volume == 'none' ) - wp_cache_setting( 'wp_cache_preload_email_me', 0 ); + \Automattic\WPSC\Config::set( 'wp_cache_preload_email_me', 0 ); else - wp_cache_setting( 'wp_cache_preload_email_me', 1 ); + \Automattic\WPSC\Config::set( 'wp_cache_preload_email_me', 1 ); if ( isset( $_POST[ 'wp_cache_preload_taxonomies' ] ) ) { $wp_cache_preload_taxonomies = 1; } else { $wp_cache_preload_taxonomies = 0; } - wp_cache_setting( 'wp_cache_preload_taxonomies', $wp_cache_preload_taxonomies ); + \Automattic\WPSC\Config::set( 'wp_cache_preload_taxonomies', $wp_cache_preload_taxonomies ); if ( isset( $_POST[ 'wp_cache_preload_on' ] ) ) { $wp_cache_preload_on = 1; } else { $wp_cache_preload_on = 0; } - wp_cache_setting( 'wp_cache_preload_on', $wp_cache_preload_on ); + \Automattic\WPSC\Config::set( 'wp_cache_preload_on', $wp_cache_preload_on ); // Ensure that preload settings are applied to scheduled cron. $next_preload = wp_next_scheduled( 'wp_cache_full_preload_hook' ); diff --git a/inc/settings-forms.php b/inc/settings-forms.php index 2d7ae73f..aa6d92b9 100644 --- a/inc/settings-forms.php +++ b/inc/settings-forms.php @@ -132,17 +132,17 @@ function wp_cache_time_update() { if( !isset( $cache_schedule_type ) ) { $cache_schedule_type = 'interval'; - wp_cache_replace_line('^ *\$cache_schedule_type', "\$cache_schedule_type = '$cache_schedule_type';", $wp_cache_config_file); + \Automattic\WPSC\Config::set( 'cache_schedule_type', $cache_schedule_type, $wp_cache_config_file ); } if( !isset( $cache_scheduled_time ) ) { $cache_scheduled_time = '00:00'; - wp_cache_replace_line('^ *\$cache_scheduled_time', "\$cache_scheduled_time = '$cache_scheduled_time';", $wp_cache_config_file); + \Automattic\WPSC\Config::set( 'cache_scheduled_time', $cache_scheduled_time, $wp_cache_config_file ); } if( !isset( $cache_max_time ) ) { $cache_max_time = 3600; - wp_cache_replace_line('^ *\$cache_max_time', "\$cache_max_time = $cache_max_time;", $wp_cache_config_file); + \Automattic\WPSC\Config::set( 'cache_max_time', $cache_max_time, $wp_cache_config_file ); } if ( !isset( $cache_time_interval ) ) { @@ -152,7 +152,7 @@ function wp_cache_time_update() { if ( isset( $_POST['wp_max_time'] ) ) { $cache_max_time = (int)$_POST['wp_max_time']; - wp_cache_replace_line('^ *\$cache_max_time', "\$cache_max_time = $cache_max_time;", $wp_cache_config_file); + \Automattic\WPSC\Config::set( 'cache_max_time', $cache_max_time, $wp_cache_config_file ); // schedule gc watcher if ( false == wp_next_scheduled( 'wp_cache_gc_watcher' ) ) wp_schedule_event( time()+600, 'hourly', 'wp_cache_gc_watcher' ); @@ -160,10 +160,10 @@ function wp_cache_time_update() { if ( isset( $_POST[ 'cache_gc_email_me' ] ) ) { $cache_gc_email_me = 1; - wp_cache_replace_line('^ *\$cache_gc_email_me', "\$cache_gc_email_me = $cache_gc_email_me;", $wp_cache_config_file); + \Automattic\WPSC\Config::set( 'cache_gc_email_me', $cache_gc_email_me, $wp_cache_config_file ); } else { $cache_gc_email_me = 0; - wp_cache_replace_line('^ *\$cache_gc_email_me', "\$cache_gc_email_me = $cache_gc_email_me;", $wp_cache_config_file); + \Automattic\WPSC\Config::set( 'cache_gc_email_me', $cache_gc_email_me, $wp_cache_config_file ); } if ( isset( $_POST[ 'cache_schedule_type' ] ) && $_POST[ 'cache_schedule_type' ] == 'interval' && isset( $_POST['cache_time_interval'] ) ) { wp_clear_scheduled_hook( 'wp_cache_gc' ); @@ -172,7 +172,7 @@ function wp_cache_time_update() { $_POST[ 'cache_time_interval' ] = 600; $cache_time_interval = (int)$_POST[ 'cache_time_interval' ]; wp_schedule_single_event( time() + $cache_time_interval, 'wp_cache_gc' ); - wp_cache_replace_line('^ *\$cache_schedule_type', "\$cache_schedule_type = '$cache_schedule_type';", $wp_cache_config_file); + \Automattic\WPSC\Config::set( 'cache_schedule_type', $cache_schedule_type, $wp_cache_config_file ); wp_cache_replace_line('^ *\$cache_time_interval', "\$cache_time_interval = '$cache_time_interval';", $wp_cache_config_file); } else { // clock wp_clear_scheduled_hook( 'wp_cache_gc' ); @@ -194,9 +194,9 @@ function wp_cache_time_update() { $cache_schedule_interval = 'daily'; if ( isset( $_POST[ 'cache_schedule_interval' ] ) && isset( $schedules[ $_POST[ 'cache_schedule_interval' ] ] ) ) $cache_schedule_interval = $_POST[ 'cache_schedule_interval' ]; - wp_cache_replace_line('^ *\$cache_schedule_type', "\$cache_schedule_type = '$cache_schedule_type';", $wp_cache_config_file); - wp_cache_replace_line('^ *\$cache_schedule_interval', "\$cache_schedule_interval = '{$cache_schedule_interval}';", $wp_cache_config_file); - wp_cache_replace_line('^ *\$cache_scheduled_time', "\$cache_scheduled_time = '$cache_scheduled_time';", $wp_cache_config_file); + \Automattic\WPSC\Config::set( 'cache_schedule_type', $cache_schedule_type, $wp_cache_config_file ); + \Automattic\WPSC\Config::set( 'cache_schedule_interval', $cache_schedule_interval, $wp_cache_config_file ); + \Automattic\WPSC\Config::set( 'cache_scheduled_time', $cache_scheduled_time, $wp_cache_config_file ); wp_schedule_event( strtotime( $cache_scheduled_time ), $cache_schedule_interval, 'wp_cache_gc' ); } } @@ -256,9 +256,9 @@ function wpsc_update_tracking_parameters() { global $wpsc_tracking_parameters, $valid_nonce, $wp_cache_config_file; if ( isset( $_POST['tracking_parameters'] ) && $valid_nonce ) { - $text = wp_cache_sanitize_value( str_replace( '\\\\', '\\', $_POST['tracking_parameters'] ), $wpsc_tracking_parameters ); - wp_cache_replace_line( '^ *\$wpsc_tracking_parameters', "\$wpsc_tracking_parameters = $text;", $wp_cache_config_file ); - wp_cache_setting( 'wpsc_ignore_tracking_parameters', isset( $_POST['wpsc_ignore_tracking_parameters'] ) ? 1 : 0 ); + wp_cache_sanitize_value( str_replace( '\\\\', '\\', $_POST['tracking_parameters'] ), $wpsc_tracking_parameters ); + \Automattic\WPSC\Config::set( 'wpsc_tracking_parameters', $wpsc_tracking_parameters, $wp_cache_config_file ); + \Automattic\WPSC\Config::set( 'wpsc_ignore_tracking_parameters', isset( $_POST['wpsc_ignore_tracking_parameters'] ) ? 1 : 0 ); } } @@ -285,8 +285,8 @@ function wp_cache_update_rejected_cookies() { global $wpsc_rejected_cookies, $wp_cache_config_file, $valid_nonce; if ( isset( $_POST['wp_rejected_cookies'] ) && $valid_nonce ) { - $text = wp_cache_sanitize_value( str_replace( '\\\\', '\\', $_POST['wp_rejected_cookies'] ), $wpsc_rejected_cookies ); - wp_cache_replace_line( '^ *\$wpsc_rejected_cookies', "\$wpsc_rejected_cookies = $text;", $wp_cache_config_file ); + wp_cache_sanitize_value( str_replace( '\\\\', '\\', $_POST['wp_rejected_cookies'] ), $wpsc_rejected_cookies ); + \Automattic\WPSC\Config::set( 'wpsc_rejected_cookies', $wpsc_rejected_cookies, $wp_cache_config_file ); } } @@ -294,8 +294,8 @@ function wp_cache_update_rejected_strings() { global $cache_rejected_uri, $wp_cache_config_file, $valid_nonce; if ( isset($_REQUEST['wp_rejected_uri']) && $valid_nonce ) { - $text = wp_cache_sanitize_value( str_replace( '\\\\', '\\', $_REQUEST['wp_rejected_uri'] ), $cache_rejected_uri ); - wp_cache_replace_line('^ *\$cache_rejected_uri', "\$cache_rejected_uri = $text;", $wp_cache_config_file); + wp_cache_sanitize_value( str_replace( '\\\\', '\\', $_REQUEST['wp_rejected_uri'] ), $cache_rejected_uri ); + \Automattic\WPSC\Config::set( 'cache_rejected_uri', $cache_rejected_uri, $wp_cache_config_file ); } } @@ -303,8 +303,8 @@ function wp_cache_update_accepted_strings() { global $cache_acceptable_files, $wp_cache_config_file, $valid_nonce; if ( isset( $_REQUEST[ 'wp_accepted_files' ] ) && $valid_nonce ) { - $text = wp_cache_sanitize_value( $_REQUEST[ 'wp_accepted_files' ], $cache_acceptable_files ); - wp_cache_replace_line( '^ *\$cache_acceptable_files', "\$cache_acceptable_files = $text;", $wp_cache_config_file ); + wp_cache_sanitize_value( $_REQUEST[ 'wp_accepted_files' ], $cache_acceptable_files ); + \Automattic\WPSC\Config::set( 'cache_acceptable_files', $cache_acceptable_files, $wp_cache_config_file ); } } @@ -315,7 +315,7 @@ function wpsc_update_debug_settings() { if ( ! isset( $wp_super_cache_comments ) ) { $wp_super_cache_comments = 1; // defaults to "enabled". - wp_cache_setting( 'wp_super_cache_comments', $wp_super_cache_comments ); + \Automattic\WPSC\Config::set( 'wp_super_cache_comments', $wp_super_cache_comments ); } if ( false == $valid_nonce ) { @@ -344,31 +344,31 @@ function wpsc_update_debug_settings() { extract( wpsc_create_debug_log( $wp_cache_debug_log, $wp_cache_debug_username ) ); // $wp_cache_debug_log, $wp_cache_debug_username } $wp_super_cache_debug = ( isset( $_POST[ 'wp_super_cache_debug' ] ) && $_POST[ 'wp_super_cache_debug' ] == 1 ) ? 1 : 0; - wp_cache_setting( 'wp_super_cache_debug', $wp_super_cache_debug ); + \Automattic\WPSC\Config::set( 'wp_super_cache_debug', $wp_super_cache_debug ); if ( isset( $_POST[ 'wp_cache_debug' ] ) ) { - wp_cache_setting( 'wp_cache_debug_username', $wp_cache_debug_username ); - wp_cache_setting( 'wp_cache_debug_log', $wp_cache_debug_log ); + \Automattic\WPSC\Config::set( 'wp_cache_debug_username', $wp_cache_debug_username ); + \Automattic\WPSC\Config::set( 'wp_cache_debug_log', $wp_cache_debug_log ); $wp_super_cache_comments = isset( $_POST[ 'wp_super_cache_comments' ] ) ? 1 : 0; - wp_cache_setting( 'wp_super_cache_comments', $wp_super_cache_comments ); + \Automattic\WPSC\Config::set( 'wp_super_cache_comments', $wp_super_cache_comments ); if ( isset( $_POST[ 'wp_cache_debug_ip' ] ) && filter_var( $_POST[ 'wp_cache_debug_ip' ], FILTER_VALIDATE_IP ) ) { $wp_cache_debug_ip = esc_html( preg_replace( '/[ <>\'\"\r\n\t\(\)\$\[\];#]/', '', $_POST[ 'wp_cache_debug_ip' ] ) ); } else { $wp_cache_debug_ip = ''; } - wp_cache_setting( 'wp_cache_debug_ip', $wp_cache_debug_ip ); + \Automattic\WPSC\Config::set( 'wp_cache_debug_ip', $wp_cache_debug_ip ); $wp_super_cache_front_page_check = isset( $_POST[ 'wp_super_cache_front_page_check' ] ) ? 1 : 0; - wp_cache_setting( 'wp_super_cache_front_page_check', $wp_super_cache_front_page_check ); + \Automattic\WPSC\Config::set( 'wp_super_cache_front_page_check', $wp_super_cache_front_page_check ); $wp_super_cache_front_page_clear = isset( $_POST[ 'wp_super_cache_front_page_clear' ] ) ? 1 : 0; - wp_cache_setting( 'wp_super_cache_front_page_clear', $wp_super_cache_front_page_clear ); + \Automattic\WPSC\Config::set( 'wp_super_cache_front_page_clear', $wp_super_cache_front_page_clear ); if ( isset( $_POST[ 'wp_super_cache_front_page_text' ] ) ) { $wp_super_cache_front_page_text = esc_html( preg_replace( '/[ <>\'\"\r\n\t\(\)\$\[\];#]/', '', $_POST[ 'wp_super_cache_front_page_text' ] ) ); } else { $wp_super_cache_front_page_text = ''; } - wp_cache_setting( 'wp_super_cache_front_page_text', $wp_super_cache_front_page_text ); + \Automattic\WPSC\Config::set( 'wp_super_cache_front_page_text', $wp_super_cache_front_page_text ); $wp_super_cache_front_page_notification = isset( $_POST[ 'wp_super_cache_front_page_notification' ] ) ? 1 : 0; - wp_cache_setting( 'wp_super_cache_front_page_notification', $wp_super_cache_front_page_notification ); + \Automattic\WPSC\Config::set( 'wp_super_cache_front_page_notification', $wp_super_cache_front_page_notification ); if ( $wp_super_cache_front_page_check == 1 && !wp_next_scheduled( 'wp_cache_check_site_hook' ) ) { wp_schedule_single_event( time() + 360 , 'wp_cache_check_site_hook' ); wp_cache_debug( 'scheduled wp_cache_check_site_hook for 360 seconds time.' ); diff --git a/ossdl-cdn.php b/ossdl-cdn.php index fcbe858a..25215a06 100755 --- a/ossdl-cdn.php +++ b/ossdl-cdn.php @@ -254,7 +254,7 @@ function scossdl_off_update() { $ossdlcdn = empty( $_POST['ossdlcdn'] ) ? 0 : 1; update_option( 'ossdl_https', $ossdl_https ); - wp_cache_setting( 'ossdlcdn', $ossdlcdn ); + \Automattic\WPSC\Config::set( 'ossdlcdn', $ossdlcdn ); } } diff --git a/rest/class.wp-super-cache-rest-update-settings.php b/rest/class.wp-super-cache-rest-update-settings.php index 4f2ea5fd..021798e1 100644 --- a/rest/class.wp-super-cache-rest-update-settings.php +++ b/rest/class.wp-super-cache-rest-update-settings.php @@ -131,7 +131,7 @@ protected function set_global( $global_name, $value ) { return false; } - wp_cache_setting( $global_name, (int)$value ); + \Automattic\WPSC\Config::set( $global_name, (int) $value ); } /** @@ -164,7 +164,7 @@ protected function set_wp_cache_location( $value ) { } $cache_path = $new_cache_path; - wp_cache_setting( 'cache_path', $cache_path ); + \Automattic\WPSC\Config::set( 'cache_path', $cache_path ); } } @@ -220,7 +220,7 @@ protected function set_super_cache_enabled( $value ) { } - wp_cache_setting( 'wp_cache_mod_rewrite', $wp_cache_mod_rewrite ); + \Automattic\WPSC\Config::set( 'wp_cache_mod_rewrite', $wp_cache_mod_rewrite ); } return true; } @@ -242,7 +242,7 @@ protected function set_wp_cache_not_logged_in( $value ) { $wp_cache_not_logged_in = 0; } - wp_cache_setting( 'wp_cache_not_logged_in', $wp_cache_not_logged_in ); + \Automattic\WPSC\Config::set( 'wp_cache_not_logged_in', $wp_cache_not_logged_in ); } /** @@ -262,7 +262,7 @@ protected function set_wp_cache_make_known_anon( $value ) { $wp_cache_make_known_anon = 0; } - wp_cache_setting( 'wp_cache_make_known_anon', $wp_cache_make_known_anon ); + \Automattic\WPSC\Config::set( 'wp_cache_make_known_anon', $wp_cache_make_known_anon ); } /** @@ -286,7 +286,7 @@ protected function set_wp_cache_object_cache( $value ) { $wp_cache_object_cache = 0; } - wp_cache_setting( 'wp_cache_object_cache', $wp_cache_object_cache ); + \Automattic\WPSC\Config::set( 'wp_cache_object_cache', $wp_cache_object_cache ); } /** @@ -300,7 +300,7 @@ protected function set_cache_compression( $value ) { $new_cache_compression = 0; if ( defined( 'WPSC_DISABLE_COMPRESSION' ) ) { $cache_compression = 0; - wp_cache_setting( 'cache_compression', $cache_compression ); + \Automattic\WPSC\Config::set( 'cache_compression', $cache_compression ); } else { if ( 1 == $value ) { @@ -313,7 +313,7 @@ protected function set_cache_compression( $value ) { if ( $new_cache_compression != $cache_compression ) { $cache_compression = $new_cache_compression; - wp_cache_setting( 'cache_compression', $cache_compression ); + \Automattic\WPSC\Config::set( 'cache_compression', $cache_compression ); if ( function_exists( 'prune_super_cache' ) ) { prune_super_cache( $cache_path, true ); } @@ -603,7 +603,7 @@ protected function restore_default_settings( $parameters ) { if ( file_exists( $wp_cache_config_file_sample ) ) { copy( $wp_cache_config_file_sample, $wp_cache_config_file ); $cache_page_secret = md5( gmdate( 'H:i:s' ) . wp_rand() ); - wp_cache_setting( 'cache_page_secret', $cache_page_secret ); + \Automattic\WPSC\Config::set( 'cache_page_secret', $cache_page_secret ); if ( function_exists( "opcache_invalidate" ) ) { @opcache_invalidate( $wp_cache_config_file ); diff --git a/src/config/class-config.php b/src/config/class-config.php new file mode 100644 index 00000000..9685c29d --- /dev/null +++ b/src/config/class-config.php @@ -0,0 +1,179 @@ + write -> rename -> chmod -> opcache_invalidate sequence is + * preserved exactly. + * + * @param string $old Regex (without delimiters) matching the line to replace. + * @param string $new Replacement line (no trailing newline). + * @param string $file Config file path. + * @return bool True on success/unchanged, false on failure. + */ + public static function write_line( $old, $new, $file ) { + if ( ! is_string( $file ) || @is_file( $file ) === false ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + if ( function_exists( 'set_transient' ) ) { + set_transient( 'wpsc_config_error', 'config_file_missing', 10 ); + } + return false; + } + if ( ! is_writeable_ACLSafe( $file ) ) { + if ( function_exists( 'set_transient' ) ) { + set_transient( 'wpsc_config_error', 'config_file_ro', 10 ); + } + trigger_error( "Error: file $file is not writable." ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error,WordPress.Security.EscapeOutput.OutputNotEscaped + return false; + } + + $found = false; + $loaded = false; + $c = 0; + $lines = array(); + while ( ! $loaded ) { + $lines = file( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_file + if ( ! empty( $lines ) && is_array( $lines ) ) { + $loaded = true; + } else { + ++$c; + if ( $c > 100 ) { + if ( function_exists( 'set_transient' ) ) { + set_transient( 'wpsc_config_error', 'config_file_not_loaded', 10 ); + } + trigger_error( "wp_cache_replace_line: Error - file $file could not be loaded." ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error,WordPress.Security.EscapeOutput.OutputNotEscaped + return false; + } + } + } + foreach ( (array) $lines as $line ) { + if ( + trim( $new ) != '' && // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual + trim( $new ) == trim( $line ) // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual + ) { + wp_cache_debug( "wp_cache_replace_line: setting not changed - $new" ); + return true; + } elseif ( preg_match( "/$old/", $line ) ) { + wp_cache_debug( 'wp_cache_replace_line: changing line ' . trim( $line ) . " to *$new*" ); + $found = true; + } + } + + $tmp_config_filename = tempnam( $GLOBALS['cache_path'], md5( (string) wp_rand( 0, 9999 ) ) ); + if ( file_exists( $tmp_config_filename . '.php' ) ) { + unlink( $tmp_config_filename . '.php' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink + if ( file_exists( $tmp_config_filename . '.php' ) ) { + die( __( 'WARNING: attempt to intercept updating of config file.', 'wp-super-cache' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + rename( $tmp_config_filename, $tmp_config_filename . '.php' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename + $tmp_config_filename .= '.php'; + wp_cache_debug( 'wp_cache_replace_line: writing to ' . $tmp_config_filename ); + $fd = fopen( $tmp_config_filename, 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen + if ( ! $fd ) { + if ( function_exists( 'set_transient' ) ) { + set_transient( 'wpsc_config_error', 'config_file_ro', 10 ); + } + trigger_error( "wp_cache_replace_line: Error - could not write to $file" ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error,WordPress.Security.EscapeOutput.OutputNotEscaped + return false; + } + if ( $found ) { + foreach ( (array) $lines as $line ) { + if ( ! preg_match( "/$old/", $line ) ) { + fwrite( $fd, $line ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite + } elseif ( $new != '' ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual + fwrite( $fd, "$new\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite + } + } + } else { + $done = false; + foreach ( (array) $lines as $line ) { + if ( $done || ! preg_match( '/^(if\ \(\ \!\ )?define|\$|\?>/', $line ) ) { + fwrite( $fd, $line ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite + } else { + fwrite( $fd, "$new\n" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite + fwrite( $fd, $line ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite + $done = true; + } + } + } + fclose( $fd ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose + + $my_file_permissions = fileperms( $file ); + + rename( $tmp_config_filename, $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename + + if ( false !== $my_file_permissions ) { + chmod( $file, $my_file_permissions ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod + } + + wp_cache_debug( 'wp_cache_replace_line: moved ' . $tmp_config_filename . ' to ' . $file ); + + if ( function_exists( 'opcache_invalidate' ) ) { + @opcache_invalidate( $file ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + } + + return true; + } +} diff --git a/tests/php/integration/ConfigClassTest.php b/tests/php/integration/ConfigClassTest.php new file mode 100644 index 00000000..13f7e236 --- /dev/null +++ b/tests/php/integration/ConfigClassTest.php @@ -0,0 +1,95 @@ +temp_dirs as $dir ) { + $this->rrmdir( $dir ); + } + $this->temp_dirs = array(); + parent::tear_down(); + } + + private function make_config_file( array $lines ) { + $dir = trailingslashit( get_temp_dir() ) . 'wpsc-cfg-' . uniqid(); + mkdir( $dir, 0700, true ); + $this->temp_dirs[] = $dir; + + $config = trailingslashit( $dir ) . 'wp-cache-config.php'; + file_put_contents( $config, "rrmdir( $path ) : unlink( $path ); + } + rmdir( $dir ); + } + + public function test_format_value_numeric() { + $this->assertSame( '5', \Automattic\WPSC\Config::format_value( 5 ) ); + $this->assertSame( '0', \Automattic\WPSC\Config::format_value( 0 ) ); + } + + public function test_format_value_boolean() { + $this->assertSame( 'true', \Automattic\WPSC\Config::format_value( true ) ); + $this->assertSame( 'false', \Automattic\WPSC\Config::format_value( false ) ); + } + + public function test_format_value_array_is_whitespace_collapsed() { + $this->assertSame( + "array ( 0 => 'a', 1 => 'b', )", + \Automattic\WPSC\Config::format_value( array( 'a', 'b' ) ) + ); + } + + public function test_format_value_string_single_quoted() { + $this->assertSame( "'hello'", \Automattic\WPSC\Config::format_value( 'hello' ) ); + } + + public function test_set_updates_global_and_file() { + $config = $this->make_config_file( array( '$wp_cache_mobile = 0;' ) ); + $this->assertTrue( \Automattic\WPSC\Config::set( 'wp_cache_mobile', 1 ) ); + $this->assertSame( 1, $GLOBALS['wp_cache_mobile'] ); + $this->assertStringContainsString( '$wp_cache_mobile = 1;', file_get_contents( $config ) ); + } + + public function test_set_array_matches_legacy_format() { + $config = $this->make_config_file( array( '$cache_acceptable_files = array();' ) ); + \Automattic\WPSC\Config::set( 'cache_acceptable_files', array( 'wp-comments-popup.php' ) ); + $this->assertStringContainsString( + "\$cache_acceptable_files = array ( 0 => 'wp-comments-popup.php', );", + file_get_contents( $config ) + ); + } + + public function test_write_line_replaces() { + $config = $this->make_config_file( array( '$x = 0;' ) ); + $this->assertTrue( \Automattic\WPSC\Config::write_line( '^ *\$x', '$x = 9;', $config ) ); + $this->assertStringContainsString( '$x = 9;', file_get_contents( $config ) ); + } + + public function test_write_line_missing_file_returns_false() { + $this->assertFalse( \Automattic\WPSC\Config::write_line( '^ *\$x', '$x = 1;', '/no/such/file.php' ) ); + } +} diff --git a/tests/php/integration/ConfigWritePathTest.php b/tests/php/integration/ConfigWritePathTest.php new file mode 100644 index 00000000..2a64f904 --- /dev/null +++ b/tests/php/integration/ConfigWritePathTest.php @@ -0,0 +1,126 @@ +temp_dirs as $dir ) { + $this->rrmdir( $dir ); + } + $this->temp_dirs = array(); + parent::tear_down(); + } + + /** + * Seed a writable temp config file and point the write-path globals at it. + * + * @param string[] $lines Config lines (without the opening PHP tag). + * @return string Absolute path to the config file. + */ + private function make_config_file( array $lines ) { + $dir = trailingslashit( get_temp_dir() ) . 'wpsc-cfg-' . uniqid(); + mkdir( $dir, 0700, true ); + $this->temp_dirs[] = $dir; + + $config = trailingslashit( $dir ) . 'wp-cache-config.php'; + file_put_contents( $config, "rrmdir( $path ) : unlink( $path ); + } + rmdir( $dir ); + } + + public function test_setting_numeric_writes_unquoted_and_sets_global() { + $config = $this->make_config_file( array( '$wp_cache_mod_rewrite = 0;' ) ); + + $this->assertTrue( wp_cache_setting( 'wp_cache_mod_rewrite', 1 ) ); + $this->assertSame( 1, $GLOBALS['wp_cache_mod_rewrite'] ); + $this->assertStringContainsString( '$wp_cache_mod_rewrite = 1;', file_get_contents( $config ) ); + } + + public function test_setting_boolean_writes_true_false_literal() { + $config = $this->make_config_file( array( '$wp_supercache_304 = false;' ) ); + + wp_cache_setting( 'wp_supercache_304', true ); + $this->assertSame( true, $GLOBALS['wp_supercache_304'] ); + $this->assertStringContainsString( '$wp_supercache_304 = true;', file_get_contents( $config ) ); + } + + public function test_setting_array_writes_collapsed_var_export() { + $config = $this->make_config_file( array( '$cache_rejected_uri = array();' ) ); + + wp_cache_setting( 'cache_rejected_uri', array( 'wp-admin', 'feed' ) ); + $this->assertSame( array( 'wp-admin', 'feed' ), $GLOBALS['cache_rejected_uri'] ); + $this->assertStringContainsString( + "\$cache_rejected_uri = array ( 0 => 'wp-admin', 1 => 'feed', );", + file_get_contents( $config ) + ); + } + + public function test_setting_string_writes_single_quoted() { + $config = $this->make_config_file( array( "\$wp_cache_debug_ip = '';" ) ); + + wp_cache_setting( 'wp_cache_debug_ip', '203.0.113.5' ); + $this->assertSame( '203.0.113.5', $GLOBALS['wp_cache_debug_ip'] ); + $this->assertStringContainsString( "\$wp_cache_debug_ip = '203.0.113.5';", file_get_contents( $config ) ); + } + + public function test_replace_line_replaces_matching_line() { + $config = $this->make_config_file( array( '$wp_cache_front_page_checks = 0;' ) ); + + $this->assertTrue( + wp_cache_replace_line( '^ *\$wp_cache_front_page_checks', '$wp_cache_front_page_checks = 1;', $config ) + ); + $this->assertStringContainsString( '$wp_cache_front_page_checks = 1;', file_get_contents( $config ) ); + $this->assertStringNotContainsString( '$wp_cache_front_page_checks = 0;', file_get_contents( $config ) ); + } + + public function test_replace_line_unchanged_is_noop_early_return() { + $config = $this->make_config_file( array( '$wp_cache_mobile = 1;' ) ); + $before = file_get_contents( $config ); + + $this->assertTrue( wp_cache_replace_line( '^ *\$wp_cache_mobile', '$wp_cache_mobile = 1;', $config ) ); + $this->assertSame( $before, file_get_contents( $config ) ); + } + + public function test_replace_line_not_found_appends_after_assignments() { + $config = $this->make_config_file( array( '$existing = 1;' ) ); + + $this->assertTrue( wp_cache_replace_line( '^ *\$brand_new_key', '$brand_new_key = 5;', $config ) ); + $this->assertStringContainsString( '$brand_new_key = 5;', file_get_contents( $config ) ); + $this->assertStringContainsString( '$existing = 1;', file_get_contents( $config ) ); + } + + public function test_replace_line_missing_file_returns_false() { + $this->assertFalse( wp_cache_replace_line( '^ *\$x', '$x = 1;', '/no/such/wp-cache-config.php' ) ); + } +} diff --git a/tests/php/integration/SettingsFormUpdatersTest.php b/tests/php/integration/SettingsFormUpdatersTest.php index e1abe241..56ddd505 100644 --- a/tests/php/integration/SettingsFormUpdatersTest.php +++ b/tests/php/integration/SettingsFormUpdatersTest.php @@ -161,6 +161,138 @@ public function test_update_tracking_parameters_parses_and_sets_ignore_flag() { $this->assertSame( 1, $GLOBALS['wpsc_ignore_tracking_parameters'] ); } + /** + * Characterizes wp_cache_time_update() interval-schedule path. + * + * Critical contract: cache_max_time is written as an UNQUOTED integer + * (e.g. `$cache_max_time = 3600;`) while cache_time_interval is written + * QUOTED even though it is also numeric (e.g. `$cache_time_interval = '600';`). + * + * This distinction is preserved verbatim across the #1062 migration: L145/L155 + * (max_time) are migrated to Config::set() whose format_value() emits numeric + * values unquoted, while L150/L176 (time_interval) are left on raw + * wp_cache_replace_line() to keep the quoted form. + */ + public function test_time_update_interval_path_writes_max_time_unquoted_and_interval_quoted() { + $config = $this->make_config_file( + array( + "\$cache_schedule_type = 'interval';", + "\$cache_scheduled_time = '00:00';", + '$cache_max_time = 1800;', + "\$cache_time_interval = '1800';", + '$cache_gc_email_me = 0;', + "\$cache_schedule_interval = 'daily';", + ) + ); + + // Pre-populate all globals so none of the "unset → initialize" branches fire. + $GLOBALS['cache_schedule_type'] = 'interval'; + $GLOBALS['cache_scheduled_time'] = '00:00'; + $GLOBALS['cache_max_time'] = 1800; + $GLOBALS['cache_time_interval'] = 1800; + $GLOBALS['cache_gc_email_me'] = 0; + $GLOBALS['cache_schedule_interval'] = 'daily'; + + $_POST['action'] = 'expirytime'; + $_POST['wp_max_time'] = '3600'; // updates cache_max_time + $_POST['cache_schedule_type'] = 'interval'; + $_POST['cache_time_interval'] = '600'; // updates cache_time_interval + + wp_cache_time_update(); + + $content = file_get_contents( $config ); + + // cache_max_time → numeric → UNQUOTED. + $this->assertStringContainsString( '$cache_max_time = 3600;', $content, 'cache_max_time must be unquoted integer' ); + $this->assertStringNotContainsString( "\$cache_max_time = '3600';", $content, 'cache_max_time must NOT be quoted' ); + + // cache_time_interval → numeric but written QUOTED by original code. + $this->assertStringContainsString( "\$cache_time_interval = '600';", $content, 'cache_time_interval must be quoted' ); + $this->assertStringNotContainsString( '$cache_time_interval = 600;', $content, 'cache_time_interval must NOT be unquoted' ); + + // Globals updated. + $this->assertSame( 3600, $GLOBALS['cache_max_time'] ); + $this->assertSame( 600, $GLOBALS['cache_time_interval'] ); + $this->assertSame( 'interval', $GLOBALS['cache_schedule_type'] ); + } + + /** + * Characterizes wp_cache_time_update() initialization branch for cache_max_time. + * + * When cache_max_time is not set in globals the function sets it to 3600 and + * writes it UNQUOTED. This is the L145 site migrated to Config::set(). + */ + public function test_time_update_initializes_max_time_as_unquoted_integer() { + $config = $this->make_config_file( + array( + "\$cache_schedule_type = 'interval';", + "\$cache_scheduled_time = '00:00';", + '$cache_max_time = 1800;', + "\$cache_time_interval = '1800';", + '$cache_gc_email_me = 0;', + ) + ); + + // Unset cache_max_time to trigger the initialization branch (L143–146). + unset( $GLOBALS['cache_max_time'] ); + $GLOBALS['cache_schedule_type'] = 'interval'; + $GLOBALS['cache_scheduled_time'] = '00:00'; + $GLOBALS['cache_time_interval'] = 1800; + $GLOBALS['cache_gc_email_me'] = 0; + + $_POST['action'] = 'expirytime'; + $_POST['cache_schedule_type'] = 'interval'; + $_POST['cache_time_interval'] = '1800'; + + wp_cache_time_update(); + + $content = file_get_contents( $config ); + + // Initialization default 3600 written UNQUOTED. + $this->assertStringContainsString( '$cache_max_time = 3600;', $content, 'init cache_max_time must be unquoted' ); + $this->assertStringNotContainsString( "\$cache_max_time = '3600';", $content, 'init cache_max_time must NOT be quoted' ); + } + + /** + * Characterizes wp_cache_time_update() clock-schedule path (L197–199). + * + * All three clock-path fields (cache_schedule_type, cache_schedule_interval, + * cache_scheduled_time) are written as QUOTED strings — verified for the + * L197/L198/L199 sites migrated to Config::set(). + */ + public function test_time_update_clock_path_writes_schedule_fields_quoted() { + $config = $this->make_config_file( + array( + "\$cache_schedule_type = 'interval';", + "\$cache_scheduled_time = '00:00';", + '$cache_max_time = 1800;', + "\$cache_time_interval = '1800';", + '$cache_gc_email_me = 0;', + "\$cache_schedule_interval = 'daily';", + ) + ); + + $GLOBALS['cache_schedule_type'] = 'interval'; + $GLOBALS['cache_scheduled_time'] = '00:00'; + $GLOBALS['cache_max_time'] = 1800; + $GLOBALS['cache_time_interval'] = 1800; + $GLOBALS['cache_gc_email_me'] = 0; + $GLOBALS['cache_schedule_interval'] = 'daily'; + + $_POST['action'] = 'expirytime'; + $_POST['cache_schedule_type'] = 'time'; // not 'interval' → clock branch + $_POST['cache_scheduled_time'] = '14:00'; + $_POST['cache_schedule_interval'] = 'daily'; + + wp_cache_time_update(); + + $content = file_get_contents( $config ); + + $this->assertStringContainsString( "\$cache_schedule_type = 'time';", $content ); + $this->assertStringContainsString( "\$cache_schedule_interval = 'daily';", $content ); + $this->assertStringContainsString( "\$cache_scheduled_time = '14:00';", $content ); + } + /** * Characterizes wpsc_update_debug_settings() without a valid nonce: it makes * no changes and returns a snapshot of the current debug-related settings. diff --git a/tests/php/integration/WpCacheSettingBackCompatTest.php b/tests/php/integration/WpCacheSettingBackCompatTest.php new file mode 100644 index 00000000..50158b2c --- /dev/null +++ b/tests/php/integration/WpCacheSettingBackCompatTest.php @@ -0,0 +1,150 @@ +temp_dirs as $dir ) { + $this->rrmdir( $dir ); + } + $this->temp_dirs = array(); + parent::tear_down(); + } + + /** + * Seed a writable temp config file and point the write-path globals at it. + * + * @param string[] $lines Config lines (without the opening PHP tag). + * @return string Absolute path to the config file. + */ + private function make_config_file( array $lines ) { + $dir = trailingslashit( get_temp_dir() ) . 'wpsc-cfg-' . uniqid(); + mkdir( $dir, 0700, true ); + $this->temp_dirs[] = $dir; + + $config = trailingslashit( $dir ) . 'wp-cache-config.php'; + file_put_contents( $config, "rrmdir( $path ) : unlink( $path ); + } + rmdir( $dir ); + } + + /** + * Both public write functions must remain defined for third-party callers. + */ + public function test_public_write_functions_exist() { + $this->assertTrue( function_exists( 'wp_cache_setting' ) ); + $this->assertTrue( function_exists( 'wp_cache_replace_line' ) ); + } + + /** + * The wp_cache_setting() signature is part of the public contract: third-party + * code calls it positionally as wp_cache_setting( $field, $value ). + */ + public function test_wp_cache_setting_signature_unchanged() { + $ref = new ReflectionFunction( 'wp_cache_setting' ); + + $this->assertSame( 2, $ref->getNumberOfParameters() ); + $this->assertSame( 2, $ref->getNumberOfRequiredParameters() ); + + $params = $ref->getParameters(); + $this->assertSame( 'field', $params[0]->getName() ); + $this->assertSame( 'value', $params[1]->getName() ); + } + + /** + * The wp_cache_replace_line() signature is part of the public contract: callers + * use wp_cache_replace_line( $old, $new, $my_file ). + */ + public function test_wp_cache_replace_line_signature_unchanged() { + $ref = new ReflectionFunction( 'wp_cache_replace_line' ); + + $this->assertSame( 3, $ref->getNumberOfParameters() ); + $this->assertSame( 3, $ref->getNumberOfRequiredParameters() ); + + $params = $ref->getParameters(); + $this->assertSame( 'old', $params[0]->getName() ); + $this->assertSame( 'new', $params[1]->getName() ); + $this->assertSame( 'my_file', $params[2]->getName() ); + } + + /** + * A positional wp_cache_setting() call (as third-party code makes it) still + * updates the runtime global AND writes the config file, returning truthy. + */ + public function test_wp_cache_setting_public_api_updates_global_and_file() { + $config = $this->make_config_file( array( '$wp_cache_mobile = 0;' ) ); + + $result = wp_cache_setting( 'wp_cache_mobile', 1 ); + + $this->assertTrue( $result ); + $this->assertSame( 1, $GLOBALS['wp_cache_mobile'] ); + $this->assertStringContainsString( '$wp_cache_mobile = 1;', file_get_contents( $config ) ); + } + + /** + * Array values keep their legacy on-disk format through the public function, + * so third-party callers persisting array settings are unaffected. + */ + public function test_wp_cache_setting_public_api_array_value() { + $config = $this->make_config_file( array( '$cache_acceptable_files = array();' ) ); + + wp_cache_setting( 'cache_acceptable_files', array( 'wp-comments-popup.php' ) ); + + $this->assertSame( array( 'wp-comments-popup.php' ), $GLOBALS['cache_acceptable_files'] ); + $this->assertStringContainsString( + "\$cache_acceptable_files = array ( 0 => 'wp-comments-popup.php', );", + file_get_contents( $config ) + ); + } + + /** + * A direct wp_cache_replace_line() call (the raw rewriter third-party code may + * use) still replaces the matching line and returns truthy. + */ + public function test_wp_cache_replace_line_public_api_replaces_line() { + $config = $this->make_config_file( array( '$wp_cache_front_page_checks = 0;' ) ); + + $result = wp_cache_replace_line( + '^ *\$wp_cache_front_page_checks', + '$wp_cache_front_page_checks = 1;', + $config + ); + + $this->assertTrue( $result ); + $this->assertStringContainsString( '$wp_cache_front_page_checks = 1;', file_get_contents( $config ) ); + $this->assertStringNotContainsString( '$wp_cache_front_page_checks = 0;', file_get_contents( $config ) ); + } +} diff --git a/wp-cache-phase2.php b/wp-cache-phase2.php index 7b69cd61..e30a1c68 100644 --- a/wp-cache-phase2.php +++ b/wp-cache-phase2.php @@ -1445,123 +1445,25 @@ function is_writeable_ACLSafe( $path ) { function wp_cache_setting( $field, $value ) { global $wp_cache_config_file; - $GLOBALS[ $field ] = $value; - if ( is_numeric( $value ) ) { - return wp_cache_replace_line( '^ *\$' . $field, "\$$field = $value;", $wp_cache_config_file ); - } elseif ( is_bool( $value ) ) { - $output_value = $value === true ? 'true' : 'false'; - return wp_cache_replace_line( '^ *\$' . $field, "\$$field = $output_value;", $wp_cache_config_file ); - } elseif ( is_object( $value ) || is_array( $value ) ) { - $text = var_export( $value, true ); - $text = preg_replace( '/[\s]+/', ' ', $text ); - return wp_cache_replace_line( '^ *\$' . $field, "\$$field = $text;", $wp_cache_config_file ); - } else { - return wp_cache_replace_line( '^ *\$' . $field, "\$$field = '$value';", $wp_cache_config_file ); - } -} - -function wp_cache_replace_line( $old, $new, $my_file ) { - if ( ! is_string( $my_file ) || @is_file( $my_file ) === false ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged - if ( function_exists( 'set_transient' ) ) { - set_transient( 'wpsc_config_error', 'config_file_missing', 10 ); - } - return false; - } - if ( ! is_writeable_ACLSafe( $my_file ) ) { - if ( function_exists( 'set_transient' ) ) { - set_transient( 'wpsc_config_error', 'config_file_ro', 10 ); + if ( ! class_exists( 'Automattic\WPSC\Config' ) ) { + if ( ! file_exists( WPCACHEHOME . 'src/config/class-config.php' ) ) { + return false; } - trigger_error( "Error: file $my_file is not writable." ); - return false; + require_once WPCACHEHOME . 'src/config/class-config.php'; } - $found = false; - $loaded = false; - $c = 0; - $lines = array(); - while ( ! $loaded ) { - $lines = file( $my_file ); - if ( ! empty( $lines ) && is_array( $lines ) ) { - $loaded = true; - } else { - ++$c; - if ( $c > 100 ) { - if ( function_exists( 'set_transient' ) ) { - set_transient( 'wpsc_config_error', 'config_file_not_loaded', 10 ); - } - trigger_error( "wp_cache_replace_line: Error - file $my_file could not be loaded." ); - return false; - } - } - } - foreach ( (array) $lines as $line ) { - if ( - trim( $new ) != '' && - trim( $new ) == trim( $line ) - ) { - wp_cache_debug( "wp_cache_replace_line: setting not changed - $new" ); - return true; - } elseif ( preg_match( "/$old/", $line ) ) { - wp_cache_debug( 'wp_cache_replace_line: changing line ' . trim( $line ) . " to *$new*" ); - $found = true; - } - } + return \Automattic\WPSC\Config::set( $field, $value, $wp_cache_config_file ); +} - $tmp_config_filename = tempnam( $GLOBALS['cache_path'], md5( (string) wp_rand( 0, 9999 ) ) ); - if ( file_exists( $tmp_config_filename . '.php' ) ) { - unlink( $tmp_config_filename . '.php' ); - if ( file_exists( $tmp_config_filename . '.php' ) ) { - die( __( 'WARNING: attempt to intercept updating of config file.', 'wp-super-cache' ) ); - } - } - rename( $tmp_config_filename, $tmp_config_filename . '.php' ); - $tmp_config_filename .= '.php'; - wp_cache_debug( 'wp_cache_replace_line: writing to ' . $tmp_config_filename ); - $fd = fopen( $tmp_config_filename, 'w' ); - if ( ! $fd ) { - if ( function_exists( 'set_transient' ) ) { - set_transient( 'wpsc_config_error', 'config_file_ro', 10 ); - } - trigger_error( "wp_cache_replace_line: Error - could not write to $my_file" ); - return false; - } - if ( $found ) { - foreach ( (array) $lines as $line ) { - if ( ! preg_match( "/$old/", $line ) ) { - fwrite( $fd, $line ); - } elseif ( $new != '' ) { - fwrite( $fd, "$new\n" ); - } - } - } else { - $done = false; - foreach ( (array) $lines as $line ) { - if ( $done || ! preg_match( '/^(if\ \(\ \!\ )?define|\$|\?>/', $line ) ) { - fwrite( $fd, $line ); - } else { - fwrite( $fd, "$new\n" ); - fwrite( $fd, $line ); - $done = true; - } +function wp_cache_replace_line( $old, $new, $my_file ) { + if ( ! class_exists( 'Automattic\WPSC\Config' ) ) { + if ( ! file_exists( WPCACHEHOME . 'src/config/class-config.php' ) ) { + return false; } + require_once WPCACHEHOME . 'src/config/class-config.php'; } - fclose( $fd ); - - $my_file_permissions = fileperms( $my_file ); - - rename( $tmp_config_filename, $my_file ); - - if ( false !== $my_file_permissions ) { - chmod( $my_file, $my_file_permissions ); - } - - wp_cache_debug( 'wp_cache_replace_line: moved ' . $tmp_config_filename . ' to ' . $my_file ); - if ( function_exists( 'opcache_invalidate' ) ) { - @opcache_invalidate( $my_file ); - } - - return true; + return \Automattic\WPSC\Config::write_line( $old, $new, $my_file ); } function wpsc_shutdown_message() { diff --git a/wp-cache.php b/wp-cache.php index 2fd103dc..367570ae 100644 --- a/wp-cache.php +++ b/wp-cache.php @@ -41,6 +41,7 @@ require_once __DIR__ . '/inc/lifecycle.php'; require_once __DIR__ . '/inc/admin-notices.php'; require_once __DIR__ . '/inc/admin-ui.php'; +require_once __DIR__ . '/src/config/class-config.php'; // Plugin activation/deactivation/uninstall hooks must be registered from the // main plugin file so __FILE__ resolves to wp-cache.php; the handlers live in