diff --git a/composer.lock b/composer.lock index 7cc441512..0ff7ae74b 100644 --- a/composer.lock +++ b/composer.lock @@ -4523,16 +4523,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.2", + "version": "1.25.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "bcad8d995980440892759db0c32acae7c8e79442" + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442", - "reference": "bcad8d995980440892759db0c32acae7c8e79442", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", "shasum": "" }, "require": { @@ -4564,9 +4564,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.2" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" }, - "time": "2023-09-26T12:28:12+00:00" + "time": "2024-01-04T17:06:16+00:00" }, { "name": "predis/predis", @@ -11694,16 +11694,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.10.39", + "version": "1.10.55", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "d9dedb0413f678b4d03cbc2279a48f91592c97c4" + "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d9dedb0413f678b4d03cbc2279a48f91592c97c4", - "reference": "d9dedb0413f678b4d03cbc2279a48f91592c97c4", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9a88f9d18ddf4cf54c922fbeac16c4cb164c5949", + "reference": "9a88f9d18ddf4cf54c922fbeac16c4cb164c5949", "shasum": "" }, "require": { @@ -11752,7 +11752,7 @@ "type": "tidelift" } ], - "time": "2023-10-17T15:46:26+00:00" + "time": "2024-01-08T12:32:40+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -11804,21 +11804,21 @@ }, { "name": "phpstan/phpstan-doctrine", - "version": "1.3.43", + "version": "1.3.54", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-doctrine.git", - "reference": "c5015035755ad2d5013bd6bf98ff423ca6150822" + "reference": "f9555a2d54d685efd7003ae33c15e3d19d7d0c36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/c5015035755ad2d5013bd6bf98ff423ca6150822", - "reference": "c5015035755ad2d5013bd6bf98ff423ca6150822", + "url": "https://api.github.com/repos/phpstan/phpstan-doctrine/zipball/f9555a2d54d685efd7003ae33c15e3d19d7d0c36", + "reference": "f9555a2d54d685efd7003ae33c15e3d19d7d0c36", "shasum": "" }, "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10.12" + "phpstan/phpstan": "^1.10.48" }, "conflict": { "doctrine/collections": "<1.0", @@ -11835,7 +11835,7 @@ "doctrine/dbal": "^2.13.8 || ^3.3.3", "doctrine/lexer": "^1.2.1", "doctrine/mongodb-odm": "^1.3 || ^2.1", - "doctrine/orm": "^2.11.0", + "doctrine/orm": "^2.14.0", "doctrine/persistence": "^1.3.8 || ^2.2.1", "gedmo/doctrine-extensions": "^3.8", "nesbot/carbon": "^2.49", @@ -11868,22 +11868,22 @@ "description": "Doctrine extensions for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-doctrine/issues", - "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.43" + "source": "https://github.com/phpstan/phpstan-doctrine/tree/1.3.54" }, - "time": "2023-09-01T15:01:13+00:00" + "time": "2024-01-05T15:44:44+00:00" }, { "name": "phpstan/phpstan-symfony", - "version": "1.3.4", + "version": "1.3.6", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan-symfony.git", - "reference": "383855999db6a7d65d0bf580ce2762e17188c2a5" + "reference": "34b3c43684834f6a20aa51af8d455480d9de8b88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/383855999db6a7d65d0bf580ce2762e17188c2a5", - "reference": "383855999db6a7d65d0bf580ce2762e17188c2a5", + "url": "https://api.github.com/repos/phpstan/phpstan-symfony/zipball/34b3c43684834f6a20aa51af8d455480d9de8b88", + "reference": "34b3c43684834f6a20aa51af8d455480d9de8b88", "shasum": "" }, "require": { @@ -11940,9 +11940,9 @@ "description": "Symfony Framework extensions and rules for PHPStan", "support": { "issues": "https://github.com/phpstan/phpstan-symfony/issues", - "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.4" + "source": "https://github.com/phpstan/phpstan-symfony/tree/1.3.6" }, - "time": "2023-09-29T14:10:11+00:00" + "time": "2023-12-22T11:22:34+00:00" }, { "name": "phpstan/phpstan-webmozart-assert", diff --git a/src/Controller/PackageController.php b/src/Controller/PackageController.php index 0eab13c46..d7f392fd5 100644 --- a/src/Controller/PackageController.php +++ b/src/Controller/PackageController.php @@ -460,6 +460,26 @@ public function markSafeAction(Request $req): RedirectResponse return $this->redirectToRoute('view_spam'); } + #[IsGranted('ROLE_ADMIN')] + #[Route(path: '/package/{name}/unfreeze', name: 'unfreeze_package', requirements: ['name' => '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?'], defaults: ['_format' => 'html'], methods: ['POST'])] + public function unfreezePackageAction(Request $req, string $name, CsrfTokenManagerInterface $csrfTokenManager): RedirectResponse + { + if (!$this->isCsrfTokenValid('unfreeze', (string) $req->request->get('token'))) { + throw new BadRequestHttpException('Invalid CSRF token'); + } + + $package = $this->getPackageByName($req, $name); + if ($package instanceof Response) { + return $package; + } + + $package->unfreeze(); + $this->getEM()->persist($package); + $this->getEM()->flush(); + + return $this->redirectToRoute('view_package', ['name' => $package->getName()]); + } + #[Route(path: '/packages/{name}.{_format}', name: 'view_package', requirements: ['name' => '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?', '_format' => '(json)'], defaults: ['_format' => 'html'], methods: ['GET'])] public function viewPackageAction(Request $req, string $name, CsrfTokenManagerInterface $csrfTokenManager, #[CurrentUser] ?User $user = null): Response { @@ -694,6 +714,9 @@ public function viewPackageAction(Request $req, string $name, CsrfTokenManagerIn if ($this->isGranted('ROLE_ANTISPAM')) { $data['markSafeCsrfToken'] = $csrfTokenManager->getToken('mark_safe'); } + if ($this->isGranted('ROLE_ADMIN')) { + $data['unfreezeCsrfToken'] = $csrfTokenManager->getToken('unfreeze'); + } return $this->render('package/view_package.html.twig', $data); } @@ -965,6 +988,8 @@ public function editAction(Request $req, #[MapEntity] Package $package, #[Curren if ($form->isSubmitted() && $form->isValid()) { // Force updating of packages once the package is viewed after the redirect. $package->setCrawledAt(null); + // Reset remoteId as if the URL changes we expect possibly a different id and that's ok + $package->setRemoteId(null); $em = $this->getEM(); $em->persist($package); diff --git a/src/Entity/Package.php b/src/Entity/Package.php index 8f718d15a..46886ef0c 100644 --- a/src/Entity/Package.php +++ b/src/Entity/Package.php @@ -33,6 +33,12 @@ use Composer\Util\HttpDownloader; use DateTimeInterface; +enum PackageFreezeReason: string +{ + case Spam = 'spam'; + case RemoteIdMismatch = 'remote_id'; +} + /** * @author Jordi Boggiano * @phpstan-import-type VersionArray from Version @@ -163,6 +169,12 @@ class Package #[ORM\Column(type: 'string', length: 255, nullable: true)] private string|null $suspect = null; + /** + * If set, the content is the reason for being frozen + */ + #[ORM\Column(nullable: true)] + private PackageFreezeReason|null $frozen = null; + /** * @internal * @var true|null|\Composer\Repository\Vcs\VcsDriverInterface @@ -720,6 +732,35 @@ public function getSuspect(): ?string return $this->suspect; } + public function freeze(PackageFreezeReason $reason): void + { + $this->frozen = $reason; + $this->setCrawledAt($dt = new \DateTimeImmutable('2100-01-01 00:00:00')); + $this->setDumpedAt($dt); + $this->setDumpedAtV2($dt); + } + + public function unfreeze(): void + { + if ($this->frozen === PackageFreezeReason::RemoteIdMismatch) { + $this->setRemoteId(null); + } + $this->frozen = null; + $this->setCrawledAt(null); + $this->setDumpedAt(null); + $this->setDumpedAtV2(null); + } + + public function isFrozen(): bool + { + return !is_null($this->frozen); + } + + public function getFreezeReason(): ?PackageFreezeReason + { + return $this->frozen; + } + public function isAbandoned(): bool { return $this->abandoned; diff --git a/src/Package/Updater.php b/src/Package/Updater.php index f32bc77cc..534c177f9 100644 --- a/src/Package/Updater.php +++ b/src/Package/Updater.php @@ -13,6 +13,7 @@ namespace App\Package; use App\Entity\Dependent; +use App\Entity\PackageFreezeReason; use cebe\markdown\GithubMarkdown; use Composer\Package\AliasPackage; use Composer\Pcre\Preg; @@ -37,6 +38,10 @@ use Doctrine\DBAL\Connection; use App\Service\VersionCache; use Composer\Package\CompletePackageInterface; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Webmozart\Assert\Assert; /** @@ -77,6 +82,9 @@ public function __construct( private ManagerRegistry $doctrine, private ProviderManager $providerManager, private VersionIdCache $versionIdCache, + private MailerInterface $mailer, + private string $mailFromEmail, + private UrlGeneratorInterface $urlGenerator, ) { ErrorHandler::register(); } @@ -103,6 +111,41 @@ public function update(IOInterface $io, Config $config, Package $package, VcsRep throw new \RuntimeException('Driver could not be established for package '.$package->getName().' ('.$package->getRepository().')'); } + $remoteId = null; + if ($driver instanceof GitHubDriver) { + $repoData = $driver->getRepoData(); + if (isset($repoData['repository']['id'])) { + $remoteId = 'github.com/'.$repoData['repository']['id']; + } + } elseif ($driver instanceof GitLabDriver) { + $repoData = $driver->getRepoData(); + if (isset($repoData['id'])) { + $remoteId = 'gitlab.com/'.$repoData['id']; + } + } + + if ($remoteId !== null) { + if (!$package->getRemoteId()) { + $package->setRemoteId($remoteId); + } + if ($package->getRemoteId() !== $remoteId) { + $package->freeze(PackageFreezeReason::RemoteIdMismatch); + $em->flush(); + $io->writeError('Skipping update as the source repository has a remote id mismatch. Expected '.$package->getRemoteId().' but got ' . $remoteId.'.'); + + $message = (new Email()) + ->subject($package->getName().' frozen due to remote id mismatch') + ->from(new Address($this->mailFromEmail)) + ->to($this->mailFromEmail) + ->text('Check out '.$this->urlGenerator->generate('view_package', ['name' => $package->getName()], UrlGeneratorInterface::ABSOLUTE_URL).' was not repo-jacked.') + ; + $message->getHeaders()->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply'); + $this->mailer->send($message); + + return $package; + } + } + $rootIdentifier = $driver->getRootIdentifier(); // always update the master branch / root identifier, as in case a package gets archived diff --git a/templates/package/view_package.html.twig b/templates/package/view_package.html.twig index ee9eecb9f..16e11affc 100644 --- a/templates/package/view_package.html.twig +++ b/templates/package/view_package.html.twig @@ -119,7 +119,16 @@ {% endif %} + {% if is_granted('ROLE_ADMIN') and package.isFrozen() %} +
+ + +
+ {% endif %} + {% if package.isFrozen() %} +

{{ ('freezing_reasons.' ~ package.getFreezeReason().value)|trans }}

+ {% endif %} {% if lastJobWarning is defined and lastJobWarning is not empty %}

{{ lastJobWarning|raw }}
View Last Update Log

{% endif %} diff --git a/translations/messages.en.yml b/translations/messages.en.yml index 17c6e12cb..cc1ff7657 100644 --- a/translations/messages.en.yml +++ b/translations/messages.en.yml @@ -172,3 +172,7 @@ Order: 'Order' Username: 'Username' asc: 'asc' desc: 'desc' + +freezing_reasons: + spam: This package was marked as spam and has been frozen as a result. + remote_id: This packages's canonical repository id has changed and the package has been frozen as a result.