From 4f92a2c9ea2db7271d6c49af101a4c63b6de65f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brian=20Sch=C3=A4ffner?= Date: Mon, 19 Oct 2026 17:11:03 +0200 Subject: [PATCH] fix: isolate nginx cache plugin slug and update identity --- .github/workflows/archive.yml | 5 +- CHANGELOG.md | 10 +++ README.md | 16 +++- Resources/config/services.yaml | 2 +- composer.json | 3 +- docs/migration.md | 33 ++++++++ docs/side-effects.md | 4 + nginx-cache.php => ns-nginx-cache.php | 4 +- phpcs.xml.dist | 2 +- src/Admin/CacheMetricsPanel.php | 4 +- src/Admin/SettingsPage.php | 2 +- src/Inspection/SiteHealth.php | 2 +- tests/Integration/compatibility.php | 3 + tests/Integration/plugin-identity-checks.php | 81 ++++++++++++++++++++ tests/Integration/v1-admin-checks.php | 1 + 15 files changed, 161 insertions(+), 11 deletions(-) rename nginx-cache.php => ns-nginx-cache.php (77%) create mode 100644 tests/Integration/plugin-identity-checks.php diff --git a/.github/workflows/archive.yml b/.github/workflows/archive.yml index c89a69c..f012b5a 100644 --- a/.github/workflows/archive.yml +++ b/.github/workflows/archive.yml @@ -32,7 +32,9 @@ jobs: /phpcs.xml /phpcs.xml.dist /phpstan.neon /phpstan.neon.dist /phpunit*.xml* /.php-cs-fixer* /.gitignore /.gitattributes /composer.review*.json /composer.review*.lock - main_file: nginx-cache.php + package_name: ns-nginx-cache + package_folder: ns-nginx-cache + main_file: ns-nginx-cache.php package_version: ${{ github.event.inputs.version }} secrets: COMPOSER_AUTH_JSON: ${{ secrets.COMPOSER_AUTH_JSON }} @@ -42,5 +44,6 @@ jobs: uses: sympress/workflows/.github/workflows/wordpress-archive-check.yml@78d083a9a0176cf0252318b0c69fc64938a727d6 with: artifact_name: ${{ needs.archive.outputs.artifact }} + package_folder: ns-nginx-cache secrets: COMPOSER_AUTH_JSON: ${{ secrets.COMPOSER_AUTH_JSON }} diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0bb14..44d97fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) where applicable. +## Unreleased + +### Fixed + +- Add a GitHub `Update URI` header so WordPress.org cannot offer updates for an unrelated plugin with a similar name, including while this plugin is inactive. + +### Changed + +- Use `ns-nginx-cache/ns-nginx-cache.php` as the WordPress plugin basename and **NS Nginx Cache** as its display name. Composer installation metadata, release archive names and folders, plugin settings links, asset URLs and version lookups follow the new entry. Existing installations require the [documented activation and directory migration](docs/migration.md#upgrading-the-sympress-plugin-slug). + ## 1.0.3 — 2026-10-16 ### Fixed diff --git a/README.md b/README.md index cbc72f2..a6d4694 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# SymPress Nginx Cache +# NS Nginx Cache [![PHP: ^8.5](https://img.shields.io/badge/php-%5E8.5-777bb4.svg)](composer.json) [![License: GPL-2.0-or-later](https://img.shields.io/badge/license-GPL--2.0--or--later-blue.svg)](composer.json) @@ -23,14 +23,26 @@ The package is discoverable by `sympress/kernel` through Composer metadata: ```json { "extra": { + "installer-name": "ns-nginx-cache", "kernel": { "bundle": "SymPress\\NginxCache\\NginxCacheBundle", - "entry": "nginx-cache/nginx-cache.php" + "entry": "ns-nginx-cache/ns-nginx-cache.php" } } } ``` +The WordPress plugin slug is `ns-nginx-cache`; release archives use the same +folder and `ns-nginx-cache.php` entry file. The Composer package remains +`sympress/nginx-cache`. Its `installer-name` sets the folder when using +[composer/installers](https://github.com/composer/installers#custom-install-names). +The [Update URI header](https://make.wordpress.org/core/2021/06/29/introducing-update-uri-plugin-header-in-wordpress-5-8/) +identifies this GitHub-distributed plugin and prevents WordPress.org from +offering updates for an unrelated plugin, including while this plugin is inactive. +This header does not install a GitHub updater; updates still use Composer or +the release archive. See [upgrading an existing installation](docs/migration.md#upgrading-the-sympress-plugin-slug) +before switching from the old `nginx-cache` folder. + ## WP-CLI ```bash diff --git a/Resources/config/services.yaml b/Resources/config/services.yaml index 00fd48e..cfe4ca7 100644 --- a/Resources/config/services.yaml +++ b/Resources/config/services.yaml @@ -122,7 +122,7 @@ services: - { name: kernel.hook, hook: 'admin_post_sympress_nginx_cache_purge', method: handlePurgeAction } - { name: kernel.hook, hook: 'admin_post_sympress_nginx_cache_flush_queue', method: handleQueueAction } - { name: kernel.hook, hook: 'admin_bar_menu', method: addAdminBarNode, accepted_args: 1, priority: 100 } - - { name: kernel.hook, hook: 'plugin_action_links_nginx-cache/nginx-cache.php', method: addPluginActionLinks, type: filter, accepted_args: 1 } + - { name: kernel.hook, hook: 'plugin_action_links_ns-nginx-cache/ns-nginx-cache.php', method: addPluginActionLinks, type: filter, accepted_args: 1 } SymPress\NginxCache\Admin\NetworkAdminPage: tags: diff --git a/composer.json b/composer.json index a85354a..59021cb 100644 --- a/composer.json +++ b/composer.json @@ -96,10 +96,11 @@ } }, "extra": { + "installer-name": "ns-nginx-cache", "wordpress-install-dir": "vendor/wordpress/wordpress", "kernel": { "bundle": "SymPress\\NginxCache\\NginxCacheBundle", - "entry": "nginx-cache/nginx-cache.php" + "entry": "ns-nginx-cache/ns-nginx-cache.php" } }, "minimum-stability": "stable", diff --git a/docs/migration.md b/docs/migration.md index 23cd334..16615ae 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -1,5 +1,38 @@ # Migrating cache plugins +## Upgrading the SymPress plugin slug + +The SymPress plugin now appears as **NS Nginx Cache** with the plugin basename +`ns-nginx-cache/ns-nginx-cache.php`. The Composer package remains +`sympress/nginx-cache`; settings, stored credentials, queues, tag-index tables, +cron hooks, REST routes, translation domain and `wp nginx-cache` commands retain +their existing identifiers. + +1. Deactivate the old **SymPress** installation before changing its directory. + Verify its author and source; the unrelated WordPress.org plugin may also + appear as Nginx Cache. +2. For Composer installations, update `sympress/nginx-cache`. Remove any explicit + root-project installer-path override that forces this package into the old + `nginx-cache` folder; the package's `installer-name` is `ns-nginx-cache`. + For archive installations, install the new release under + `wp-content/plugins/ns-nginx-cache/` and retain the deactivated old files + outside the plugins directory as a backup. Do not use WordPress's Delete + action for this migration: an enabled delete-on-uninstall setting can remove + the stored configuration and queues. +3. Activate **NS Nginx Cache** again, using the previous per-site or network + activation scope. WordPress stores activation by plugin basename, so the old + activation does not follow the renamed entry file. Update any deployment + allowlist or explicit kernel entry to `ns-nginx-cache/ns-nginx-cache.php`. +4. Open Dashboard → Updates and choose **Check again** to refresh a previously + cached update notice. Verify the plugin settings and Site Health version. + +The main file includes the GitHub `Update URI`, which tells WordPress.org to +exclude this plugin from its update responses even while it is inactive. +Installations still running an older release without that header need the new +files before this protection applies. No GitHub update client is added. + +## Importing settings from another cache plugin + Preview Nginx Helper settings before importing: ```sh diff --git a/docs/side-effects.md b/docs/side-effects.md index 3aa284c..63efe30 100644 --- a/docs/side-effects.md +++ b/docs/side-effects.md @@ -17,6 +17,10 @@ request to local deletion and follow-up work. | Anonymous cacheable response | `SurrogateTagSubscriber` → `TagIndexRepository` | Registers canonical URL/tag mappings; fresh identical mappings issue no writes | Schedules tag maintenance | | Tag-maintenance cron / side-effect tick | `TagIndexMaintenance` | Deletes at most 500 index rows per call under the index mutation lock, enforcing TTL and retention limits | Schedules another maintenance event; no filesystem deletion or HTTP | +The plugin-list settings link is registered through +`plugin_action_links_ns-nginx-cache/ns-nginx-cache.php`, matching the Composer +and archive entry. It only links to the settings page and triggers no purge. + ## Tag retention, setup and translated requests Tag retention no longer prunes on each insertion. Maintenance commits the diff --git a/nginx-cache.php b/ns-nginx-cache.php similarity index 77% rename from nginx-cache.php rename to ns-nginx-cache.php index 2e942b3..eecc280 100644 --- a/nginx-cache.php +++ b/ns-nginx-cache.php @@ -3,7 +3,9 @@ declare(strict_types=1); /** - * Plugin Name: Nginx Cache + * Plugin Name: NS Nginx Cache + * Plugin URI: https://github.com/SymPress/nginx-cache + * Update URI: https://github.com/SymPress/nginx-cache * Description: Kernel-integrated purge controls for Nginx FastCGI, proxy and uWSGI cache zones. * Version: 1.0.3 * Requires at least: 6.9 diff --git a/phpcs.xml.dist b/phpcs.xml.dist index cc3e446..3610367 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -13,7 +13,7 @@ - nginx-cache.php + ns-nginx-cache.php uninstall.php src diff --git a/src/Admin/CacheMetricsPanel.php b/src/Admin/CacheMetricsPanel.php index fd9f883..40900e2 100644 --- a/src/Admin/CacheMetricsPanel.php +++ b/src/Admin/CacheMetricsPanel.php @@ -10,10 +10,10 @@ { public function enqueueStyles(): void { - wp_enqueue_style('sympress-nginx-cache-simple', plugins_url('Resources/assets/cache-simple.css', __DIR__ . '/../../nginx-cache.php'), [], (string) filemtime(__DIR__ . '/../../Resources/assets/cache-simple.css')); + wp_enqueue_style('sympress-nginx-cache-simple', plugins_url('Resources/assets/cache-simple.css', dirname(__DIR__, 2) . '/ns-nginx-cache.php'), [], (string) filemtime(__DIR__ . '/../../Resources/assets/cache-simple.css')); wp_enqueue_style( 'sympress-nginx-cache-metrics', - plugins_url('Resources/assets/cache-metrics.css', __DIR__ . '/../../nginx-cache.php'), + plugins_url('Resources/assets/cache-metrics.css', dirname(__DIR__, 2) . '/ns-nginx-cache.php'), [], (string) filemtime(__DIR__ . '/../../Resources/assets/cache-metrics.css'), ); diff --git a/src/Admin/SettingsPage.php b/src/Admin/SettingsPage.php index c95dc47..ee0229a 100644 --- a/src/Admin/SettingsPage.php +++ b/src/Admin/SettingsPage.php @@ -264,7 +264,7 @@ public function render(): void $lastPurge = is_array($diagnostics['last_purge'] ?? null) ? $diagnostics['last_purge'] : null; $metrics = is_array($diagnostics['metrics'] ?? null) ? $diagnostics['metrics'] : []; $scanUsable = $status->exists && $status->directory && $status->error === null; - $pluginData = get_file_data(__DIR__ . '/../../nginx-cache.php', ['version' => 'Version']); + $pluginData = get_file_data(__DIR__ . '/../../ns-nginx-cache.php', ['version' => 'Version']); $configMissing = $diagnostics['nginx_config']['missing_directives']; $environment = $diagnostics['environment']; $queueCount = (int) $diagnostics['queue']['pending']; diff --git a/src/Inspection/SiteHealth.php b/src/Inspection/SiteHealth.php index d87939d..dad989c 100644 --- a/src/Inspection/SiteHealth.php +++ b/src/Inspection/SiteHealth.php @@ -86,7 +86,7 @@ public function check(string $name): array */ public function debug(array $information): array { - $data = get_file_data(__DIR__ . '/../../nginx-cache.php', ['version' => 'Version']); + $data = get_file_data(__DIR__ . '/../../ns-nginx-cache.php', ['version' => 'Version']); $values = ['version' => $data['version'], 'backend' => $this->backend->string('purge_backend'), 'profile' => $this->settings->profile()->value, 'path' => $this->settings->cachePath(), 'pending_purges' => $this->queue->count(), 'pending_follow_up' => $this->effects->count(), 'scope' => $this->scope->isIsolated() ? 'isolated-site-root' : 'shared-root-site-key-scan', 'worker_heartbeat' => (int) get_option(CacheWorker::HEARTBEAT, 0), 'tag_index' => wp_json_encode($this->tags->stats())]; $fields = []; foreach ($values as $name => $value) { diff --git a/tests/Integration/compatibility.php b/tests/Integration/compatibility.php index f14400f..88a8173 100644 --- a/tests/Integration/compatibility.php +++ b/tests/Integration/compatibility.php @@ -30,6 +30,9 @@ throw new RuntimeException('Unexpected consumer HTTP request.'); })); $container->get(HookLoader::class)->register(); +check(has_filter('plugin_action_links_ns-nginx-cache/ns-nginx-cache.php') !== false, 'compiled kernel registers settings links for the new plugin basename'); +check(str_contains(implode('', apply_filters('plugin_action_links_ns-nginx-cache/ns-nginx-cache.php', [])), 'page=sympress-nginx-cache'), 'plugin-list settings link opens the existing settings page'); +require __DIR__ . '/plugin-identity-checks.php'; do_action('admin_init'); $registered = get_registered_settings(); check(isset($registered[CompatibilitySettings::PREFIX . 'purge_backend'], $registered[CompatibilitySettings::PREFIX . 'prewarm_sitemap'], $registered[CompatibilitySettings::REDIS_PASSWORD]), 'compiled kernel hooks register compatibility settings'); diff --git a/tests/Integration/plugin-identity-checks.php b/tests/Integration/plugin-identity-checks.php new file mode 100644 index 0000000..977b089 --- /dev/null +++ b/tests/Integration/plugin-identity-checks.php @@ -0,0 +1,81 @@ + ['code' => 200], 'body' => wp_json_encode(['plugins' => ['fixture/fixture.php' => ['slug' => 'fixture', 'plugin' => 'fixture/fixture.php', 'new_version' => '2.0.0', 'package' => 'https://example.test/fixture.zip']], 'no_update' => [], 'translations' => []])]; + }; + add_filter('pre_option_active_plugins', $activePlugins); + add_filter('pre_http_request', $updateRequest, 100, 3); + add_filter('update_plugins_github.com', $externalUpdate, 10, 3); + try { + wp_register_plugin_realpath($pluginDirectory . '/' . basename($basename)); + wp_clean_plugins_cache(false); + $plugins = get_plugins(); + check(isset($plugins[$basename]) && $plugins[$basename]['UpdateURI'] === $headers['UpdateURI'] && !is_plugin_active($basename), 'WordPress discovers the protected plugin while inactive'); + $plugins['fixture/fixture.php'] = array_replace($headers, ['Name' => 'Unrelated update fixture', 'UpdateURI' => '', 'Version' => '1.0.0']); + wp_cache_set('plugins', ['' => $plugins], 'plugins'); + foreach ([[], [$basename]] as $active) { + delete_site_transient('update_plugins'); + wp_update_plugins(); + $updates = get_site_transient('update_plugins'); + check(!isset($updates->response[$basename]) && isset($updates->response['fixture/fixture.php']), 'protected plugin receives no directory update while unrelated updates remain visible'); + } + check($requestCount === 2 && $externalChecks === 2, 'inactive and active plugins both retain the external update identity'); + (new CacheMetricsPanel())->enqueueStyles(); + foreach (['simple', 'metrics'] as $style) { + check(str_ends_with(wp_styles()->registered['sympress-nginx-cache-' . $style]->src, '/' . $slug . '/Resources/assets/cache-' . $style . '.css'), 'dashboard stylesheet resolves under the new plugin folder: ' . $style); + } + } catch (Throwable $error) { + fwrite(STDERR, (string) $error . PHP_EOL); + throw $error; + } finally { + remove_filter('pre_option_active_plugins', $activePlugins); + remove_filter('pre_http_request', $updateRequest, 100); + remove_filter('update_plugins_github.com', $externalUpdate); + $GLOBALS['wp_plugin_paths'] = $originalPaths; + wp_cache_delete('plugins', 'plugins'); + if ($originalCache !== false) { wp_cache_set('plugins', $originalCache, 'plugins'); } + if ($originalUpdates === false) { delete_site_transient('update_plugins'); } + else { set_site_transient('update_plugins', $originalUpdates); } + unlink($pluginDirectory); + } +})(); diff --git a/tests/Integration/v1-admin-checks.php b/tests/Integration/v1-admin-checks.php index b4dc29b..3769d7f 100644 --- a/tests/Integration/v1-admin-checks.php +++ b/tests/Integration/v1-admin-checks.php @@ -105,6 +105,7 @@ update_option(CompatibilitySettings::REDIS_PASSWORD, (new SecretCipher())->encrypt('fixture-debug-redis', CompatibilitySettings::REDIS_PASSWORD)); update_option($settings::OPTION_CLOUDFLARE_API_TOKEN, (new SecretCipher())->encrypt('fixture-debug-cloudflare', $settings::OPTION_CLOUDFLARE_API_TOKEN)); $health = new SiteHealth($settings, new CompatibilitySettings($settings), new \SymPress\NginxCache\Filesystem\CachePathValidator($fs), $queue, $effects, $scope, $clock, $index); +check($health->debug([])[$settings::TEXT_DOMAIN]['fields']['version']['value'] === get_file_data(dirname(__DIR__, 2) . '/ns-nginx-cache.php', ['version' => 'Version'])['version'], 'Site Health reads the version from the renamed plugin entry'); $debug = serialize($health->debug([])); check(!str_contains($debug, 'fixture-debug-redis') && !str_contains($debug, 'fixture-debug-cloudflare') && !str_contains($debug, SecretCipher::PREFIX) && !str_contains($debug, 'redis_password'), 'Site Health export whitelists fields and omits credentials'); check(count($health->tests([])['direct']) === 6 && $health->check('worker')['status'] === 'good', 'direct Site Health checks recognize the recent worker heartbeat');