├── .editorconfig ├── .env ├── .env.test ├── .github ├── dependabot.yml └── workflows │ ├── conductor.yaml │ ├── continuous-integration.yml │ ├── lint.yml │ └── phpstan.yml ├── .gitignore ├── .php-cs-fixer.php ├── LICENSE ├── README.md ├── bin └── console ├── composer.json ├── composer.lock ├── config ├── algolia_settings.yml ├── bundles.php ├── packages │ ├── beelab_recaptcha2.yaml │ ├── cache.yaml │ ├── debug.yaml │ ├── doctrine.yaml │ ├── framework.yaml │ ├── knpu_oauth2_client.yaml │ ├── lock.yaml │ ├── mailer.yaml │ ├── monolog.yaml │ ├── nelmio_cors.yaml │ ├── nelmio_security.yaml │ ├── routing.yaml │ ├── scheb_2fa.yaml │ ├── security.yaml │ ├── snc_redis.yaml │ ├── translation.yaml │ ├── twig.yaml │ ├── twig_extensions.yaml │ ├── validator.yaml │ └── web_profiler.yaml ├── parameters.yaml ├── preload.php ├── routes.yaml ├── routes │ ├── framework.yaml │ ├── scheb_2fa.yaml │ ├── security.yaml │ └── web_profiler.yaml ├── services.yaml └── services_test.yaml ├── css ├── app.scss └── charts.css ├── docker-compose.node.yaml ├── docker-compose.yaml ├── esbuild.js ├── js ├── app.js ├── charts.js ├── jquery.js ├── notifier.jsx ├── search.js ├── submitPackage.js └── view.js ├── package-lock.json ├── package.json ├── phpstan-baseline.neon ├── phpstan-bootstrap.php ├── phpstan.neon ├── phpunit.xml.dist ├── rector.php ├── src ├── ArgumentResolver │ └── UserResolver.php ├── Attribute │ └── VarName.php ├── Audit │ └── AuditRecordType.php ├── Command │ ├── CleanIndexCommand.php │ ├── CleanSpamPackagesCommand.php │ ├── ClearVersionsCommand.php │ ├── CompileStatsCommand.php │ ├── ConfigureAlgoliaCommand.php │ ├── DumpPackagesCommand.php │ ├── DumpPackagesV2Command.php │ ├── GenerateTokensCommand.php │ ├── IndexPackagesCommand.php │ ├── MigrateDataTypesCommand.php │ ├── MigrateDownloadCountsCommand.php │ ├── MigratePhpStatsCommand.php │ ├── PopulateDependentsSuggestersCommand.php │ ├── PopulateVersionIdCacheCommand.php │ ├── RegenerateMainPhpStatsCommand.php │ ├── RunWorkersCommand.php │ ├── SymlinkMetadataMirrorCommand.php │ ├── UnfreezePackageCommand.php │ ├── UpdatePackagesCommand.php │ ├── UpdateSecurityAdvisoriesCommand.php │ └── UploadMetadataToCdnCommand.php ├── Controller │ ├── .gitignore │ ├── ApiController.php │ ├── ChangePasswordController.php │ ├── Controller.php │ ├── ExploreController.php │ ├── ExtensionController.php │ ├── FeedController.php │ ├── GitHubLoginController.php │ ├── HealthCheckController.php │ ├── InternalController.php │ ├── PackageController.php │ ├── ProfileController.php │ ├── RegistrationController.php │ ├── ResetPasswordController.php │ ├── SecurityController.php │ ├── UserController.php │ └── WebController.php ├── DataFixtures │ ├── DownloadFixtures.php │ ├── PackageFixtures.php │ └── UserFixtures.php ├── Entity │ ├── .gitignore │ ├── AuditRecord.php │ ├── AuditRecordRepository.php │ ├── ConflictLink.php │ ├── Dependent.php │ ├── DependentRepository.php │ ├── DevRequireLink.php │ ├── Download.php │ ├── DownloadRepository.php │ ├── EmptyReferenceCache.php │ ├── Job.php │ ├── JobRepository.php │ ├── Package.php │ ├── PackageLink.php │ ├── PackageRepository.php │ ├── PhpStat.php │ ├── PhpStatRepository.php │ ├── ProvideLink.php │ ├── ReplaceLink.php │ ├── RequireLink.php │ ├── SecurityAdvisory.php │ ├── SecurityAdvisoryRepository.php │ ├── SecurityAdvisorySource.php │ ├── SuggestLink.php │ ├── Suggester.php │ ├── SuggesterRepository.php │ ├── Tag.php │ ├── TemporaryTwoFactorUser.php │ ├── User.php │ ├── UserRepository.php │ ├── Vendor.php │ ├── VendorRepository.php │ ├── Version.php │ └── VersionRepository.php ├── EventListener │ ├── CacheListener.php │ ├── LogoutListener.php │ ├── OriginListener.php │ ├── PackageListener.php │ ├── RequestStatsListener.php │ ├── ResolvedTwoFactorCodeCredentialsListener.php │ ├── SecurityAdvisoryUpdateListener.php │ └── VersionListener.php ├── Form │ ├── ChangePasswordFormType.php │ ├── EventSubscriber │ │ ├── FormBruteForceSubscriber.php │ │ └── FormInvalidPasswordSubscriber.php │ ├── Extension │ │ └── RecaptchaExtension.php │ ├── Model │ │ ├── EnableTwoFactorRequest.php │ │ └── MaintainerRequest.php │ ├── RegistrationFormType.php │ ├── ResetPasswordFormType.php │ ├── ResetPasswordRequestFormType.php │ └── Type │ │ ├── AbandonedType.php │ │ ├── AddMaintainerRequestType.php │ │ ├── EnableTwoFactorAuthType.php │ │ ├── InvisibleRecaptchaType.php │ │ ├── PackageType.php │ │ ├── ProfileFormType.php │ │ └── RemoveMaintainerRequestType.php ├── HealthCheck │ ├── MetadataDirCheck.php │ └── RedisHealthCheck.php ├── HtmlSanitizer │ ├── ReadmeImageSanitizer.php │ └── ReadmeLinkSanitizer.php ├── Kernel.php ├── Logger │ └── LogIdProcessor.php ├── Menu │ └── MenuBuilder.php ├── Model │ ├── DownloadManager.php │ ├── FavoriteManager.php │ ├── PackageManager.php │ ├── ProviderManager.php │ ├── RedisAdapter.php │ └── VersionIdCache.php ├── Package │ ├── SymlinkDumper.php │ ├── Updater.php │ └── V2Dumper.php ├── Redis │ ├── DownloadsIncr.php │ ├── FailedLoginCounter.php │ ├── FetchVersionIds.php │ └── PackagesExist.php ├── Search │ ├── Algolia.php │ ├── Query.php │ └── ResultTransformer.php ├── Security │ ├── AccountEmailExistsWithoutGitHubException.php │ ├── AccountUsernameExistsWithoutGitHubException.php │ ├── BruteForceLoginFormAuthenticator.php │ ├── EmailVerifier.php │ ├── GitHubAuthenticator.php │ ├── NoGitHubNicknameFoundException.php │ ├── NoVerifiedGitHubEmailFoundException.php │ ├── Passport │ │ └── Badge │ │ │ └── ResolvedTwoFactorCodeCredentials.php │ ├── Provider │ │ └── UserProvider.php │ ├── RecaptchaContext.php │ ├── RecaptchaHelper.php │ ├── TwoFactorAuthManager.php │ ├── TwoFactorAuthRateLimiter.php │ ├── UserChecker.php │ ├── UserNotifier.php │ └── Voter │ │ ├── PackageActions.php │ │ └── PackageVoter.php ├── SecurityAdvisory │ ├── AdvisoryIdGenerator.php │ ├── AdvisoryParser.php │ ├── FriendsOfPhpSecurityAdvisoriesSource.php │ ├── GitHubSecurityAdvisoriesSource.php │ ├── RemoteSecurityAdvisory.php │ ├── RemoteSecurityAdvisoryCollection.php │ ├── SecurityAdvisoryResolver.php │ ├── SecurityAdvisorySourceInterface.php │ └── Severity.php ├── Service │ ├── CdnClient.php │ ├── FallbackGitHubAuthProvider.php │ ├── GitHubUserMigrationWorker.php │ ├── Locker.php │ ├── QueueWorker.php │ ├── ReplicaClient.php │ ├── Scheduler.php │ ├── SecurityAdvisoryWorker.php │ ├── UpdaterWorker.php │ └── VersionCache.php ├── Twig │ └── PackagistExtension.php ├── Util │ ├── DoctrineTrait.php │ ├── HttpDownloaderOptionsFactory.php │ ├── Killswitch.php │ ├── LoggingHttpDownloader.php │ └── UserAgentParser.php └── Validator │ ├── Copyright.php │ ├── CopyrightValidator.php │ ├── NotProhibitedPassword.php │ ├── NotProhibitedPasswordValidator.php │ ├── NotReservedWord.php │ ├── NotReservedWordValidator.php │ ├── Password.php │ ├── PopularPackageSafety.php │ ├── PopularPackageSafetyValidator.php │ ├── RateLimitingRecaptcha.php │ ├── RateLimitingRecaptchaValidator.php │ ├── TwoFactorCode.php │ ├── TwoFactorCodeValidator.php │ ├── TypoSquatters.php │ ├── TypoSquattersValidator.php │ ├── UniquePackage.php │ ├── UniquePackageValidator.php │ ├── ValidPackageRepository.php │ ├── ValidPackageRepositoryValidator.php │ ├── VendorWritable.php │ └── VendorWritableValidator.php ├── symfony.lock ├── templates ├── about │ └── about.html.twig ├── api_doc │ └── index.html.twig ├── base_nolayout.html.twig ├── bundles │ ├── SchebTwoFactorBundle │ │ └── Authentication │ │ │ └── form.html.twig │ └── TwigBundle │ │ └── Exception │ │ └── error404.html.twig ├── email │ ├── alert_change.txt.twig │ ├── maintainer_added.txt.twig │ ├── two_factor_disabled.txt.twig │ ├── two_factor_enabled.txt.twig │ └── update_failed.txt.twig ├── explore │ ├── explore.html.twig │ └── popular.html.twig ├── extensions │ └── list.html.twig ├── feed │ └── feeds.html.twig ├── forms.html.twig ├── layout.html.twig ├── macros.html.twig ├── mirrors │ └── index.html.twig ├── package │ ├── _security_advisory_list.html.twig │ ├── abandon.html.twig │ ├── dependents.html.twig │ ├── edit.html.twig │ ├── php_stats.html.twig │ ├── providers.html.twig │ ├── security_advisories.html.twig │ ├── security_advisory.html.twig │ ├── spam.html.twig │ ├── stats.html.twig │ ├── stats_base.html.twig │ ├── submit_package.html.twig │ ├── suggesters.html.twig │ ├── version_details.html.twig │ ├── version_list.html.twig │ ├── view_package.html.twig │ └── view_vendor.html.twig ├── registration │ ├── confirmation_email.html.twig │ ├── confirmation_email.txt.twig │ └── register.html.twig ├── reset_password │ ├── check_email.html.twig │ ├── email.txt.twig │ ├── request.html.twig │ └── reset.html.twig ├── user │ ├── change_password.html.twig │ ├── configure_two_factor_auth.html.twig │ ├── confirm_two_factor_auth.html.twig │ ├── disable_two_factor_auth.html.twig │ ├── edit.html.twig │ ├── enable_two_factor_auth.html.twig │ ├── favorites.html.twig │ ├── layout.html.twig │ ├── login.html.twig │ ├── my_profile.html.twig │ ├── packages.html.twig │ └── public_profile.html.twig └── web │ ├── index.html.twig │ ├── list.html.twig │ ├── php_stats.html.twig │ ├── search.html.twig │ ├── search_section.html.twig │ ├── stats.html.twig │ └── stats_base.html.twig ├── tests ├── Audit │ ├── PackageAuditRecordTest.php │ └── VersionAuditRecordTest.php ├── Controller │ ├── AboutControllerTest.php │ ├── ApiControllerTest.php │ ├── ChangePasswordControllerTest.php │ ├── ControllerTestCase.php │ ├── FeedControllerTest.php │ ├── PackageControllerTest.php │ ├── ProfileControllerTest.php │ ├── RegistrationControllerTest.php │ ├── ResetPasswordControllerTest.php │ ├── UserControllerTest.php │ ├── WebControllerTest.php │ └── responses │ │ ├── search-with-query-tag.json │ │ ├── search-with-query-tags.json │ │ └── search-with-query.json ├── Entity │ ├── PackageTest.php │ ├── SecurityAdvisoryTest.php │ ├── TagTest.php │ └── VersionTest.php ├── Mock │ └── TotpAuthenticatorStub.php ├── Model │ └── PackageManagerTest.php ├── Package │ ├── SymlinkDumperTest.php │ └── UpdaterTest.php ├── Search │ ├── AlgoliaMock.php │ ├── QueryTest.php │ ├── ResultTransformerTest.php │ ├── results │ │ ├── search-paged.php │ │ ├── search-with-abandoned.php │ │ ├── search-with-query-tag.php │ │ ├── search-with-query-tags.php │ │ ├── search-with-query.php │ │ └── search-with-virtual.php │ └── transformed │ │ ├── search-paged.php │ │ ├── search-with-abandoned.php │ │ ├── search-with-query-tag.php │ │ ├── search-with-query-tags.php │ │ ├── search-with-query.php │ │ └── search-with-virtual.php ├── Security │ ├── BruteForceLoginFormAuthenticatorTest.php │ └── RecaptchaHelperTest.php ├── SecurityAdvisory │ ├── AdvisoryParserTest.php │ ├── GitHubSecurityAdvisoriesSourceTest.php │ ├── RemoteSecurityAdvisoryTest.php │ ├── SecurityAdvisoryResolverTest.php │ └── SeverityTest.php ├── SecurityAdvisoryWorkerTest.php ├── Validator │ └── RateLimitingRecaptchaValidatorTest.php ├── bootstrap.php ├── console-application.php └── object-manager.php ├── translations ├── .gitignore └── messages.en.yml └── web ├── .htaccess ├── app.php ├── apple-touch-icon-precomposed.png ├── apple-touch-icon.png ├── css ├── github │ └── markdown.css └── humane │ └── jackedup.css ├── favicon.ico ├── font ├── config.json ├── fontello.eot ├── fontello.svg ├── fontello.ttf ├── fontello.woff ├── open-sans-v40-latin-300.woff2 ├── open-sans-v40-latin-300italic.woff2 ├── open-sans-v40-latin-500.woff2 ├── open-sans-v40-latin-600.woff2 └── open-sans-v40-latin-regular.woff2 ├── img ├── algolia-logo-light.svg ├── bunny-net.svg ├── datadog-light.png ├── loader-white.gif ├── loader.gif ├── logo-small.png ├── logo.png └── private-packagist.svg ├── robots.txt ├── search.osd ├── static-error ├── 404.html ├── 404.json ├── 502.html └── 503.html └── touch-icon-192x192.png /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [*.md] 15 | trim_trailing_whitespace = true 16 | indent_size = 4 17 | 18 | [{*.css,*.less,*.js}] 19 | indent_size = 4 20 | 21 | [{*.json,*.yml,*.yaml}] 22 | indent_size = 4 23 | 24 | [composer.json] 25 | indent_size = 4 26 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # define your env variables for the test env here 2 | KERNEL_CLASS='App\Kernel' 3 | APP_SECRET='$ecretf0rt3st' 4 | SYMFONY_DEPRECATIONS_HELPER=999999 5 | PANTHER_APP_ENV=panther 6 | DATABASE_URL="mysql://root@127.0.0.1:3306/packagist?serverVersion=8.0.28" 7 | MAILER_DSN=null://null 8 | REDIS_URL=redis://localhost/14 9 | APP_MAILER_FROM_EMAIL=packagist@example.org 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | labels: [] 8 | -------------------------------------------------------------------------------- /.github/workflows/conductor.yaml: -------------------------------------------------------------------------------- 1 | # See the Conductor setup guide at https://packagist.com/docs/conductor/getting-started 2 | 3 | on: 4 | repository_dispatch: 5 | types: 6 | - dependency_update 7 | 8 | name: Private Packagist Conductor 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | conductor: 15 | name: Private Packagist Conductor 16 | runs-on: ubuntu-24.04-arm 17 | 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | 21 | - uses: shivammathur/setup-php@cf4cade2721270509d5b1c766ab3549210a39a2a # 2.33.0 22 | with: 23 | coverage: "none" 24 | extensions: "intl, zip, apcu" 25 | php-version: "8.4" 26 | 27 | # See the Conductor GitHub Action at https://github.com/packagist/conductor-github-action 28 | - name: "Running Conductor" 29 | uses: packagist/conductor-github-action@7a58e5118119cdff527b1022db8642e3218b5422 # 1.3.0 30 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: "Continuous Integration" 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | env: 8 | APP_ENV: test 9 | DATABASE_URL: "mysql://root:root@127.0.0.1:3306/packagist?serverVersion=8.0" 10 | 11 | jobs: 12 | tests: 13 | name: "CI" 14 | 15 | runs-on: ubuntu-24.04-arm 16 | 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | 20 | - uses: shivammathur/setup-php@cf4cade2721270509d5b1c766ab3549210a39a2a # 2.33.0 21 | with: 22 | coverage: "none" 23 | extensions: "intl, zip, apcu" 24 | php-version: "8.4" 25 | tools: composer 26 | ini-values: apc.enable_cli=1 27 | 28 | - name: "Start MySQL" 29 | run: sudo systemctl start mysql.service 30 | 31 | - uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # 3.1.1 32 | with: 33 | composer-options: "--ansi --no-interaction --no-progress --prefer-dist" 34 | 35 | - name: Start Redis 36 | uses: supercharge/redis-github-action@ea9b21c6ecece47bd99595c532e481390ea0f044 # 1.8.0 37 | with: 38 | redis-version: 6 39 | 40 | - name: "Setup DB" 41 | run: | 42 | bin/console doctrine:database:create -n 43 | bin/console doctrine:schema:create -n 44 | 45 | - name: "Run tests" 46 | run: "composer test" 47 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: "PHP Lint" 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | tests: 9 | name: "Lint" 10 | 11 | runs-on: ubuntu-24.04-arm 12 | 13 | steps: 14 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 15 | 16 | - uses: shivammathur/setup-php@cf4cade2721270509d5b1c766ab3549210a39a2a # 2.33.0 17 | with: 18 | coverage: "none" 19 | extensions: "intl" 20 | php-version: "8.4" 21 | 22 | - name: "Lint PHP files" 23 | run: "find src/ -type f -name '*.php' -print0 | xargs -0 -L1 -P4 -- php -l -f" 24 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: "PHPStan" 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | env: 8 | APP_ENV: dev 9 | DATABASE_URL: "mysql://root:root@127.0.0.1:3306/packagist?serverVersion=8.0" 10 | 11 | jobs: 12 | tests: 13 | name: "PHPStan" 14 | 15 | runs-on: ubuntu-24.04-arm 16 | 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | 20 | - uses: shivammathur/setup-php@cf4cade2721270509d5b1c766ab3549210a39a2a # 2.33.0 21 | with: 22 | coverage: "none" 23 | extensions: "intl, zip, apcu" 24 | php-version: "8.4" 25 | tools: composer 26 | ini-values: apc.enable_cli=1 27 | 28 | - name: "Start MySQL" 29 | run: sudo systemctl start mysql.service 30 | 31 | - uses: ramsey/composer-install@3cf229dc2919194e9e36783941438d17239e8520 # 3.1.1 32 | with: 33 | composer-options: "--ansi --no-interaction --no-progress --prefer-dist" 34 | 35 | - name: "Setup DB" 36 | run: | 37 | bin/console doctrine:database:create -n 38 | bin/console doctrine:schema:create -n 39 | 40 | - name: Run PHPStan 41 | run: composer phpstan 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | web/packages*.json 3 | web/providers-*.json 4 | web/p/ 5 | web/p2/ 6 | web/build/ 7 | /config/parameters.yml 8 | /.settings 9 | /.buildpath 10 | /.project 11 | /.idea 12 | /composer.phar 13 | /nbproject 14 | /.php-cs-fixer.cache 15 | /.phpstan-dba.cache 16 | 17 | ###> symfony/framework-bundle ### 18 | /.env.local 19 | /.env.local.php 20 | /.env.*.local 21 | /config/secrets/prod/prod.decrypt.private.php 22 | /web/bundles/ 23 | /var/ 24 | /vendor/ 25 | ###< symfony/framework-bundle ### 26 | 27 | ###> phpunit/phpunit ### 28 | /phpunit.xml 29 | .phpunit.result.cache 30 | ###< phpunit/phpunit ### 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Jordi Boggiano, Nils Adermann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ['all' => true], 5 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 6 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 7 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 8 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true], 9 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 10 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 11 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 12 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 13 | BabDev\PagerfantaBundle\BabDevPagerfantaBundle::class => ['all' => true], 14 | Beelab\Recaptcha2Bundle\BeelabRecaptcha2Bundle::class => ['all' => true], 15 | Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true], 16 | Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true], 17 | Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true], 18 | Snc\RedisBundle\SncRedisBundle::class => ['all' => true], 19 | Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], 20 | SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true], 21 | KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], 22 | Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true], 23 | ]; 24 | -------------------------------------------------------------------------------- /config/packages/beelab_recaptcha2.yaml: -------------------------------------------------------------------------------- 1 | beelab_recaptcha2: 2 | enabled: '%env(bool:APP_RECAPTCHA_ENABLED)%' 3 | site_key: '%env(APP_RECAPTCHA_SITE_KEY)%' 4 | secret: '%env(APP_RECAPTCHA_SECRET)%' 5 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | app: cache.adapter.redis 4 | system: cache.adapter.apcu 5 | default_redis_provider: snc_redis.cache 6 | pools: 7 | doctrine.cache: null 8 | prefix_seed: packagist 9 | -------------------------------------------------------------------------------- /config/packages/debug.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | debug: 3 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. 4 | # See the "server:dump" command to start a new server. 5 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" 6 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | dbal: 3 | url: '%env(resolve:DATABASE_URL)%' 4 | profiling_collect_backtrace: '%kernel.debug%' 5 | use_savepoints: true 6 | options: 7 | # PDO::ATTR_TIMEOUT 8 | 2: 1.3 9 | orm: 10 | auto_generate_proxy_classes: true 11 | enable_lazy_ghost_objects: true 12 | report_fields_where_declared: true 13 | auto_mapping: true 14 | mappings: 15 | App: 16 | is_bundle: false 17 | dir: '%kernel.project_dir%/src/Entity' 18 | prefix: 'App\Entity' 19 | alias: App 20 | controller_resolver: 21 | enabled: false 22 | auto_mapping: false 23 | 24 | when@test: 25 | doctrine: 26 | dbal: 27 | # "TEST_TOKEN" is typically set by ParaTest 28 | dbname_suffix: '_test%env(default::TEST_TOKEN)%' 29 | 30 | when@prod: 31 | doctrine: 32 | orm: 33 | auto_generate_proxy_classes: false 34 | proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' 35 | query_cache_driver: 36 | type: pool 37 | pool: doctrine.system_cache_pool 38 | result_cache_driver: 39 | type: pool 40 | pool: doctrine.result_cache_pool 41 | 42 | framework: 43 | cache: 44 | pools: 45 | doctrine.result_cache_pool: 46 | adapter: cache.app 47 | doctrine.system_cache_pool: 48 | adapter: cache.system 49 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | parameters: 3 | default_trusted_hosts: '^.*$' 4 | 5 | framework: 6 | secret: '%env(APP_SECRET)%' 7 | csrf_protection: true 8 | 9 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 10 | # Remove or comment this section to explicitly disable session support. 11 | session: 12 | name: packagist 13 | cookie_lifetime: 3600 14 | cookie_httponly: true 15 | cookie_secure: auto 16 | cookie_samesite: lax 17 | handler_id: Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler 18 | 19 | #esi: true 20 | fragments: true 21 | form: true 22 | assets: 23 | version: 'v=%env(default::ASSETS_VERSION)%' 24 | 25 | http_client: 26 | default_options: 27 | headers: 28 | 'User-Agent': 'packagist.org' 29 | max_redirects: 5 30 | retry_failed: 31 | max_retries: 3 32 | max_duration: 30 # default total duration timeout in seconds, override it per use case with more appropriate values 33 | timeout: 3 34 | 35 | trusted_hosts: ['%env(string:default:default_trusted_hosts:TRUSTED_HOSTS)%'] 36 | # remote_addr is set to the correct client IP but we need to mark it trusted so that Symfony picks up the X-Forwarded-Host, 37 | # X-Forwarded-Port and X-Forwarded-Proto headers correctly and sees the right request URL 38 | trusted_proxies: '127.0.0.1,REMOTE_ADDR' 39 | # Use all X-Forwarded-* headers from ELB except X-Forwarded-For as nginx handles the IP resolution 40 | trusted_headers: ['x-forwarded-proto', 'x-forwarded-port'] 41 | 42 | when@test: 43 | framework: 44 | test: true 45 | session: 46 | storage_factory_id: session.storage.factory.mock_file 47 | -------------------------------------------------------------------------------- /config/packages/knpu_oauth2_client.yaml: -------------------------------------------------------------------------------- 1 | knpu_oauth2_client: 2 | clients: 3 | # the key "github" can be anything, it will create a service: "knpu.oauth2.client.github" 4 | github: 5 | type: github 6 | client_id: '%env(APP_GITHUB_CLIENT_ID)%' 7 | client_secret: '%env(APP_GITHUB_CLIENT_SECRET)%' 8 | # the route that you're redirected to after 9 | # see the controller example below 10 | redirect_route: login_github_check 11 | redirect_params: {} 12 | -------------------------------------------------------------------------------- /config/packages/lock.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | lock: ~ 3 | -------------------------------------------------------------------------------- /config/packages/mailer.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | mailer: 3 | dsn: '%env(MAILER_DSN)%' 4 | 5 | when@dev: 6 | framework: 7 | mailer: 8 | envelope: 9 | recipients: ['%env(APP_DEV_EMAIL_RECIPIENT)%'] 10 | -------------------------------------------------------------------------------- /config/packages/nelmio_cors.yaml: -------------------------------------------------------------------------------- 1 | nelmio_cors: 2 | defaults: 3 | allow_origin: ['*'] 4 | allow_headers: ['*'] 5 | max_age: 3600 6 | paths: 7 | '^/packages/list\.json$': 8 | allow_methods: ['GET'] 9 | forced_allow_origin_value: '*' 10 | '^/search\.json$': 11 | allow_methods: ['GET'] 12 | '^/packages/[^/]+/[^/]+\.json$': 13 | allow_methods: ['GET'] 14 | forced_allow_origin_value: '*' 15 | -------------------------------------------------------------------------------- /config/packages/nelmio_security.yaml: -------------------------------------------------------------------------------- 1 | nelmio_security: 2 | clickjacking: 3 | paths: 4 | '^/.*': DENY 5 | forced_ssl: 6 | enabled: '%force_ssl%' 7 | hosts: '%forced_ssl_hosts%' 8 | hsts_max_age: 31104000 # 1y 9 | csp: 10 | enabled: true 11 | report_logger_service: logger 12 | hosts: [] 13 | content_types: [] 14 | enforce: 15 | browser_adaptive: 16 | enabled: false 17 | default-src: 18 | - 'self' 19 | script-src: 20 | - 'unsafe-eval' # TODO get rid of this, but it requires getting rid of hogan (part of instantsearch, maybe upgrade to v4 will fix this) 21 | - 'https://www.gstatic.com/recaptcha/' # TODO could be replaced by simply 'https:' for simplicity's sake once strict-dynamic support is more broadly available 75% in early 2022 per https://caniuse.com/?search=csp%20strict-dynamic 22 | - 'strict-dynamic' 23 | connect-src: 24 | - 'self' 25 | - 'https://*.algolia.net' 26 | - 'https://*.algolianet.com' 27 | img-src: 28 | - 'self' 29 | - 'https:' 30 | - 'data:' 31 | object-src: 32 | - 'none' 33 | style-src: 34 | - 'self' 35 | - 'unsafe-inline' 36 | font-src: 37 | - 'self' 38 | frame-src: 39 | - 'https://www.google.com/recaptcha/' 40 | base-uri: 41 | - 'none' 42 | block-all-mixed-content: true # defaults to false, blocks HTTP content over HTTPS transport 43 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 4 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 5 | #default_uri: http://localhost 6 | 7 | when@prod: 8 | framework: 9 | router: 10 | strict_requirements: null 11 | -------------------------------------------------------------------------------- /config/packages/scheb_2fa.yaml: -------------------------------------------------------------------------------- 1 | # See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/6.x/configuration.html 2 | scheb_two_factor: 3 | security_tokens: 4 | - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken 5 | - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken 6 | 7 | backup_codes: 8 | enabled: true 9 | manager: App\Security\TwoFactorAuthManager 10 | 11 | totp: 12 | enabled: true 13 | server_name: '%env(APP_HOSTNAME)%' 14 | issuer: Packagist 15 | leeway: 10 16 | 17 | trusted_device: 18 | enabled: true 19 | lifetime: 2592000 # 30 days 20 | 21 | # prevent reusing 2FA codes 22 | code_reuse_cache: 'cache.app' 23 | -------------------------------------------------------------------------------- /config/packages/snc_redis.yaml: -------------------------------------------------------------------------------- 1 | snc_redis: 2 | clients: 3 | # Define your clients here. The example below connects to database 0 of the default Redis server. 4 | # 5 | # See https://github.com/snc/SncRedisBundle/blob/master/docs/README.md for instructions on 6 | # how to configure the bundle. 7 | # 8 | # default: 9 | # type: phpredis 10 | # alias: default 11 | # dsn: "%env(REDIS_URL)%" 12 | default: 13 | type: predis 14 | alias: default 15 | dsn: '%env(REDIS_URL)%' 16 | options: 17 | connection_timeout: 1 18 | commands: 19 | fetchVersionIds: 'App\Redis\FetchVersionIds' 20 | downloadsIncr: 'App\Redis\DownloadsIncr' 21 | packagesExist: 'App\Redis\PackagesExist' 22 | 23 | cache: 24 | type: predis 25 | alias: cache 26 | dsn: '%env(REDIS_CACHE_URL)%' 27 | options: 28 | connection_timeout: 1 29 | commands: 30 | incrFailedLoginCounter: 'App\Redis\FailedLoginCounter' 31 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: en 3 | translator: 4 | default_path: '%kernel.project_dir%/translations' 5 | fallbacks: 6 | - en 7 | providers: 8 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' 5 | exception_controller: null 6 | form_themes: 7 | - 'forms.html.twig' 8 | globals: 9 | packagist_host: '%env(APP_HOSTNAME)%' 10 | recaptcha_site_key: '%env(APP_RECAPTCHA_SITE_KEY)%' 11 | algolia: 12 | app_id: '%env(ALGOLIA_APP_ID)%' 13 | search_key: '%env(ALGOLIA_SEARCH_KEY)%' 14 | index_name: '%env(ALGOLIA_INDEX_NAME)%' 15 | file_name_pattern: '*.twig' 16 | 17 | when@test: 18 | twig: 19 | strict_variables: true 20 | -------------------------------------------------------------------------------- /config/packages/twig_extensions.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | public: false 4 | autowire: true 5 | autoconfigure: true 6 | 7 | # Uncomment any lines below to activate that Twig extension 8 | #Twig\Extensions\ArrayExtension: null 9 | #Twig\Extensions\DateExtension: null 10 | #Twig\Extensions\IntlExtension: null 11 | Twig\Extensions\TextExtension: null 12 | -------------------------------------------------------------------------------- /config/packages/validator.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | # Enables validator auto-mapping support. 4 | # For instance, basic validation constraints will be inferred from Doctrine's metadata. 5 | #auto_mapping: 6 | # App\Entity\: [] 7 | 8 | when@test: 9 | framework: 10 | validation: 11 | not_compromised_password: false 12 | -------------------------------------------------------------------------------- /config/packages/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | when@dev: 2 | web_profiler: 3 | toolbar: true 4 | intercept_redirects: false 5 | 6 | framework: 7 | profiler: 8 | only_exceptions: false 9 | collect_serializer_data: true 10 | 11 | when@test: 12 | web_profiler: 13 | toolbar: false 14 | intercept_redirects: false 15 | 16 | framework: 17 | profiler: { collect: false } 18 | -------------------------------------------------------------------------------- /config/parameters.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | fallback_gh_tokens: [] 3 | -------------------------------------------------------------------------------- /config/preload.php: -------------------------------------------------------------------------------- 1 | { 2 | const esbuild = require('esbuild'); 3 | const sassPlugin = require('esbuild-plugin-sass'); 4 | 5 | const result = await esbuild.build({ 6 | logLevel: 'info', 7 | entryPoints: ['js/app.js', 'js/charts.js'], 8 | bundle: true, 9 | outdir: 'web/build', 10 | sourcemap: process.argv.includes('--dev'), 11 | watch: process.argv.includes('--dev'), 12 | minify: !process.argv.includes('--dev'), 13 | metafile: process.argv.includes('--analyze'), 14 | loader: { 15 | '.gif':'file', 16 | '.eot':'file', 17 | '.ttf':'file', 18 | '.svg':'file', 19 | '.woff':'file', 20 | '.woff2':'file', 21 | }, 22 | target: ['chrome58', 'firefox57', 'safari11', 'edge95'], 23 | plugins: [sassPlugin()], 24 | }) 25 | 26 | if (process.argv.includes('--analyze')) { 27 | const text = await esbuild.analyzeMetafile(result.metafile) 28 | console.log(text); 29 | } 30 | })().catch((e) => console.error(e) || process.exit(1)); 31 | -------------------------------------------------------------------------------- /js/jquery.js: -------------------------------------------------------------------------------- 1 | import jQuery from 'jquery'; 2 | window.jQuery = window.$ = jQuery; 3 | -------------------------------------------------------------------------------- /js/notifier.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import create from 'zustand' 4 | 5 | let uid = 0; 6 | const container = document.createElement('div'); 7 | container.className = "notifications-container"; 8 | 9 | const useStore = create(set => ({ 10 | notifications: [], 11 | log: (msg, options = {}, details = undefined) => set(state => { 12 | if (!container.parentNode) { 13 | document.body.prepend(container); 14 | } 15 | const notifications = [...state.notifications]; 16 | notifications.push({msg, options, details, id: uid++}); 17 | if (options.timeout !== undefined && options.timeout > 0) { 18 | setTimeout( 19 | () => { 20 | state.remove(); 21 | }, 22 | options.timeout 23 | ) 24 | } 25 | return { notifications }; 26 | }), 27 | remove: () => set({ notifications: [] }) 28 | })) 29 | 30 | ReactDOM.render( 31 | , 32 | container 33 | ); 34 | 35 | function Notifier() { 36 | const {notifications, remove} = useStore(); 37 | 38 | return
0 ? "visible" : "")}> 39 | {notifications.map((notification) => { 40 | return
{}}> 41 | {notification.options.timeout ? null : x} 42 |
{notification.msg}
43 | {notification.details &&
} 44 |
; 45 | })} 46 |
; 47 | } 48 | 49 | const {log, remove} = useStore.getState() 50 | 51 | export default {log, remove}; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "packagist.org", 3 | "dependencies": { 4 | "bootstrap": "3.3.5", 5 | "d3": "^3.5.17", 6 | "instantsearch.js": "^2.7.4", 7 | "jquery": "^3.6.0", 8 | "nvd3": "^1.8.6", 9 | "plausible-tracker": "^0.3", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "zustand": "^3.6.9" 13 | }, 14 | "devDependencies": { 15 | "esbuild": "^0.15", 16 | "esbuild-plugin-sass": "^1.0.1" 17 | }, 18 | "scripts": { 19 | "dev": "node esbuild.js --dev", 20 | "build": "node esbuild.js", 21 | "analyze": "node esbuild.js --analyze" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpstan-bootstrap.php: -------------------------------------------------------------------------------- 1 | stringifyTypes(false); 17 | // $config->analyzeQueryPlans(true); 18 | // $config->debugMode(true); 19 | // $config->utilizeSqlAst(true); // requires sqlftw/sqlftw 20 | 21 | (new Dotenv())->bootEnv(__DIR__ . '/.env'); 22 | $dsn = parse_url($_SERVER['DATABASE_URL']); 23 | 24 | QueryReflection::setupReflector( 25 | new RecordingQueryReflector( 26 | ReflectionCache::create( 27 | $cacheFile 28 | ), 29 | new MysqliQueryReflector(new mysqli($dsn['host'], $dsn['user'], $dsn['pass'] ?? '', trim($dsn['path'], '/'), $dsn['port'] ?? 3306)) 30 | ), 31 | $config 32 | ); 33 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | 22 | 23 | src 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 18 | __DIR__ . '/src', 19 | __DIR__ . '/tests', 20 | ]); 21 | 22 | // register a single rule 23 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 24 | 25 | $rectorConfig->ruleWithConfiguration(AnnotationToAttributeRector::class, [ 26 | new AnnotationToAttribute(Password::class), 27 | new AnnotationToAttribute(TypoSquatters::class), 28 | new AnnotationToAttribute(Copyright::class), 29 | ]); 30 | 31 | // define sets of rules 32 | $rectorConfig->sets([ 33 | SymfonySetList::SYMFONY_62, 34 | DoctrineSetList::DOCTRINE_ORM_29, 35 | PHPUnitLevelSetList::UP_TO_PHPUNIT_100, 36 | ]); 37 | }; 38 | -------------------------------------------------------------------------------- /src/Attribute/VarName.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Attribute; 14 | 15 | #[\Attribute(\Attribute::TARGET_PARAMETER)] 16 | class VarName 17 | { 18 | public function __construct( 19 | /** 20 | * @readonly 21 | */ 22 | public string $name, 23 | ) { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Audit/AuditRecordType.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Audit; 14 | 15 | enum AuditRecordType: string 16 | { 17 | # package ownership 18 | case AddMaintainer = 'add_maintainer'; # TODO 19 | case RemoveMaintainer = 'remove_maintainer'; # TODO 20 | case TransferPackage = 'transfer_package'; # TODO 21 | 22 | # package management 23 | case PackageCreated = 'package_created'; 24 | case PackageDeleted = 'package_deleted'; 25 | case CanonicalUrlChange = 'canonical_url_change'; 26 | case VersionDeleted = 'version_deleted'; 27 | case VersionReferenceChange = 'version_reference_change'; 28 | case PackageAbandoned = 'package_abandoned'; # TODO 29 | case PackageUnabandoned = 'package_unabandoned'; # TODO 30 | 31 | # user management 32 | case UserCreated = 'user_created'; # TODO 33 | case UserDeleted = 'user_deleted'; # TODO 34 | case PasswordResetRequest = 'password_reset_request'; # TODO 35 | case PasswordReset = 'password_reset'; # TODO 36 | case PasswordChange = 'password_change'; # TODO 37 | case EmailChange = 'email_change'; # TODO 38 | case UsernameChange = 'username_change'; # TODO 39 | case GitHubLinkedWithUser = 'github_linked_with_user'; # TODO 40 | case GitHubDisconnectedFromUser = 'github_disconnected_from_user'; # TODO 41 | case TwoFaActivated = 'two_fa_activated'; # TODO 42 | case TwoFaDeactivated = 'two_fa_deactivated'; # TODO 43 | } 44 | -------------------------------------------------------------------------------- /src/Command/ConfigureAlgoliaCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Command; 14 | 15 | use Algolia\AlgoliaSearch\SearchClient; 16 | use Symfony\Component\Console\Attribute\AsCommand; 17 | use Symfony\Component\Console\Command\Command; 18 | use Symfony\Component\Console\Input\InputInterface; 19 | use Symfony\Component\Console\Output\OutputInterface; 20 | use Symfony\Component\Yaml\Yaml; 21 | 22 | #[AsCommand(name: 'algolia:configure', description: 'Configure Algolia index')] 23 | class ConfigureAlgoliaCommand extends Command 24 | { 25 | public function __construct( 26 | private SearchClient $algolia, 27 | private string $algoliaIndexName, 28 | private string $configDir, 29 | ) { 30 | parent::__construct(); 31 | } 32 | 33 | protected function execute(InputInterface $input, OutputInterface $output): int 34 | { 35 | $yaml = file_get_contents($this->configDir.'algolia_settings.yml'); 36 | 37 | if (!$yaml) { 38 | throw new \RuntimeException('Algolia config file not readable.'); 39 | } 40 | 41 | $settings = Yaml::parse($yaml); 42 | 43 | $index = $this->algolia->initIndex($this->algoliaIndexName); 44 | 45 | $index->setSettings($settings); 46 | 47 | return 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Command/GenerateTokensCommand.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Command; 14 | 15 | use App\Util\DoctrineTrait; 16 | use Doctrine\Persistence\ManagerRegistry; 17 | use App\Entity\User; 18 | use Symfony\Component\Console\Command\Command; 19 | use Symfony\Component\Console\Input\InputInterface; 20 | use Symfony\Component\Console\Output\OutputInterface; 21 | 22 | /** 23 | * @author Jordi Boggiano 24 | */ 25 | class GenerateTokensCommand extends Command 26 | { 27 | use DoctrineTrait; 28 | 29 | private ManagerRegistry $doctrine; 30 | 31 | public function __construct(ManagerRegistry $doctrine) 32 | { 33 | $this->doctrine = $doctrine; 34 | parent::__construct(); 35 | } 36 | 37 | protected function configure(): void 38 | { 39 | $this 40 | ->setName('packagist:tokens:generate') 41 | ->setDescription('Generates all missing user tokens') 42 | ; 43 | } 44 | 45 | protected function execute(InputInterface $input, OutputInterface $output): int 46 | { 47 | $userRepo = $this->getEM()->getRepository(User::class); 48 | 49 | $users = $userRepo->findUsersMissingApiToken(); 50 | foreach ($users as $user) { 51 | $user->initializeApiToken(); 52 | } 53 | $this->doctrine->getManager()->flush(); 54 | $this->doctrine->getManager()->clear(); 55 | 56 | do { 57 | $users = $userRepo->findUsersMissingSafeApiToken(); 58 | foreach ($users as $user) { 59 | $user->initializeSafeApiToken(); 60 | } 61 | $this->doctrine->getManager()->flush(); 62 | $this->doctrine->getManager()->clear(); 63 | } while (\count($users) > 0); 64 | 65 | return 0; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Controller/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composer/packagist/375bd0956f80b245978b69f15eac0874860840a3/src/Controller/.gitignore -------------------------------------------------------------------------------- /src/Controller/ChangePasswordController.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Controller; 14 | 15 | use App\Entity\User; 16 | use App\Form\ChangePasswordFormType; 17 | use App\Security\UserNotifier; 18 | use Symfony\Component\Security\Http\Attribute\IsGranted; 19 | use Symfony\Component\HttpFoundation\Request; 20 | use Symfony\Component\HttpFoundation\Response; 21 | use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; 22 | use Symfony\Component\Routing\Attribute\Route; 23 | use Symfony\Component\Security\Http\Attribute\CurrentUser; 24 | 25 | class ChangePasswordController extends Controller 26 | { 27 | #[IsGranted('ROLE_USER')] 28 | #[Route(path: '/profile/change-password', name: 'change_password')] 29 | public function changePasswordAction(Request $request, UserPasswordHasherInterface $passwordHasher, UserNotifier $userNotifier, #[CurrentUser] User $user): Response 30 | { 31 | $form = $this->createForm(ChangePasswordFormType::class, $user); 32 | $form->handleRequest($request); 33 | 34 | if ($form->isSubmitted() && $form->isValid()) { 35 | $user->resetPasswordRequest(); 36 | $user->setPassword( 37 | $passwordHasher->hashPassword( 38 | $user, 39 | $form->get('plainPassword')->getData() 40 | ) 41 | ); 42 | 43 | $this->getEM()->persist($user); 44 | $this->getEM()->flush(); 45 | 46 | $userNotifier->notifyChange($user->getEmail(), 'Your password has been changed'); 47 | 48 | return $this->redirectToRoute('my_profile'); 49 | } 50 | 51 | return $this->render('user/change_password.html.twig', [ 52 | 'form' => $form, 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Controller/SecurityController.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Controller; 14 | 15 | use Symfony\Component\HttpFoundation\Response; 16 | use Symfony\Component\Routing\Attribute\Route; 17 | use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; 18 | 19 | class SecurityController extends Controller 20 | { 21 | #[Route(path: '/login/', name: 'login')] 22 | public function loginAction(AuthenticationUtils $authenticationUtils): Response 23 | { 24 | $error = $authenticationUtils->getLastAuthenticationError(); 25 | $lastUsername = $authenticationUtils->getLastUsername(); 26 | 27 | return $this->render('user/login.html.twig', [ 28 | 'lastUsername' => $lastUsername, 29 | 'error' => $error, 30 | ]); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/DataFixtures/UserFixtures.php: -------------------------------------------------------------------------------- 1 | setEmail('admin@example.org'); 35 | $dev->setUsername('admin'); 36 | $dev->setPassword($this->passwordHasher->hashPassword($dev, 'admin')); 37 | $dev->setEnabled(true); 38 | $dev->setRoles(['ROLE_SUPERADMIN']); 39 | 40 | $manager->persist($dev); 41 | 42 | $dev = new User; 43 | $dev->setEmail('dev@example.org'); 44 | $dev->setUsername('dev'); 45 | $dev->setPassword($this->passwordHasher->hashPassword($dev, 'dev')); 46 | $dev->setEnabled(true); 47 | $dev->setRoles([]); 48 | 49 | $manager->persist($dev); 50 | 51 | $user = new User; 52 | $user->setEmail('user@example.org'); 53 | $user->setUsername('user'); 54 | $user->setPassword($this->passwordHasher->hashPassword($user, 'user')); 55 | $user->setEnabled(true); 56 | $user->setRoles([]); 57 | 58 | $manager->persist($user); 59 | $manager->flush(); 60 | 61 | $this->addReference(self::PACKAGE_MAINTAINER, $dev); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Entity/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composer/packagist/375bd0956f80b245978b69f15eac0874860840a3/src/Entity/.gitignore -------------------------------------------------------------------------------- /src/Entity/ConflictLink.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\ORM\Mapping as ORM; 16 | 17 | /** 18 | * @author Jordi Boggiano 19 | */ 20 | #[ORM\Entity] 21 | #[ORM\Table(name: 'link_conflict')] 22 | class ConflictLink extends PackageLink 23 | { 24 | #[ORM\ManyToOne(targetEntity: 'App\Entity\Version', inversedBy: 'conflict')] 25 | #[ORM\JoinColumn(name: 'version_id', nullable: false, referencedColumnName: 'id')] 26 | protected Version $version; 27 | } 28 | -------------------------------------------------------------------------------- /src/Entity/Dependent.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\ORM\Mapping as ORM; 16 | 17 | #[ORM\Entity(repositoryClass: DependentRepository::class)] 18 | #[ORM\Table(name: 'dependent')] 19 | #[ORM\Index(name: 'all_deps', columns: ['package_id', 'packageName'])] 20 | #[ORM\Index(name: 'by_type', columns: ['packageName', 'type'])] 21 | class Dependent 22 | { 23 | public const TYPE_REQUIRE = 1; 24 | public const TYPE_REQUIRE_DEV = 2; 25 | 26 | #[ORM\Id] 27 | #[ORM\ManyToOne(targetEntity: Package::class)] 28 | #[ORM\JoinColumn(nullable: false)] 29 | private Package $package; 30 | 31 | #[ORM\Id] 32 | #[ORM\Column(type: 'string', length: 191)] 33 | private string $packageName; 34 | 35 | /** 36 | * @var self::TYPE_* 37 | */ 38 | #[ORM\Id] 39 | #[ORM\Column(type: 'smallint')] 40 | private int $type; 41 | 42 | /** 43 | * @param self::TYPE_* $type 44 | */ 45 | public function __construct(Package $sourcePackage, string $targetPackageName, int $type) 46 | { 47 | $this->package = $sourcePackage; 48 | $this->packageName = $targetPackageName; 49 | $this->type = $type; 50 | } 51 | 52 | public function getPackage(): Package 53 | { 54 | return $this->package; 55 | } 56 | 57 | public function getPackageName(): string 58 | { 59 | return $this->packageName; 60 | } 61 | 62 | /** 63 | * @return self::TYPE_* 64 | */ 65 | public function getType(): int 66 | { 67 | return $this->type; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Entity/DevRequireLink.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\ORM\Mapping as ORM; 16 | 17 | /** 18 | * @author Jordi Boggiano 19 | */ 20 | #[ORM\Entity] 21 | #[ORM\Table(name: 'link_require_dev')] 22 | #[ORM\Index(name: 'link_require_dev_package_name_idx', columns: ['version_id', 'packageName'])] 23 | #[ORM\Index(name: 'link_require_dev_name_idx', columns: ['packageName'])] 24 | class DevRequireLink extends PackageLink 25 | { 26 | #[ORM\ManyToOne(targetEntity: 'App\Entity\Version', inversedBy: 'devRequire')] 27 | #[ORM\JoinColumn(name: 'version_id', nullable: false, referencedColumnName: 'id')] 28 | protected Version $version; 29 | } 30 | -------------------------------------------------------------------------------- /src/Entity/EmptyReferenceCache.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\ORM\Mapping as ORM; 16 | 17 | /** 18 | * @author Jordi Boggiano 19 | */ 20 | #[ORM\Entity] 21 | #[ORM\Table(name: 'empty_references')] 22 | class EmptyReferenceCache 23 | { 24 | #[ORM\Id] 25 | #[ORM\OneToOne(targetEntity: 'App\Entity\Package')] 26 | private Package $package; 27 | 28 | /** 29 | * @var list 30 | */ 31 | #[ORM\Column(type: 'json')] 32 | private array $emptyReferences = []; 33 | 34 | public function __construct(Package $package) 35 | { 36 | $this->package = $package; 37 | } 38 | 39 | public function setPackage(Package $package): void 40 | { 41 | $this->package = $package; 42 | } 43 | 44 | /** 45 | * @param list $emptyReferences 46 | */ 47 | public function setEmptyReferences(array $emptyReferences): void 48 | { 49 | $this->emptyReferences = $emptyReferences; 50 | } 51 | 52 | /** 53 | * @return list 54 | */ 55 | public function getEmptyReferences(): array 56 | { 57 | return $this->emptyReferences; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Entity/ProvideLink.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\ORM\Mapping as ORM; 16 | 17 | /** 18 | * @author Jordi Boggiano 19 | */ 20 | #[ORM\Entity] 21 | #[ORM\Table(name: 'link_provide')] 22 | class ProvideLink extends PackageLink 23 | { 24 | #[ORM\ManyToOne(targetEntity: 'App\Entity\Version', inversedBy: 'provide')] 25 | #[ORM\JoinColumn(name: 'version_id', nullable: false, referencedColumnName: 'id')] 26 | protected Version $version; 27 | } 28 | -------------------------------------------------------------------------------- /src/Entity/ReplaceLink.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\ORM\Mapping as ORM; 16 | 17 | /** 18 | * @author Jordi Boggiano 19 | */ 20 | #[ORM\Entity] 21 | #[ORM\Table(name: 'link_replace')] 22 | class ReplaceLink extends PackageLink 23 | { 24 | #[ORM\ManyToOne(targetEntity: 'App\Entity\Version', inversedBy: 'replace')] 25 | #[ORM\JoinColumn(name: 'version_id', nullable: false, referencedColumnName: 'id')] 26 | protected Version $version; 27 | } 28 | -------------------------------------------------------------------------------- /src/Entity/RequireLink.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\ORM\Mapping as ORM; 16 | 17 | /** 18 | * @author Jordi Boggiano 19 | */ 20 | #[ORM\Entity] 21 | #[ORM\Table(name: 'link_require')] 22 | #[ORM\Index(name: 'link_require_package_name_idx', columns: ['version_id', 'packageName'])] 23 | #[ORM\Index(name: 'link_require_name_idx', columns: ['packageName'])] 24 | class RequireLink extends PackageLink 25 | { 26 | #[ORM\ManyToOne(targetEntity: 'App\Entity\Version', inversedBy: 'require')] 27 | #[ORM\JoinColumn(name: 'version_id', nullable: false, referencedColumnName: 'id')] 28 | protected Version $version; 29 | } 30 | -------------------------------------------------------------------------------- /src/Entity/SecurityAdvisorySource.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use App\SecurityAdvisory\RemoteSecurityAdvisory; 16 | use App\SecurityAdvisory\Severity; 17 | use Doctrine\ORM\Mapping as ORM; 18 | 19 | #[ORM\Entity] 20 | #[ORM\Table(name: 'security_advisory_source')] 21 | #[ORM\Index(name: 'source_source_idx', columns: ['source'])] 22 | class SecurityAdvisorySource 23 | { 24 | #[ORM\Id] 25 | #[ORM\ManyToOne(targetEntity: SecurityAdvisory::class, inversedBy: 'sources')] 26 | #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] 27 | private SecurityAdvisory $securityAdvisory; 28 | 29 | #[ORM\Id] 30 | #[ORM\Column(type: 'string')] 31 | private string $remoteId; 32 | 33 | #[ORM\Id] 34 | #[ORM\Column(type: 'string')] 35 | private string $source; 36 | 37 | #[ORM\Column(nullable: true)] 38 | private Severity|null $severity; 39 | 40 | public function __construct(SecurityAdvisory $securityAdvisory, string $remoteId, string $source, Severity|null $severity) 41 | { 42 | $this->securityAdvisory = $securityAdvisory; 43 | $this->remoteId = $remoteId; 44 | $this->source = $source; 45 | $this->severity = $severity; 46 | } 47 | 48 | public function getRemoteId(): string 49 | { 50 | return $this->remoteId; 51 | } 52 | 53 | public function getSource(): string 54 | { 55 | return $this->source; 56 | } 57 | 58 | public function getSeverity(): ?Severity 59 | { 60 | return $this->severity; 61 | } 62 | 63 | public function update(RemoteSecurityAdvisory $advisory): void 64 | { 65 | $this->remoteId = $advisory->id; 66 | $this->severity = $advisory->severity; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Entity/SuggestLink.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\ORM\Mapping as ORM; 16 | 17 | /** 18 | * @author Jordi Boggiano 19 | */ 20 | #[ORM\Entity] 21 | #[ORM\Table(name: 'link_suggest')] 22 | #[ORM\Index(name: 'link_suggest_package_name_idx', columns: ['version_id', 'packageName'])] 23 | #[ORM\Index(name: 'link_suggest_name_idx', columns: ['packageName'])] 24 | class SuggestLink extends PackageLink 25 | { 26 | #[ORM\ManyToOne(targetEntity: 'App\Entity\Version', inversedBy: 'suggest')] 27 | #[ORM\JoinColumn(name: 'version_id', nullable: false, referencedColumnName: 'id')] 28 | protected Version $version; 29 | } 30 | -------------------------------------------------------------------------------- /src/Entity/Suggester.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\ORM\Mapping as ORM; 16 | 17 | #[ORM\Entity(repositoryClass: SuggesterRepository::class)] 18 | #[ORM\Table(name: 'suggester')] 19 | #[ORM\Index(name: 'all_suggesters', columns: ['packageName'])] 20 | class Suggester 21 | { 22 | #[ORM\Id] 23 | #[ORM\ManyToOne(targetEntity: Package::class)] 24 | #[ORM\JoinColumn(nullable: false)] 25 | private Package $package; 26 | 27 | #[ORM\Id] 28 | #[ORM\Column(type: 'string', length: 191)] 29 | private string $packageName; 30 | 31 | public function __construct(Package $sourcePackage, string $targetPackageName) 32 | { 33 | $this->package = $sourcePackage; 34 | $this->packageName = $targetPackageName; 35 | } 36 | 37 | public function getPackage(): Package 38 | { 39 | return $this->package; 40 | } 41 | 42 | public function getPackageName(): string 43 | { 44 | return $this->packageName; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Entity/SuggesterRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; 16 | use Doctrine\Persistence\ManagerRegistry; 17 | 18 | /** 19 | * @extends ServiceEntityRepository 20 | */ 21 | class SuggesterRepository extends ServiceEntityRepository 22 | { 23 | public function __construct(ManagerRegistry $registry) 24 | { 25 | parent::__construct($registry, Suggester::class); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Entity/TemporaryTwoFactorUser.php: -------------------------------------------------------------------------------- 1 | user->getTotpAuthenticationUsername(); 25 | } 26 | 27 | public function getTotpAuthenticationConfiguration(): TotpConfigurationInterface 28 | { 29 | return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Entity/Vendor.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\ORM\Mapping as ORM; 16 | 17 | /** 18 | * @author Jordi Boggiano 19 | */ 20 | #[ORM\Entity(repositoryClass: 'App\Entity\VendorRepository')] 21 | #[ORM\Table(name: 'vendor')] 22 | #[ORM\Index(name: 'verified_idx', columns: ['verified'])] 23 | class Vendor 24 | { 25 | /** 26 | * Unique vendor name 27 | */ 28 | #[ORM\Id] 29 | #[ORM\Column(length: 191)] 30 | private string $name; 31 | 32 | #[ORM\Column(type: 'boolean')] 33 | private bool $verified = false; 34 | 35 | public function __construct(string $name) 36 | { 37 | $this->name = $name; 38 | } 39 | 40 | public function setName(string $name): void 41 | { 42 | $this->name = $name; 43 | } 44 | 45 | public function getName(): string 46 | { 47 | return $this->name; 48 | } 49 | 50 | public function setVerified(bool $verified): void 51 | { 52 | $this->verified = $verified; 53 | } 54 | 55 | public function getVerified(): bool 56 | { 57 | return $this->verified; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Entity/VendorRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Entity; 14 | 15 | use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; 16 | use Doctrine\Persistence\ManagerRegistry; 17 | 18 | /** 19 | * @author Jordi Boggiano 20 | * @extends ServiceEntityRepository 21 | */ 22 | class VendorRepository extends ServiceEntityRepository 23 | { 24 | public function __construct(ManagerRegistry $registry) 25 | { 26 | parent::__construct($registry, Vendor::class); 27 | } 28 | 29 | public function isVerified(string $vendor): bool 30 | { 31 | return (bool) $this->getEntityManager()->getConnection()->fetchOne('SELECT verified FROM vendor WHERE name = :vendor', ['vendor' => $vendor]); 32 | } 33 | 34 | public function createIfNotExists(string $vendor): void 35 | { 36 | $this->getEntityManager()->getConnection()->executeStatement( 37 | 'INSERT INTO vendor (name, verified) VALUES (:vendor, 0) ON DUPLICATE KEY UPDATE name=name', 38 | ['vendor' => $vendor] 39 | ); 40 | } 41 | 42 | public function verify(string $vendor): void 43 | { 44 | $this->getEntityManager()->getConnection()->executeStatement( 45 | 'INSERT INTO vendor (name, verified) VALUES (:vendor, 1) ON DUPLICATE KEY UPDATE verified=1', 46 | ['vendor' => $vendor] 47 | ); 48 | $this->getEntityManager()->getConnection()->executeStatement( 49 | 'UPDATE package SET suspect = NULL WHERE vendor = :vendor', 50 | ['vendor' => $vendor] 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/EventListener/CacheListener.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\EventListener; 14 | 15 | use Symfony\Component\EventDispatcher\Attribute\AsEventListener; 16 | use Symfony\Component\HttpKernel\Event\ResponseEvent; 17 | 18 | #[AsEventListener] 19 | class CacheListener 20 | { 21 | public function __invoke(ResponseEvent $e): void 22 | { 23 | $resp = $e->getResponse(); 24 | 25 | // add nginx-cache compatible header 26 | if ($resp->headers->hasCacheControlDirective('public') && ($cache = $resp->headers->getCacheControlDirective('s-maxage'))) { 27 | $resp->headers->set('X-Accel-Expires', (string) $cache); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/EventListener/LogoutListener.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\EventListener; 14 | 15 | use Symfony\Component\EventDispatcher\Attribute\AsEventListener; 16 | use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface; 17 | use Symfony\Component\HttpKernel\Event\ExceptionEvent; 18 | use Symfony\Component\Security\Core\Exception\LogoutException; 19 | use Symfony\Component\HttpFoundation\RedirectResponse; 20 | 21 | class LogoutListener 22 | { 23 | #[AsEventListener] 24 | public function handleExpiredCsrfError(ExceptionEvent $event): void 25 | { 26 | $e = $event->getThrowable(); 27 | do { 28 | if ($e instanceof LogoutException) { 29 | if ($e->getMessage() !== 'Invalid CSRF token.') { 30 | return; 31 | } 32 | 33 | try { 34 | $session = $event->getRequest()->getSession(); 35 | if ($session instanceof FlashBagAwareSessionInterface) { 36 | $session->getFlashBag()->add('warning', 'Invalid CSRF token, try logging out again.'); 37 | } 38 | 39 | $event->setResponse(new RedirectResponse('/')); 40 | $event->allowCustomResponseCode(); 41 | } catch (\Exception $e) { 42 | $event->setThrowable($e); 43 | } 44 | 45 | return; 46 | } 47 | } while (null !== $e = $e->getPrevious()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/EventListener/ResolvedTwoFactorCodeCredentialsListener.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\EventListener; 14 | 15 | use App\Security\Passport\Badge\ResolvedTwoFactorCodeCredentials; 16 | use Scheb\TwoFactorBundle\Security\Http\Authenticator\TwoFactorAuthenticator; 17 | use Symfony\Component\EventDispatcher\Attribute\AsEventListener; 18 | use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent; 19 | 20 | class ResolvedTwoFactorCodeCredentialsListener 21 | { 22 | #[AsEventListener(event: AuthenticationTokenCreatedEvent::class, priority: 512)] 23 | public function onAuthenticationTokenCreated(AuthenticationTokenCreatedEvent $event): void 24 | { 25 | if ($event->getPassport()->getBadge(ResolvedTwoFactorCodeCredentials::class)) { 26 | $event->getAuthenticatedToken()->setAttribute(TwoFactorAuthenticator::FLAG_2FA_COMPLETE, true); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Form/EventSubscriber/FormBruteForceSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form\EventSubscriber; 14 | 15 | use App\Validator\RateLimitingRecaptcha; 16 | use Beelab\Recaptcha2Bundle\Validator\Constraints\Recaptcha2; 17 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 18 | use Symfony\Component\Form\Form; 19 | use Symfony\Component\Form\FormEvent; 20 | use Symfony\Component\Form\FormEvents; 21 | use Symfony\Component\Validator\ConstraintViolation; 22 | 23 | /** 24 | * In case we encounter a brute force error e.g. missing/invalid recaptcha, remove all other form errors to not accidentally leak any information. 25 | */ 26 | class FormBruteForceSubscriber implements EventSubscriberInterface 27 | { 28 | public function onPostSubmit(FormEvent $event): void 29 | { 30 | $form = $event->getForm(); 31 | 32 | if ($form->isRoot() && $form instanceof Form) { 33 | foreach ($form->getErrors(true) as $error) { 34 | $recaptchaMessage = (new Recaptcha2)->message; 35 | $cause = $error->getCause(); 36 | if ( 37 | $cause instanceof ConstraintViolation && ( 38 | $cause->getCode() === RateLimitingRecaptcha::INVALID_RECAPTCHA_ERROR || 39 | $error->getMessage() === $recaptchaMessage 40 | )) { 41 | $form->clearErrors(true); 42 | $error->getOrigin()?->addError($error); 43 | } 44 | } 45 | } 46 | } 47 | 48 | public static function getSubscribedEvents(): array 49 | { 50 | return [FormEvents::POST_SUBMIT => ['onPostSubmit', -2048]]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Form/EventSubscriber/FormInvalidPasswordSubscriber.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form\EventSubscriber; 14 | 15 | use App\Security\RecaptchaHelper; 16 | use App\Validator\TwoFactorCode; 17 | use Symfony\Component\EventDispatcher\EventSubscriberInterface; 18 | use Symfony\Component\Form\FormEvent; 19 | use Symfony\Component\Form\FormEvents; 20 | use Symfony\Component\Security\Core\Validator\Constraints\UserPassword; 21 | use Symfony\Component\Validator\ConstraintViolation; 22 | 23 | class FormInvalidPasswordSubscriber implements EventSubscriberInterface 24 | { 25 | public function __construct( 26 | private readonly RecaptchaHelper $recaptchaHelper, 27 | ) {} 28 | 29 | public function onPostSubmit(FormEvent $event): void 30 | { 31 | $form = $event->getForm(); 32 | 33 | if ($form->isRoot()) { 34 | foreach ($form->getErrors(true) as $error) { 35 | $cause = $error->getCause(); 36 | // increment for invalid password 37 | if ($cause instanceof ConstraintViolation && $cause->getCode() === UserPassword::INVALID_PASSWORD_ERROR) { 38 | $context = $this->recaptchaHelper->buildContext(); 39 | $this->recaptchaHelper->increaseCounter($context); 40 | } 41 | 42 | // increment for invalid 2fa code 43 | if ($cause instanceof ConstraintViolation && $cause->getConstraint() instanceof TwoFactorCode) { 44 | $context = $this->recaptchaHelper->buildContext(); 45 | $this->recaptchaHelper->increaseCounter($context); 46 | } 47 | } 48 | } 49 | } 50 | 51 | public static function getSubscribedEvents(): array 52 | { 53 | return [FormEvents::POST_SUBMIT => ['onPostSubmit', -1024]]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Form/Extension/RecaptchaExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form\Extension; 14 | 15 | use App\Form\EventSubscriber\FormBruteForceSubscriber; 16 | use App\Form\EventSubscriber\FormInvalidPasswordSubscriber; 17 | use Symfony\Component\Form\AbstractTypeExtension; 18 | use Symfony\Component\Form\Extension\Core\Type\FormType; 19 | use Symfony\Component\Form\FormBuilderInterface; 20 | 21 | class RecaptchaExtension extends AbstractTypeExtension 22 | { 23 | public function __construct( 24 | private readonly FormInvalidPasswordSubscriber $formInvalidPasswordSubscriber, 25 | private readonly FormBruteForceSubscriber $formBruteForceSubscriber, 26 | ) {} 27 | 28 | public function buildForm(FormBuilderInterface $builder, array $options): void 29 | { 30 | $builder->addEventSubscriber($this->formInvalidPasswordSubscriber); 31 | $builder->addEventSubscriber($this->formBruteForceSubscriber); 32 | } 33 | 34 | public static function getExtendedTypes(): iterable 35 | { 36 | return [FormType::class]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Form/Model/EnableTwoFactorRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form\Model; 14 | 15 | use Symfony\Component\Validator\Constraints as Assert; 16 | 17 | class EnableTwoFactorRequest 18 | { 19 | #[Assert\NotBlank] 20 | protected ?string $code = null; 21 | 22 | public function getCode(): ?string 23 | { 24 | return $this->code; 25 | } 26 | 27 | public function setCode(string $code): void 28 | { 29 | $this->code = $code; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Form/Model/MaintainerRequest.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form\Model; 14 | 15 | class MaintainerRequest 16 | { 17 | private ?string $user = null; 18 | 19 | public function setUser(string $username): void 20 | { 21 | $this->user = $username; 22 | } 23 | 24 | public function getUser(): ?string 25 | { 26 | return $this->user; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Form/ResetPasswordRequestFormType.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form; 14 | 15 | use App\Form\Type\InvisibleRecaptchaType; 16 | use Symfony\Component\Form\AbstractType; 17 | use Symfony\Component\Form\Extension\Core\Type\TextType; 18 | use Symfony\Component\Form\FormBuilderInterface; 19 | use Symfony\Component\OptionsResolver\OptionsResolver; 20 | use Symfony\Component\Validator\Constraints\NotBlank; 21 | 22 | /** 23 | * @extends AbstractType 24 | */ 25 | class ResetPasswordRequestFormType extends AbstractType 26 | { 27 | public function buildForm(FormBuilderInterface $builder, array $options): void 28 | { 29 | $builder 30 | ->add('email', TextType::class, [ 31 | 'constraints' => [ 32 | new NotBlank([ 33 | 'message' => 'Please enter your email', 34 | ]), 35 | ], 36 | 'label' => 'Username / Email', 37 | ]) 38 | ->add('captcha', InvisibleRecaptchaType::class) 39 | ; 40 | } 41 | 42 | public function configureOptions(OptionsResolver $resolver): void 43 | { 44 | $resolver->setDefaults([]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Form/Type/AbandonedType.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form\Type; 14 | 15 | use Symfony\Component\Form\AbstractType; 16 | use Symfony\Component\Form\Extension\Core\Type\TextType; 17 | use Symfony\Component\Form\FormBuilderInterface; 18 | 19 | /** 20 | * Class AbandonedType 21 | * 22 | * Form used to acquire replacement Package information for abandoned package. 23 | * 24 | * @extends AbstractType 25 | */ 26 | class AbandonedType extends AbstractType 27 | { 28 | public function buildForm(FormBuilderInterface $builder, array $options): void 29 | { 30 | $builder->add( 31 | 'replacement', 32 | TextType::class, 33 | [ 34 | 'required' => false, 35 | 'label' => 'Replacement package', 36 | 'attr' => ['placeholder' => 'optional package name'], 37 | ] 38 | ); 39 | } 40 | 41 | /** 42 | * @inheritDoc 43 | */ 44 | public function getBlockPrefix(): string 45 | { 46 | return 'package'; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Form/Type/AddMaintainerRequestType.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form\Type; 14 | 15 | use App\Form\Model\MaintainerRequest; 16 | use Symfony\Component\Form\AbstractType; 17 | use Symfony\Component\Form\FormBuilderInterface; 18 | use Symfony\Component\OptionsResolver\OptionsResolver; 19 | 20 | /** 21 | * @extends AbstractType 22 | */ 23 | class AddMaintainerRequestType extends AbstractType 24 | { 25 | public function buildForm(FormBuilderInterface $builder, array $options): void 26 | { 27 | $builder->add('user'); 28 | } 29 | 30 | public function configureOptions(OptionsResolver $resolver): void 31 | { 32 | $resolver->setDefaults([ 33 | 'data_class' => MaintainerRequest::class, 34 | ]); 35 | } 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | public function getBlockPrefix(): string 41 | { 42 | return 'add_maintainer_form'; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Form/Type/EnableTwoFactorAuthType.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form\Type; 14 | 15 | use App\Form\Model\EnableTwoFactorRequest; 16 | use App\Validator\TwoFactorCode; 17 | use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface; 18 | use Symfony\Component\Form\AbstractType; 19 | use Symfony\Component\Form\Extension\Core\Type\TextType; 20 | use Symfony\Component\Form\FormBuilderInterface; 21 | use Symfony\Component\OptionsResolver\OptionsResolver; 22 | 23 | /** 24 | * @extends AbstractType 25 | */ 26 | class EnableTwoFactorAuthType extends AbstractType 27 | { 28 | public function buildForm(FormBuilderInterface $builder, array $options): void 29 | { 30 | $builder->add('code', TextType::class, [ 31 | 'constraints' => [new TwoFactorCode($options['user'])] 32 | ]); 33 | } 34 | 35 | public function configureOptions(OptionsResolver $resolver): void 36 | { 37 | $resolver 38 | ->setDefaults([ 39 | 'data_class' => EnableTwoFactorRequest::class, 40 | ]) 41 | ->define('user') 42 | ->allowedTypes(TwoFactorInterface::class) 43 | ->required(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Form/Type/PackageType.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form\Type; 14 | 15 | use App\Entity\Package; 16 | use Symfony\Component\Form\AbstractType; 17 | use Symfony\Component\Form\Extension\Core\Type\TextType; 18 | use Symfony\Component\Form\FormBuilderInterface; 19 | use Symfony\Component\OptionsResolver\OptionsResolver; 20 | 21 | /** 22 | * @extends AbstractType 23 | */ 24 | class PackageType extends AbstractType 25 | { 26 | public function buildForm(FormBuilderInterface $builder, array $options): void 27 | { 28 | $builder->add('repository', TextType::class, [ 29 | 'label' => 'Repository URL (Git/Svn/Hg)', 30 | 'attr' => [ 31 | 'placeholder' => 'e.g.: https://github.com/composer/composer', 32 | ], 33 | ]); 34 | } 35 | 36 | public function configureOptions(OptionsResolver $resolver): void 37 | { 38 | $resolver->setDefaults([ 39 | 'data_class' => Package::class, 40 | 'validation_groups' => ['Default', 'Create'], 41 | ]); 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | public function getBlockPrefix(): string 48 | { 49 | return 'package'; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Form/Type/RemoveMaintainerRequestType.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Form\Type; 14 | 15 | use App\Entity\Package; 16 | use App\Entity\User; 17 | use App\Entity\UserRepository; 18 | use App\Form\Model\MaintainerRequest; 19 | use Symfony\Bridge\Doctrine\Form\Type\EntityType; 20 | use Symfony\Component\Form\AbstractType; 21 | use Symfony\Component\Form\FormBuilderInterface; 22 | use Symfony\Component\OptionsResolver\OptionsResolver; 23 | 24 | /** 25 | * @extends AbstractType 26 | */ 27 | class RemoveMaintainerRequestType extends AbstractType 28 | { 29 | /** 30 | * @param array{package: Package, excludeUser: User} $options 31 | */ 32 | public function buildForm(FormBuilderInterface $builder, array $options): void 33 | { 34 | $builder->add('user', EntityType::class, [ 35 | 'class' => User::class, 36 | 'query_builder' => static function (UserRepository $er) use ($options) { 37 | return $er->getPackageMaintainersQueryBuilder($options['package'], $options['excludeUser']); 38 | }, 39 | ]); 40 | } 41 | 42 | public function configureOptions(OptionsResolver $resolver): void 43 | { 44 | $resolver->setRequired(['package']); 45 | $resolver->setDefaults([ 46 | 'excludeUser' => null, 47 | 'data_class' => MaintainerRequest::class, 48 | ]); 49 | } 50 | 51 | public function getBlockPrefix(): string 52 | { 53 | return 'remove_maintainer_form'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/HtmlSanitizer/ReadmeImageSanitizer.php: -------------------------------------------------------------------------------- 1 | host) { 29 | 'github.com' => 'https://raw.github.com/'.$this->ownerRepo.'/HEAD/'.$this->basePath.$value, 30 | 'gitlab.com' => 'https://gitlab.com/'.$this->ownerRepo.'/-/raw/HEAD/'.$this->basePath.$value, 31 | 'bitbucket.org' => 'https://bitbucket.org/'.$this->ownerRepo.'/raw/HEAD/'.$this->basePath.$value, 32 | default => $value, 33 | }; 34 | } 35 | 36 | if (str_starts_with($value, 'https://private-user-images.githubusercontent.com/')) { 37 | return Preg::replace('{^https://private-user-images.githubusercontent.com/\d+/\d+-([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\.\w+\?.*$}', 'https://github.com/user-attachments/assets/$1', $value, 1); 38 | } 39 | 40 | return $value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App; 14 | 15 | use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; 16 | use Symfony\Component\Config\Loader\LoaderInterface; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; 19 | use Symfony\Component\HttpKernel\Kernel as BaseKernel; 20 | 21 | ini_set('date.timezone', 'UTC'); 22 | 23 | class Kernel extends BaseKernel 24 | { 25 | use MicroKernelTrait { 26 | configureContainer as parentConfigureContainer; 27 | } 28 | 29 | protected function configureContainer(ContainerConfigurator $container, LoaderInterface $loader, ContainerBuilder $builder): void 30 | { 31 | $configDir = $this->getConfigDir(); 32 | 33 | $this->parentConfigureContainer($container, $loader, $builder); 34 | 35 | $container->import($configDir.'/{parameters}.yaml'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Logger/LogIdProcessor.php: -------------------------------------------------------------------------------- 1 | reqId = bin2hex(random_bytes(6)); 17 | } 18 | 19 | public function startJob(string $id): void 20 | { 21 | $this->jobId = $id; 22 | } 23 | 24 | public function reset(): void 25 | { 26 | $this->reqId = null; 27 | $this->jobId = null; 28 | } 29 | 30 | public function __invoke(LogRecord $record): LogRecord 31 | { 32 | if ($this->jobId !== null) { 33 | $record->extra['job_id'] = $this->jobId; 34 | } 35 | if ($this->reqId !== null) { 36 | $record->extra['req_id'] = $this->reqId; 37 | } 38 | 39 | return $record; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Model/RedisAdapter.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Model; 14 | 15 | use App\Entity\Package; 16 | use App\Entity\User; 17 | use Pagerfanta\Adapter\AdapterInterface; 18 | 19 | /** 20 | * @author Jordi Boggiano 21 | * @template-implements AdapterInterface 22 | */ 23 | class RedisAdapter implements AdapterInterface 24 | { 25 | public function __construct(private FavoriteManager $model, private User $instance) 26 | { 27 | } 28 | 29 | public function getNbResults(): int 30 | { 31 | return $this->model->getFavoriteCount($this->instance); 32 | } 33 | 34 | public function getSlice(int $offset, int $length): iterable 35 | { 36 | return $this->model->getFavorites($this->instance, $length, $offset); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Redis/FailedLoginCounter.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Redis; 14 | 15 | class FailedLoginCounter extends \Predis\Command\ScriptCommand 16 | { 17 | /** 18 | * @var array 19 | */ 20 | private array $args; 21 | 22 | public function getKeysCount(): int 23 | { 24 | if (!$this->args) { 25 | throw new \LogicException('getKeysCount called before setArguments'); 26 | } 27 | 28 | return count($this->args); 29 | } 30 | 31 | /** 32 | * @param array $arguments 33 | */ 34 | public function setArguments(array $arguments): void 35 | { 36 | $this->args = $arguments; 37 | 38 | parent::setArguments($arguments); 39 | } 40 | 41 | public function getScript(): string 42 | { 43 | return << 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Redis; 14 | 15 | class FetchVersionIds extends \Predis\Command\ScriptCommand 16 | { 17 | public function getScript(): string 18 | { 19 | return << 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Redis; 14 | 15 | class PackagesExist extends \Predis\Command\ScriptCommand 16 | { 17 | public function getScript(): string 18 | { 19 | return << 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Search; 14 | 15 | use Algolia\AlgoliaSearch\Exceptions\AlgoliaException; 16 | use Algolia\AlgoliaSearch\SearchClient; 17 | 18 | /** 19 | * @phpstan-import-type SearchResult from ResultTransformer 20 | */ 21 | final class Algolia 22 | { 23 | public function __construct( 24 | private SearchClient $algolia, 25 | private string $algoliaIndexName, 26 | private ResultTransformer $transformer, 27 | ) { 28 | } 29 | 30 | /** 31 | * @phpstan-return SearchResult 32 | * 33 | * @throws AlgoliaException 34 | */ 35 | public function search(Query $query): array 36 | { 37 | $index = $this->algolia->initIndex($this->algoliaIndexName); 38 | 39 | return $this->transformer->transform( 40 | $query, 41 | $index->search($query->query, $query->getOptions()) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Security/AccountEmailExistsWithoutGitHubException.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Security; 14 | 15 | use Symfony\Component\Security\Core\Exception\UserNotFoundException; 16 | 17 | class AccountEmailExistsWithoutGitHubException extends UserNotFoundException 18 | { 19 | public function __construct(private string $email) 20 | { 21 | } 22 | 23 | /** 24 | * @inheritDoc 25 | */ 26 | public function getMessageKey(): string 27 | { 28 | return 'An account with your GitHub email ('.$this->email.') already exists on Packagist.org but it is not linked to your GitHub account. ' 29 | . 'Please log in to it via username/password and then connect your GitHub account from the Profile > Settings page.'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Security/AccountUsernameExistsWithoutGitHubException.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Security; 14 | 15 | use Symfony\Component\Security\Core\Exception\UserNotFoundException; 16 | 17 | class AccountUsernameExistsWithoutGitHubException extends UserNotFoundException 18 | { 19 | public function __construct(private string $username) 20 | { 21 | } 22 | 23 | public function getMessageKey(): string 24 | { 25 | return 'An account with your GitHub username ('.$this->username.') already exists on Packagist.org but it is not linked to your GitHub account. ' 26 | . 'Please log in to it via username/password and then connect your GitHub account from the Profile > Settings page.'; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Security/NoGitHubNicknameFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Security; 14 | 15 | use Symfony\Component\Security\Core\Exception\UserNotFoundException; 16 | 17 | class NoGitHubNicknameFoundException extends UserNotFoundException 18 | { 19 | public function getMessageKey(): string 20 | { 21 | return 'No username/nickname was found on your GitHub account, so we can not automatically log you in. ' 22 | . 'Please register an account manually and then connect your GitHub account from the Profile > Settings page.'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Security/NoVerifiedGitHubEmailFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Security; 14 | 15 | use Symfony\Component\Security\Core\Exception\UserNotFoundException; 16 | 17 | class NoVerifiedGitHubEmailFoundException extends UserNotFoundException 18 | { 19 | public function getMessageKey(): string 20 | { 21 | return 'No verified email address was found on your GitHub account, so we can not automatically log you in. ' 22 | . 'Please register an account manually and then connect your GitHub account from the Profile > Settings page.'; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Security/Passport/Badge/ResolvedTwoFactorCodeCredentials.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Security\Passport\Badge; 14 | 15 | use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface; 16 | 17 | class ResolvedTwoFactorCodeCredentials implements BadgeInterface 18 | { 19 | public function isResolved(): bool 20 | { 21 | return true; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Security/RecaptchaContext.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Security; 14 | 15 | use Symfony\Component\HttpFoundation\Request; 16 | 17 | class RecaptchaContext 18 | { 19 | private const LOGIN_BASE_KEY_IP = 'bf:login:ip:'; 20 | private const LOGIN_BASE_KEY_USER = 'bf:login:user:'; 21 | 22 | public function __construct( 23 | public readonly string $ip, 24 | public readonly string $username, 25 | public readonly bool $hasRecaptcha, 26 | ) {} 27 | 28 | /** 29 | * @return string[] 30 | */ 31 | public function getRedisKeys(bool $forClear = false): array 32 | { 33 | return array_filter([ 34 | ! $forClear && $this->ip ? self::LOGIN_BASE_KEY_IP . $this->ip : null, 35 | $this->username ? self::LOGIN_BASE_KEY_USER . strtolower($this->username) : null, 36 | ]); 37 | } 38 | 39 | public static function fromRequest(Request $request): self 40 | { 41 | return new self( 42 | $request->getClientIp() ?: '', 43 | (string) $request->request->get('_username'), 44 | $request->request->has('g-recaptcha-response'), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Security/UserChecker.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Security; 14 | 15 | use App\Entity\User; 16 | use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException; 17 | use Symfony\Component\Security\Core\User\UserCheckerInterface; 18 | use Symfony\Component\Security\Core\User\UserInterface; 19 | 20 | class UserChecker implements UserCheckerInterface 21 | { 22 | public function checkPreAuth(UserInterface $user): void 23 | { 24 | if (!$user instanceof User) { 25 | return; 26 | } 27 | 28 | if (!$user->isEnabled() || $user->hasRole('ROLE_SPAMMER')) { 29 | throw new CustomUserMessageAccountStatusException('Your user account is not yet enabled. Please make sure you confirm your email address or trigger a password reset to receive another email.'); 30 | } 31 | } 32 | 33 | public function checkPostAuth(UserInterface $user): void 34 | { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Security/UserNotifier.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Security; 14 | 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Bridge\Twig\Mime\TemplatedEmail; 17 | use Symfony\Component\Mailer\Exception\TransportExceptionInterface; 18 | use Symfony\Component\Mailer\MailerInterface; 19 | use Symfony\Component\Mime\Address; 20 | 21 | /** 22 | * @author Pierre Ambroise 23 | */ 24 | class UserNotifier 25 | { 26 | public function __construct( 27 | private string $mailFromEmail, 28 | private string $mailFromName, 29 | private MailerInterface $mailer, 30 | private LoggerInterface $logger, 31 | ) {} 32 | 33 | /** 34 | * @param array $templateVars 35 | */ 36 | public function notifyChange(string $email, string $reason = '', string $template = 'email/alert_change.txt.twig', string $subject = 'A change has been made to your Packagist.org account', ...$templateVars): void 37 | { 38 | $email = (new TemplatedEmail()) 39 | ->from(new Address($this->mailFromEmail, $this->mailFromName)) 40 | ->to($email) 41 | ->subject($subject) 42 | ->textTemplate($template) 43 | ->context([ 44 | 'reason' => $reason, 45 | ...$templateVars 46 | ]); 47 | 48 | try { 49 | $this->mailer->send($email); 50 | } catch (TransportExceptionInterface $e) { 51 | $this->logger->error('['.get_class($e).'] '.$e->getMessage()); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Security/Voter/PackageActions.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\SecurityAdvisory; 14 | 15 | class AdvisoryIdGenerator 16 | { 17 | // All alphanumeric symbols except vowels and some to avoid misspellings (I, O, l, 0), case insensitive, 34 character alphabet 18 | private const ALNUM_SAFE_CI = "bcdfghjkmnpqrstvwxyz123456789"; 19 | 20 | public static function generate(): string 21 | { 22 | $letterPool = self::ALNUM_SAFE_CI; 23 | $token = 'PKSA-'; 24 | $len = strlen($letterPool) - 1; 25 | for ($i = 0; $i < 3; $i++) { 26 | for ($j = 0; $j < 4; $j++) { 27 | $token .= $letterPool[random_int(0, $len)]; 28 | } 29 | 30 | $token .= '-'; 31 | } 32 | 33 | return trim($token, '-'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SecurityAdvisory/AdvisoryParser.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\SecurityAdvisory; 14 | 15 | use Composer\Pcre\Preg; 16 | 17 | class AdvisoryParser 18 | { 19 | public static function isValidCve(?string $cve): bool 20 | { 21 | return $cve && Preg::isMatch('#^CVE-[0-9]{4}-[0-9]{4,}$#', $cve); 22 | } 23 | 24 | public static function titleWithoutCve(string $title): string 25 | { 26 | if (Preg::isMatchStrictGroups('#^(CVE-[0-9a-z*?-]+:)(.*)$#i', $title, $matches)) { 27 | return trim($matches[2]); 28 | } 29 | 30 | return $title; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/SecurityAdvisory/RemoteSecurityAdvisoryCollection.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\SecurityAdvisory; 14 | 15 | class RemoteSecurityAdvisoryCollection 16 | { 17 | /** @var array> */ 18 | private array $groupedSecurityAdvisories = []; 19 | 20 | /** 21 | * @param list $advisories 22 | */ 23 | public function __construct(array $advisories) 24 | { 25 | foreach ($advisories as $advisory) { 26 | $this->groupedSecurityAdvisories[$advisory->packageName][] = $advisory; 27 | } 28 | } 29 | 30 | /** 31 | * @return list 32 | */ 33 | public function getAdvisoriesForPackageName(string $packageName): array 34 | { 35 | return $this->groupedSecurityAdvisories[$packageName] ?? []; 36 | } 37 | 38 | /** 39 | * @return list 40 | */ 41 | public function getPackageNames(): array 42 | { 43 | return array_keys($this->groupedSecurityAdvisories); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/SecurityAdvisory/SecurityAdvisorySourceInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\SecurityAdvisory; 14 | 15 | use Composer\IO\ConsoleIO; 16 | 17 | interface SecurityAdvisorySourceInterface 18 | { 19 | public function getAdvisories(ConsoleIO $io): ?RemoteSecurityAdvisoryCollection; 20 | } 21 | -------------------------------------------------------------------------------- /src/SecurityAdvisory/Severity.php: -------------------------------------------------------------------------------- 1 | 9 | * Nils Adermann 10 | * 11 | * For the full copyright and license information, please view the LICENSE 12 | * file that was distributed with this source code. 13 | */ 14 | 15 | /** 16 | * Common Vulnerability Scoring System v3 17 | * 18 | * - None: 0.0 19 | * - Low: 0.1 - 3.9 20 | * - Medium: 4.0 - 6.9 21 | * - High: 7.0 - 8.9 22 | * - Critical: 9.0 - 10.0 23 | * 24 | * @see https://www.first.org/cvss/specification-document 25 | */ 26 | enum Severity: string 27 | { 28 | case NONE = 'none'; 29 | case LOW = 'low'; 30 | case MEDIUM = 'medium'; 31 | case HIGH = 'high'; 32 | case CRITICAL = 'critical'; 33 | 34 | /** 35 | * @see https://docs.github.com/en/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/about-the-github-advisory-database#about-cvss-levels 36 | */ 37 | public static function fromGitHub(?string $githubSeverity): ?Severity 38 | { 39 | if (!$githubSeverity) { 40 | return null; 41 | } 42 | 43 | // GitHub uses moderate instead of medium 44 | if (strtolower($githubSeverity) === 'moderate') { 45 | return self::MEDIUM; 46 | } 47 | 48 | return Severity::tryFrom(strtolower($githubSeverity)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Service/FallbackGitHubAuthProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Service; 14 | 15 | use Doctrine\Persistence\ManagerRegistry; 16 | use App\Entity\User; 17 | 18 | class FallbackGitHubAuthProvider 19 | { 20 | public function __construct( 21 | /** @var list */ 22 | private array $fallbackGhTokens, 23 | private ManagerRegistry $doctrine, 24 | ) { 25 | } 26 | 27 | public function getAuthToken(): string|null 28 | { 29 | if ($this->fallbackGhTokens) { 30 | $fallbackUser = $this->doctrine->getRepository(User::class)->findOneBy(['usernameCanonical' => $this->fallbackGhTokens[random_int(0, count($this->fallbackGhTokens) - 1)]]); 31 | if (null === $fallbackUser) { 32 | throw new \LogicException('Invalid fallback user was not found'); 33 | } 34 | $fallbackToken = $fallbackUser->getGithubToken(); 35 | if (null === $fallbackToken) { 36 | throw new \LogicException('Invalid fallback user '.$fallbackUser->getUsername().' has no token'); 37 | } 38 | return $fallbackToken; 39 | } 40 | 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Service/ReplicaClient.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private array $replicaIps, 17 | private string $internalSecret, 18 | ) {} 19 | 20 | public function uploadMetadata(string $path, string $contents, int $filemtime): void 21 | { 22 | $sig = hash_hmac('sha256', $path.$contents.$filemtime, $this->internalSecret); 23 | 24 | $resps = []; 25 | foreach ($this->replicaIps as $ip) { 26 | $resps[] = $this->httpClient->request('POST', 'http://'.$ip.'/internal/update-metadata', [ 27 | 'headers' => [ 28 | 'Internal-Signature' => $sig, 29 | 'Host' => 'packagist.org', 30 | ], 31 | 'body' => [ 32 | 'path' => $path, 33 | 'contents' => $contents, 34 | 'filemtime' => $filemtime, 35 | ], 36 | ]); 37 | } 38 | 39 | foreach ($resps as $resp) { 40 | if ($resp->getStatusCode() !== Response::HTTP_ACCEPTED) { 41 | throw new TransportException('Invalid response code', $resp->getStatusCode()); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Util/DoctrineTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Util; 14 | 15 | use Doctrine\ORM\EntityManager; 16 | 17 | /** 18 | * Requires a property doctrine or type Doctrine\Persistence\ManagerRegistry to be present 19 | */ 20 | trait DoctrineTrait 21 | { 22 | protected function getEM(): EntityManager 23 | { 24 | /** @var EntityManager $em */ 25 | $em = $this->doctrine->getManager(); 26 | 27 | return $em; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Util/HttpDownloaderOptionsFactory.php: -------------------------------------------------------------------------------- 1 | }, prevent_ip_access_callable: (callable(string): bool), max_file_size: int} 12 | */ 13 | public static function getOptions(): array 14 | { 15 | $options['http']['header'][] = 'User-Agent: Packagist.org'; 16 | $options['prevent_ip_access_callable'] = fn (string $ip) => IpUtils::isPrivateIp($ip); 17 | $options['max_file_size'] = 128_000_000; 18 | 19 | Platform::putEnv('COMPOSER_MAX_PARALLEL_HTTP', '99'); 20 | 21 | return $options; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Util/Killswitch.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Util; 14 | 15 | /** 16 | * Allows easily disabling some functionality 17 | */ 18 | class Killswitch 19 | { 20 | // package metadata update and other background workers 21 | public const WORKERS_ENABLED = true; 22 | 23 | // dependent/suggester counts and pages 24 | public const LINKS_ENABLED = true; 25 | 26 | // download stats pages (global and per package) 27 | public const DOWNLOADS_ENABLED = true; 28 | 29 | // package page details (security advisories, forms, dependent/suggester counts) 30 | public const PAGE_DETAILS_ENABLED = true; 31 | 32 | // package pages 33 | public const PAGES_ENABLED = true; 34 | 35 | /** 36 | * Silly workaround to avoid phpstan reporting "this condition is always true/false" when using the constants directly 37 | * @param self::* $feature 38 | */ 39 | public static function isEnabled(bool $feature): bool 40 | { 41 | return $feature; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Validator/Copyright.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Attribute; 16 | use Symfony\Component\Validator\Constraint; 17 | 18 | #[Attribute(Attribute::TARGET_CLASS)] 19 | class Copyright extends Constraint 20 | { 21 | public string $message = ''; 22 | 23 | public function getTargets(): string 24 | { 25 | return self::CLASS_CONSTRAINT; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Validator/NotProhibitedPassword.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Attribute; 16 | use Symfony\Component\Validator\Constraint; 17 | 18 | #[Attribute(Attribute::TARGET_CLASS)] 19 | class NotProhibitedPassword extends Constraint 20 | { 21 | public string $message = 'Password should not match your email or any of your names.'; 22 | 23 | public function getTargets(): string 24 | { 25 | return self::CLASS_CONSTRAINT; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Validator/NotProhibitedPasswordValidator.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use App\Entity\User; 16 | use Symfony\Component\Form\Form; 17 | use Symfony\Component\Validator\Constraint; 18 | use Symfony\Component\Validator\ConstraintValidator; 19 | use Symfony\Component\Validator\Exception\UnexpectedTypeException; 20 | 21 | class NotProhibitedPasswordValidator extends ConstraintValidator 22 | { 23 | public function validate(mixed $value, Constraint $constraint): void 24 | { 25 | if (!$constraint instanceof NotProhibitedPassword) { 26 | throw new UnexpectedTypeException($constraint, NotProhibitedPassword::class); 27 | } 28 | 29 | if (!$value instanceof User) { 30 | throw new UnexpectedTypeException($value, User::class); 31 | } 32 | 33 | $form = $this->context->getRoot(); 34 | if (!$form instanceof Form) { 35 | throw new UnexpectedTypeException($form, Form::class); 36 | } 37 | 38 | $user = $value; 39 | $password = $form->get('plainPassword')->getData(); 40 | 41 | $prohibitedPasswords = [ 42 | $user->getEmail(), 43 | $user->getEmailCanonical(), 44 | $user->getUsername(), 45 | $user->getUsernameCanonical(), 46 | ]; 47 | 48 | foreach ($prohibitedPasswords as $prohibitedPassword) { 49 | if ($password === $prohibitedPassword) { 50 | $this->context 51 | ->buildViolation($constraint->message) 52 | ->atPath('plainPassword') 53 | ->addViolation(); 54 | 55 | return; 56 | } 57 | } 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Validator/NotReservedWord.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Attribute; 16 | use Symfony\Component\Validator\Constraint; 17 | 18 | #[Attribute(Attribute::TARGET_PROPERTY)] 19 | class NotReservedWord extends Constraint 20 | { 21 | public string $message = 'This is a reserved word.'; 22 | 23 | public function getTargets(): string 24 | { 25 | return self::PROPERTY_CONSTRAINT; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Validator/NotReservedWordValidator.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use App\Entity\User; 16 | use Symfony\Component\Form\Form; 17 | use Symfony\Component\Validator\Constraint; 18 | use Symfony\Component\Validator\ConstraintValidator; 19 | use Symfony\Component\Validator\Exception\UnexpectedTypeException; 20 | 21 | class NotReservedWordValidator extends ConstraintValidator 22 | { 23 | public function validate(mixed $value, Constraint $constraint): void 24 | { 25 | if (!$constraint instanceof NotReservedWord) { 26 | throw new UnexpectedTypeException($constraint, NotReservedWord::class); 27 | } 28 | 29 | if (!is_string($value)) { 30 | return; 31 | } 32 | 33 | $reservedWords = [ 34 | 'composer', 35 | 'packagist', 36 | 'php', 37 | 'automation', // used to describe background workers doing things automatically in audit log 38 | ]; 39 | 40 | foreach ($reservedWords as $reservedWord) { 41 | if ($reservedWord === mb_strtolower($value)) { 42 | $this->context 43 | ->buildViolation($constraint->message) 44 | ->addViolation(); 45 | 46 | return; 47 | } 48 | } 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Validator/Password.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Attribute; 16 | use Symfony\Component\Validator\Constraints\Compound; 17 | use Symfony\Component\Validator\Constraints as Assert; 18 | 19 | #[Attribute(Attribute::TARGET_PROPERTY)] 20 | class Password extends Compound 21 | { 22 | /** 23 | * @param array $options 24 | */ 25 | protected function getConstraints(array $options): array 26 | { 27 | return [ 28 | new Assert\NotBlank([ 29 | 'message' => 'Please enter a password', 30 | ]), 31 | new Assert\Type('string'), 32 | new Assert\Length([ 33 | 'min' => 8, 34 | 'minMessage' => 'Your password should be at least {{ limit }} characters', 35 | // max length allowed by Symfony for security reasons 36 | 'max' => 4096, 37 | ]), 38 | new Assert\NotCompromisedPassword(skipOnError: true), 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Validator/PopularPackageSafety.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Attribute; 16 | use Symfony\Component\Validator\Constraint; 17 | 18 | #[Attribute(Attribute::TARGET_CLASS)] 19 | class PopularPackageSafety extends Constraint 20 | { 21 | public string $message = 'This package is very popular and URL editing has been disabled for security reasons. Please add a note on the old repo pointing to the new one if possible then get in touch at contact@packagist.org so we can get it sorted.'; 22 | 23 | public function getTargets(): string 24 | { 25 | return self::CLASS_CONSTRAINT; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Validator/RateLimitingRecaptcha.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Symfony\Component\Validator\Constraint; 16 | 17 | /** 18 | * Internal class used in InvisibleRecaptchaType directly, do not use 19 | */ 20 | class RateLimitingRecaptcha extends Constraint 21 | { 22 | public const INVALID_RECAPTCHA_ERROR = 'invalid-recaptcha'; 23 | 24 | public const INVALID_RECAPTCHA_MESSAGE = 'Invalid ReCaptcha.'; 25 | public const MISSING_RECAPTCHA_MESSAGE = 'We detected too many failed attempts. Please try again with ReCaptcha.'; 26 | 27 | // !! Must be set on the InvisibleRecaptchaType options 28 | // If this is set to true, the RecaptchaHelper::increaseCounter must be called on failure (typically wrong password) to trigger the recaptcha enforcement after X attempts 29 | // by default (false) recaptcha will always be required 30 | public bool $onlyShowAfterIncrementTrigger = false; 31 | } 32 | -------------------------------------------------------------------------------- /src/Validator/RateLimitingRecaptchaValidator.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use App\Security\RecaptchaHelper; 16 | use Beelab\Recaptcha2Bundle\Recaptcha\RecaptchaException; 17 | use Beelab\Recaptcha2Bundle\Recaptcha\RecaptchaVerifier; 18 | use Symfony\Component\Validator\Constraint; 19 | use Symfony\Component\Validator\ConstraintValidator; 20 | 21 | class RateLimitingRecaptchaValidator extends ConstraintValidator 22 | { 23 | public function __construct( 24 | private readonly RecaptchaHelper $recaptchaHelper, 25 | private readonly RecaptchaVerifier $recaptchaVerifier, 26 | ) {} 27 | 28 | /** 29 | * @param RateLimitingRecaptcha $constraint 30 | */ 31 | public function validate(mixed $value, Constraint $constraint): void 32 | { 33 | $context = $this->recaptchaHelper->buildContext(); 34 | 35 | if ($constraint->onlyShowAfterIncrementTrigger && !$this->recaptchaHelper->requiresRecaptcha($context)) { 36 | return; 37 | } 38 | 39 | try { 40 | $this->recaptchaVerifier->verify(); 41 | } catch (RecaptchaException) { 42 | $this->context 43 | ->buildViolation($context->hasRecaptcha ? RateLimitingRecaptcha::INVALID_RECAPTCHA_MESSAGE : RateLimitingRecaptcha::MISSING_RECAPTCHA_MESSAGE) 44 | ->setCode(RateLimitingRecaptcha::INVALID_RECAPTCHA_ERROR) 45 | ->addViolation(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Validator/TwoFactorCode.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface; 16 | use Symfony\Component\Validator\Constraint; 17 | 18 | class TwoFactorCode extends Constraint 19 | { 20 | public function __construct( 21 | public readonly TwoFactorInterface $user, 22 | public readonly string $message = 'Invalid authenticator code', 23 | ) { 24 | parent::__construct(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Validator/TwoFactorCodeValidator.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface; 16 | use Symfony\Component\Validator\Constraint; 17 | use Symfony\Component\Validator\ConstraintValidator; 18 | 19 | class TwoFactorCodeValidator extends ConstraintValidator 20 | { 21 | public function __construct( 22 | private readonly TotpAuthenticatorInterface $totpAuthenticator, 23 | ) {} 24 | 25 | /** 26 | * @param TwoFactorCode $constraint 27 | */ 28 | public function validate(mixed $value, Constraint $constraint): void 29 | { 30 | if (!$this->totpAuthenticator->checkCode($constraint->user, (string) $value)) { 31 | $this->context->addViolation($constraint->message); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Validator/TypoSquatters.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Attribute; 16 | use Symfony\Component\Validator\Constraint; 17 | 18 | #[Attribute(Attribute::TARGET_CLASS)] 19 | class TypoSquatters extends Constraint 20 | { 21 | public string $message = 'Your package name "{{ name }}" is blocked as its name is too close to "{{ existing }}"'; 22 | 23 | public function getTargets(): string 24 | { 25 | return self::CLASS_CONSTRAINT; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Validator/UniquePackage.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Attribute; 16 | use Symfony\Component\Validator\Constraint; 17 | 18 | #[Attribute(Attribute::TARGET_CLASS)] 19 | class UniquePackage extends Constraint 20 | { 21 | public string $message = ''; 22 | 23 | public function getTargets(): string 24 | { 25 | return self::CLASS_CONSTRAINT; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Validator/ValidPackageRepository.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Attribute; 16 | use Symfony\Component\Validator\Constraint; 17 | 18 | #[Attribute(Attribute::TARGET_CLASS)] 19 | class ValidPackageRepository extends Constraint 20 | { 21 | public string $message = ''; 22 | 23 | public function getTargets(): string 24 | { 25 | return self::CLASS_CONSTRAINT; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Validator/VendorWritable.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Validator; 14 | 15 | use Attribute; 16 | use Symfony\Component\Validator\Constraint; 17 | 18 | #[Attribute(Attribute::TARGET_CLASS)] 19 | class VendorWritable extends Constraint 20 | { 21 | public string $message = ''; 22 | 23 | public function getTargets(): string 24 | { 25 | return self::CLASS_CONSTRAINT; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /templates/base_nolayout.html.twig: -------------------------------------------------------------------------------- 1 | {% block content %}{% endblock %} -------------------------------------------------------------------------------- /templates/bundles/TwigBundle/Exception/error404.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |

Oh noes, 404!

8 |

It looks like you requested a page that was not found.

9 |

Go back to the homepage or use the search above to find the package you want.

10 |
11 |
12 |
13 | {% endblock %} 14 | 15 | {% block script_init %} 16 | 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /templates/email/alert_change.txt.twig: -------------------------------------------------------------------------------- 1 | {% autoescape false -%} 2 | 3 | A critical account security change has been made to your account: 4 | 5 | ------------------------------- 6 | Change: {{ reason }} 7 | Time: {{ 'now'|date('c') }} 8 | ------------------------------- 9 | 10 | {%- endautoescape %} 11 | -------------------------------------------------------------------------------- /templates/email/maintainer_added.txt.twig: -------------------------------------------------------------------------------- 1 | {% autoescape false -%} 2 | 3 | You have been added to package {{ package_name }} as a maintainer. 4 | 5 | {{ url('view_package', { 'name': package_name }) }} 6 | 7 | {%- endautoescape %} 8 | -------------------------------------------------------------------------------- /templates/email/two_factor_disabled.txt.twig: -------------------------------------------------------------------------------- 1 | {% autoescape false -%} 2 | 3 | Two-factor authentication has been disabled on your Packagist account. 4 | 5 | ------------------------------- 6 | Reason: {{ reason }} 7 | Time: {{ 'now'|date('c') }} 8 | ------------------------------- 9 | 10 | You can re-enable this at any time from your account page: {{ url('user_2fa_configure', { 'name': username }) }} 11 | 12 | {%- endautoescape %} 13 | -------------------------------------------------------------------------------- /templates/email/two_factor_enabled.txt.twig: -------------------------------------------------------------------------------- 1 | {% autoescape false -%} 2 | 3 | Two-factor authentication has been enabled on your Packagist account. 4 | 5 | You can disable this at any time from your account page: {{ url('user_2fa_configure', { 'name': username }) }} 6 | 7 | {%- endautoescape %} 8 | -------------------------------------------------------------------------------- /templates/email/update_failed.txt.twig: -------------------------------------------------------------------------------- 1 | {% autoescape false -%} 2 | The {{ package.name }} package of which you are a maintainer has 3 | failed to update due to invalid data contained in your composer.json. 4 | Please address this as soon as possible since the package stopped updating. 5 | 6 | It is recommended that you use `composer validate` to check for errors when you 7 | change your composer.json. 8 | 9 | Below is the update log which should highlight errors as 10 | "Skipped branch ...": 11 | 12 | [{{ exception }}]: {{ exceptionMessage }} 13 | 14 | {{ details }} 15 | 16 | -- 17 | If you do not wish to receive such emails in the future you can disable 18 | notifications on your profile page: https://packagist.org/profile/edit 19 | {%- endautoescape %} 20 | -------------------------------------------------------------------------------- /templates/explore/explore.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% import "macros.html.twig" as macros %} 4 | 5 | {% block content %} 6 | {% block content_title %}

{{ 'explore.title'|trans }}

{% endblock %} 7 | {% block lists %} 8 |
9 |
10 |

{{ 'explore.newreleases'|trans }} RSS

11 | 12 | {{ macros.listPackagesShort(newlyReleased, true) }} 13 |
14 | 15 |
16 |

{{ 'explore.newpackages'|trans }} RSS

17 | 18 | {{ macros.listPackagesShort(newlySubmitted) }} 19 |
20 | 21 |
22 | 23 |
24 |

{{ 'explore.popularpackages'|trans }} {{ 'listing.viewall'|trans }}

25 | 26 | {{ macros.listPackagesShort(popular, false, true) }} 27 |
28 | 29 |
30 |

{{ 'explore.randompackages'|trans }}

31 | 32 | {{ macros.listPackagesShort(random) }} 33 |
34 |
35 | {% endblock %} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /templates/explore/popular.html.twig: -------------------------------------------------------------------------------- 1 | {% embed "web/list.html.twig" %} 2 | {% block content_title %} 3 |

{{ 'explore.popularpackages'|trans }}

4 | {% endblock %} 5 | {% endembed %} 6 | -------------------------------------------------------------------------------- /templates/extensions/list.html.twig: -------------------------------------------------------------------------------- 1 | {% embed "web/list.html.twig" %} 2 | {% block title %}PHP Extensions - Packagist{% endblock %} 3 | {% block description %}List of all PHP extensions installable via pie{% endblock %} 4 | {% block content_title %} 5 |

PHP Extensions installable via pie

6 | {% endblock %} 7 | {% endembed %} 8 | -------------------------------------------------------------------------------- /templates/feed/feeds.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block content %} 4 |

Atom/RSS Feeds

5 | 6 |

Global Feeds

7 |

Newly Submitted Packages: RSS, Atom

8 |

New Releases: RSS, Atom

9 | 10 |

Vendor Feed

11 |

New Releases for a specific vendor namespace: {{ url('feed_vendor', {vendor: 'XXX', _format: 'rss'})|replace({XXX: '%vendor%'}) }}

12 |

Replace %vendor% by the vendor name, and change rss to atom if you would like an atom feed.

13 | 14 |

Package Feed

15 |

New Releases for a specific package: {{ url('feed_package', {package: 'X/X', _format: 'rss'})|replace({'X/X': '%vendor/package%'}) }}

16 |

Replace %vendor/package% by the package name, and change rss to atom if you would like an atom feed.

17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/forms.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'form_div_layout.html.twig' %} 2 | 3 | {% block textarea_widget -%} 4 | {% set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) %} 5 | {{- parent() -}} 6 | {%- endblock textarea_widget %} 7 | 8 | {% block form_widget_simple -%} 9 | {% if type is not defined or type not in ['file', 'hidden'] %} 10 | {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%} 11 | {% endif %} 12 | {%- set type = type|default('text') -%} 13 | {{- parent() -}} 14 | {%- endblock form_widget_simple %} 15 | 16 | {% block form_row -%} 17 |
18 | {{- form_label(form) }} {# -#} 19 | {{ form_widget(form) }} {# -#} 20 | {%- for error in errors -%} 21 | 22 | {{- 23 | error.messagePluralization is null 24 | ? error.messageTemplate|trans(error.messageParameters, 'validators') 25 | : error.messageTemplate|trans(error.messageParameters|merge({count: error.messagePluralization}), 'validators') 26 | -}} 27 | 28 | {%- endfor -%} 29 |
30 | {%- endblock form_row %} 31 | 32 | {% block form_errors -%} 33 | {% if errors|length > 0 -%} 34 | {%- for error in errors -%} 35 |
{{ error.message }}
36 | {%- endfor -%} 37 | {%- endif %} 38 | {%- endblock form_errors %} 39 | 40 | {# Used by InvisibleRecaptchaType #} 41 | {% block invisible_recaptcha_widget -%} 42 | {% if only_show_after_increment_trigger == false or requires_recaptcha() %} 43 | {# @see layout.html.twig for onloadRecaptchaCallback #} 44 | 45 | 46 | {% endif %} 47 | {%- endblock %} 48 | -------------------------------------------------------------------------------- /templates/package/abandon.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block title %}{{ package.name }} - {{ parent() }}{% endblock %} 4 | 5 | {% block content %} 6 |

7 | {{ package.vendor }}/{{ package.packageName }} 8 |

9 | 10 |
11 | {{ form_start(form, { attr: { class: 'col-sm-6' } }) }} 12 | {{ form_widget(form) }} 13 | 14 | 15 | {{ form_end(form) }} 16 | 17 |
18 | {{ 'abandon.warning'|trans|raw }} 19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/package/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% block title %} 4 | {{ 'edit.page_title'|trans({ '%name%':package.name }) }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |

{{ 'edit.title'|trans({ '%name%':package.name }) }}

9 | 10 |
11 | {{ form_start(form, { attr: { class: 'col-md-6' } }) }} 12 | {{ form_widget(form) }} 13 | 14 | 15 | {{ form_end(form) }} 16 |
17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/package/providers.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "web/list.html.twig" %} 2 | 3 | {% block content_title %}

{{ 'packages.providers_title'|trans({ '%name%': name }) }}

{% endblock %} 4 | -------------------------------------------------------------------------------- /templates/package/security_advisories.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% set showSearchDesc = 'hide' %} 4 | 5 | {% block head_additions %}{% endblock %} 6 | 7 | {% block title %}{{ 'packages.security_advisory_title'|trans }} - {{ name }} - {{ parent() }}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 |

14 | {{ name }} 15 | {{ 'packages.security_advisories'|trans }} 16 | {% if version is defined %} 17 | for {{ version }} 18 | {% endif %} 19 | ({{ count }}) 20 |

21 |
22 |
23 |
24 | 25 | {% include 'package/_security_advisory_list.html.twig' %} 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /templates/package/security_advisory.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% set showSearchDesc = 'hide' %} 4 | 5 | {% block head_additions %}{% endblock %} 6 | 7 | {% block title %}{{ 'packages.security_advisory_title'|trans }} - {{ id }} - {{ parent() }}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 |

14 | {{ id }} Security Advisory 15 |

16 |
17 |
18 |
19 | 20 | {% include 'package/_security_advisory_list.html.twig' with {withPackage: true} %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/package/spam.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% set showSearchDesc = 'hide' %} 4 | 5 | {% block head_additions %}{% endblock %} 6 | 7 | {% block title %}Suspect Packages - {{ parent() }}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 |

14 | Suspect Packages 15 | ({{ count }}) 16 |

17 |
18 |
19 |
20 |
21 | {% for p in packages %} 22 | 23 | {% endfor %} 24 | 25 | 26 |
27 |
28 |
29 | 30 |
31 |
32 | {% embed "web/list.html.twig" with {noLayout: 'true', showAutoUpdateWarning: false} %} 33 | {% block content_title %} 34 | {% endblock %} 35 | {% endembed %} 36 |
37 |
38 |
39 |
40 |
41 | {% for p in packages %} 42 | 43 | {% endfor %} 44 | 45 | 46 |
47 |
48 |
49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /templates/package/stats_base.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% set showSearchDesc = 'hide' %} 4 | 5 | {% block head_additions %}{% endblock %} 6 | 7 | {% block content %} 8 | {% set packageCount = 0 %} 9 | 10 |
11 |
12 |
13 |

14 | {{ package.name }} statistics 15 | Package Installs{#--#} 16 | PHP Versions 17 |

18 |
19 |
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/package/submit_package.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% set showSearchDesc = 'hide' %} 4 | 5 | {% block content %} 6 |

{{ 'submit.title'|trans }}

7 | 8 |
9 | {{ form_start(form, { attr: { class: 'col-md-6', 'data-check-url': path('submit.fetch_info'), id: 'submit-package-form' } }) }} 10 | {{ form_widget(form) }} 11 | 12 | {{ form_rest(form) }} 13 | 14 | 15 | 16 |
17 |

Trying to share private code?

18 |

Use Private Packagist to share code through Composer without publishing it for everyone on Packagist.org.

19 | {{ form_end(form) }} 20 | 21 |
22 | {{ 'submit.guide'|trans({ '%path_about%': path('about') })|raw }} 23 |
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /templates/package/suggesters.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% set showSearchDesc = 'hide' %} 4 | 5 | {% block head_additions %}{% endblock %} 6 | 7 | {% block title %}{{ 'packages.suggesters_title'|trans }} - {{ name }} - {{ parent() }}{% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |
13 |

14 | {{ name }} {{ 'packages.suggesters'|trans }} 15 | ({{ count }}) 16 |

17 |
18 |
19 |
20 | 21 |
22 |
23 | {% embed "web/list.html.twig" with {noLayout: 'true', showAutoUpdateWarning: false} %} 24 | {% block content_title %} 25 | {% endblock %} 26 | {% endembed %} 27 |
28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /templates/package/view_vendor.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "web/list.html.twig" %} 2 | 3 | {% block head_feeds %} 4 | 5 | {{ parent() }} 6 | {% endblock %} 7 | 8 | {% block content_title %}

{{ 'packages.from'|trans({ '%vendor%':vendor }) }}

{% endblock %} 9 | -------------------------------------------------------------------------------- /templates/registration/confirmation_email.html.twig: -------------------------------------------------------------------------------- 1 |

Hi,

2 | 3 |

4 | Please confirm your email address by clicking the following link:

5 | Confirm my Email.

6 | This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}. 7 |

8 | 9 |

Best Regards

10 |

The Packagist.org Team

11 | -------------------------------------------------------------------------------- /templates/registration/confirmation_email.txt.twig: -------------------------------------------------------------------------------- 1 | Hi, 2 | 3 | Please confirm your email address by clicking the following link: 4 | 5 | {{ signedUrl }} 6 | 7 | This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}. 8 | 9 | Best Regards 10 | The Packagist.org Team 11 | -------------------------------------------------------------------------------- /templates/registration/register.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'user/layout.html.twig' %} 2 | 3 | {% block title %}Create account - {{ parent() }}{% endblock %} 4 | 5 | {% block user_content %} 6 |
7 |

8 | Create account 9 |

10 |
11 | 12 | {% for flashError in app.flashes('verify_email_error') %} 13 | 14 | {% endfor %} 15 | 16 | {{ form_start(registrationForm, {attr: {class: "register col-md-6", id: "register_form"}}) }} 17 |
18 | 19 |
20 | {{ form_errors(registrationForm.email) }} 21 | {{ form_widget(registrationForm.email) }} 22 | 23 |
24 |
25 | 26 |
27 | 28 |
29 | {{ form_errors(registrationForm.username) }} 30 | {{ form_widget(registrationForm.username) }} 31 | 32 |
33 |
34 | 35 |
36 | 37 |
38 | {{ form_errors(registrationForm.plainPassword) }} 39 | {{ form_widget(registrationForm.plainPassword) }} 40 | 41 |
42 |
43 | 44 | 45 | {{ form_end(registrationForm) }} 46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /templates/reset_password/check_email.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'user/layout.html.twig' %} 2 | 3 | {% block title %}Password Reset Email Sent - {{ parent() }}{% endblock %} 4 | 5 | {% block user_content %} 6 |

7 | An email has been sent that contains a link that you can click to reset your password. 8 | This link will expire in 24 hours. 9 |

10 |

If you don't receive an email please check your spam folder or try again.

11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /templates/reset_password/email.txt.twig: -------------------------------------------------------------------------------- 1 | Hi, 2 | 3 | To reset your password, please visit the following link: 4 | 5 | {{ url('do_pwd_reset', {token: token}) }} 6 | 7 | This link will expire in 24 hours. 8 | 9 | Best Regards 10 | The Packagist.org Team 11 | -------------------------------------------------------------------------------- /templates/reset_password/request.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'user/layout.html.twig' %} 2 | 3 | {% block title %}Reset your password - {{ parent() }}{% endblock %} 4 | 5 | {% block user_content %} 6 | {% for flashError in app.flashes('reset_password_error') %} 7 | 8 | {% endfor %} 9 | 10 | {{ form_start(requestForm, {attr: {class: "request_pwd_reset col-md-6", id: "request_password"}}) }} 11 |
12 | {{ form_label(requestForm.email) }} 13 |
14 | {{ form_widget(requestForm.email) }} 15 | 16 |
17 |
18 | 19 |
Enter your email address, and we will send you a link to reset your password.
20 | 21 | 22 | {{ form_end(requestForm) }} 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /templates/reset_password/reset.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'user/layout.html.twig' %} 2 | 3 | {% block title %}Reset your password - {{ parent() }}{% endblock %} 4 | 5 | {% block user_content %} 6 |

Reset your password

7 | 8 | {{ form_start(resetForm, {attr: {id: 'reset_password'}}) }} 9 | {{ form_row(resetForm.plainPassword) }} 10 | {% if resetForm.twoFactorCode is defined %} 11 | {{ form_row(resetForm.twoFactorCode) }} 12 | {% endif %} 13 | 14 | {{ form_end(resetForm) }} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /templates/user/change_password.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "user/layout.html.twig" %} 2 | 3 | {% block user_content %} 4 | {{ form_start(form, { 'action': path('change_password'), 'attr': { 'class': 'change_password col-md-6', 'id': 'change_password' } }) }} 5 | {{ form_errors(form) }} 6 | 7 |
8 | {{ form_label(form.current_password) }} 9 |
10 | {{ form_errors(form.current_password) }} 11 | {{ form_widget(form.current_password) }} 12 | 13 |
14 |
15 | 16 |
17 | {{ form_label(form.plainPassword) }} 18 |
19 | {{ form_errors(form.plainPassword) }} 20 | {{ form_widget(form.plainPassword) }} 21 | 22 |
23 |
24 | 25 | {{ form_widget(form) }} 26 | 27 | 28 | {{ form_end(form) }} 29 | {% endblock user_content %} 30 | -------------------------------------------------------------------------------- /templates/user/favorites.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "user/packages.html.twig" %} 2 | 3 | {% import "macros.html.twig" as macros %} 4 | 5 | {% block content %} 6 | {% set isActualUser = app.user and app.user.username is same as(user.username) %} 7 | 8 |

9 | {{ user.username }} 10 | {%- if not isActualUser %} 11 | 12 | member since: {{ user.createdAt|date('M d, Y') }} 13 | {%- if is_granted('ROLE_ADMIN') %} 14 | {{ user.email }} 15 | {%- endif %} 16 | 17 | {%- endif %} 18 |

19 | 20 |
21 | {% if isActualUser %} 22 |
23 | {{ knp_menu_render('profile_menu', {currentClass: 'active', allow_safe_labels: true}) }} 24 |
25 | {% endif %} 26 | 27 |
28 | {% embed "web/list.html.twig" with {noLayout: 'true', showAutoUpdateWarning: isActualUser} %} 29 | {% block content_title %} 30 |

{{ (isActualUser ? 'packages.my_favorites' : 'packages.users_favorites')|trans({ '%user%': user.username }) }}

31 | {% endblock %} 32 | {% endembed %} 33 |
34 |
35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /templates/user/layout.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block content %} 4 | {% if app.user is null %} 5 |
6 | {{ block('user_content') }} 7 |
8 | {% else %} 9 |

10 | {{ app.user.username }} 11 |

12 | 13 |
14 |
15 | {{ knp_menu_render('profile_menu', {'currentClass': 'active', 'allow_safe_labels': true}) }} 16 |
17 | 18 | {{ block('user_content') }} 19 |
20 | {% endif %} 21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /templates/user/packages.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% import "macros.html.twig" as macros %} 4 | 5 | 6 | {% block content %} 7 | {% set isActualUser = app.user and app.user.username is same as(user.username) %} 8 |

9 | {{ user.username }} 10 | {%- if not isActualUser %} 11 | 12 | member since: {{ user.createdAt|date('M d, Y') }} 13 | {%- if is_granted('ROLE_ADMIN') %} 14 | {{ user.email }} 15 | {%- endif %} 16 | 17 | {%- endif %} 18 |

19 | 20 |
21 | {% if isActualUser %} 22 |
23 | {{ knp_menu_render('profile_menu', {'currentClass': 'active', 'allow_safe_labels': true}) }} 24 |
25 | {% endif %} 26 | 27 |
28 | {% embed "web/list.html.twig" with {noLayout: 'true', showAutoUpdateWarning: isActualUser} %} 29 | {% block content_title %} 30 |

{{ (isActualUser ? 'packages.mine' : 'packages.maintained_by')|trans({ '%user%': user.username }) }}

31 | {% endblock %} 32 | {% endembed %} 33 |
34 |
35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /templates/user/public_profile.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% import "macros.html.twig" as macros %} 4 | 5 | {% block head_additions %} 6 | {% if user.hasRole('ROLE_SPAMMER') %} 7 | 8 | {% endif %} 9 | {% endblock %} 10 | 11 | {% block content %} 12 | {% set isActualUser = app.user and app.user.username is same as(user.username) %} 13 |

14 | {{ user.username }} 15 | 16 | {% if is_granted('ROLE_ANTISPAM') and user.hasRole('ROLE_SPAMMER') %} 17 | [SPAMMER] 18 | {% endif %} 19 | {{ 'user.member_since'|trans }}: {{ user.createdAt|date('M d, Y') }} 20 | {%- if is_granted('ROLE_ADMIN') %} 21 | {{ user.email }} 22 | {%- endif %} 23 | {%- if deleteForm is defined and (is_granted('ROLE_ADMIN') or isActualUser) %} 24 |
25 | {{ form_widget(deleteForm._token) }} 26 | 27 |
28 | {%- endif %} 29 | {%- if is_granted('ROLE_ANTISPAM') %} 30 |
31 | {{ form_widget(spammerForm._token) }} 32 | 33 |
34 | {%- endif %} 35 |
36 |

37 | 38 |
39 |
40 | {% embed "web/list.html.twig" with {noLayout: 'true', showAutoUpdateWarning: isActualUser} %} 41 | {% block content_title %} 42 |

{{ 'user.packages'|trans({ '%username%':user.username }) }}

43 | {% endblock %} 44 | {% endembed %} 45 |
46 |
47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /templates/web/list.html.twig: -------------------------------------------------------------------------------- 1 | {% extends noLayout|default(false) ? "base_nolayout.html.twig" : "layout.html.twig" %} 2 | 3 | {% import "macros.html.twig" as macros %} 4 | 5 | {% block content %} 6 | {% block content_title %}

{{ 'listing.title'|trans }}

{% endblock %} 7 | 8 | {% block list %} 9 | {% if packages|length %} 10 | {{ macros.listPackages(packages, paginate is not defined or paginate, showAutoUpdateWarning|default(false), meta|default(null)) }} 11 | {% else %} 12 |
13 |

{{ 'listing.nopackages'|trans }}

14 |
15 | {% endif %} 16 | {% endblock %} 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /templates/web/php_stats.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "web/stats_base.html.twig" %} 2 | 3 | {% block title %}PHP Version Statistics - {{ parent() }}{% endblock %} 4 | 5 | {% block content %} 6 | {{ parent() }} 7 | 8 | {% if type == 'phpplatform' %} 9 |

Platform PHP Stats are made of the config.platform.php version if it is set, and the effective PHP version otherwise.

10 | {% endif %} 11 | 12 |
13 |
14 |

Package installations by PHP minor version, daily

15 | 16 |

17 | 18 | Sorry, the graph can't be displayed because your browser doesn't support <svg> html element. 19 | 20 |

21 |
22 |
23 | 24 |
25 |
26 |

Package installations by PHP minor version, monthly

27 | 28 |

29 | 30 | Sorry, the graph can't be displayed because your browser doesn't support <svg> html element. 31 | 32 |

33 |
34 |
35 | {% endblock %} 36 | 37 | {% block stylesheets %} 38 | 39 | {% endblock %} 40 | 41 | {% block scripts %} 42 | 43 | 49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /templates/web/search.html.twig: -------------------------------------------------------------------------------- 1 | {% embed "web/list.html.twig" %} 2 | {% block content %} 3 | {% endblock %} 4 | {% endembed %} 5 | -------------------------------------------------------------------------------- /templates/web/search_section.html.twig: -------------------------------------------------------------------------------- 1 | 23 | -------------------------------------------------------------------------------- /templates/web/stats_base.html.twig: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.twig" %} 2 | 3 | {% set showSearchDesc = 'hide' %} 4 | 5 | {% block head_additions %}{% endblock %} 6 | 7 | {% block content %} 8 | {% set packageCount = 0 %} 9 | 10 |

11 | {{ 'statistics.title'|trans }} 12 | Package Installs{#--#} 13 | PHP Versions{#--#} 14 | {# Platform PHP Versions #} 15 |

16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /tests/Controller/AboutControllerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Tests\Controller; 14 | 15 | class AboutControllerTest extends ControllerTestCase 16 | { 17 | public function testPackagist(): void 18 | { 19 | $crawler = $this->client->request('GET', '/about'); 20 | static::assertResponseIsSuccessful(); 21 | static::assertEquals('What is Packagist?', $crawler->filter('h2.title')->first()->text()); 22 | } 23 | 24 | public function testComposerRedirect(): void 25 | { 26 | $this->client->request('GET', '/about-composer'); 27 | static::assertResponseRedirects('https://getcomposer.org/', 301); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Controller/FeedControllerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Tests\Controller; 14 | 15 | use PHPUnit\Framework\Attributes\DataProvider; 16 | use Symfony\Component\Routing\Generator\UrlGeneratorInterface; 17 | 18 | class FeedControllerTest extends ControllerTestCase 19 | { 20 | #[DataProvider('provideForFeed')] 21 | public function testFeedAction(string $feed, string $format, ?string $vendor = null): void 22 | { 23 | $url = static::getContainer()->get(UrlGeneratorInterface::class)->generate($feed, ['_format' => $format, 'vendor' => $vendor]); 24 | 25 | $this->client->request('GET', $url); 26 | 27 | $response = $this->client->getResponse(); 28 | $this->assertEquals(200, $response->getStatusCode(), $response->getContent()); 29 | $this->assertStringContainsString($format, $response->getContent()); 30 | 31 | if ($vendor !== null) { 32 | $this->assertStringContainsString($vendor, $response->getContent()); 33 | } 34 | } 35 | 36 | public static function provideForFeed(): array 37 | { 38 | return [ 39 | ['feed_packages', 'rss'], 40 | ['feed_packages', 'atom'], 41 | ['feed_releases', 'rss'], 42 | ['feed_releases', 'atom'], 43 | ['feed_vendor', 'rss', 'symfony'], 44 | ['feed_vendor', 'atom', 'symfony'], 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Controller/PackageControllerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Tests\Controller; 14 | 15 | use App\Entity\Package; 16 | use App\Search\Query; 17 | use App\Tests\Search\AlgoliaMock; 18 | 19 | class PackageControllerTest extends ControllerTestCase 20 | { 21 | public function testView(): void 22 | { 23 | $package = self::createPackage('test/pkg', 'https://example.com/test/pkg'); 24 | $this->store($package); 25 | 26 | $crawler = $this->client->request('GET', '/packages/test/pkg'); 27 | self::assertResponseIsSuccessful(); 28 | self::assertSame('composer require test/pkg', $crawler->filter('.requireme input')->attr('value')); 29 | } 30 | 31 | public function testEdit(): void 32 | { 33 | $user = self::createUser(); 34 | $package = self::createPackage('test/pkg', 'https://example.com/test/pkg', maintainers: [$user]); 35 | 36 | $this->store($user, $package); 37 | 38 | $this->client->loginUser($user); 39 | 40 | $crawler = $this->client->request('GET', '/packages/test/pkg'); 41 | self::assertResponseIsSuccessful(); 42 | self::assertSame('example.com/test/pkg', $crawler->filter('.canonical')->text()); 43 | 44 | $form = $crawler->selectButton('Edit')->form(); 45 | $crawler = $this->client->submit($form); 46 | 47 | self::assertResponseIsSuccessful(); 48 | 49 | $form = $crawler->selectButton('Update')->form(['form[repository]' => 'https://github.com/composer/composer']); 50 | $this->client->submit($form); 51 | self::assertResponseRedirects(); 52 | $crawler = $this->client->followRedirect(); 53 | 54 | self::assertResponseIsSuccessful(); 55 | self::assertSame('github.com/composer/composer', $crawler->filter('.canonical')->text()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Controller/UserControllerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Tests\Controller; 14 | 15 | use App\Entity\User; 16 | use App\Tests\Mock\TotpAuthenticatorStub; 17 | 18 | class UserControllerTest extends ControllerTestCase 19 | { 20 | public function testEnableTwoFactorCode(): void 21 | { 22 | $user = self::createUser(); 23 | $this->store($user); 24 | 25 | $this->client->loginUser($user); 26 | 27 | $crawler = $this->client->request('GET', sprintf('/users/%s/2fa/enable', $user->getUsername())); 28 | $form = $crawler->selectButton('Enable Two-Factor Authentication')->form(); 29 | $form->setValues([ 30 | 'enable_two_factor_auth[code]' => 123456, 31 | ]); 32 | 33 | $crawler = $this->client->submit($form); 34 | $this->assertResponseStatusCodeSame(422); 35 | 36 | $form = $crawler->selectButton('Enable Two-Factor Authentication')->form(); 37 | $form->setValues([ 38 | 'enable_two_factor_auth[code]' => TotpAuthenticatorStub::MOCKED_VALID_CODE, 39 | ]); 40 | 41 | $this->client->submit($form); 42 | $this->assertResponseStatusCodeSame(302); 43 | 44 | $em = self::getEM(); 45 | $em->clear(); 46 | $this->assertTrue($em->getRepository(User::class)->find($user->getId())->isTotpAuthenticationEnabled()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Controller/responses/search-with-query-tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [ 3 | { 4 | "name": "phpunit/php-code-coverage", 5 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 6 | "url": "http://localhost/packages/phpunit/php-code-coverage", 7 | "repository": "https://github.com/sebastianbergmann/php-code-coverage", 8 | "downloads": 0, 9 | "favers": 0 10 | } 11 | ], 12 | "total": 1 13 | } 14 | -------------------------------------------------------------------------------- /tests/Controller/responses/search-with-query-tags.json: -------------------------------------------------------------------------------- 1 | { 2 | "results": [ 3 | { 4 | "name": "phpspec/prophecy", 5 | "description": "Highly opinionated mocking framework for PHP 5.3+", 6 | "url": "http://localhost/packages/phpspec/prophecy", 7 | "repository": "https://github.com/phpspec/prophecy", 8 | "downloads": 0, 9 | "favers": 0 10 | }, 11 | { 12 | "name": "phpunit/php-code-coverage", 13 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 14 | "url": "http://localhost/packages/phpunit/php-code-coverage", 15 | "repository": "https://github.com/sebastianbergmann/php-code-coverage", 16 | "downloads": 0, 17 | "favers": 0 18 | } 19 | ], 20 | "total": 2 21 | } 22 | -------------------------------------------------------------------------------- /tests/Entity/TagTest.php: -------------------------------------------------------------------------------- 1 | isDev()); 19 | } 20 | 21 | public static function provideValidNames(): array 22 | { 23 | return [ 24 | ['dev'], 25 | ['testing'], 26 | ['static analysis'], 27 | ]; 28 | } 29 | 30 | #[DataProvider('provideInvalidNames')] 31 | public function testIsDevWithInvalidNames(string $name): void 32 | { 33 | $tag = new Tag($name); 34 | 35 | self::assertFalse($tag->isDev()); 36 | } 37 | 38 | public static function provideInvalidNames(): array 39 | { 40 | return [ 41 | ['orm'], 42 | ['project'], 43 | ['static-analysis'], 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Entity/VersionTest.php: -------------------------------------------------------------------------------- 1 | addTag(new Tag($tag)); 21 | } 22 | 23 | self::assertTrue($version->hasDevTag()); 24 | } 25 | 26 | public static function provideValidDevTagSets(): array 27 | { 28 | return [ 29 | 'only dev' => [['dev']], 30 | 'dev first' => [['dev', 'database']], 31 | 'dev last' => [['database', 'dev']], 32 | 'dev middle' => [['orm', 'dev', 'database']], 33 | 'multiple' => [['dev', 'testing']], 34 | ]; 35 | } 36 | #[DataProvider('provideInvalidDevTagSets')] 37 | public function testHasDevTagWithout(array $tags): void 38 | { 39 | $version = new Version(); 40 | 41 | foreach ($tags as $tag) { 42 | $version->addTag(new Tag($tag)); 43 | } 44 | 45 | self::assertFalse($version->hasDevTag()); 46 | } 47 | 48 | public static function provideInvalidDevTagSets(): array 49 | { 50 | return [ 51 | 'none' => [[]], 52 | 'one' => [['orm']], 53 | 'two' => [['database', 'orm']], 54 | 'three' => [['currency', 'database', 'clock']], 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/Mock/TotpAuthenticatorStub.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Tests\Mock; 14 | 15 | use Scheb\TwoFactorBundle\Model\Totp\TwoFactorInterface; 16 | use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpAuthenticatorInterface; 17 | use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Totp\TotpFactory; 18 | use ParagonIE\ConstantTime\Base32; 19 | 20 | class TotpAuthenticatorStub implements TotpAuthenticatorInterface 21 | { 22 | public const MOCKED_VALID_CODE = '999999'; 23 | 24 | public function __construct( 25 | private readonly TotpFactory $totpFactory, 26 | ) {} 27 | 28 | public function checkCode(TwoFactorInterface $user, string $code): bool 29 | { 30 | return $code === self::MOCKED_VALID_CODE; 31 | } 32 | 33 | public function getQRContent(TwoFactorInterface $user): string 34 | { 35 | return $this->totpFactory->createTotpForUser($user)->getProvisioningUri(); 36 | } 37 | 38 | public function generateSecret(): string 39 | { 40 | return Base32::encodeUpperUnpadded(random_bytes(32)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Model/PackageManagerTest.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Tests\Model; 14 | 15 | use App\Entity\Package; 16 | use PHPUnit\Framework\TestCase; 17 | 18 | class PackageManagerTest extends TestCase 19 | { 20 | public function testNotifyFailure(): void 21 | { 22 | $this->markTestSkipped('Do it!'); 23 | 24 | $client = self::createClient(); 25 | 26 | $package = new Package; 27 | $package->setRepository($url); 28 | 29 | $user = new User; 30 | $user->addPackage($package); 31 | 32 | $repo = $this->createMock('App\Entity\UserRepository'); 33 | $em = $this->createMock('Doctrine\ORM\EntityManager'); 34 | $updater = $this->createMock('App\Package\Updater'); 35 | 36 | $repo->expects($this->once()) 37 | ->method('findOneBy') 38 | ->with($this->equalTo(['username' => 'test', 'apiToken' => 'token'])) 39 | ->will($this->returnValue($user)); 40 | 41 | static::$kernel->getContainer()->set('test.user_repo', $repo); 42 | static::$kernel->getContainer()->set('doctrine.orm.entity_manager', $em); 43 | static::$kernel->getContainer()->set('App\Package\Updater', $updater); 44 | 45 | $payload = json_encode(['repository' => ['url' => 'git://github.com/composer/composer']]); 46 | $client->request('POST', '/api/github?username=test&apiToken=token', ['payload' => $payload]); 47 | $this->assertEquals(202, $client->getResponse()->getStatusCode()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Search/AlgoliaMock.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Tests\Search; 14 | 15 | use Algolia\AlgoliaSearch\SearchClient; 16 | use App\Search\Query; 17 | use PHPUnit\Framework\Assert; 18 | use Symfony\Bundle\FrameworkBundle\KernelBrowser; 19 | 20 | final class AlgoliaMock extends SearchClient 21 | { 22 | private Query $query; 23 | private array $result; 24 | 25 | public static function setup(KernelBrowser $client, Query $query, string $resultName): self 26 | { 27 | $mock = (new \ReflectionClass(__CLASS__))->newInstanceWithoutConstructor(); 28 | $mock->query = $query; 29 | 30 | if (false === $result = @include __DIR__.'/results/'.$resultName.'.php') { 31 | throw new \InvalidArgumentException('Result set with name '.$resultName.' is not available.'); 32 | } 33 | 34 | $mock->result = $result; 35 | 36 | $client->getContainer()->set(SearchClient::class, $mock); 37 | 38 | return $mock; 39 | } 40 | 41 | /** 42 | * @override \Algolia\AlgoliaSearch\SearchClient::initIndex 43 | */ 44 | public function initIndex($indexName): self 45 | { 46 | return $this; 47 | } 48 | 49 | /** 50 | * @override \Algolia\AlgoliaSearch\SearchIndex::search 51 | */ 52 | public function search($query, $requestOptions = []): array 53 | { 54 | $queryMessage = sprintf('AlgoliaMock expected query string \'%s\', but got \'%s\'.', $this->query->query, $query); 55 | Assert::assertSame($this->query->query, $query, $queryMessage); 56 | Assert::assertSame($this->query->getOptions(), $requestOptions, 'AlgoliaMock expected different request options.'); 57 | 58 | return $this->result; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Search/transformed/search-paged.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | return [ 14 | 'results' => [[ 15 | 'name' => 'symfony/event-dispatcher', 16 | 'description' => 'Provides tools that allow your application components to communicate with each other by dispatching events and listening to them', 17 | 'url' => 'http://localhost/packages/symfony/event-dispatcher', 18 | 'repository' => 'https://github.com/symfony/event-dispatcher', 19 | 'downloads' => 0, 20 | 'favers' => 0, 21 | ], [ 22 | 'name' => 'sebastian/version', 23 | 'description' => 'Library that helps with managing the version number of Git-hosted PHP projects', 24 | 'url' => 'http://localhost/packages/sebastian/version', 25 | 'repository' => 'https://github.com/sebastianbergmann/version', 26 | 'downloads' => 0, 27 | 'favers' => 0, 28 | ], [ 29 | 'name' => 'sebastian/recursion-context', 30 | 'description' => 'Provides functionality to recursively process PHP variables', 31 | 'url' => 'http://localhost/packages/sebastian/recursion-context', 32 | 'repository' => 'https://github.com/sebastianbergmann/recursion-context', 33 | 'downloads' => 0, 34 | 'favers' => 0, 35 | ]], 36 | 'total' => 11, 37 | 'next' => 'http://localhost/search.json?q=pro&page=3&per_page=3', 38 | ]; 39 | -------------------------------------------------------------------------------- /tests/Search/transformed/search-with-abandoned.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | return [ 14 | 'results' => [[ 15 | 'name' => 'fzaninotto/faker', 16 | 'description' => 'Faker is a PHP library that generates fake data for you.', 17 | 'url' => 'http://localhost/packages/fzaninotto/faker', 18 | 'repository' => 'https://github.com/fzaninotto/faker', 19 | 'downloads' => 0, 20 | 'favers' => 0, 21 | 'abandoned' => true, 22 | ], [ 23 | 'name' => 'swiftmailer/swiftmailer', 24 | 'description' => 'Swiftmailer, free feature-rich PHP mailer', 25 | 'url' => 'http://localhost/packages/swiftmailer/swiftmailer', 26 | 'repository' => 'https://github.com/swiftmailer/swiftmailer', 27 | 'downloads' => 0, 28 | 'favers' => 0, 29 | 'abandoned' => 'symfony/mailer', 30 | ]], 31 | 'total' => 2, 32 | ]; 33 | -------------------------------------------------------------------------------- /tests/Search/transformed/search-with-query-tag.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | return [ 14 | 'results' => [[ 15 | 'name' => 'phpunit/php-code-coverage', 16 | 'description' => 'Library that provides collection, processing, and rendering functionality for PHP code coverage information.', 17 | 'url' => 'http://localhost/packages/phpunit/php-code-coverage', 18 | 'repository' => 'https://github.com/sebastianbergmann/php-code-coverage', 19 | 'downloads' => 0, 20 | 'favers' => 0, 21 | ]], 22 | 'total' => 1, 23 | ]; 24 | -------------------------------------------------------------------------------- /tests/Search/transformed/search-with-query-tags.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | return [ 14 | 'results' => [[ 15 | 'name' => 'phpspec/prophecy', 16 | 'description' => 'Highly opinionated mocking framework for PHP 5.3+', 17 | 'url' => 'http://localhost/packages/phpspec/prophecy', 18 | 'repository' => 'https://github.com/phpspec/prophecy', 19 | 'downloads' => 0, 20 | 'favers' => 0, 21 | ], [ 22 | 'name' => 'phpunit/php-code-coverage', 23 | 'description' => 'Library that provides collection, processing, and rendering functionality for PHP code coverage information.', 24 | 'url' => 'http://localhost/packages/phpunit/php-code-coverage', 25 | 'repository' => 'https://github.com/sebastianbergmann/php-code-coverage', 26 | 'downloads' => 0, 27 | 'favers' => 0, 28 | ]], 29 | 'total' => 2, 30 | ]; 31 | -------------------------------------------------------------------------------- /tests/Search/transformed/search-with-virtual.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | return [ 14 | 'results' => [[ 15 | 'name' => 'symfony/event-dispatcher-implementation', 16 | 'description' => '', 17 | 'url' => 'http://localhost/providers/symfony/event-dispatcher-implementation', 18 | 'repository' => '', 19 | 'virtual' => true, 20 | ]], 21 | 'total' => 1, 22 | ]; 23 | -------------------------------------------------------------------------------- /tests/SecurityAdvisory/AdvisoryParserTest.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace App\Tests\SecurityAdvisory; 14 | 15 | use App\SecurityAdvisory\AdvisoryParser; 16 | use PHPUnit\Framework\Attributes\DataProvider; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class AdvisoryParserTest extends TestCase 20 | { 21 | #[DataProvider('cveProvider')] 22 | public function testIsValidCve(bool $expected, string $cve): void 23 | { 24 | $this->assertSame($expected, AdvisoryParser::isValidCve($cve)); 25 | } 26 | 27 | public static function cveProvider(): array 28 | { 29 | return [ 30 | [true, 'CVE-2022-99999'], 31 | [false, 'CVE-2022-xxxx'], 32 | ]; 33 | } 34 | 35 | #[DataProvider('titleProvider')] 36 | public function test(string $expected, string $title): void 37 | { 38 | $this->assertSame($expected, AdvisoryParser::titleWithoutCve($title)); 39 | } 40 | 41 | public static function titleProvider(): array 42 | { 43 | return [ 44 | ['CSRF token missing in forms', 'CVE-2022-99999999999: CSRF token missing in forms'], 45 | ['CSRF token missing in forms', 'CVE-2022-xxxx: CSRF token missing in forms'], 46 | ['CSRF token missing in forms', 'CVE-2022-XXXX: CSRF token missing in forms'], 47 | ['CSRF token missing in forms', 'CVE-2022-xxxx-2: CSRF token missing in forms'], 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/SecurityAdvisory/SeverityTest.php: -------------------------------------------------------------------------------- 1 | 9 | * Nils Adermann 10 | * 11 | * For the full copyright and license information, please view the LICENSE 12 | * file that was distributed with this source code. 13 | */ 14 | 15 | use App\SecurityAdvisory\Severity; 16 | use PHPUnit\Framework\Attributes\DataProvider; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class SeverityTest extends TestCase 20 | { 21 | #[DataProvider('gitHubSeverityProvider')] 22 | public function testFromGitHub(?string $gitHubSeverity, ?Severity $expected): void 23 | { 24 | $this->assertSame($expected, Severity::fromGitHub($gitHubSeverity)); 25 | } 26 | 27 | public static function gitHubSeverityProvider(): iterable 28 | { 29 | yield ['CRITICAL', Severity::CRITICAL]; 30 | yield ['HIGH', Severity::HIGH]; 31 | yield ['MODERATE', Severity::MEDIUM]; 32 | yield ['LOW', Severity::LOW]; 33 | yield [null, null]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use Symfony\Component\Dotenv\Dotenv; 14 | 15 | require dirname(__DIR__).'/vendor/autoload.php'; 16 | 17 | if (file_exists(dirname(__DIR__).'/config/bootstrap.php')) { 18 | require dirname(__DIR__).'/config/bootstrap.php'; 19 | } elseif (method_exists(Dotenv::class, 'bootEnv')) { 20 | (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); 21 | } 22 | 23 | // hack for PHPUnit 11, see https://github.com/symfony/symfony/issues/53812 24 | set_exception_handler([new Symfony\Component\ErrorHandler\ErrorHandler(), 'handleException']); 25 | 26 | if ($_SERVER['APP_DEBUG']) { 27 | umask(0000); 28 | } 29 | 30 | /** 31 | * Executes a given command. 32 | * 33 | * @param string $command a command to execute 34 | * 35 | * @throws Exception when the return code is not 0. 36 | */ 37 | function executeCommand(string $command, bool $errorHandling = true): void { 38 | $output = []; 39 | 40 | $returnCode = null; 41 | 42 | exec($command, $output, $returnCode); 43 | 44 | if ($errorHandling && $returnCode !== 0) { 45 | throw new Exception( 46 | sprintf( 47 | 'Error executing command "%s", return code was "%s".', 48 | $command, 49 | $returnCode 50 | ) 51 | ); 52 | } 53 | } 54 | 55 | if (!getenv('QUICK')) { 56 | echo 'For quicker test runs without a fresh DB schema, prefix the test command with a QUICK=1 env var.' . PHP_EOL; 57 | 58 | executeCommand('php ./bin/console doctrine:database:drop --env=test --force -q', false); 59 | executeCommand('php ./bin/console doctrine:database:create --env=test -q'); 60 | executeCommand('php ./bin/console doctrine:schema:create --env=test -q'); 61 | executeCommand('php ./bin/console redis:query flushall --env=test -n -q'); 62 | } 63 | 64 | \Composer\Util\Platform::putEnv('PACKAGIST_TESTS_ARE_RUNNING', '1'); 65 | -------------------------------------------------------------------------------- /tests/console-application.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use App\Kernel; 14 | use Symfony\Bundle\FrameworkBundle\Console\Application; 15 | use Symfony\Component\Dotenv\Dotenv; 16 | 17 | require __DIR__ . '/../vendor/autoload.php'; 18 | 19 | (new Dotenv())->bootEnv(__DIR__ . '/../.env'); 20 | 21 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 22 | 23 | return new Application($kernel); 24 | -------------------------------------------------------------------------------- /tests/object-manager.php: -------------------------------------------------------------------------------- 1 | 7 | * Nils Adermann 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | use App\Kernel; 14 | use Symfony\Component\Dotenv\Dotenv; 15 | 16 | require __DIR__ . '/../vendor/autoload.php'; 17 | 18 | (new Dotenv())->bootEnv(__DIR__ . '/../.env'); 19 | 20 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 21 | $kernel->boot(); 22 | 23 | return $kernel->getContainer()->get('doctrine')->getManager(); 24 | -------------------------------------------------------------------------------- /translations/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composer/packagist/375bd0956f80b245978b69f15eac0874860840a3/translations/.gitignore -------------------------------------------------------------------------------- /web/app.php: -------------------------------------------------------------------------------- 1 | 2 | 4 | Packagist 5 | Packagist PHP Package Search 6 | Use Packagist.org to search for PHP packages. 7 | packagist composer php 8 | contact@packagist.org 9 | 10 | 11 | en-us 12 | UTF-8 13 | UTF-8 14 | https://packagist.org/apple-touch-icon.png 15 | https://packagist.org/favicon.ico 16 | https://packagist.org/ 17 | 18 | -------------------------------------------------------------------------------- /web/static-error/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404 Not Found - Packagist 6 | 7 | 8 | 9 | 10 | 40 | 41 | 42 | 49 |
50 |

404 Not Found :(

51 |

Try somewhere else?

52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /web/static-error/404.json: -------------------------------------------------------------------------------- 1 | "404 not found, no packages here" 2 | -------------------------------------------------------------------------------- /web/static-error/502.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 502 Bad Gateway - Packagist 6 | 7 | 8 | 9 | 10 | 40 | 41 | 42 | 48 |
49 |

502 Bad Gateway :(

50 |

Looks like this mirror node temporarily lost its connection to the primary servers. Please try again in a bit.

51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /web/static-error/503.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 503 Too Much Going On - Packagist 6 | 7 | 8 | 9 | 10 | 40 | 41 | 42 | 48 |
49 |

503 Gateway Timeout :(

50 |

Most likely we are under heavy load right now. Please try again in a bit.

51 |
52 | 53 | 54 | -------------------------------------------------------------------------------- /web/touch-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/composer/packagist/375bd0956f80b245978b69f15eac0874860840a3/web/touch-icon-192x192.png --------------------------------------------------------------------------------