Skip to content
Draft
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
56 changes: 6 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ Package wpackagist-plugin/my-closed-plugin is abandoned because https://wordpres

## Why

When a plugin is closed on WordPress.org, [WPackagist](https://wpackagist.org/) not always remove it from its database immediately.
As a result, some closed plugins remain available for installation via WPackagist.
When a plugin is closed on WordPress.org, Composer repositories (e.g: [WPackagist](https://wpackagist.org/) & [WP Packages](https://wp-packages.org/) not always remove it from its database immediately.
Worse still, even after the Composer repositories removed it., a closed plugin might persists in `composer.lock` with references to download.wordPress.org endpoint and [WordPress plugin subversion repository](https://plugins.svn.wordpress.org/)
As a result, closed plugins remain installable.

Moreover, even if a plugin is closed, its existing versions are still downloadable from WordPress.org and the subversion repository.
```json
{
"repositories": [
Expand All @@ -87,7 +87,7 @@ Moreover, even if a plugin is closed, its existing versions are still downloadab
"version": "1.0",
"source": {
"type": "svn",
"url": "https://plugins.svn.wordpress.org/my-closed-plugin/",
"url": "https://plugins.svn.wordpress.org/my-closed-plugin/", // subversion repository
"reference": "tags/1.0"
}
}
Expand All @@ -99,15 +99,15 @@ Moreover, even if a plugin is closed, its existing versions are still downloadab
"version": "1.0",
"dist": {
"type": "zip",
"url": "https://downloads.wordpress.org/plugin/your-closed-plugin.1.0.zip"
"url": "https://downloads.wordpress.org/plugin/your-closed-plugin.1.0.zip" // download.wordPress.org endpoint
}
}
}
]
}
```

To catch these closed plugins, `WP Org Closed Plugin` queries [WordPress.org API](https://codex.wordpress.org/WordPress.org_API#Plugins) to check whether a plugin is closed and mark them as abandoned in Composer.
To catch these closed plugins, `WP Org Closed Plugin` queries [WP Packages API](https://wp-packages.org/api/packages/wp-plugin/closed) for the list of plugins closed on WordPress.org and marks them as abandoned in Composer.

## What to do when a plugin is closed?

Expand Down Expand Up @@ -151,50 +151,6 @@ Skipped checking for closed plugins because of --locked.

You should run `composer audit` without `--locked` to check for closed plugins.

### Cache

WordPress.org API responses are cached for 10 minutes.

If you must clear the cache, delete the `<composer-cache-dir>/wp-org-closed-plugin` directory.

```sh
rm -rf $(composer config cache-dir)/wp-org-closed-plugin
```

### Why `allow_self_signed` when connecting to `https://api.wordpress.org`?

> [!IMPORTANT]
> **Help Wanted!**
>
> Please send pull requests if you know how to get around the error:
>
> ```console
> $ curl --http3-only 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=better-delete-revision'
> curl: (56) ngtcp2_conn_writev_stream returned error: ERR_DRAINING
> ```

It is a hack to disallow HTTP/3, forcing `HttpDownloader` to use `RemoteFilesystem` instead of `CurlDownloader`.

I suspect api.wordpress.org does not properly support HTTP/3:

```console
$ curl --http1.1 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=better-delete-revision'
...json response

$ curl --http2 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=better-delete-revision'
...json response

$ curl --http3-only 'https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&slug=better-delete-revision'
...sometimes json response
...but most of the time ERR_DRAINING
curl: (56) ngtcp2_conn_writev_stream returned error: ERR_DRAINING
```

See:
- https://github.com/composer/composer/pull/12363
- https://github.com/composer/composer/blob/f5854b140ca27164d352ce30deece798acf3e36b/src/Composer/Util/HttpDownloader.php#L537
- https://github.com/typisttech/wp-org-closed-plugin/pull/22

> [!TIP]
> **Hire Tang Rufus!**
>
Expand Down
41 changes: 5 additions & 36 deletions src/MarkClosedPluginAsAbandoned.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,30 @@
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Package\CompletePackageInterface;
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\ArrayCache;
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\CacheProxy;
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\Client;
use TypistTech\WpOrgClosedPlugin\WpOrg\Api\FileCache;
use TypistTech\WpOrgClosedPlugin\WpOrg\UrlParser\DownloadUrlParser;
use TypistTech\WpOrgClosedPlugin\WpOrg\UrlParser\MultiUrlParser;
use TypistTech\WpOrgClosedPlugin\WpOrg\UrlParser\SvnUrlParser;
use TypistTech\WpOrgClosedPlugin\WpOrg\UrlParser\UrlParserInterface;
use TypistTech\WpOrgClosedPlugin\WpPackages\Api\Client;

readonly class MarkClosedPluginAsAbandoned
{
public static function create(Composer $composer, IOInterface $io): self
{
$loop = $composer->getLoop();

$config = $composer->getConfig();
$cachePath = "{$config->get('cache-dir')}/wp-org-closed-plugin";
$composerCache = new Cache($io, $cachePath);
$composerCache->setReadOnly($config->get('cache-read-only'));
$cache = new CacheProxy(
new ArrayCache,
new FileCache($composerCache),
);
$cache = new Cache($io, $cachePath);
$cache->setReadOnly($config->get('cache-read-only'));

return new self(
new MultiUrlParser(
new DownloadUrlParser,
new SvnUrlParser,
),
new Client(
$loop->getHttpDownloader(),
$loop,
$composer->getLoop()->getHttpDownloader(),
$cache,
$composer->getEventDispatcher(),
),
$io,
);
Expand All @@ -52,28 +43,6 @@ public function __construct(
private IOInterface $io,
) {}

public function warmCache(CompletePackageInterface ...$packages): void
{
$slugs = array_map(
fn (CompletePackageInterface $package): ?string => $this->slug(
...$package->getDistUrls(),
...$package->getSourceUrls(),
),
$packages,
);
$slugs = array_filter($slugs, static fn (?string $slug) => $slug !== null);

$slugCount = count($slugs);
$message = sprintf(
'Warming WordPress.org plugin status cache for %d %s',
$slugCount,
$slugCount === 1 ? 'slug' : 'slugs',
);
$this->io->debug($message);

$this->client->warmCache(...$slugs);
}

public function __invoke(CompletePackageInterface $package): void
{
$this->io->debug(
Expand Down
42 changes: 10 additions & 32 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,8 @@ public function uninstall(Composer $composer, IOInterface $io): void
public static function getSubscribedEvents(): array
{
return [
PackageEvents::PRE_PACKAGE_INSTALL => [
['warmCache', PHP_INT_MAX - 500],
['markClosedAsAbandoned', PHP_INT_MAX - 1000],
],
PackageEvents::PRE_PACKAGE_UPDATE => [
['warmCache', PHP_INT_MAX - 500],
['markClosedAsAbandoned', PHP_INT_MAX - 1000],
],
PackageEvents::PRE_PACKAGE_INSTALL => ['markClosedAsAbandoned', PHP_INT_MAX - 1000],
PackageEvents::PRE_PACKAGE_UPDATE => ['markClosedAsAbandoned', PHP_INT_MAX - 1000],

ScriptEvents::POST_INSTALL_CMD => ['markClosedLockedPackagesIfNotAlready', PHP_INT_MAX - 1000],
ScriptEvents::POST_UPDATE_CMD => ['markClosedLockedPackagesIfNotAlready', PHP_INT_MAX - 1000],
Expand All @@ -64,22 +58,6 @@ public static function getSubscribedEvents(): array
];
}

public function warmCache(PackageEvent $event): void
{
$packages = array_map(
static fn ($operation) => match (true) {
$operation instanceof InstallOperation => $operation->getPackage(),
$operation instanceof UpdateOperation => $operation->getTargetPackage(),
default => null,
},
$event->getOperations(),
);
$packages = array_filter($packages, static fn ($package) => $package instanceof CompletePackageInterface);
$packages = array_filter($packages, static fn ($package) => ! $package->isAbandoned());

$this->markClosedAsAbandoned->warmCache(...$packages);
}

public function markClosedAsAbandoned(PackageEvent $event): void
{
$operation = $event->getOperation();
Expand Down Expand Up @@ -109,18 +87,18 @@ public function markClosedLockedPackagesIfNotAlready(ScriptEvent $event): void
->getLocker()
->getLockedRepository(true);

$packages = $lockedRepository->getPackages();
$packages = array_filter($packages, static fn ($package) => $package instanceof CompletePackage);
$packages = array_filter($packages, static fn ($package) => ! $package->isAbandoned());
$packages = array_filter(
$packages,
fn ($package) => ! in_array($package->getPrettyName(), $this->marked, true),
$lockedRepository->getPackages(),
static fn ($package) => $package instanceof CompletePackage && ! $package->isAbandoned(),
);

$this->markClosedAsAbandoned->warmCache(...$packages);

foreach ($packages as $package) {
$this->marked[] = $package->getPrettyName();
$prettyName = $package->getPrettyName();
if (in_array($prettyName, $this->marked, true)) {
continue;
}

$this->marked[] = $prettyName;
$this->markClosedAsAbandoned->__invoke($package);
}
}
Expand Down
21 changes: 0 additions & 21 deletions src/WpOrg/Api/ArrayCache.php

This file was deleted.

12 changes: 0 additions & 12 deletions src/WpOrg/Api/CacheInterface.php

This file was deleted.

34 changes: 0 additions & 34 deletions src/WpOrg/Api/CacheProxy.php

This file was deleted.

Loading