From ace73002a03267ccd8a944f26d74cc4b92b57778 Mon Sep 17 00:00:00 2001 From: Vladimir Tsykun Date: Sat, 23 Nov 2019 23:38:22 +0300 Subject: [PATCH] Allow add the multiple ssh keys from UI for different git repositories - refactor create composer config logic - remove validation logic from entity class - small fixes --- app/config/security.yml | 2 +- composer.json | 4 +- .../WebBundle/Composer/PackagistFactory.php | 73 ++++++ .../Composer/Repository/VcsRepository.php | 57 +++++ .../Composer/Util/ProcessExecutor.php | 86 +++++++ .../WebBundle/Composer/VcsDriverFactory.php | 102 ++++++++ .../Composer/VcsRepositoryFactory.php | 42 ++++ .../WebBundle/Controller/ApiController.php | 68 ++--- .../Controller/PackageController.php | 64 ++++- .../Controller/ProviderController.php | 3 +- .../WebBundle/Controller/UserController.php | 34 +++ src/Packagist/WebBundle/Entity/Package.php | 238 ++---------------- .../WebBundle/Entity/PackageRepository.php | 13 +- .../WebBundle/Entity/SshCredentials.php | 4 + .../WebBundle/Entity/VersionRepository.php | 39 +++ .../WebBundle/Form/Type/CredentialType.php | 36 +++ .../Form/Type/OAuthRegistrationFormType.php | 47 ---- .../WebBundle/Form/Type/PackageType.php | 61 +++-- .../WebBundle/Form/Type/PrivateKeyType.php | 49 ++++ .../Form/Type/SshKeyCredentialType.php | 37 +++ src/Packagist/WebBundle/Menu/MenuBuilder.php | 5 +- .../WebBundle/Model/PackageManager.php | 74 ++++-- src/Packagist/WebBundle/Package/Updater.php | 11 +- .../WebBundle/Resources/config/services.yml | 51 +++- .../WebBundle/Resources/config/validation.yml | 7 + .../Resources/translations/messages.en.yml | 4 +- .../Resources/views/ApiDoc/index.html.twig | 23 +- .../Resources/views/User/sshkey.html.twig | 13 + .../WebBundle/Service/DistConfig.php | 2 +- .../WebBundle/Service/DistManager.php | 23 +- src/Packagist/WebBundle/Service/Locker.php | 1 + .../WebBundle/Service/QueueWorker.php | 2 +- .../WebBundle/Service/UpdaterWorker.php | 14 +- .../WebBundle/Twig/PackagistExtension.php | 19 +- .../WebBundle/Util/ChangelogUtils.php | 66 +++++ src/Packagist/WebBundle/Util/SshKeyHelper.php | 38 +++ .../Constraint/PackageRepository.php | 18 ++ .../Constraint/PackageRepositoryValidator.php | 134 ++++++++++ .../Validator/Constraint/PackageUnique.php | 20 ++ .../Constraint/PackageUniqueValidator.php | 59 +++++ 40 files changed, 1228 insertions(+), 415 deletions(-) create mode 100644 src/Packagist/WebBundle/Composer/PackagistFactory.php create mode 100644 src/Packagist/WebBundle/Composer/Repository/VcsRepository.php create mode 100644 src/Packagist/WebBundle/Composer/Util/ProcessExecutor.php create mode 100644 src/Packagist/WebBundle/Composer/VcsDriverFactory.php create mode 100644 src/Packagist/WebBundle/Composer/VcsRepositoryFactory.php create mode 100644 src/Packagist/WebBundle/Form/Type/CredentialType.php delete mode 100644 src/Packagist/WebBundle/Form/Type/OAuthRegistrationFormType.php create mode 100644 src/Packagist/WebBundle/Form/Type/PrivateKeyType.php create mode 100644 src/Packagist/WebBundle/Form/Type/SshKeyCredentialType.php create mode 100644 src/Packagist/WebBundle/Resources/config/validation.yml create mode 100644 src/Packagist/WebBundle/Resources/views/User/sshkey.html.twig create mode 100644 src/Packagist/WebBundle/Util/ChangelogUtils.php create mode 100644 src/Packagist/WebBundle/Util/SshKeyHelper.php create mode 100644 src/Packagist/WebBundle/Validator/Constraint/PackageRepository.php create mode 100644 src/Packagist/WebBundle/Validator/Constraint/PackageRepositoryValidator.php create mode 100644 src/Packagist/WebBundle/Validator/Constraint/PackageUnique.php create mode 100644 src/Packagist/WebBundle/Validator/Constraint/PackageUniqueValidator.php diff --git a/app/config/security.yml b/app/config/security.yml index ac9c6bd2..d1cd7eef 100644 --- a/app/config/security.yml +++ b/app/config/security.yml @@ -11,7 +11,7 @@ security: firewalls: packages: - pattern: (^(/packages.json$|/p/|/zipball/|/feeds/.+(\.rss|\.atom)|/packages/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?\.json|/packages/list\.json|/downloads/|/api/))+ + pattern: (^(/packages.json$|/p/|/zipball/|/feeds/.+(\.rss|\.atom)|/packages/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?(\.json|/changelog)|/packages/list\.json|/downloads/|/api/))+ api_basic: true main: diff --git a/composer.json b/composer.json index cce46c58..c2cecc80 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "exclude-from-classmap": [ "src/Packagist/WebBundle/Tests/" ] }, "require": { - "php": ">=7.0", + "php": ">=7.1", "symfony/symfony": "^3.4", "doctrine/orm": "^2.6", "doctrine/doctrine-bundle": "^1.2", @@ -45,7 +45,7 @@ "jms/security-extra-bundle": "^1.5", "jms/di-extra-bundle": "^1.4", "oro/doctrine-extensions": "^1.2", - "composer/composer": "^1.6", + "composer/composer": "^1.9", "friendsofsymfony/user-bundle": "^2.1", "predis/predis": "^1.0", "snc/redis-bundle": "^2.0", diff --git a/src/Packagist/WebBundle/Composer/PackagistFactory.php b/src/Packagist/WebBundle/Composer/PackagistFactory.php new file mode 100644 index 00000000..5a441c60 --- /dev/null +++ b/src/Packagist/WebBundle/Composer/PackagistFactory.php @@ -0,0 +1,73 @@ +repositoryFactory = $repositoryFactory; + $this->tmpDir = $tmpDir ?: sys_get_temp_dir(); + } + + /** + * @param SshCredentials|null $credentials + * @return \Composer\Config + */ + public function createConfig(SshCredentials $credentials = null) + { + $config = Factory::createConfig(); + + if (null !== $credentials) { + $credentialsFile = rtrim($this->tmpDir, '/') . '/packagist_priv_key_' . $credentials->getId(); + if (!file_exists($credentialsFile)) { + file_put_contents($credentialsFile, $credentials->getKey()); + chmod($credentialsFile, 0600); + } + putenv("GIT_SSH_COMMAND=ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i $credentialsFile"); + ProcessExecutor::inheritEnv(['GIT_SSH_COMMAND']); + + $config->merge(['config' => ['ssh-key-file' => $credentialsFile]]); + } else { + ProcessExecutor::inheritEnv([]); + putenv('GIT_SSH_COMMAND'); + } + + return $config; + } + + /** + * @param string $url + * @param IOInterface|null $io + * @param Config|null $config + * @param SshCredentials|null $credentials + * @param array $repoConfig + * + * @return Repository\VcsRepository + */ + public function createRepository(string $url, IOInterface $io = null, Config $config = null, SshCredentials $credentials = null, array $repoConfig = []) + { + $io = $io ?: new NullIO(); + if (null === $config) { + $config = $this->createConfig($credentials); + $io->loadConfiguration($config); + } + + $repoConfig['url'] = $url; + + return $this->repositoryFactory->create($repoConfig, $io, $config); + } +} diff --git a/src/Packagist/WebBundle/Composer/Repository/VcsRepository.php b/src/Packagist/WebBundle/Composer/Repository/VcsRepository.php new file mode 100644 index 00000000..63ac6e31 --- /dev/null +++ b/src/Packagist/WebBundle/Composer/Repository/VcsRepository.php @@ -0,0 +1,57 @@ +driverFactory = $driverFactory; + } + + /** + * @return VcsDriver|null + */ + public function getDriver() + { + if (false !== $this->driver) { + return $this->driver; + } + + return $this->driver = $this->driverFactory->createDriver( + $this->repoConfig, + $this->io, + $this->config, + $this->type, + ['url' => $this->url] + ); + } + + /** + * @return Config + */ + public function getConfig() + { + return $this->config; + } +} diff --git a/src/Packagist/WebBundle/Composer/Util/ProcessExecutor.php b/src/Packagist/WebBundle/Composer/Util/ProcessExecutor.php new file mode 100644 index 00000000..a163c2fd --- /dev/null +++ b/src/Packagist/WebBundle/Composer/Util/ProcessExecutor.php @@ -0,0 +1,86 @@ +io && $this->io->isDebug()) { + $safeCommand = preg_replace_callback('{://(?P[^:/\s]+):(?P[^@\s/]+)@}i', function ($m) { + if (preg_match('{^[a-f0-9]{12,}$}', $m['user'])) { + return '://***:***@'; + } + + return '://'.$m['user'].':***@'; + }, $command); + $safeCommand = preg_replace("{--password (.*[^\\\\]\') }", '--password \'***\' ', $safeCommand); + $this->io->writeError('Executing command ('.($cwd ?: 'CWD').'): '.$safeCommand); + } + + // make sure that null translate to the proper directory in case the dir is a symlink + // and we call a git command, because msysgit does not handle symlinks properly + if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) { + $cwd = realpath(getcwd()); + } + + $this->captureOutput = func_num_args() > 1; + $this->errorOutput = null; + $env = $this->getInheritedEnv(); + + // in v3, commands should be passed in as arrays of cmd + args + if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { + $process = Process::fromShellCommandline($command, $cwd, $env, null, static::getTimeout()); + } else { + $process = new Process($command, $cwd, $env, null, static::getTimeout()); + } + + $callback = is_callable($output) ? $output : array($this, 'outputHandler'); + $process->run($callback); + + if ($this->captureOutput && !is_callable($output)) { + $output = $process->getOutput(); + } + + $this->errorOutput = $process->getErrorOutput(); + + return $process->getExitCode(); + } + + /** + * Sets the environment variables for child process. + * + * @param array $variables + */ + public static function inheritEnv(?array $variables): void + { + static::$inheritEnv = $variables; + } + + /** + * Sets the environment variables. + */ + protected function getInheritedEnv(): ?array + { + $env = []; + foreach (static::$inheritEnv as $name) { + if (getenv($name)) { + $env[$name] = getenv($name); + } + } + + return $env ?: null; + } +} diff --git a/src/Packagist/WebBundle/Composer/VcsDriverFactory.php b/src/Packagist/WebBundle/Composer/VcsDriverFactory.php new file mode 100644 index 00000000..118b9b38 --- /dev/null +++ b/src/Packagist/WebBundle/Composer/VcsDriverFactory.php @@ -0,0 +1,102 @@ +drivers = $drivers ?: [ + 'github' => 'Composer\Repository\Vcs\GitHubDriver', + 'gitlab' => 'Composer\Repository\Vcs\GitLabDriver', + 'git-bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver', + 'git' => 'Composer\Repository\Vcs\GitDriver', + 'hg-bitbucket' => 'Composer\Repository\Vcs\HgBitbucketDriver', + 'hg' => 'Composer\Repository\Vcs\HgDriver', + 'perforce' => 'Composer\Repository\Vcs\PerforceDriver', + 'fossil' => 'Composer\Repository\Vcs\FossilDriver', + // svn must be last because identifying a subversion server for sure is practically impossible + 'svn' => 'Composer\Repository\Vcs\SvnDriver', + ]; + } + + /** + * @param string $type + * @param string $class + */ + public function setDriverClass(string $type, string $class): void + { + $this->drivers[$type] = $class; + } + + /** + * @param string $classOrType + * @param array $repoConfig + * @param IOInterface $io + * @param Config $config + * @param array $options + * + * @return VcsDriver|VcsDriverInterface + */ + public function createDriver(array $repoConfig, IOInterface $io, Config $config, string $classOrType = null, array $options = []) + { + $process = $this->createProcessExecutor($io); + + $driver = null; + if ($classOrType && class_exists($classOrType)) { + $driver = new $classOrType($repoConfig, $io, $config, $process); + return $driver; + } + + if (null === $driver && null !== $classOrType && isset($this->drivers[$classOrType])) { + $class = $this->drivers[$classOrType]; + $driver = new $class($repoConfig, $io, $config); + return $driver; + } + + if (null === $driver && isset($options['url'])) { + foreach ($this->drivers as $driverClass) { + if ($driverClass::supports($io, $config, $options['url'])) { + $driver = new $driverClass($repoConfig, $io, $config, $process); + break; + } + } + } + + if (null === $driver && isset($options['url'])) { + foreach ($this->drivers as $driverClass) { + if ($driverClass::supports($io, $config, $options['url'], true)) { + $driver = new $driverClass($repoConfig, $io, $config, $process); + break; + } + } + } + + if ($driver instanceof VcsDriverInterface) { + $driver->initialize(); + } + + return $driver; + } + + protected function createProcessExecutor(IOInterface $io) + { + return new ProcessExecutor($io); + } +} diff --git a/src/Packagist/WebBundle/Composer/VcsRepositoryFactory.php b/src/Packagist/WebBundle/Composer/VcsRepositoryFactory.php new file mode 100644 index 00000000..d3b9828b --- /dev/null +++ b/src/Packagist/WebBundle/Composer/VcsRepositoryFactory.php @@ -0,0 +1,42 @@ +driverFactory = $driverFactory; + } + + /** + * @param array $repoConfig + * @param IOInterface $io + * @param Config $config + * + * @return VcsRepository + */ + public function create(array $repoConfig, IOInterface $io, Config $config) + { + return new VcsRepository( + $repoConfig, + $io, + $config, + $this->driverFactory + ); + } +} diff --git a/src/Packagist/WebBundle/Controller/ApiController.php b/src/Packagist/WebBundle/Controller/ApiController.php index c64ed96f..fb47d477 100644 --- a/src/Packagist/WebBundle/Controller/ApiController.php +++ b/src/Packagist/WebBundle/Controller/ApiController.php @@ -14,6 +14,7 @@ use Packagist\WebBundle\Entity\Package; use Packagist\WebBundle\Entity\User; +use Packagist\WebBundle\Model\PackageManager; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; @@ -35,22 +36,21 @@ public function createPackageAction(Request $request) { $payload = json_decode($request->getContent(), true); if (!$payload) { - return new JsonResponse(array('status' => 'error', 'message' => 'Missing payload parameter'), 406); + return new JsonResponse(['status' => 'error', 'message' => 'Missing payload parameter'], 406); } $url = $payload['repository']['url']; $package = new Package; - $package->setEntityRepository($this->getDoctrine()->getRepository('PackagistWebBundle:Package')); - $package->setRouter($this->get('router')); $user = $this->getUser(); $package->addMaintainer($user); $package->setRepository($url); + $this->get(PackageManager::class)->updatePackageUrl($package); $errors = $this->get('validator')->validate($package); if (count($errors) > 0) { - $errorArray = array(); + $errorArray = []; foreach ($errors as $error) { $errorArray[$error->getPropertyPath()] = $error->getMessage(); } - return new JsonResponse(array('status' => 'error', 'message' => $errorArray), 406); + return new JsonResponse(['status' => 'error', 'message' => $errorArray], 406); } try { $em = $this->getDoctrine()->getManager(); @@ -58,10 +58,10 @@ public function createPackageAction(Request $request) $em->flush(); } catch (\Exception $e) { $this->get('logger')->critical($e->getMessage(), array('exception', $e)); - return new JsonResponse(array('status' => 'error', 'message' => 'Error saving package'), 500); + return new JsonResponse(['status' => 'error', 'message' => 'Error saving package'], 500); } - return new JsonResponse(array('status' => 'success'), 202); + return new JsonResponse(['status' => 'success'], 202); } /** @@ -69,6 +69,8 @@ public function createPackageAction(Request $request) * @Route("/api/github", name="github_postreceive", defaults={"_format" = "json"}) * @Route("/api/bitbucket", name="bitbucket_postreceive", defaults={"_format" = "json"}) * @Method({"POST"}) + * + * {@inheritdoc} */ public function updatePackageAction(Request $request) { @@ -116,7 +118,7 @@ public function editPackageAction(Request $request, Package $package) { $user = $this->getUser(); if (!$package->getMaintainers()->contains($user) && !$this->isGranted('ROLE_EDIT_PACKAGES')) { - throw new AccessDeniedException; + throw new AccessDeniedException(); } $payload = json_decode($request->request->get('payload'), true); @@ -125,13 +127,14 @@ public function editPackageAction(Request $request, Package $package) } $package->setRepository($payload['repository']); - $errors = $this->get('validator')->validate($package, array("Update")); + $this->get(PackageManager::class)->updatePackageUrl($package); + $errors = $this->get('validator')->validate($package, ["Update"]); if (count($errors) > 0) { - $errorArray = array(); + $errorArray = []; foreach ($errors as $error) { $errorArray[$error->getPropertyPath()] = $error->getMessage(); } - return new JsonResponse(array('status' => 'error', 'message' => $errorArray), 406); + return new JsonResponse(['status' => 'error', 'message' => $errorArray], 406); } $package->setCrawledAt(null); @@ -140,7 +143,7 @@ public function editPackageAction(Request $request, Package $package) $em->persist($package); $em->flush(); - return new JsonResponse(array('status' => 'success'), 200); + return new JsonResponse(['status' => 'success'], 200); } /** @@ -188,11 +191,10 @@ public function trackDownloadsAction(Request $request) { $contents = json_decode($request->getContent(), true); if (empty($contents['downloads']) || !is_array($contents['downloads'])) { - return new JsonResponse(array('status' => 'error', 'message' => 'Invalid request format, must be a json object containing a downloads key filled with an array of name/version objects'), 200); + return new JsonResponse(['status' => 'error', 'message' => 'Invalid request format, must be a json object containing a downloads key filled with an array of name/version objects'], 200); } - $failed = array(); - + $failed = []; $ip = $request->headers->get('X-'.$this->container->getParameter('trusted_ip_header')); if (!$ip) { $ip = $request->getClientIp(); @@ -212,10 +214,10 @@ public function trackDownloadsAction(Request $request) $this->get('packagist.download_manager')->addDownloads($jobs); if ($failed) { - return new JsonResponse(array('status' => 'partial', 'message' => 'Packages '.json_encode($failed).' not found'), 200); + return new JsonResponse(['status' => 'partial', 'message' => 'Packages '.json_encode($failed).' not found'], 200); } - return new JsonResponse(array('status' => 'success'), 201); + return new JsonResponse(['status' => 'success'], 201); } /** @@ -248,20 +250,20 @@ protected function receivePost(Request $request, $url, $urlRegex) { // try to parse the URL first to avoid the DB lookup on malformed requests if (!preg_match($urlRegex, $url)) { - return new Response(json_encode(array('status' => 'error', 'message' => 'Could not parse payload repository URL')), 406); + return new Response(json_encode(['status' => 'error', 'message' => 'Could not parse payload repository URL']), 406); } // find the user $user = $this->getUser(); if (!$user) { - return new Response(json_encode(array('status' => 'error', 'message' => 'Invalid credentials')), 403); + return new Response(json_encode(['status' => 'error', 'message' => 'Invalid credentials']), 403); } // try to find the all package $packages = $this->findPackagesByUrl($url, $urlRegex); if (!$packages) { - return new Response(json_encode(array('status' => 'error', 'message' => 'Could not find a package that matches this request (does user maintain the package?)')), 404); + return new Response(json_encode(['status' => 'error', 'message' => 'Could not find a package that matches this request (does user maintain the package?)']), 404); } // put both updating the database and scanning the repository in a transaction @@ -280,32 +282,6 @@ protected function receivePost(Request $request, $url, $urlRegex) return new JsonResponse(['status' => 'success', 'jobs' => $jobs], 202); } - /** - * Find a user by his username and API token - * - * @param Request $request - * @return User|null the found user or null otherwise - */ - protected function findUser(Request $request) - { - $username = $request->request->has('username') ? - $request->request->get('username') : - $request->query->get('username'); - - $apiToken = $request->request->has('apiToken') ? - $request->request->get('apiToken') : - $request->query->get('apiToken'); - - $user = $this->get('packagist.user_repository') - ->findOneBy(array('username' => $username, 'apiToken' => $apiToken)); - - if ($user && !$user->isEnabled()) { - return null; - } - - return $user; - } - /** * Find a user package given by its full URL * diff --git a/src/Packagist/WebBundle/Controller/PackageController.php b/src/Packagist/WebBundle/Controller/PackageController.php index c60d003a..cd5df970 100644 --- a/src/Packagist/WebBundle/Controller/PackageController.php +++ b/src/Packagist/WebBundle/Controller/PackageController.php @@ -4,6 +4,8 @@ use Composer\Factory; use Composer\IO\BufferIO; +use Packagist\WebBundle\Form\Type\CredentialType; +use Packagist\WebBundle\Util\ChangelogUtils; use Symfony\Component\Console\Output\OutputInterface; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\ValidatingArrayLoader; @@ -101,9 +103,7 @@ public function submitPackageAction(Request $req) throw new AccessDeniedException(); } - $package = new Package; - $package->setEntityRepository($this->getDoctrine()->getRepository('PackagistWebBundle:Package')); - $package->setRouter($this->get('router')); + $package = new Package(); $form = $this->createForm(PackageType::class, $package, [ 'action' => $this->generateUrl('submit'), ]); @@ -140,8 +140,6 @@ public function fetchInfoAction(Request $req) } $package = new Package; - $package->setEntityRepository($this->getDoctrine()->getRepository('PackagistWebBundle:Package')); - $package->setRouter($this->get('router')); $form = $this->createForm(PackageType::class, $package); $user = $this->getUser(); $package->addMaintainer($user); @@ -391,6 +389,59 @@ public function viewPackageAction(Request $req, $name) return $data; } + /** + * @Route( + * "/packages/{package}/changelog", + * requirements={"package"="[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?"}, + * name="package_changelog" + * ) + * + * @param string $package + * @param Request $request + * @return Response + * + * @Method({"GET"}) + */ + public function changelogAction($package, Request $request) + { + $package = $this->get('doctrine')->getRepository('PackagistWebBundle:Package') + ->findOneBy(['name' => $package]); + if (null === $package) { + return new JsonResponse(['error' => 'Not found'], 404); + } + if (!$this->isGranted('ROLE_ADMIN', $package)) { + return new JsonResponse(['error' => 'Access denied'], 403); + } + + $changelogBuilder = $this->get(ChangelogUtils::class); + $fromVersion = $request->get('from'); + $toVersion = $request->get('to'); + if (!$toVersion) { + return new JsonResponse(['error' => 'Parameters: "to" can not be empty'], 400); + } + + if (!$fromVersion) { + $fromVersion = $this->get('packagist.version_repository') + ->getPreviousRelease($package->getName(), $toVersion); + if (!$fromVersion) { + return new JsonResponse(['error' => 'Previous release do not exists'], 400); + } + } + + $changeLog = $changelogBuilder->getChangelog($package, $fromVersion, $toVersion); + return new JsonResponse( + [ + 'result' => $changeLog, + 'error' => null, + 'metadata' => [ + 'from' => $fromVersion, + 'to' => $toVersion, + 'package' => $package->getName() + ] + ] + ); + } + /** * @Route( * "/packages/{name}/downloads.{_format}", @@ -745,7 +796,8 @@ public function editAction(Request $req, Package $package) throw new AccessDeniedException; } - $form = $this->createFormBuilder($package, array("validation_groups" => array("Update"))) + $form = $this->createFormBuilder($package, ["validation_groups" => ["Update"]]) + ->add('credentials', CredentialType::class) ->add('repository', TextType::class) ->setMethod('POST') ->setAction($this->generateUrl('edit_package', ['name' => $package->getName()])) diff --git a/src/Packagist/WebBundle/Controller/ProviderController.php b/src/Packagist/WebBundle/Controller/ProviderController.php index ef2c20b5..6c0c6262 100644 --- a/src/Packagist/WebBundle/Controller/ProviderController.php +++ b/src/Packagist/WebBundle/Controller/ProviderController.php @@ -92,8 +92,7 @@ public function packageAction($package) */ public function zipballAction(Package $package, $hash) { - $config = $this->container->get('packagist.dist_config'); - $distManager = new DistManager($config); + $distManager = $this->container->get(DistManager::class); if (false === \preg_match('{[a-f0-9]{40}}i', $hash, $match)) { return new JsonResponse(['status' => 'error', 'message' => 'Not Found'], 404); } diff --git a/src/Packagist/WebBundle/Controller/UserController.php b/src/Packagist/WebBundle/Controller/UserController.php index acb2e14a..29d491ec 100644 --- a/src/Packagist/WebBundle/Controller/UserController.php +++ b/src/Packagist/WebBundle/Controller/UserController.php @@ -15,9 +15,12 @@ use Doctrine\ORM\NoResultException; use FOS\UserBundle\Model\UserInterface; use Packagist\WebBundle\Entity\Package; +use Packagist\WebBundle\Entity\SshCredentials; use Packagist\WebBundle\Entity\User; use Packagist\WebBundle\Form\Type\CustomerUserType; +use Packagist\WebBundle\Form\Type\SshKeyCredentialType; use Packagist\WebBundle\Model\RedisAdapter; +use Packagist\WebBundle\Util\SshKeyHelper; use Pagerfanta\Adapter\DoctrineORMAdapter; use Pagerfanta\Pagerfanta; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; @@ -156,6 +159,37 @@ public function myProfileAction(Request $req) ); } + /** + * @Template("PackagistWebBundle:User:sshkey.html.twig") + * @Route("/users/sshkey", name="user_add_sshkey") + * {@inheritdoc} + */ + public function addSSHKeyAction(Request $request) + { + $sshKey = new SshCredentials(); + $form = $this->createForm(SshKeyCredentialType::class, $sshKey); + + if ($request->getMethod() === 'POST') { + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $sshKey = $form->getData(); + $em = $this->getDoctrine()->getManager(); + $fingerprint = SshKeyHelper::getFingerprint($sshKey->getKey()); + $sshKey->setFingerprint($fingerprint); + + $em->persist($sshKey); + $em->flush(); + + $this->addFlash('success', 'Ssh key was added successfully'); + return new RedirectResponse('/'); + } + } + + return [ + 'form' => $form->createView(), + ]; + } + /** * @Template() * @Route("/users/{name}/", name="user_profile") diff --git a/src/Packagist/WebBundle/Entity/Package.php b/src/Packagist/WebBundle/Entity/Package.php index 328fd104..e2e718a8 100644 --- a/src/Packagist/WebBundle/Entity/Package.php +++ b/src/Packagist/WebBundle/Entity/Package.php @@ -1,25 +1,9 @@ - * Nils Adermann - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace Packagist\WebBundle\Entity; -use Composer\Factory; -use Composer\IO\NullIO; -use Composer\Repository\VcsRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Composer\Repository\Vcs\GitHubDriver; /** * @ORM\Entity(repositoryClass="Packagist\WebBundle\Entity\PackageRepository") @@ -32,8 +16,6 @@ * @ORM\Index(name="dumped_idx",columns={"dumpedAt"}) * } * ) - * @Assert\Callback(callback="isPackageUnique") - * @Assert\Callback(callback="isRepositoryValid", groups={"Update", "Default"}) * @author Jordi Boggiano */ class Package @@ -105,7 +87,6 @@ class Package /** * @ORM\Column() - * @Assert\NotBlank(groups={"Update", "Default"}) */ private $repository; @@ -166,14 +147,16 @@ class Package */ private $credentials; - private $entityRepository; - private $router; - /** + * @internal * @var \Composer\Repository\Vcs\VcsDriverInterface */ - private $vcsDriver = true; - private $vcsDriverError; + public $vcsDriver = true; + + /** + * @internal + */ + public $vcsDriverError; /** * @var array lookup table for versions @@ -188,8 +171,7 @@ public function __construct() public function toArray(VersionRepository $versionRepo) { - $versions = array(); - $versionIds = []; + $versions = $versionIds = []; $this->versions = $versionRepo->refreshVersions($this->getVersions()); foreach ($this->getVersions() as $version) { $versionIds[] = $version->getId(); @@ -199,12 +181,12 @@ public function toArray(VersionRepository $versionRepo) /** @var $version Version */ $versions[$version->getVersion()] = $version->toArray($versionData); } - $maintainers = array(); + $maintainers = []; foreach ($this->getMaintainers() as $maintainer) { /** @var $maintainer User */ $maintainers[] = $maintainer->toArray(); } - $data = array( + $data = [ 'name' => $this->getName(), 'description' => $this->getDescription(), 'time' => $this->getCreatedAt()->format('c'), @@ -217,7 +199,7 @@ public function toArray(VersionRepository $versionRepo) 'github_forks' => $this->getGitHubForks(), 'github_open_issues' => $this->getGitHubOpenIssues(), 'language' => $this->getLanguage(), - ); + ]; if ($this->isAbandoned()) { $data['abandoned'] = $this->getReplacementPackage() ?: true; @@ -226,151 +208,6 @@ public function toArray(VersionRepository $versionRepo) return $data; } - public function isRepositoryValid(ExecutionContextInterface $context) - { - // vcs driver was not nulled which means the repository was not set/modified and is still valid - if (true === $this->vcsDriver && null !== $this->getName()) { - return; - } - - $property = 'repository'; - $driver = $this->vcsDriver; - if (!is_object($driver)) { - if (preg_match('{https?://.+@}', $this->repository)) { - $context->buildViolation('URLs with user@host are not supported, use a read-only public URL') - ->atPath($property) - ->addViolation() - ; - } elseif (is_string($this->vcsDriverError)) { - $context->buildViolation('Uncaught Exception: '.htmlentities($this->vcsDriverError, ENT_COMPAT, 'utf-8')) - ->atPath($property) - ->addViolation() - ; - } else { - $context->buildViolation('No valid/supported repository was found at the given URL') - ->atPath($property) - ->addViolation() - ; - } - return; - } - try { - $information = $driver->getComposerInformation($driver->getRootIdentifier()); - - if (false === $information) { - $context->buildViolation('No composer.json was found in the '.$driver->getRootIdentifier().' branch.') - ->atPath($property) - ->addViolation() - ; - return; - } - - if (empty($information['name'])) { - $context->buildViolation('The package name was not found in the composer.json, make sure there is a name present.') - ->atPath($property) - ->addViolation() - ; - return; - } - - if (!preg_match('{^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9]([_.-]?[a-z0-9]+)*$}i', $information['name'])) { - $context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is invalid, it should have a vendor name, a forward slash, and a package name. The vendor and package name can be words separated by -, . or _. The complete name should match "[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9]([_.-]?[a-z0-9]+)*".') - ->atPath($property) - ->addViolation() - ; - return; - } - - if (preg_match('{(free.*watch|watch.*free|movie.*free|free.*movie|watch.*movie|watch.*full|generate.*resource|generate.*unlimited|hack.*coin|coin.*hack|v[.-]?bucks|(fortnite|pubg).*free|hack.*cheat|cheat.*hack|putlocker)}i', $information['name'])) { - $context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is blocked, if you think this is a mistake please get in touch with us.') - ->atPath($property) - ->addViolation() - ; - return; - } - - $reservedNames = ['nul', 'con', 'prn', 'aux', 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9']; - $bits = explode('/', strtolower($information['name'])); - if (in_array($bits[0], $reservedNames, true) || in_array($bits[1], $reservedNames, true)) { - $context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is reserved, package and vendor names can not match any of: '.implode(', ', $reservedNames).'.') - ->atPath($property) - ->addViolation() - ; - return; - } - - if (preg_match('{\.json$}', $information['name'])) { - $context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is invalid, package names can not end in .json, consider renaming it or perhaps using a -json suffix instead.') - ->atPath($property) - ->addViolation() - ; - return; - } - - if (preg_match('{[A-Z]}', $information['name'])) { - $suggestName = preg_replace('{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}', '\\1\\3-\\2\\4', $information['name']); - $suggestName = strtolower($suggestName); - - $context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is invalid, it should not contain uppercase characters. We suggest using '.$suggestName.' instead.') - ->atPath($property) - ->addViolation() - ; - return; - } - } catch (\Exception $e) { - $context->buildViolation('We had problems parsing your composer.json file, the parser reports: '.htmlentities($e->getMessage(), ENT_COMPAT, 'utf-8')) - ->atPath($property) - ->addViolation() - ; - } - if (null === $this->getName()) { - $context->buildViolation('An unexpected error has made our parser fail to find a package name in your repository, if you think this is incorrect please try again') - ->atPath($property) - ->addViolation() - ; - } - } - - public function setEntityRepository($repository) - { - $this->entityRepository = $repository; - } - - public function setRouter($router) - { - $this->router = $router; - } - - public function isPackageUnique(ExecutionContextInterface $context) - { - try { - if ($this->entityRepository->findOneByName($this->name)) { - $context->buildViolation('A package with the name '.$this->name.' already exists.') - ->atPath('repository') - ->addViolation() - ; - } - } catch (\Doctrine\ORM\NoResultException $e) {} - } - - public function isVendorWritable(ExecutionContextInterface $context) - { - try { - $vendor = $this->getVendor(); - if ($vendor && $this->entityRepository->isVendorTaken($vendor, reset($this->maintainers))) { - $context->buildViolation('The vendor is already taken by someone else. ' - . 'You may ask them to add your package and give you maintainership access. ' - . 'If they add you as a maintainer on any package in that vendor namespace, ' - . 'you will then be able to add new packages in that namespace. ' - . 'The packages already in that vendor namespace can be found at ' - . ''.$vendor.'') - ->atPath('repository') - ->addViolation() - ; - } - } catch (\Doctrine\ORM\NoResultException $e) {} - } - /** * Get id * @@ -572,44 +409,16 @@ public function getCreatedAt() */ public function setRepository($repoUrl) { - $this->vcsDriver = null; - // prevent local filesystem URLs if (preg_match('{^(\.|[a-z]:|/)}i', $repoUrl)) { return; } - $this->loadCredentials(); - // normalize protocol case $repoUrl = preg_replace_callback('{^(https?|git|svn)://}i', function ($match) { return strtolower($match[1]) . '://'; }, $repoUrl); - - $this->repository = $repoUrl; - - // avoid user@host URLs - if (preg_match('{https?://.+@}', $repoUrl)) { - return; - } - - try { - $io = new NullIO(); - $config = Factory::createConfig(); - $io->loadConfiguration($config); - $repository = new VcsRepository(array('url' => $this->repository), $io, $config); - - $driver = $this->vcsDriver = $repository->getDriver(); - if (!$driver) { - return; - } - $information = $driver->getComposerInformation($driver->getRootIdentifier()); - if (!isset($information['name'])) { - return; - } - if (null === $this->getName()) { - $this->setName($information['name']); - } - } catch (\Exception $e) { - $this->vcsDriverError = '['.get_class($e).'] '.$e->getMessage(); + if ($this->repository !== $repoUrl) { + $this->repository = $repoUrl; + $this->vcsDriver = $this->vcsDriverError = null; } } @@ -636,19 +445,6 @@ public function getBrowsableRepository() return preg_replace('{^(git://github.com/|git@github.com:)}', 'https://github.com/', $this->repository); } - public function loadCredentials() - { - if ($this->credentials) { - $credentialsFile = \sys_get_temp_dir() . '/' . \sha1($this->credentials->getId()); - if (!\file_exists($credentialsFile)) { - \file_put_contents($credentialsFile, $this->credentials->getKey()); - \chmod($credentialsFile, 0600); - } - //GIT_SSH_COMMAND='echo $SSH_KEY | ssh -i /dev/stdin' ??? - \putenv("GIT_SSH_COMMAND=ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i $credentialsFile"); - } - } - /** * Add versions * @@ -775,7 +571,7 @@ public function getDumpedAt() /** * Add maintainers * - * @param User $maintainer + * @param User|object $maintainer */ public function addMaintainer(User $maintainer) { @@ -785,7 +581,7 @@ public function addMaintainer(User $maintainer) /** * Get maintainers * - * @return \Doctrine\Common\Collections\Collection + * @return \Doctrine\Common\Collections\Collection|User[] */ public function getMaintainers() { @@ -902,7 +698,7 @@ public function setCredentials(SshCredentials $credentials = null) return $this; } - public static function sortVersions($a, $b) + public static function sortVersions(Version $a, Version $b) { $aVersion = $a->getNormalizedVersion(); $bVersion = $b->getNormalizedVersion(); diff --git a/src/Packagist/WebBundle/Entity/PackageRepository.php b/src/Packagist/WebBundle/Entity/PackageRepository.php index 15d0b758..6fdadce5 100644 --- a/src/Packagist/WebBundle/Entity/PackageRepository.php +++ b/src/Packagist/WebBundle/Entity/PackageRepository.php @@ -114,12 +114,7 @@ private function getPackageNamesForQuery($query) $names[] = $row['name']; } - if (defined('SORT_FLAG_CASE')) { - sort($names, SORT_STRING | SORT_FLAG_CASE); - } else { - sort($names, SORT_STRING); - } - + sort($names, SORT_STRING | SORT_FLAG_CASE); return $names; } @@ -137,10 +132,10 @@ public function getStalePackages() ) ORDER BY p.id ASC', array( - // crawl packages without auto-update once a week - 'crawled' => date('Y-m-d H:i:s', strtotime('-4hour')), + // crawl packages without auto-update once a hour + 'crawled' => date('Y-m-d H:i:s', strtotime('-1hour')), // crawl auto-updated packages once a week just in case - 'autocrawled' => date('Y-m-d H:i:s', strtotime('-7day')), + 'autocrawled' => date('Y-m-d H:i:s', strtotime('-1day')), ) ); } diff --git a/src/Packagist/WebBundle/Entity/SshCredentials.php b/src/Packagist/WebBundle/Entity/SshCredentials.php index b9d2fe93..30d5960f 100644 --- a/src/Packagist/WebBundle/Entity/SshCredentials.php +++ b/src/Packagist/WebBundle/Entity/SshCredentials.php @@ -49,6 +49,10 @@ class SshCredentials */ private $fingerprint; + public function __construct() + { + $this->createdAt = new \DateTime('now', new \DateTimeZone('UTC')); + } /** * Get id diff --git a/src/Packagist/WebBundle/Entity/VersionRepository.php b/src/Packagist/WebBundle/Entity/VersionRepository.php index 5f67a862..df6ec9cf 100644 --- a/src/Packagist/WebBundle/Entity/VersionRepository.php +++ b/src/Packagist/WebBundle/Entity/VersionRepository.php @@ -211,6 +211,45 @@ public function getLatestReleases($count = 10) return $res; } + /** + * @param string $package + * @param string $version + * + * @return string|null + */ + public function getPreviousRelease(string $package, string $version) + { + $subQb = $this->getEntityManager()->createQueryBuilder(); + $releasedAt = $subQb->select('v.releasedAt') + ->from('PackagistWebBundle:Version', 'v') + ->leftJoin('v.package', 'p') + ->where('v.version = :version') + ->andWhere('p.name = :name') + ->setParameter('version', $version) + ->setParameter('name', $package) + ->setMaxResults(1) + ->getQuery()->getSingleScalarResult(); + + $qb = $this->getEntityManager()->createQueryBuilder(); + $qb->select('v.version') + ->from('PackagistWebBundle:Version', 'v') + ->leftJoin('v.package', 'p') + ->andWhere('v.development = false') + ->andWhere('p.name = :name') + ->setParameter('name', $package) + ->orderBy('v.releasedAt', 'DESC') + ->setMaxResults(1); + + if ($releasedAt) { + $qb->andWhere('v.releasedAt < :releasedAt') + ->setParameter('releasedAt', $releasedAt); + } else { + $qb->setFirstResult(1); + } + + return $qb->getQuery()->getSingleScalarResult(); + } + public function getVersionStatisticsByMonthAndYear() { $qb = $this->getEntityManager()->createQueryBuilder(); diff --git a/src/Packagist/WebBundle/Form/Type/CredentialType.php b/src/Packagist/WebBundle/Form/Type/CredentialType.php new file mode 100644 index 00000000..4d290b6f --- /dev/null +++ b/src/Packagist/WebBundle/Form/Type/CredentialType.php @@ -0,0 +1,36 @@ +setDefaults([ + 'class' => SshCredentials::class, + 'choice_label' => function (SshCredentials $credentials) { + return $credentials->getName() . ($credentials->getFingerprint() ? + (' (' . $credentials->getFingerprint() . ')') : ''); + }, + 'label' => 'SSH Credentials (optional, uses for Git to set GIT_SSH_COMMAND)', + 'required' => false, + ]); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return EntityType::class; + } +} diff --git a/src/Packagist/WebBundle/Form/Type/OAuthRegistrationFormType.php b/src/Packagist/WebBundle/Form/Type/OAuthRegistrationFormType.php deleted file mode 100644 index f5e243f8..00000000 --- a/src/Packagist/WebBundle/Form/Type/OAuthRegistrationFormType.php +++ /dev/null @@ -1,47 +0,0 @@ - - * Nils Adermann - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Packagist\WebBundle\Form\Type; - -use Packagist\WebBundle\Entity\User; -use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\EmailType; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class OAuthRegistrationFormType extends AbstractType -{ - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder - ->add('username', null, array('label' => 'form.username', 'translation_domain' => 'FOSUserBundle')) - ->add('email', EmailType::class, array('label' => 'form.email', 'translation_domain' => 'FOSUserBundle')) - ; - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults(array( - 'data_class' => User::class, - 'intention' => 'registration', - 'validation_groups' => array('Default', 'Profile'), - )); - } - - /** - * {@inheritdoc} - */ - public function getBlockPrefix() - { - return 'packagist_oauth_user_registration'; - } -} diff --git a/src/Packagist/WebBundle/Form/Type/PackageType.php b/src/Packagist/WebBundle/Form/Type/PackageType.php index ebcc78b2..8ed32f2a 100644 --- a/src/Packagist/WebBundle/Form/Type/PackageType.php +++ b/src/Packagist/WebBundle/Form/Type/PackageType.php @@ -1,52 +1,69 @@ - * Nils Adermann - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ +declare(strict_types=1); namespace Packagist\WebBundle\Form\Type; use Packagist\WebBundle\Entity\Package; -use Packagist\WebBundle\Entity\SshCredentials; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Packagist\WebBundle\Model\PackageManager; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; -/** - * @author Jordi Boggiano - */ class PackageType extends AbstractType { + /** + * @var PackageManager + */ + protected $packageManager; + + /** + * @param PackageManager $packageManager + */ + public function __construct(PackageManager $packageManager) + { + $this->packageManager = $packageManager; + } + + /** + * {@inheritdoc} + */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('credentials', EntityType::class, [ - 'label' => 'SSH Credentials (optional)', - 'required' => false, - 'class' => SshCredentials::class, - 'choice_label' => 'name' - ]) + ->add('credentials', CredentialType::class) ->add('repository', TextType::class, [ 'label' => 'Repository URL (Git/Svn/Hg)', 'attr' => [ 'placeholder' => 'e.g.: https://github.com/composer/composer', ] ]); + + $builder->addEventListener(FormEvents::POST_SUBMIT, [$this, 'updateRepository']); + } + + /** + * @param FormEvent $event + */ + public function updateRepository(FormEvent $event) + { + $package = $event->getData(); + if ($package instanceof Package) { + $this->packageManager->updatePackageUrl($package); + } } + /** + * {@inheritdoc} + */ public function configureOptions(OptionsResolver $resolver) { - $resolver->setDefaults(array( + $resolver->setDefaults([ 'data_class' => Package::class, - )); + ]); } /** diff --git a/src/Packagist/WebBundle/Form/Type/PrivateKeyType.php b/src/Packagist/WebBundle/Form/Type/PrivateKeyType.php new file mode 100644 index 00000000..d215b5b7 --- /dev/null +++ b/src/Packagist/WebBundle/Form/Type/PrivateKeyType.php @@ -0,0 +1,49 @@ +setDefault('constraints', [new Callback([__CLASS__, 'validatePrivateKey'])]); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return TextareaType::class; + } + + /** + * @param ExecutionContextInterface $context + * @param string|null $value + */ + public static function validatePrivateKey($value, ExecutionContextInterface $context): void + { + if (empty($value)) { + return; + } + + if ($key = openssl_pkey_get_private($value)) { + if ($pubInfo = openssl_pkey_get_details($key)) { + return; + } + } + + $context->addViolation('This private key is not valid'); + } +} diff --git a/src/Packagist/WebBundle/Form/Type/SshKeyCredentialType.php b/src/Packagist/WebBundle/Form/Type/SshKeyCredentialType.php new file mode 100644 index 00000000..3c11aa85 --- /dev/null +++ b/src/Packagist/WebBundle/Form/Type/SshKeyCredentialType.php @@ -0,0 +1,37 @@ +add('name', TextType::class, [ + 'constraints' => [new NotBlank()] + ]) + ->add('key', PrivateKeyType::class, [ + 'constraints' => [new NotBlank()] + ]); + } + + /** + * {@inheritdoc} + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', SshCredentials::class); + } +} diff --git a/src/Packagist/WebBundle/Menu/MenuBuilder.php b/src/Packagist/WebBundle/Menu/MenuBuilder.php index 360eb3e5..0b438b7b 100644 --- a/src/Packagist/WebBundle/Menu/MenuBuilder.php +++ b/src/Packagist/WebBundle/Menu/MenuBuilder.php @@ -63,8 +63,9 @@ public function createAdminMenu() $menu = $this->factory->createItem('root'); $menu->setChildrenAttribute('class', 'list-unstyled'); - $menu->addChild($this->translator->trans('menu.my_users'), ['label' => '' . $this->translator->trans('menu.my_users'), 'route' => 'users_list', 'extras' =>['safe_label' => true]]); - $menu->addChild($this->translator->trans('menu.my_groups'), ['label' => '' . $this->translator->trans('menu.my_groups'), 'route' => 'groups_index', 'extras' =>['safe_label' => true]]); + $menu->addChild($this->translator->trans('menu.my_users'), ['label' => '' . $this->translator->trans('menu.my_users'), 'route' => 'users_list', 'extras' => ['safe_label' => true]]); + $menu->addChild($this->translator->trans('menu.my_groups'), ['label' => '' . $this->translator->trans('menu.my_groups'), 'route' => 'groups_index', 'extras' => ['safe_label' => true]]); + $menu->addChild($this->translator->trans('menu.ssh_keys'), ['label' => '' . $this->translator->trans('menu.ssh_keys'), 'route' => 'user_add_sshkey', 'extras' => ['safe_label' => true]]); return $menu; } diff --git a/src/Packagist/WebBundle/Model/PackageManager.php b/src/Packagist/WebBundle/Model/PackageManager.php index 1146e9de..d0ac05ef 100644 --- a/src/Packagist/WebBundle/Model/PackageManager.php +++ b/src/Packagist/WebBundle/Model/PackageManager.php @@ -1,19 +1,12 @@ - * Nils Adermann - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ +declare(strict_types=1); namespace Packagist\WebBundle\Model; use Doctrine\Common\Cache\ApcuCache; use Doctrine\Common\Cache\Cache; +use Packagist\WebBundle\Composer\PackagistFactory; use Packagist\WebBundle\Entity\User; use Packagist\WebBundle\Entity\VersionRepository; use Packagist\WebBundle\Package\InMemoryDumper; @@ -21,11 +14,8 @@ use Packagist\WebBundle\Entity\Package; use Psr\Log\LoggerInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Twig\Environment; - -/** - * @author Jordi Boggiano - */ class PackageManager { protected $doctrine; @@ -37,16 +27,18 @@ class PackageManager protected $dumper; protected $cache; protected $authorizationChecker; + protected $packagistFactory; public function __construct( RegistryInterface $doctrine, \Swift_Mailer $mailer, - \Twig_Environment $twig, + Environment $twig, LoggerInterface $logger, array $options, ProviderManager $providerManager, InMemoryDumper $dumper, AuthorizationCheckerInterface $authorizationChecker, + PackagistFactory $packagistFactory, Cache $cache = null ) { $this->doctrine = $doctrine; @@ -57,6 +49,7 @@ public function __construct( $this->providerManager = $providerManager; $this->authorizationChecker = $authorizationChecker; $this->dumper = $dumper; + $this->packagistFactory = $packagistFactory; if ($cache === null) { $cache = new ApcuCache(); $cache->setNamespace('package_manager'); @@ -79,6 +72,43 @@ public function deletePackage(Package $package) $em->flush(); } + /** + * @param Package $package + */ + public function updatePackageUrl(Package $package): void + { + if (!$package->getRepository() || $package->vcsDriver === true) { + return; + } + // avoid user@host URLs + if (preg_match('{https?://.+@}', $package->getRepository())) { + return; + } + + try { + $repository = $this->packagistFactory->createRepository( + $package->getRepository(), + null, + null, + $package->getCredentials() + ); + + $driver = $package->vcsDriver = $repository->getDriver(); + if (!$driver) { + return; + } + $information = $driver->getComposerInformation($driver->getRootIdentifier()); + if (!isset($information['name'])) { + return; + } + if (null === $package->getName()) { + $package->setName($information['name']); + } + } catch (\Exception $e) { + $package->vcsDriverError = '['.get_class($e).'] '.$e->getMessage(); + } + } + public function notifyUpdateFailure(Package $package, \Exception $e, $details = null) { if (!$package->isUpdateFailureNotified()) { @@ -150,7 +180,12 @@ public function getRootPackagesJson(User $user = null) return $packagesData[0]; } - public function getProvidersJson(User $user = null, $hash) + /** + * @param User|null|object $user + * @param string $hash + * @return bool + */ + public function getProvidersJson(?User $user, $hash) { list($root, $providers) = $this->dumpInMemory($user); $rootHash = \reset($root['provider-includes']); @@ -161,7 +196,14 @@ public function getProvidersJson(User $user = null, $hash) return $providers; } - public function getPackageJson(User $user = null, string $package, string $hash) + /** + * @param User|null|object $user + * @param string $package + * @param string $hash + * + * @return mixed + */ + public function getPackageJson(?User $user, string $package, string $hash) { list($root, $providers, $packages) = $this->dumpInMemory($user); diff --git a/src/Packagist/WebBundle/Package/Updater.php b/src/Packagist/WebBundle/Package/Updater.php index 66b91bc6..3b611e8b 100644 --- a/src/Packagist/WebBundle/Package/Updater.php +++ b/src/Packagist/WebBundle/Package/Updater.php @@ -278,8 +278,8 @@ public function update(IOInterface $io, Config $config, Package $package, Reposi $this->updateReadme($io, $package, $repository); } - $package->setUpdatedAt(new \DateTime); - $package->setCrawledAt(new \DateTime); + $package->setUpdatedAt(new \DateTime('now', new \DateTimeZone('UTC'))); + $package->setCrawledAt(new \DateTime('now', new \DateTimeZone('UTC'))); $em->flush(); if ($repository->hadInvalidBranches()) { throw new InvalidRepositoryException('Some branches contained invalid data and were discarded, it is advised to review the log and fix any issues present in branches'); @@ -556,7 +556,12 @@ private function updateInformation( $version->getSuggest()->clear(); } - return ['updated' => true, 'id' => $version->getId(), 'version' => strtolower($normVersion), 'object' => $version]; + return [ + 'updated' => true, + 'id' => $version->getId(), + 'version' => strtolower($normVersion), + 'object' => $version + ]; } private function updateArchive(ArchiveManager $archiveManager, PackageInterface $data) diff --git a/src/Packagist/WebBundle/Resources/config/services.yml b/src/Packagist/WebBundle/Resources/config/services.yml index 58fd0a4b..5feecc26 100644 --- a/src/Packagist/WebBundle/Resources/config/services.yml +++ b/src/Packagist/WebBundle/Resources/config/services.yml @@ -70,10 +70,11 @@ services: - '@event_dispatcher' - '@request_stack' - packagist.oauth.registration_form_type: - class: Packagist\WebBundle\Form\Type\OAuthRegistrationFormType + fos_user.form.package_type: + class: Packagist\WebBundle\Form\Type\PackageType + arguments: ['@packagist.package_manager'] tags: - - { name: form.type, alias: packagist_oauth_user_registration } + - { name: form.type } packagist.oauth.registration_form: factory: ['@form.factory', create] @@ -111,6 +112,11 @@ services: - '@packagist.provider_manager' - '@packagist.in_memory_dumper' - '@security.authorization_checker' + - '@packagist_factory' + + Packagist\WebBundle\Model\PackageManager: + alias: packagist.package_manager + public: true packagist.profile.form.type: class: Packagist\WebBundle\Form\Type\ProfileFormType @@ -165,7 +171,15 @@ services: updater_worker: class: Packagist\WebBundle\Service\UpdaterWorker - arguments: ["@logger", "@doctrine", "@packagist.package_updater", "@locker", "@scheduler", "@packagist.package_manager", "@packagist.download_manager"] + arguments: + - "@logger" + - "@doctrine" + - "@packagist.package_updater" + - "@locker" + - "@scheduler" + - "@packagist.package_manager" + - "@packagist.download_manager" + - "@packagist_factory" packagist.log_resetter: class: Packagist\WebBundle\Service\LogResetter @@ -174,3 +188,32 @@ services: packagist.console_stack_trace_line_formatter: class: Symfony\Bridge\Monolog\Formatter\ConsoleFormatter arguments: [] + + packagist_factory: + class: Packagist\WebBundle\Composer\PackagistFactory + public: true + arguments: + - '@Packagist\WebBundle\Composer\VcsRepositoryFactory' + + Packagist\WebBundle\Service\DistManager: + public: true + arguments: + - '@packagist.dist_config' + - '@packagist_factory' + + Packagist\WebBundle\Composer\PackagistFactory: + alias: packagist_factory + public: true + + Packagist\WebBundle\Util\ChangelogUtils: + public: true + arguments: + - '@packagist_factory' + + Packagist\WebBundle\Composer\VcsDriverFactory: + public: true + + Packagist\WebBundle\Composer\VcsRepositoryFactory: + public: true + arguments: + - '@Packagist\WebBundle\Composer\VcsDriverFactory' diff --git a/src/Packagist/WebBundle/Resources/config/validation.yml b/src/Packagist/WebBundle/Resources/config/validation.yml new file mode 100644 index 00000000..10ce2479 --- /dev/null +++ b/src/Packagist/WebBundle/Resources/config/validation.yml @@ -0,0 +1,7 @@ +Packagist\WebBundle\Entity\Package: + constraints: + - Packagist\WebBundle\Validator\Constraint\PackageUnique: ~ + - Packagist\WebBundle\Validator\Constraint\PackageRepository: { groups: [Update, Default] } + properties: + repository: + - NotBlank: { groups: [Update, Default] } diff --git a/src/Packagist/WebBundle/Resources/translations/messages.en.yml b/src/Packagist/WebBundle/Resources/translations/messages.en.yml index 1bd1aea7..02bb5a0f 100644 --- a/src/Packagist/WebBundle/Resources/translations/messages.en.yml +++ b/src/Packagist/WebBundle/Resources/translations/messages.en.yml @@ -24,6 +24,7 @@ menu: my_favorites: My favorites my_users: My users my_groups: My groups + ssh_keys: Ssh keys signinbox: register: No account yet? Create one now! @@ -130,8 +131,9 @@ statistics: api_doc: title: API documentation - intro: Packagist has a public API to consume data. See bellow the most important URIs: + intro: 'Packagist has a public API to consume data. See bellow the most important URIs:' listing_names: Listing package names + changelog_name: 'Get the package git changelog' all_packages: All packages by_organization: By organization by_name: By name diff --git a/src/Packagist/WebBundle/Resources/views/ApiDoc/index.html.twig b/src/Packagist/WebBundle/Resources/views/ApiDoc/index.html.twig index 373e9ebc..bfc0d98d 100644 --- a/src/Packagist/WebBundle/Resources/views/ApiDoc/index.html.twig +++ b/src/Packagist/WebBundle/Resources/views/ApiDoc/index.html.twig @@ -11,13 +11,7 @@
  • {{ 'api_doc.by_type'|trans }}
  • -
  • {{ 'api_doc.searching'|trans }} - -
  • +
  • {{ 'api_doc.changelog_name'|trans }}
  • {{ 'api_doc.get_package_data'|trans }}
  • {% set apiToken = app.user.username ~ ':' ~ app.user.apiToken %} @@ -65,6 +59,21 @@ GET https://{{ packagist_host }}/packages/list.json?type=[type]&token={{ apiToke +
    +

    {{ 'api_doc.changelog_name'|trans }}

    +
    +GET https://{{ packagist_host }}/packages/{name}/changelog?token={{ apiToken }}&from=1.0.0&to=1.0.1
    +
    +{
    +  "packageNames": [
    +    "[vendor]/[package]",
    +    ...
    +  ]
    +}
    +
    +

    Working example: https://{{ packagist_host }}/packages/{{ examplePackage }}/changelog?token={{ apiToken }}&from=1.0.0&to=1.0.1

    + +

    {{ 'api_doc.get_package_data'|trans }}

    diff --git a/src/Packagist/WebBundle/Resources/views/User/sshkey.html.twig b/src/Packagist/WebBundle/Resources/views/User/sshkey.html.twig new file mode 100644 index 00000000..af64e784 --- /dev/null +++ b/src/Packagist/WebBundle/Resources/views/User/sshkey.html.twig @@ -0,0 +1,13 @@ +{% extends "PackagistWebBundle::layout.html.twig" %} + +{% block content %} +

    Add ssh keys

    +
    + {{ form_start(form, { attr: { class: 'col-md-6' } }) }} + {{ form_rest(form) }} + + {{ form_end(form) }} +
    +
    +
    +{% endblock %} diff --git a/src/Packagist/WebBundle/Service/DistConfig.php b/src/Packagist/WebBundle/Service/DistConfig.php index 20e8b901..d6f2f070 100644 --- a/src/Packagist/WebBundle/Service/DistConfig.php +++ b/src/Packagist/WebBundle/Service/DistConfig.php @@ -102,7 +102,7 @@ public function generateRoute(string $name, string $reference): string ['package' => $name, 'hash' => $reference . '.' . $this->getArchiveFormat()] ); - return $this->config['endpoint'] . $uri; + return rtrim($this->config['endpoint'], '/') . $uri; } /** diff --git a/src/Packagist/WebBundle/Service/DistManager.php b/src/Packagist/WebBundle/Service/DistManager.php index cdaaf7be..8082cf68 100644 --- a/src/Packagist/WebBundle/Service/DistManager.php +++ b/src/Packagist/WebBundle/Service/DistManager.php @@ -6,7 +6,9 @@ use Composer\Factory; use Composer\IO\BufferIO; +use Composer\IO\NullIO; use Composer\Repository\VcsRepository; +use Packagist\WebBundle\Composer\PackagistFactory; use Packagist\WebBundle\Entity\Version; use Symfony\Component\Console\Output\StreamOutput; use Symfony\Component\Filesystem\Filesystem; @@ -16,10 +18,12 @@ class DistManager { private $config; private $fileSystem; + private $packagistFactory; - public function __construct(DistConfig $config) + public function __construct(DistConfig $config, PackagistFactory $packagistFactory) { $this->config = $config; + $this->packagistFactory = $packagistFactory; $this->fileSystem = new Filesystem(); } @@ -59,16 +63,17 @@ public function lookupInCache(string $reference, string $packageName): ?array private function download(Version $version): ?string { $package = $version->getPackage(); - $package->loadCredentials(); - - $io = new BufferIO('', StreamOutput::VERBOSITY_VERBOSE); - $config = Factory::createConfig(); - $io->loadConfiguration($config); - $repository = new VcsRepository(['url' => $package->getRepository()], $io, $config); + $io = new NullIO(); + $repository = $this->packagistFactory->createRepository( + $package->getRepository(), + $io, + null, + $package->getCredentials() + ); $factory = new Factory(); - $dm = $factory->createDownloadManager($io, $config); - $archiveManager = $factory->createArchiveManager($config, $dm); + $dm = $factory->createDownloadManager($io, $repository->getConfig()); + $archiveManager = $factory->createArchiveManager($repository->getConfig(), $dm); $archiveManager->setOverwriteFiles(false); $versions = $repository->getPackages(); diff --git a/src/Packagist/WebBundle/Service/Locker.php b/src/Packagist/WebBundle/Service/Locker.php index dff8a791..252fcd6a 100644 --- a/src/Packagist/WebBundle/Service/Locker.php +++ b/src/Packagist/WebBundle/Service/Locker.php @@ -2,6 +2,7 @@ namespace Packagist\WebBundle\Service; +use Doctrine\DBAL\Connection; use Symfony\Bridge\Doctrine\RegistryInterface; class Locker diff --git a/src/Packagist/WebBundle/Service/QueueWorker.php b/src/Packagist/WebBundle/Service/QueueWorker.php index d2d22a00..78eae41f 100644 --- a/src/Packagist/WebBundle/Service/QueueWorker.php +++ b/src/Packagist/WebBundle/Service/QueueWorker.php @@ -53,7 +53,7 @@ public function processMessages(int $count) $nextScheduledJobCheck = $this->checkForScheduledJobs($signal); } - $result = $this->redis->brpop('jobs', 10); + $result = $this->redis->brpop('jobs', 2); if (!$result) { $this->logger->debug('No message in queue'); continue; diff --git a/src/Packagist/WebBundle/Service/UpdaterWorker.php b/src/Packagist/WebBundle/Service/UpdaterWorker.php index 754dd649..169ac38d 100644 --- a/src/Packagist/WebBundle/Service/UpdaterWorker.php +++ b/src/Packagist/WebBundle/Service/UpdaterWorker.php @@ -2,6 +2,7 @@ namespace Packagist\WebBundle\Service; +use Packagist\WebBundle\Composer\PackagistFactory; use Packagist\WebBundle\Model\ValidatingArrayLoader; use Psr\Log\LoggerInterface; use Composer\Package\Loader\ArrayLoader; @@ -30,6 +31,7 @@ class UpdaterWorker private $scheduler; private $packageManager; private $downloadManager; + private $packagistFactory; public function __construct( LoggerInterface $logger, @@ -38,7 +40,8 @@ public function __construct( Locker $locker, Scheduler $scheduler, PackageManager $packageManager, - DownloadManager $downloadManager + DownloadManager $downloadManager, + PackagistFactory $packagistFactory ) { $this->logger = $logger; $this->doctrine = $doctrine; @@ -47,6 +50,7 @@ public function __construct( $this->scheduler = $scheduler; $this->packageManager = $packageManager; $this->downloadManager = $downloadManager; + $this->packagistFactory = $packagistFactory; } public function process(Job $job, SignalHandler $signal): array @@ -69,7 +73,7 @@ public function process(Job $job, SignalHandler $signal): array $this->logger->info('Updating '.$package->getName()); - $config = Factory::createConfig(); + $config = $this->packagistFactory->createConfig($package->getCredentials()); $io = new BufferIO('', OutputInterface::VERBOSITY_VERY_VERBOSE, new HtmlOutputFormatter(Factory::createAdditionalStyles())); $io->loadConfiguration($config); @@ -86,8 +90,7 @@ public function process(Job $job, SignalHandler $signal): array $loader = new ValidatingArrayLoader(new ArrayLoader()); // prepare repository - $package->loadCredentials(); - $repository = new VcsRepository(array('url' => $package->getRepository()), $io, $config); + $repository = $this->packagistFactory->createRepository($package->getRepository(), $io, $config); $repository->setLoader($loader); // perform the actual update (fetch and re-scan the repository's source) @@ -139,9 +142,6 @@ public function process(Job $job, SignalHandler $signal): array // detected a 404 so mark the package as gone and prevent updates for 1y if ($found404) { - $package->setCrawledAt(new \DateTime('+1 year')); - $this->doctrine->getEntityManager()->flush($package); - return [ 'status' => Job::STATUS_PACKAGE_GONE, 'message' => 'Update of '.$package->getName().' failed, package appears to be 404/gone and has been marked as crawled for 1year', diff --git a/src/Packagist/WebBundle/Twig/PackagistExtension.php b/src/Packagist/WebBundle/Twig/PackagistExtension.php index 9b0b9481..e5bfc986 100644 --- a/src/Packagist/WebBundle/Twig/PackagistExtension.php +++ b/src/Packagist/WebBundle/Twig/PackagistExtension.php @@ -3,8 +3,11 @@ namespace Packagist\WebBundle\Twig; use Symfony\Component\DependencyInjection\ContainerInterface; +use Twig\Extension\AbstractExtension; +use Twig\TwigFilter; +use Twig\TwigTest; -class PackagistExtension extends \Twig_Extension +class PackagistExtension extends AbstractExtension { /** * @var ContainerInterface @@ -19,18 +22,18 @@ public function __construct(ContainerInterface $container) public function getTests() { return array( - new \Twig_SimpleTest('existing_package', [$this, 'packageExistsTest']), - new \Twig_SimpleTest('existing_provider', [$this, 'providerExistsTest']), - new \Twig_SimpleTest('numeric', [$this, 'numericTest']), + new TwigTest('existing_package', [$this, 'packageExistsTest']), + new TwigTest('existing_provider', [$this, 'providerExistsTest']), + new TwigTest('numeric', [$this, 'numericTest']), ); } public function getFilters() { - return array( - new \Twig_SimpleFilter('prettify_source_reference', [$this, 'prettifySourceReference']), - new \Twig_SimpleFilter('gravatar_hash', [$this, 'generateGravatarHash']) - ); + return [ + new TwigFilter('prettify_source_reference', [$this, 'prettifySourceReference']), + new TwigFilter('gravatar_hash', [$this, 'generateGravatarHash']) + ]; } public function getName() diff --git a/src/Packagist/WebBundle/Util/ChangelogUtils.php b/src/Packagist/WebBundle/Util/ChangelogUtils.php new file mode 100644 index 00000000..31a6f5da --- /dev/null +++ b/src/Packagist/WebBundle/Util/ChangelogUtils.php @@ -0,0 +1,66 @@ +factory = $factory; + } + + /** + * @param Package|object $package + * @param string $fromVersion + * @param string $toVersion + * + * @return array + */ + public function getChangelog(Package $package, string $fromVersion, string $toVersion): array + { + $config = $this->factory->createConfig($package->getCredentials()); + + // see GitDriver + $repoDir = $config->get('cache-vcs-dir') . '/' . preg_replace('{[^a-z0-9.]}i', '-', $package->getRepository()) . '/'; + if (!is_dir($repoDir)) { + return []; + } + + $diff = escapeshellarg("$fromVersion..$toVersion"); + $cmd = "cd $repoDir; git log $diff --pretty=format:'- %B' --decorate=full --no-merges --date=short"; + if (!$output = shell_exec($cmd)) { + return []; + } + + $commitMessages = []; + $changeLogs = explode("\n\n", $output); + foreach ($changeLogs as $changeLog) { + if ($changeLog) { + $commitMessages[] = $changeLog; + } + } + + $commitMessages = array_values(array_unique($commitMessages)); + return array_map([$this, 'trim'], $commitMessages); + } + + private function trim(string $value): string + { + return trim($value, " -\t"); + } +} diff --git a/src/Packagist/WebBundle/Util/SshKeyHelper.php b/src/Packagist/WebBundle/Util/SshKeyHelper.php new file mode 100644 index 00000000..c4995ab5 --- /dev/null +++ b/src/Packagist/WebBundle/Util/SshKeyHelper.php @@ -0,0 +1,38 @@ +vcsDriver && null !== $value->getName()) { + return; + } + if (null === $value->getRepository()) { + return; + } + + $property = 'repository'; + $driver = $value->vcsDriver; + if (!is_object($driver)) { + if (preg_match('{https?://.+@}', $value->getRepository())) { + $this->context->buildViolation('URLs with user@host are not supported, use a read-only public URL') + ->atPath($property) + ->addViolation() + ; + } elseif (is_string($value->vcsDriverError)) { + $this->context->buildViolation('Uncaught Exception: '.htmlentities($value->vcsDriverError, ENT_COMPAT, 'utf-8')) + ->atPath($property) + ->addViolation() + ; + } else { + $this->context->buildViolation('No valid/supported repository was found at the given URL') + ->atPath($property) + ->addViolation() + ; + } + return; + } + + try { + $information = $driver->getComposerInformation($driver->getRootIdentifier()); + if (false === $information) { + $this->context->buildViolation('No composer.json was found in the '.$driver->getRootIdentifier().' branch.') + ->atPath($property) + ->addViolation() + ; + return; + } + + if (empty($information['name'])) { + $this->context->buildViolation('The package name was not found in the composer.json, make sure there is a name present.') + ->atPath($property) + ->addViolation() + ; + return; + } + + if (!preg_match('{^[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9]([_.-]?[a-z0-9]+)*$}i', $information['name'])) { + $this->context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is invalid, it should have a vendor name, a forward slash, and a package name. The vendor and package name can be words separated by -, . or _. The complete name should match "[a-z0-9]([_.-]?[a-z0-9]+)*/[a-z0-9]([_.-]?[a-z0-9]+)*".') + ->atPath($property) + ->addViolation() + ; + return; + } + + if (preg_match('{(free.*watch|watch.*free|movie.*free|free.*movie|watch.*movie|watch.*full|generate.*resource|generate.*unlimited|hack.*coin|coin.*hack|v[.-]?bucks|(fortnite|pubg).*free|hack.*cheat|cheat.*hack|putlocker)}i', $information['name'])) { + $this->context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is blocked, if you think this is a mistake please get in touch with us.') + ->atPath($property) + ->addViolation() + ; + return; + } + + $reservedNames = ['nul', 'con', 'prn', 'aux', 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9', 'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9']; + $bits = explode('/', strtolower($information['name'])); + if (in_array($bits[0], $reservedNames, true) || in_array($bits[1], $reservedNames, true)) { + $this->context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is reserved, package and vendor names can not match any of: '.implode(', ', $reservedNames).'.') + ->atPath($property) + ->addViolation() + ; + return; + } + + if (preg_match('{\.json$}', $information['name'])) { + $this->context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is invalid, package names can not end in .json, consider renaming it or perhaps using a -json suffix instead.') + ->atPath($property) + ->addViolation() + ; + return; + } + + if (preg_match('{[A-Z]}', $information['name'])) { + $suggestName = preg_replace('{(?:([a-z])([A-Z])|([A-Z])([A-Z][a-z]))}', '\\1\\3-\\2\\4', $information['name']); + $suggestName = strtolower($suggestName); + + $this->context->buildViolation('The package name '.htmlentities($information['name'], ENT_COMPAT, 'utf-8').' is invalid, it should not contain uppercase characters. We suggest using '.$suggestName.' instead.') + ->atPath($property) + ->addViolation() + ; + return; + } + } catch (\Exception $e) { + $this->context->buildViolation('We had problems parsing your composer.json file, the parser reports: '.htmlentities($e->getMessage(), ENT_COMPAT, 'utf-8')) + ->atPath($property) + ->addViolation() + ; + } + + if (null === $value->getName()) { + $this->context->buildViolation('An unexpected error has made our parser fail to find a package name in your repository, if you think this is incorrect please try again') + ->atPath($property) + ->addViolation() + ; + } + } +} diff --git a/src/Packagist/WebBundle/Validator/Constraint/PackageUnique.php b/src/Packagist/WebBundle/Validator/Constraint/PackageUnique.php new file mode 100644 index 00000000..7a66a3c9 --- /dev/null +++ b/src/Packagist/WebBundle/Validator/Constraint/PackageUnique.php @@ -0,0 +1,20 @@ +{{ name }} already exists.'; + + /** + * {@inheritdoc} + */ + public function getTargets() + { + return self::CLASS_CONSTRAINT; + } +} diff --git a/src/Packagist/WebBundle/Validator/Constraint/PackageUniqueValidator.php b/src/Packagist/WebBundle/Validator/Constraint/PackageUniqueValidator.php new file mode 100644 index 00000000..9e449e4c --- /dev/null +++ b/src/Packagist/WebBundle/Validator/Constraint/PackageUniqueValidator.php @@ -0,0 +1,59 @@ +doctrine = $doctrine; + $this->router = $router; + } + + /** + * {@inheritdoc} + * @param PackageUnique $constraint + */ + public function validate($value, Constraint $constraint) + { + if (null === $value || '' === $value) { + return; + } + + if (!$value instanceof Package) { + throw new UnexpectedTypeException($constraint, Package::class); + } + + + + $repo = $this->doctrine->getRepository(Package::class); + + if ($name = $value->getName()) { + try { + if ($repo->findOneByName($name)) { + $this->context + ->buildViolation($constraint->packageExists, [ + '{{ name }}' => $name, + '{{ route_name }}' => $this->router->generate('view_package', ['name' => $name]) + ]) + ->atPath('repository') + ->addViolation() + ; + } + } catch (\Doctrine\ORM\NoResultException $e) {} + } + } +}