├── .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 |
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 |
9 |
10 |
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 |
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 |
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 |
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 |
18 |
19 |
20 |
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 |
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 |
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 |
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 |
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 | {{ flashError }}
14 | {% endfor %}
15 |
16 | {{ form_start(registrationForm, {attr: {class: "register col-md-6", id: "register_form"}}) }}
17 |
25 |
26 |
34 |
35 |
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 | {{ flashError }}
8 | {% endfor %}
9 |
10 | {{ form_start(requestForm, {attr: {class: "request_pwd_reset col-md-6", id: "request_password"}}) }}
11 |
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 |
15 |
16 |
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 |
28 | {%- endif %}
29 | {%- if is_granted('ROLE_ANTISPAM') %}
30 |
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 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Package installations by PHP minor version, monthly
27 |
28 |
29 |
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 |
2 |
3 |
12 |
13 | {%- if showSearchDesc == 'show' %}
14 |
15 |
16 |
 }})
17 |
{{ 'search.claim_html'|trans|raw }}
18 |
19 |
20 | {%- endif %}
21 |
22 |
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 | {#--#}
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 |
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
--------------------------------------------------------------------------------