Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/archive.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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 }}
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
33 changes: 33 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 4 additions & 0 deletions docs/side-effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion nginx-cache.php → ns-nginx-cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion phpcs.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<!-- Project-root-relative standards for plugin-free Canary installs. -->
<config name="installed_paths" value="vendor/automattic/vipwpcs,vendor/phpcompatibility/php-compatibility,vendor/phpcsstandards/phpcsextra,vendor/phpcsstandards/phpcsutils,vendor/sirbrillig/phpcs-variable-analysis,vendor/slevomat/coding-standard,vendor/sympress/coding-standards,vendor/wp-coding-standards/wpcs" />

<file>nginx-cache.php</file>
<file>ns-nginx-cache.php</file>
<file>uninstall.php</file>
<file>src</file>

Expand Down
4 changes: 2 additions & 2 deletions src/Admin/CacheMetricsPanel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
);
Expand Down
2 changes: 1 addition & 1 deletion src/Admin/SettingsPage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
2 changes: 1 addition & 1 deletion src/Inspection/SiteHealth.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions tests/Integration/compatibility.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
81 changes: 81 additions & 0 deletions tests/Integration/plugin-identity-checks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

use SymPress\NginxCache\Admin\CacheMetricsPanel;
use Symfony\Component\Yaml\Yaml;

// Use real WordPress discovery and update checks; intercept every update HTTP request.
(static function (): void {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
$root = dirname(__DIR__, 2);
$composer = json_decode(file_get_contents($root . '/composer.json'), true, 512, JSON_THROW_ON_ERROR);
$basename = $composer['extra']['kernel']['entry'];
$slug = $composer['extra']['installer-name'];
$mainFile = $root . '/' . basename($basename);
check($slug === 'ns-nginx-cache' && dirname($basename) === $slug && is_file($mainFile), 'Composer install name resolves the real plugin entry');
$headers = get_plugin_data($mainFile, false, false);
check($headers['Name'] === 'NS Nginx Cache' && $headers['UpdateURI'] === 'https://github.com/SymPress/nginx-cache', 'WordPress reads the distinct plugin name and external Update URI');
$workflow = Yaml::parseFile($root . '/.github/workflows/archive.yml');
$archive = $workflow['jobs']['archive']['with'];
check($archive['package_name'] === $slug && $archive['package_folder'] === $slug && $archive['main_file'] === basename($basename) && $workflow['jobs']['archive-check']['with']['package_folder'] === $slug, 'release archive and archive check use the Composer plugin basename');

$pluginDirectory = WP_PLUGIN_DIR . '/' . $slug;
wp_mkdir_p(WP_PLUGIN_DIR);
check(!file_exists($pluginDirectory) && symlink($root, $pluginDirectory), 'plugin discovery fixture owns a new symlink');
$originalPaths = $GLOBALS['wp_plugin_paths'];
$originalCache = wp_cache_get('plugins', 'plugins');
$originalUpdates = get_site_transient('update_plugins');
$requestCount = 0;
$externalChecks = 0;
$active = [];
$activePlugins = static function () use (&$active): array { return $active; };
$externalUpdate = static function (mixed $update, array $data, string $file) use (&$externalChecks, $basename, $headers): bool {
check($file === $basename && $data['UpdateURI'] === $headers['UpdateURI'], 'WordPress routes this plugin to its external update host');
++$externalChecks;
return false;
};
$updateRequest = static function (mixed $pre, array $args, string $url) use (&$requestCount, &$active, $basename, $headers): mixed {
if (!str_contains($url, 'api.wordpress.org/plugins/update-check/')) { return $pre; }
++$requestCount;
$payload = json_decode($args['body']['plugins'], true, 512, JSON_THROW_ON_ERROR);
check($payload['plugins'][$basename]['UpdateURI'] === $headers['UpdateURI'] && $payload['active'] === $active, 'update request carries the Update URI with the current activation state');
// WordPress.org excludes non-directory Update URIs; unrelated updates remain available.
return ['response' => ['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);
}
})();
1 change: 1 addition & 0 deletions tests/Integration/v1-admin-checks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading