├── .dockerignore ├── .env ├── .env.test ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── assets ├── .gitignore ├── images │ ├── logo_black.png │ └── logo_white.png ├── js │ └── app.js └── scss │ ├── _variables.scss │ └── app.scss ├── bin ├── composer ├── console ├── doctrine ├── doctrine-dbal ├── jsonlint ├── phpstan-console-loader.php ├── phpstan-doctrine-loader.php ├── sql-formatter ├── symfony_requirements ├── validate-json ├── var-dump-server └── yaml-lint ├── composer-require-checker.json ├── composer.json ├── composer.lock ├── config ├── bootstrap.php ├── bundles.php ├── packages │ ├── assets.yaml │ ├── cache.yaml │ ├── dev │ │ ├── framework.yaml │ │ ├── monolog.yaml │ │ ├── routing.yaml │ │ └── web_profiler.yaml │ ├── doctrine.yaml │ ├── doctrine_migrations.yaml │ ├── framework.yaml │ ├── github_api.yaml │ ├── knp_paginator.yaml │ ├── nyholm_psr7.yaml │ ├── prod │ │ ├── doctrine.yaml │ │ └── monolog.yaml │ ├── routing.yaml │ ├── security.yaml │ ├── sensio_framework_extra.yaml │ ├── test │ │ ├── framework.yaml │ │ ├── monolog.yaml │ │ └── web_profiler.yaml │ ├── translation.yaml │ ├── twig.yaml │ └── webpack_encore.yaml ├── routes.yaml ├── routes │ └── dev │ │ ├── framework.yaml │ │ └── web_profiler.yaml └── services.yaml ├── docker-compose.yml ├── docker └── php │ └── docker-healthcheck.sh ├── docs ├── README.md └── _config.yml ├── package.json ├── phpcs.xml ├── phpstan-baseline.neon ├── phpstan.neon ├── phpunit.xml ├── public ├── .htaccess └── index.php ├── src ├── .htaccess ├── Collection │ ├── PackageCollection.php │ └── VersionCollection.php ├── Command │ ├── PackagesQueueCommand.php │ └── PackagesUpdateCommand.php ├── Composer │ ├── ComposerManager.php │ └── ConfigFactory.php ├── Controller │ ├── ApiController.php │ ├── ClientController.php │ ├── DefaultController.php │ ├── DownloadController.php │ ├── LoginController.php │ ├── PackageController.php │ ├── ProfileController.php │ ├── RepositoryController.php │ ├── SetupController.php │ └── UserController.php ├── Domain │ └── Role.php ├── Entity │ ├── Client.php │ ├── Common │ │ ├── AbstractEntity.php │ │ ├── Entity.php │ │ ├── EntityId.php │ │ ├── Modified.php │ │ └── ModifiedAt.php │ ├── Download.php │ ├── Package.php │ ├── UpdateQueue.php │ ├── User.php │ └── Version.php ├── EventListener │ ├── EntityListener.php │ ├── ErrorListener.php │ ├── PackageListener.php │ └── SetupListener.php ├── Form │ ├── Type │ │ ├── Forms │ │ │ ├── ClientType.php │ │ │ ├── FilterType.php │ │ │ ├── PackageAbandonType.php │ │ │ ├── PackageType.php │ │ │ ├── UserPasswordType.php │ │ │ ├── UserProfileType.php │ │ │ ├── UserSetupType.php │ │ │ └── UserType.php │ │ └── Widgets │ │ │ ├── NewPasswordType.php │ │ │ └── PackageTypeType.php │ └── Validator │ │ └── PackageValidator.php ├── Infrastructure │ ├── ArgumentResolver │ │ └── UserValueResolver.php │ ├── Error │ │ └── InvalidArgumentTypeError.php │ └── Migrations │ │ ├── Version20201225173740.php │ │ ├── Version20201226154212.php │ │ ├── Version20201229113210.php │ │ ├── Version20210101213326.php │ │ └── Version20210102150832.php ├── Kernel.php ├── Model │ └── PackageAdapter.php ├── Repository │ ├── ClientRepository.php │ ├── DownloadRepository.php │ ├── PackageRepository.php │ ├── UpdateQueueRepository.php │ ├── UserRepository.php │ └── VersionRepository.php ├── Security │ ├── ApiProvider.php │ ├── Checker │ │ └── UserChecker.php │ └── Guard │ │ └── Authenticator │ │ ├── ApiAuthenticator.php │ │ └── LoginFormAuthenticator.php ├── Service │ ├── DistSynchronization.php │ ├── FlashBagHelper.php │ ├── PackageSynchronization.php │ ├── PackagesDumper.php │ └── RepositoryHelper.php └── Twig │ └── Extension │ └── DevliverExtension.php ├── symfony.lock ├── templates ├── .gitkeep ├── _partials │ ├── bootstrap_5_horizontal_layout.html.twig │ ├── bootstrap_5_layout.html.twig │ ├── footer.html.twig │ ├── header.html.twig │ └── pagination.html.twig ├── client │ ├── add.html.twig │ ├── edit.html.twig │ └── index.html.twig ├── default │ └── howto.html.twig ├── layout.html.twig ├── login │ └── login.html.twig ├── package │ ├── _details.html.twig │ ├── abandon.html.twig │ ├── add.html.twig │ ├── edit.html.twig │ ├── index.html.twig │ └── view.html.twig ├── profile │ ├── base.html.twig │ ├── change_password.html.twig │ └── edit.html.twig ├── setup │ └── index.html.twig └── user │ ├── add.html.twig │ ├── edit.html.twig │ └── index.html.twig ├── tests ├── .gitkeep ├── Collection │ ├── PackageCollectionTest.php │ └── VersionCollectionTest.php └── bootstrap.php ├── translations ├── .gitkeep ├── FOSUserBundle.de.yml ├── messages.de.yml └── messages.en.yml ├── webpack.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/*.md 3 | **/*.php~ 4 | **/._* 5 | **/.dockerignore 6 | **/.DS_Store 7 | **/.git/ 8 | **/.gitattributes 9 | **/.gitignore 10 | **/.gitmodules 11 | **/docker-compose.*.yaml 12 | **/docker-compose.*.yml 13 | **/docker-compose.yaml 14 | **/docker-compose.yml 15 | **/Dockerfile 16 | **/Thumbs.db 17 | .editorconfig 18 | .env.*.local 19 | .env.test 20 | .env.local 21 | .env.local.php 22 | .phpcs.cache 23 | composer-require-checker.json 24 | Makefile 25 | LICENSE 26 | phpstan.neon 27 | phpstan-baseline.neon 28 | phpcs.xml 29 | phpunit.xml 30 | bin/* 31 | !bin/console 32 | .github/ 33 | var/ 34 | node_modules/ 35 | vendor/ 36 | tests/ -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | LOCALE=en 2 | 3 | ###> symfony/framework-bundle ### 4 | APP_ENV=prod 5 | APP_SECRET=6iys723i2ypn3ew7bhf9tzx44vzxw4bh 6 | ###< symfony/framework-bundle ### 7 | 8 | DATABASE_NAME=devliver 9 | DATABASE_USER=devliver 10 | DATABASE_PASSWORD=devliver 11 | DATABASE_HOST=database 12 | DATABASE_PORT=3306 13 | DATABASE_URL=mysql://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME} 14 | ###< doctrine/doctrine-bundle ### 15 | 16 | ###> knplabs/github-api ### 17 | GITHUB_AUTH_METHOD=http_password 18 | GITHUB_USERNAME=username 19 | GITHUB_SECRET=password_or_token 20 | ###< knplabs/github-api ### 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .web-server-pid 2 | .idea/ 3 | .phpcs.cache 4 | docker-compose.override.yml 5 | build/ 6 | parameters.yml 7 | version 8 | composer/ 9 | data/ 10 | 11 | ###> symfony/framework-bundle ### 12 | /.env.local 13 | /.env.local.php 14 | public/bundles/ 15 | public/vendor/ 16 | var/ 17 | /vendor/ 18 | ###< symfony/framework-bundle ### 19 | 20 | ###> symfony/webpack-encore-bundle ### 21 | /node_modules/ 22 | /public/build/ 23 | npm-debug.log 24 | yarn-error.log 25 | ###< symfony/webpack-encore-bundle ### 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@shapecode.de. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM thecodingmachine/php:8.0-v4-apache-node14 2 | 3 | ENV TEMPLATE_PHP_INI="production" 4 | 5 | ENV CRON_USER_1="docker" \ 6 | CRON_SCHEDULE_1="* * * * *" \ 7 | CRON_COMMAND_1="bin/console app:queue:execute" 8 | 9 | ENV STARTUP_COMMAND_1="bin/console cache:clear" \ 10 | STARTUP_COMMAND_2="bin/console doctrine:migrations:migrate --no-interaction" 11 | 12 | ENV APACHE_DOCUMENT_ROOT="public/" 13 | 14 | RUN touch /home/docker/.bashrc && printf '\ 15 | HISTFILE=~/bash_history\n\ 16 | PROMPT_COMMAND="history -a;history -n"\n\ 17 | umask 027\n' >> /home/docker/.bashrc 18 | 19 | COPY . /var/www/html/ 20 | RUN sudo chown -R docker:docker /var/www/html/ 21 | 22 | RUN composer install --no-dev --no-interaction --no-progress --classmap-authoritative && \ 23 | yarn install && \ 24 | yarn prod && \ 25 | sudo rm -rf assets docker node_modules tests 26 | 27 | VOLUME /var/www/html/ 28 | EXPOSE 80 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ARGS = $(filter-out $@,$(MAKECMDGOALS)) 2 | MAKEFLAGS += --silent 3 | 4 | list: 5 | sh -c "echo; $(MAKE) -p no_targets__ | awk -F':' '/^[a-zA-Z0-9][^\$$#\/\\t=]*:([^=]|$$)/ {split(\$$1,A,/ /);for(i in A)print A[i]}' | grep -v '__\$$' | grep -v 'Makefile'| sort" 6 | 7 | ############################# 8 | # Docker machine states 9 | ############################# 10 | 11 | build: 12 | docker-compose build 13 | 14 | up: 15 | docker-compose up -d 16 | 17 | down: 18 | docker-compose down 19 | 20 | start: 21 | docker-compose start 22 | 23 | stop: 24 | docker-compose stop 25 | 26 | state: 27 | docker-compose ps 28 | 29 | rebuild: 30 | docker-compose down -v 31 | docker-compose rm --force -v 32 | docker-compose build --pull 33 | docker-compose up -d --force-recreate 34 | 35 | ############################# 36 | # General 37 | ############################# 38 | 39 | bash: 40 | docker-compose exec app bash 41 | 42 | ############################# 43 | # Argument fix workaround 44 | ############################# 45 | %: 46 | @: 47 | -------------------------------------------------------------------------------- /assets/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicklog/devliver/e086cbc711a54c5c9c836a39ab5ccf00c7589d15/assets/.gitignore -------------------------------------------------------------------------------- /assets/images/logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicklog/devliver/e086cbc711a54c5c9c836a39ab5ccf00c7589d15/assets/images/logo_black.png -------------------------------------------------------------------------------- /assets/images/logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicklog/devliver/e086cbc711a54c5c9c836a39ab5ccf00c7589d15/assets/images/logo_white.png -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | // any CSS you require will output into a single css file (app.css in this case) 2 | require('../scss/app.scss'); 3 | 4 | require('@popperjs/core'); 5 | const bootstrap = window.bootstrap = require('bootstrap'); 6 | require('@tabler/core'); 7 | const hljs = require('highlight.js'); 8 | 9 | (function() { 10 | hljs.highlightAll(); 11 | 12 | let packageDetails = Object.values(document.getElementsByClassName('package-details')); 13 | let packageLinks = Object.values(document.getElementsByClassName('package-link')); 14 | 15 | packageLinks.forEach(link => { 16 | link.addEventListener('click', event => { 17 | event.preventDefault(); 18 | 19 | packageDetails.forEach(item => { 20 | item.style.display = 'none'; 21 | }) 22 | 23 | document.getElementById(link.getAttribute('data-package')).style.display = ''; 24 | }); 25 | }) 26 | 27 | // 28 | // $('[data-confirmation]').confirmation({ 29 | // rootSelector: '[data-confirmation]', 30 | // popout: true, 31 | // singleton: true, 32 | // btnOkLabel: 'Confirm', 33 | // btnCancelLabel: 'Cancel!', 34 | // btnOkClass: 'btn btn-xs btn-success', 35 | // btnCancelClass: 'btn btn-xs btn-danger', 36 | // title: 'Confirmation', 37 | // content: function () { 38 | // return $(this).data('confirmation'); 39 | // } 40 | // }); 41 | })(); 42 | -------------------------------------------------------------------------------- /assets/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | $input-btn-padding-y-sm: .4rem; 2 | $input-btn-padding-x-sm: .4rem; -------------------------------------------------------------------------------- /assets/scss/app.scss: -------------------------------------------------------------------------------- 1 | // vars 2 | @import "./variables"; 3 | 4 | //@import "~admin-lte/build/scss/adminlte"; 5 | @import "~@tabler/core/src/scss/tabler"; 6 | @import "~@fortawesome/fontawesome-free/css/all.css"; 7 | @import "~highlight.js/styles/github.css"; 8 | 9 | .hljs { 10 | background: transparent !important; 11 | } -------------------------------------------------------------------------------- /bin/composer: -------------------------------------------------------------------------------- 1 | ../vendor/composer/composer/bin/composer -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 19 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 20 | } 21 | 22 | if ($input->hasParameterOption('--no-debug', true)) { 23 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 24 | } 25 | 26 | require dirname(__DIR__).'/config/bootstrap.php'; 27 | 28 | if ($_SERVER['APP_DEBUG']) { 29 | umask(0000); 30 | 31 | if (class_exists(Debug::class)) { 32 | Debug::enable(); 33 | } 34 | } 35 | 36 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool)$_SERVER['APP_DEBUG']); 37 | $application = new Application($kernel); 38 | $application->run($input); 39 | -------------------------------------------------------------------------------- /bin/doctrine: -------------------------------------------------------------------------------- 1 | ../vendor/doctrine/orm/bin/doctrine -------------------------------------------------------------------------------- /bin/doctrine-dbal: -------------------------------------------------------------------------------- 1 | ../vendor/doctrine/dbal/bin/doctrine-dbal -------------------------------------------------------------------------------- /bin/jsonlint: -------------------------------------------------------------------------------- 1 | ../vendor/seld/jsonlint/bin/jsonlint -------------------------------------------------------------------------------- /bin/phpstan-console-loader.php: -------------------------------------------------------------------------------- 1 | boot(); 11 | 12 | return $kernel->getContainer()->get('doctrine')->getManager(); 13 | -------------------------------------------------------------------------------- /bin/sql-formatter: -------------------------------------------------------------------------------- 1 | ../vendor/doctrine/sql-formatter/bin/sql-formatter -------------------------------------------------------------------------------- /bin/symfony_requirements: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getPhpIniConfigPath(); 9 | 10 | echo_title('Symfony Requirements Checker'); 11 | 12 | echo '> PHP is using the following php.ini file:'.PHP_EOL; 13 | if ($iniPath) { 14 | echo_style('green', ' '.$iniPath); 15 | } else { 16 | echo_style('yellow', ' WARNING: No configuration file (php.ini) used by PHP!'); 17 | } 18 | 19 | echo PHP_EOL.PHP_EOL; 20 | 21 | echo '> Checking Symfony requirements:'.PHP_EOL.' '; 22 | 23 | $messages = array(); 24 | foreach ($symfonyRequirements->getRequirements() as $req) { 25 | if ($helpText = get_error_message($req, $lineSize)) { 26 | echo_style('red', 'E'); 27 | $messages['error'][] = $helpText; 28 | } else { 29 | echo_style('green', '.'); 30 | } 31 | } 32 | 33 | $checkPassed = empty($messages['error']); 34 | 35 | foreach ($symfonyRequirements->getRecommendations() as $req) { 36 | if ($helpText = get_error_message($req, $lineSize)) { 37 | echo_style('yellow', 'W'); 38 | $messages['warning'][] = $helpText; 39 | } else { 40 | echo_style('green', '.'); 41 | } 42 | } 43 | 44 | if ($checkPassed) { 45 | echo_block('success', 'OK', 'Your system is ready to run Symfony projects'); 46 | } else { 47 | echo_block('error', 'ERROR', 'Your system is not ready to run Symfony projects'); 48 | 49 | echo_title('Fix the following mandatory requirements', 'red'); 50 | 51 | foreach ($messages['error'] as $helpText) { 52 | echo ' * '.$helpText.PHP_EOL; 53 | } 54 | } 55 | 56 | if (!empty($messages['warning'])) { 57 | echo_title('Optional recommendations to improve your setup', 'yellow'); 58 | 59 | foreach ($messages['warning'] as $helpText) { 60 | echo ' * '.$helpText.PHP_EOL; 61 | } 62 | } 63 | 64 | echo PHP_EOL; 65 | echo_style('title', 'Note'); 66 | echo ' The command console could use a different php.ini file'.PHP_EOL; 67 | echo_style('title', '~~~~'); 68 | echo ' than the one used with your web server. To be on the'.PHP_EOL; 69 | echo ' safe side, please check the requirements from your web'.PHP_EOL; 70 | echo ' server using the '; 71 | echo_style('yellow', 'web/config.php'); 72 | echo ' script.'.PHP_EOL; 73 | echo PHP_EOL; 74 | 75 | exit($checkPassed ? 0 : 1); 76 | 77 | function get_error_message(Requirement $requirement, $lineSize) 78 | { 79 | if ($requirement->isFulfilled()) { 80 | return; 81 | } 82 | 83 | $errorMessage = wordwrap($requirement->getTestMessage(), $lineSize - 3, PHP_EOL.' ').PHP_EOL; 84 | $errorMessage .= ' > '.wordwrap($requirement->getHelpText(), $lineSize - 5, PHP_EOL.' > ').PHP_EOL; 85 | 86 | return $errorMessage; 87 | } 88 | 89 | function echo_title($title, $style = null) 90 | { 91 | $style = $style ?: 'title'; 92 | 93 | echo PHP_EOL; 94 | echo_style($style, $title.PHP_EOL); 95 | echo_style($style, str_repeat('~', strlen($title)).PHP_EOL); 96 | echo PHP_EOL; 97 | } 98 | 99 | function echo_style($style, $message) 100 | { 101 | // ANSI color codes 102 | $styles = array( 103 | 'reset' => "\033[0m", 104 | 'red' => "\033[31m", 105 | 'green' => "\033[32m", 106 | 'yellow' => "\033[33m", 107 | 'error' => "\033[37;41m", 108 | 'success' => "\033[37;42m", 109 | 'title' => "\033[34m", 110 | ); 111 | $supports = has_color_support(); 112 | 113 | echo($supports ? $styles[$style] : '').$message.($supports ? $styles['reset'] : ''); 114 | } 115 | 116 | function echo_block($style, $title, $message) 117 | { 118 | $message = ' '.trim($message).' '; 119 | $width = strlen($message); 120 | 121 | echo PHP_EOL.PHP_EOL; 122 | 123 | echo_style($style, str_repeat(' ', $width)); 124 | echo PHP_EOL; 125 | echo_style($style, str_pad(' ['.$title.']', $width, ' ', STR_PAD_RIGHT)); 126 | echo PHP_EOL; 127 | echo_style($style, $message); 128 | echo PHP_EOL; 129 | echo_style($style, str_repeat(' ', $width)); 130 | echo PHP_EOL; 131 | } 132 | 133 | function has_color_support() 134 | { 135 | static $support; 136 | 137 | if (null === $support) { 138 | if (DIRECTORY_SEPARATOR == '\\') { 139 | $support = false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI'); 140 | } else { 141 | $support = function_exists('posix_isatty') && @posix_isatty(STDOUT); 142 | } 143 | } 144 | 145 | return $support; 146 | } 147 | -------------------------------------------------------------------------------- /bin/validate-json: -------------------------------------------------------------------------------- 1 | ../vendor/justinrainbow/json-schema/bin/validate-json -------------------------------------------------------------------------------- /bin/var-dump-server: -------------------------------------------------------------------------------- 1 | ../vendor/symfony/var-dumper/Resources/bin/var-dump-server -------------------------------------------------------------------------------- /bin/yaml-lint: -------------------------------------------------------------------------------- 1 | ../vendor/symfony/yaml/Resources/bin/yaml-lint -------------------------------------------------------------------------------- /composer-require-checker.json: -------------------------------------------------------------------------------- 1 | { 2 | "symbol-whitelist": [ 3 | "null", 4 | "true", 5 | "false", 6 | "static", 7 | "self", 8 | "parent", 9 | "array", 10 | "string", 11 | "int", 12 | "float", 13 | "bool", 14 | "iterable", 15 | "callable", 16 | "void", 17 | "object" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /config/bootstrap.php: -------------------------------------------------------------------------------- 1 | =1.2) 9 | if (is_array($env = @include dirname(__DIR__).'/.env.local.php')) { 10 | $_SERVER += $env; 11 | $_ENV += $env; 12 | } elseif (!class_exists(Dotenv::class)) { 13 | throw new RuntimeException('Please run "composer require symfony/dotenv" to load the ".env" files configuring the application.'); 14 | } else { 15 | // load all the .env files 16 | (new Dotenv())->loadEnv(dirname(__DIR__).'/.env'); 17 | } 18 | 19 | $_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = ($_SERVER['APP_ENV'] ?? $_ENV['APP_ENV'] ?? null) ?: 'dev'; 20 | $_SERVER['APP_DEBUG'] = $_SERVER['APP_DEBUG'] ?? $_ENV['APP_DEBUG'] ?? 'prod' !== $_SERVER['APP_ENV']; 21 | $_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = (int) $_SERVER['APP_DEBUG'] || filter_var($_SERVER['APP_DEBUG'], FILTER_VALIDATE_BOOLEAN) ? '1' : '0'; 22 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 6 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], 7 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 8 | Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], 9 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], 10 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 11 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true, 'test' => true], 12 | Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], 13 | Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], 14 | SensioLabs\RichModelForms\RichModelFormsBundle::class => ['all' => true], 15 | Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true], 16 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], 17 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 18 | ]; 19 | -------------------------------------------------------------------------------- /config/packages/assets.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | assets: 3 | json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' 4 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | prefix_seed: devliver 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /config/packages/dev/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | validation: 3 | not_compromised_password: 4 | enabled: false 5 | -------------------------------------------------------------------------------- /config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: rotating_file 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: error 7 | channels: ["!event"] 8 | max_files: 14 9 | console: 10 | type: console 11 | process_psr_3_messages: false 12 | channels: ["!event", "!doctrine", "!console"] 13 | -------------------------------------------------------------------------------- /config/packages/dev/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: true 4 | -------------------------------------------------------------------------------- /config/packages/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { only_exceptions: false } 7 | -------------------------------------------------------------------------------- /config/packages/doctrine.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | env(DATABASE_URL): '' 3 | 4 | doctrine: 5 | dbal: 6 | driver: 'pdo_mysql' 7 | charset: utf8mb4 8 | logging: false 9 | default_table_options: 10 | charset: utf8mb4 11 | collate: utf8mb4_unicode_ci 12 | 13 | url: '%env(resolve:DATABASE_URL)%' 14 | 15 | types: 16 | datetime: Shapecode\Doctrine\DBAL\Types\DateTimeUTCType 17 | datetimeutc: Shapecode\Doctrine\DBAL\Types\DateTimeUTCType 18 | orm: 19 | auto_generate_proxy_classes: '%kernel.debug%' 20 | naming_strategy: doctrine.orm.naming_strategy.underscore 21 | auto_mapping: true 22 | mappings: 23 | App: 24 | is_bundle: false 25 | type: annotation 26 | dir: '%kernel.project_dir%/src/Entity' 27 | prefix: 'App\Entity' 28 | alias: App 29 | resolve_target_entities: 30 | Symfony\Component\Security\Core\User\UserInterface: App\Entity\User 31 | FOS\UserBundle\Model\UserInterface: App\Entity\User 32 | -------------------------------------------------------------------------------- /config/packages/doctrine_migrations.yaml: -------------------------------------------------------------------------------- 1 | doctrine_migrations: 2 | migrations_paths: 3 | 'App\Infrastructure\Migrations': 'src/Infrastructure/Migrations' 4 | 5 | all_or_nothing: true 6 | check_database_platform: false 7 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | secret: '%env(APP_SECRET)%' 3 | csrf_protection: true 4 | #http_method_override: true 5 | session: 6 | handler_id: session.handler.native_file 7 | save_path: '%kernel.project_dir%/var/sessions' 8 | # 9 | # router: 10 | # default_uri: '%env(string:HTTP_HOST)%' -------------------------------------------------------------------------------- /config/packages/github_api.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | Github\Client: 3 | class: Github\Client 4 | # Uncomment to enable authentication 5 | #calls: 6 | # - ['authenticate', ['%env(GITHUB_USERNAME)%', '%env(GITHUB_SECRET)%', '%env(GITHUB_AUTH_METHOD)%']] 7 | -------------------------------------------------------------------------------- /config/packages/knp_paginator.yaml: -------------------------------------------------------------------------------- 1 | knp_paginator: 2 | 3 | page_range: 5 4 | 5 | default_options: 6 | page_name: page 7 | sort_field_name: sort 8 | sort_direction_name: direction 9 | distinct: true 10 | 11 | template: 12 | pagination: '_partials/pagination.html.twig' 13 | sortable: '@KnpPaginator/Pagination/sortable_link.html.twig' 14 | filtration: '@KnpPaginator/Pagination/filtration.html.twig' -------------------------------------------------------------------------------- /config/packages/nyholm_psr7.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | public: false 4 | 5 | # Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories) 6 | Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory' 7 | Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory' 8 | Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory' 9 | Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory' 10 | Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory' 11 | Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory' 12 | 13 | nyholm.psr7.psr17_factory: 14 | class: Nyholm\Psr7\Factory\Psr17Factory 15 | -------------------------------------------------------------------------------- /config/packages/prod/doctrine.yaml: -------------------------------------------------------------------------------- 1 | doctrine: 2 | orm: 3 | metadata_cache_driver: 4 | type: service 5 | id: doctrine.system_cache_provider 6 | query_cache_driver: 7 | type: service 8 | id: doctrine.system_cache_provider 9 | result_cache_driver: 10 | type: service 11 | id: doctrine.result_cache_provider 12 | 13 | services: 14 | doctrine.result_cache_provider: 15 | class: Symfony\Component\Cache\DoctrineProvider 16 | public: false 17 | arguments: 18 | - '@doctrine.result_cache_pool' 19 | doctrine.system_cache_provider: 20 | class: Symfony\Component\Cache\DoctrineProvider 21 | public: false 22 | arguments: 23 | - '@doctrine.system_cache_pool' 24 | 25 | framework: 26 | cache: 27 | pools: 28 | doctrine.result_cache_pool: 29 | adapter: cache.app 30 | doctrine.system_cache_pool: 31 | adapter: cache.system 32 | -------------------------------------------------------------------------------- /config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_404s: 8 | # regex: exclude all 404 errors from the logs 9 | - ^/ 10 | nested: 11 | type: rotating_file 12 | path: "%kernel.logs_dir%/%kernel.environment%.log" 13 | level: debug 14 | max_files: 14 15 | console: 16 | type: console 17 | process_psr_3_messages: false 18 | channels: ["!event", "!doctrine"] 19 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: ~ 4 | -------------------------------------------------------------------------------- /config/packages/security.yaml: -------------------------------------------------------------------------------- 1 | security: 2 | 3 | role_hierarchy: 4 | ROLE_ADMIN: [ROLE_USER, ROLE_MANAGER, ROLE_REPO] 5 | ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] 6 | 7 | encoders: 8 | App\Entity\User: 'auto' 9 | App\Entity\Client: 'auto' 10 | 11 | providers: 12 | users: 13 | entity: 14 | class: App\Entity\User 15 | clients: 16 | entity: 17 | class: App\Entity\Client 18 | property: name 19 | api_provider: 20 | id: App\Security\ApiProvider 21 | 22 | firewalls: 23 | dev: 24 | pattern: ^/(_(profiler|wdt)|css|images|js)/ 25 | security: false 26 | # 27 | api: 28 | pattern: ^/api/(.*) 29 | context: api 30 | anonymous: false 31 | stateless: true 32 | logout: ~ 33 | provider: api_provider 34 | guard: 35 | authenticators: 36 | - App\Security\Guard\Authenticator\ApiAuthenticator 37 | # 38 | repo: 39 | pattern: ^/repo/(.*) 40 | context: repo 41 | anonymous: false 42 | stateless: true 43 | logout: ~ 44 | provider: clients 45 | http_basic: 46 | realm: Secured Area 47 | 48 | main: 49 | user_checker: App\Security\Checker\UserChecker 50 | pattern: ^/ 51 | context: user 52 | anonymous: true 53 | provider: users 54 | guard: 55 | authenticators: 56 | - App\Security\Guard\Authenticator\LoginFormAuthenticator 57 | logout: 58 | path: /logout 59 | target: / 60 | invalidate_session: true 61 | 62 | access_control: 63 | - { path: ^/packages.json, role: IS_AUTHENTICATED_ANONYMOUSLY } 64 | - { path: ^/track-downloads, role: IS_AUTHENTICATED_ANONYMOUSLY } 65 | - { path: ^/api, role: IS_AUTHENTICATED_ANONYMOUSLY } 66 | 67 | - { path: ^/setup, role: IS_AUTHENTICATED_ANONYMOUSLY } 68 | - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY } 69 | - { path: ^/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY } 70 | - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY } 71 | 72 | - { path: ^/repo/, role: ROLE_REPO } 73 | 74 | - { path: ^/.*, role: ROLE_USER } -------------------------------------------------------------------------------- /config/packages/sensio_framework_extra.yaml: -------------------------------------------------------------------------------- 1 | sensio_framework_extra: 2 | request: 3 | converters: true 4 | auto_convert: true 5 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: debug 7 | channels: ["!event"] 8 | -------------------------------------------------------------------------------- /config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /config/packages/translation.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | default_locale: '%locale%' 3 | translator: 4 | paths: 5 | - '%kernel.project_dir%/translations' 6 | fallbacks: 7 | - '%locale%' 8 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | paths: ['%kernel.project_dir%/templates'] 3 | debug: '%kernel.debug%' 4 | strict_variables: '%kernel.debug%' 5 | form_themes: 6 | - '_partials/bootstrap_5_layout.html.twig' 7 | date: 8 | format: d.m.Y H:i 9 | interval_format: '%%d days' -------------------------------------------------------------------------------- /config/packages/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | webpack_encore: 2 | # The path where Encore is building the assets - i.e. Encore.setOutputPath() 3 | output_path: '%kernel.project_dir%/public/build' 4 | # If multiple builds are defined (as shown below), you can disable the default build: 5 | # output_path: false 6 | 7 | # if using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials') 8 | # crossorigin: 'anonymous' 9 | 10 | # preload all rendered script and link tags automatically via the http2 Link header 11 | # preload: true 12 | 13 | # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data 14 | # strict_mode: false 15 | 16 | # if you have multiple builds: 17 | # builds: 18 | # pass "frontend" as the 3rg arg to the Twig functions 19 | # {{ encore_entry_script_tags('entry1', null, 'frontend') }} 20 | 21 | # frontend: '%kernel.project_dir%/public/frontend/build' 22 | 23 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 24 | # Put in config/packages/prod/webpack_encore.yaml 25 | # cache: true 26 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | devliver: 2 | resource: "../src/Controller" 3 | type: annotation 4 | 5 | app_logout: 6 | path: /logout 7 | -------------------------------------------------------------------------------- /config/routes/dev/framework.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /config/routes/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler_wdt: 2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 3 | prefix: /_wdt 4 | 5 | web_profiler_profiler: 6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 7 | prefix: /_profiler 8 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | locale: "%env(LOCALE)%" 3 | 4 | devliver_dir: '%kernel.project_dir%/data' 5 | devliver_dist_dir: '%devliver_dir%/dist' 6 | devliver_composer_dir: '%devliver_dir%/composer' 7 | 8 | services: 9 | # default configuration for services in *this* file 10 | _defaults: 11 | autowire: true 12 | autoconfigure: true 13 | public: true 14 | bind: 15 | $composerDirectory: '%devliver_composer_dir%' 16 | $projectDir: '%kernel.project_dir%' 17 | $distDir: '%devliver_dist_dir%' 18 | $apiKey: '%env(APP_API_KEY)%' 19 | Symfony\Component\Cache\Adapter\TagAwareAdapterInterface: '@cache.tag.app' 20 | 21 | _instanceof: 22 | Doctrine\Common\EventSubscriber: 23 | tags: ['doctrine.event_subscriber'] 24 | 25 | App\: 26 | resource: '../src/*' 27 | exclude: '../src/{Controller,Domain,DependencyInjection,Entity,Model,Infrastructure,Tests,Kernel.php,bootstrap.php}' 28 | 29 | cache.tag.app: 30 | class: Symfony\Component\Cache\Adapter\TagAwareAdapter 31 | decorates: cache.app 32 | arguments: ['@cache.tag.app.inner'] 33 | 34 | # # controller 35 | App\Controller\: 36 | resource: '../src/Controller/*' 37 | tags: ['controller.service_arguments'] 38 | 39 | App\Infrastructure\ArgumentResolver\UserValueResolver: ~ -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | volumes: 8 | - .:/var/www/html 9 | - ./var/bash_history:/home/docker/bash_history 10 | - ${HOME}/.ssh:/home/docker/.ssh 11 | - ${HOME}/.composer/:/home/docker/.composer/ 12 | environment: 13 | - TZ=Europe/Berlin 14 | - APP_ENV=dev 15 | - APP_API_KEY=abc 16 | - DATABASE_NAME=devliver 17 | - DATABASE_USER=devliver 18 | - DATABASE_PASSWORD=devliver 19 | - DATABASE_HOST=database 20 | - DATABASE_PORT=3306 21 | depends_on: 22 | - database 23 | networks: 24 | - default 25 | ports: 26 | - "9000:80" 27 | 28 | database: 29 | image: mariadb:latest 30 | env_file: 31 | - .env 32 | environment: 33 | - MYSQL_DATABASE=${DATABASE_NAME} 34 | - MYSQL_USER=${DATABASE_USER} 35 | - MYSQL_PASSWORD=${DATABASE_PASSWORD} 36 | - MYSQL_ROOT_PASSWORD=${DATABASE_PASSWORD} 37 | networks: 38 | - default 39 | ports: 40 | - "3307:3306" -------------------------------------------------------------------------------- /docker/php/docker-healthcheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | export SCRIPT_NAME=/ping 5 | export SCRIPT_FILENAME=/ping 6 | export REQUEST_METHOD=GET 7 | 8 | if cgi-fcgi -bind -connect 127.0.0.1:9000; then 9 | exit 0 10 | fi 11 | 12 | exit 1 13 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Devliver 2 | 3 | Your private self-hosted composer repository. 4 | 5 | [![Buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/nicklog) 6 | 7 | [![Docker Build](https://img.shields.io/docker/cloud/build/nicklog/devliver.svg?style=flat-square&logo=docker)](https://hub.docker.com/r/nicklog/devliver) 8 | [![License](https://img.shields.io/github/license/nicklog/devliver.svg?style=flat-square&logo=license)](https://github.com/nicklog/devliver) 9 | 10 | 11 | ## Requirements 12 | 13 | * **Docker** 14 | * **MariaDB/MySQL** 15 | * the running docker container has **access** to **private** git repositories with **ssh**. 16 | 17 | ## Installation 18 | 19 | Create a `docker-compose.yml` file in an empty directory. 20 | 21 | ```yaml 22 | version: '3.6' 23 | 24 | services: 25 | devliver: 26 | image: nicklog/devliver:latest 27 | volumes: 28 | - ./data:/var/www/html/data 29 | - ${HOME}/.ssh:/home/docker/.ssh 30 | - ${HOME}/.composer/:/home/docker/.composer/ 31 | environment: 32 | - TZ=Europe/Berlin 33 | - APP_API_KEY=A-TOP-SECRET-KEY 34 | - DATABASE_NAME=devliver 35 | - DATABASE_USER=devliver 36 | - DATABASE_PASSWORD=devliver 37 | - DATABASE_HOST=database 38 | - DATABASE_PORT=3306 39 | depends_on: 40 | - database 41 | networks: 42 | - default 43 | ports: 44 | - "9000:80" 45 | 46 | database: 47 | image: mariadb:latest 48 | environment: 49 | - MYSQL_DATABASE=devliver 50 | - MYSQL_USER=devliver 51 | - MYSQL_PASSWORD=devliver 52 | - MYSQL_ROOT_PASSWORD=devliver 53 | networks: 54 | - default 55 | ``` 56 | Change any settings to your needs and then run simply `docker-compose up -d`. 57 | You should now be able to access the site under port `9000` or the port you set. 58 | 59 | With this example setup the website is not secured by https. 60 | When you want to secure it I suggest to use a reverse proxy. 61 | 62 | ## User 63 | 64 | On first call of the website you can create a user. Create one and then login. 65 | The first user becomes an admin and can create more user if necessary. 66 | 67 | ## Clients 68 | 69 | The packages.json, available under `https://devliver-domain.url/packages.json`, is secured by basic http authentication. 70 | Add clients. These clients have access to the packages.json and can download archives. 71 | Each client gets a token automatically. 72 | 73 | ## Repository Authentication 74 | 75 | The git repositories will usually be protected. 76 | You have to store an SSH key in the ssh directory in the home directory for the corresponding web server 77 | or in the directory of the docker-compose directory. 78 | No matter, it must be ensured in any case that the SSH keys are available in the Docker container like in the example. 79 | 80 | ## How to use in composer.json 81 | 82 | To use your Devliver installation in Composer, there is a package repository you have to add to the composer.json in your projects. 83 | This is your repository of private packages. Composer will ask you for credentials. 84 | Use a client `name` as `username` and the `token` as `password`. If you want store these credentials in auth.json. 85 | Otherwise Composer will aks you always again. 86 | 87 | ```json 88 | { 89 | "repositories": [ 90 | { 91 | "type": "composer", 92 | "url": "https://devliver-domain.url", 93 | } 94 | ] 95 | } 96 | ``` 97 | 98 | ## Webhooks 99 | 100 | Creating a Webhook ensures that your package will always be updated instantly when you push to your repository. 101 | 102 | ### GitHub 103 | 104 | - Go to your GitHub repository 105 | - Click the `Settings` button 106 | - Click `Webhooks` and click on `Add webhook` 107 | - Enter `https://devliver-domain.url/api/update-package?token=APP_API_TOKEN` in `Payload URL` 108 | - Select `application/json` in `Content type` and let `Secret` empty 109 | - `Which events would you like to trigger this webhook?` - `Just the push event.` 110 | - Finally click on the green button `Add webhook` 111 | 112 | ### GitLab 113 | 114 | - Go to your Admin Area 115 | - Click the "System Hooks" button in den left panel 116 | - Enter `https://devliver-domain.url/api/update-package?token=APP_API_TOKEN` in url field 117 | - Let "Secret Token" empty 118 | - Enable "Push events" and "Tag push events" 119 | - Submit the form 120 | 121 | ### Bitbucket 122 | 123 | - Go to your BitBucket repository 124 | - Open the settings and select "Webhooks" in the menu 125 | - Add a new hook. 126 | - Enter `https://devliver-domain.url/api/update-package?token=APP_API_TOKEN` as URL 127 | - Save your changes and you're done. 128 | 129 | ### Manual 130 | 131 | If you do not use Bitbucket or GitHub there is a generic endpoint you can call manually from a git post-receive hook or similar. 132 | You have to do a `POST` request to `https://devliver-domain.url/api/update-package?token=APP_API_TOKEN` with a request body looking like this: 133 | 134 | ```json 135 | { 136 | "repository": { 137 | "git_url": "REPOSITORY_GIT_URL" 138 | } 139 | } 140 | ``` 141 | 142 | For example with curl 143 | 144 | ```bash 145 | curl -XPOST -H'content-type:application/json' 'https://devliver-domain.url/api/update-package?token=APP_API_TOKEN' -d'{"repository":{"git_url":"REPOSITORY_GIT_URL"}}' 146 | ``` 147 | 148 | ## Update 149 | 150 | Just update the docker image with... 151 | ```bash 152 | docker-compose pull 153 | docker-compose up -d 154 | ``` 155 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@fortawesome/fontawesome-free": "^5.15.2", 4 | "@popperjs/core": "^2.6", 5 | "@symfony/webpack-encore": "^1.2.0", 6 | "@tabler/core": "^1.0.0-beta2", 7 | "bootstrap": "^5.0.0", 8 | "bootstrap-confirmation2": "^4.0", 9 | "core-js": "^3.12.1", 10 | "file-loader": "^6.0.0", 11 | "highlight.js": "^10.5.0", 12 | "node-sass": "^6.0.0", 13 | "sass-loader": "^11.0.1", 14 | "webpack-notifier": "^1.6" 15 | }, 16 | "license": "UNLICENSED", 17 | "private": true, 18 | "scripts": { 19 | "dev-server": "encore dev-server", 20 | "dev": "encore dev", 21 | "watch": "encore dev --watch", 22 | "prod": "encore production", 23 | "build": "encore production" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | bin/ 11 | src/ 12 | tests/ 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 5 35 | 36 | 37 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Instanceof between App\\\\Entity\\\\Common\\\\AbstractEntity and App\\\\Entity\\\\Common\\\\AbstractEntity will always evaluate to true\\.$#" 5 | count: 1 6 | path: src/Entity/Common/AbstractEntity.php 7 | 8 | - 9 | message: "#^Parameter \\#1 \\$callback of function call_user_func_array expects callable\\(\\)\\: mixed, array\\(Composer\\\\Package\\\\CompletePackageInterface, string\\) given\\.$#" 10 | count: 1 11 | path: src/Model/PackageAdapter.php 12 | 13 | - 14 | message: "#^Parameter \\#1 \\$origin of method Symfony\\\\Component\\\\Filesystem\\\\Filesystem\\:\\:rename\\(\\) expects string, React\\\\Promise\\\\PromiseInterface\\|null given\\.$#" 15 | count: 1 16 | path: src/Service/DistSynchronization.php 17 | 18 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan/conf/bleedingEdge.neon 3 | - vendor/phpstan/phpstan-strict-rules/rules.neon 4 | - vendor/phpstan/phpstan-deprecation-rules/rules.neon 5 | - vendor/phpstan/phpstan-phpunit/extension.neon 6 | - vendor/phpstan/phpstan-phpunit/rules.neon 7 | - vendor/phpstan/phpstan-symfony/extension.neon 8 | - vendor/phpstan/phpstan-symfony/rules.neon 9 | - vendor/phpstan/phpstan-doctrine/extension.neon 10 | - vendor/phpstan/phpstan-doctrine/rules.neon 11 | - phpstan-baseline.neon 12 | 13 | parameters: 14 | level: max 15 | paths: 16 | - src 17 | - tests 18 | 19 | tmpDir: var/cache 20 | 21 | parallel: 22 | maximumNumberOfProcesses: 4 23 | 24 | checkGenericClassInNonGenericObjectType: false 25 | checkMissingIterableValueType: false 26 | 27 | symfony: 28 | container_xml_path: 'var/cache/test/App_KernelContainer.xml' 29 | console_application_loader: bin/phpstan-console-loader.php 30 | 31 | doctrine: 32 | objectManagerLoader: bin/phpstan-doctrine-loader.php 33 | 34 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | tests 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | # Use the front controller as index file. It serves as a fallback solution when 2 | # every other rewrite/redirect fails (e.g. in an aliased environment without 3 | # mod_rewrite). Additionally, this reduces the matching process for the 4 | # start page (path "/") because otherwise Apache will apply the rewriting rules 5 | # to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). 6 | DirectoryIndex index.php 7 | 8 | # By default, Apache does not evaluate symbolic links if you did not enable this 9 | # feature in your server configuration. Uncomment the following line if you 10 | # install assets as symlinks or if you experience problems related to symlinks 11 | # when compiling LESS/Sass/CoffeScript assets. 12 | # Options FollowSymlinks 13 | 14 | # Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve 15 | # to the front controller "/index.php" but be rewritten to "/index.php/index". 16 | 17 | Options -MultiViews 18 | 19 | 20 | 21 | RewriteEngine On 22 | 23 | # Determine the RewriteBase automatically and set it as environment variable. 24 | # If you are using Apache aliases to do mass virtual hosting or installed the 25 | # project in a subdirectory, the base path will be prepended to allow proper 26 | # resolution of the index.php file and to redirect to the correct URI. It will 27 | # work in environments without path prefix as well, providing a safe, one-size 28 | # fits all solution. But as you do not need it in this case, you can comment 29 | # the following 2 lines to eliminate the overhead. 30 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ 31 | RewriteRule ^(.*) - [E=BASE:%1] 32 | 33 | # Sets the HTTP_AUTHORIZATION header removed by Apache 34 | RewriteCond %{HTTP:Authorization} . 35 | RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 36 | 37 | # Redirect to URI without front controller to prevent duplicate content 38 | # (with and without `/index.php`). Only do this redirect on the initial 39 | # rewrite by Apache and not on subsequent cycles. Otherwise we would get an 40 | # endless redirect loop (request -> rewrite to front controller -> 41 | # redirect -> request -> ...). 42 | # So in case you get a "too many redirects" error or you always get redirected 43 | # to the start page because your Apache does not expose the REDIRECT_STATUS 44 | # environment variable, you have 2 choices: 45 | # - disable this feature by commenting the following 2 lines or 46 | # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the 47 | # following RewriteCond (best solution) 48 | RewriteCond %{ENV:REDIRECT_STATUS} ^$ 49 | RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] 50 | 51 | # If the requested filename exists, simply serve it. 52 | # We only want to let Apache serve files and not directories. 53 | RewriteCond %{REQUEST_FILENAME} -f 54 | RewriteRule ^ - [L] 55 | 56 | # Rewrite all other queries to the front controller. 57 | RewriteRule ^ %{ENV:BASE}/index.php [L] 58 | 59 | 60 | 61 | 62 | # When mod_rewrite is not available, we instruct a temporary redirect of 63 | # the start page to the front controller explicitly so that the website 64 | # and the generated links can still be used. 65 | RedirectMatch 307 ^/$ /index.php/ 66 | # RedirectTemp cannot be used instead 67 | 68 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | 12 | if ($_SERVER['APP_DEBUG']) { 13 | umask(0000); 14 | 15 | Debug::enable(); 16 | } 17 | 18 | if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { 19 | Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO); 20 | } 21 | 22 | if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { 23 | Request::setTrustedHosts([$trustedHosts]); 24 | } 25 | 26 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 27 | $request = Request::createFromGlobals(); 28 | $response = $kernel->handle($request); 29 | $response->send(); 30 | $kernel->terminate($request, $response); 31 | -------------------------------------------------------------------------------- /src/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | Require all denied 3 | 4 | 5 | Order deny,allow 6 | Deny from all 7 | 8 | -------------------------------------------------------------------------------- /src/Collection/PackageCollection.php: -------------------------------------------------------------------------------- 1 | filter(static fn (PackageInterface $package) => ! $package->isDev()); 24 | 25 | foreach ($this->toArray() as $p) { 26 | if (! $p->isDev()) { 27 | return $p; 28 | } 29 | } 30 | 31 | if ($packages->isEmpty()) { 32 | return null; 33 | } 34 | 35 | return $packages->first(); 36 | } 37 | 38 | public function getType(): string 39 | { 40 | return PackageInterface::class; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Collection/VersionCollection.php: -------------------------------------------------------------------------------- 1 | toArray(); 22 | 23 | uasort($versions, static function (Version $aVersion, Version $bVersion) { 24 | $a = $aVersion->getPackageInformation(); 25 | $b = $bVersion->getPackageInformation(); 26 | 27 | if ($a->isDev() && ! $b->isDev()) { 28 | return 1; 29 | } 30 | 31 | if (! $a->isDev() && $b->isDev()) { 32 | return -1; 33 | } 34 | 35 | if ($a->getReleaseDate() > $b->getReleaseDate()) { 36 | return -1; 37 | } 38 | 39 | if ($a->getReleaseDate() < $b->getReleaseDate()) { 40 | return 1; 41 | } 42 | 43 | return 0; 44 | }); 45 | 46 | return self::create(...$versions); 47 | } 48 | 49 | public function toPackageCollection(): PackageCollection 50 | { 51 | $packages = $this->map(static fn (Version $version) => $version->getPackageInformation()); 52 | 53 | return new PackageCollection($packages->toArray()); 54 | } 55 | 56 | public function getType(): string 57 | { 58 | return Version::class; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Command/PackagesQueueCommand.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 31 | $this->packageSynchronization = $packageSynchronization; 32 | $this->updateQueueRepository = $updateQueueRepository; 33 | } 34 | 35 | protected function configure(): void 36 | { 37 | $this 38 | ->setName('app:queue:execute'); 39 | } 40 | 41 | protected function execute(InputInterface $input, OutputInterface $output): int 42 | { 43 | $em = $this->registry->getManager(); 44 | 45 | $queues = $this->updateQueueRepository->findUnlocked(); 46 | 47 | foreach ($queues as $queue) { 48 | $queue->setLockedAt(new DateTime()); 49 | $em->persist($queue); 50 | } 51 | 52 | $em->flush(); 53 | 54 | foreach ($queues as $queue) { 55 | $this->packageSynchronization->sync($queue->getPackage()); 56 | 57 | $em->remove($queue); 58 | $em->flush(); 59 | } 60 | 61 | return 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Command/PackagesUpdateCommand.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 33 | $this->packageSynchronization = $packageSynchronization; 34 | } 35 | 36 | protected function configure(): void 37 | { 38 | $this 39 | ->setName('app:packages:update') 40 | ->setHelp('Syncs all or a specific package') 41 | ->addArgument('package', InputArgument::OPTIONAL); 42 | } 43 | 44 | protected function execute(InputInterface $input, OutputInterface $output): int 45 | { 46 | $store = new SemaphoreStore(); 47 | $factory = new LockFactory($store); 48 | 49 | $lock = $factory->createLock('packages_update'); 50 | 51 | // another job is still active 52 | if (! $lock->acquire()) { 53 | $output->writeln('Aborting, lock file is present.'); 54 | 55 | return 1; 56 | } 57 | 58 | $package = $input->getArgument('package'); 59 | 60 | if ($package !== null) { 61 | $repo = $this->registry->getRepository(Package::class); 62 | $package = $repo->findOneBy([ 63 | 'name' => $package, 64 | ]); 65 | 66 | // another job is still active 67 | if ($package === null) { 68 | $output->writeln('Aborting, package not found.'); 69 | 70 | return 1; 71 | } 72 | } 73 | 74 | ini_set('memory_limit', '-1'); 75 | set_time_limit(600); 76 | 77 | if ($package !== null) { 78 | $this->packageSynchronization->sync($package); 79 | } else { 80 | $this->packageSynchronization->syncAll(); 81 | } 82 | 83 | return 0; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Composer/ComposerManager.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 29 | $this->configFactory = $configFactory; 30 | } 31 | 32 | public function createRepository(Package $package): RepositoryInterface 33 | { 34 | $config = $this->createRepositoryConfig($package); 35 | 36 | return RepositoryFactory::createRepo( 37 | new NullIO(), 38 | $config, 39 | $package->getConfig() 40 | ); 41 | } 42 | 43 | public function createComposer(): Composer 44 | { 45 | return Factory::create( 46 | new NullIO(), 47 | $this->getConfig()->all() 48 | ); 49 | } 50 | 51 | public function createRepositoryByUrl(string $url, string $type = 'vcs'): RepositoryInterface 52 | { 53 | $config = $this->configFactory->create(); 54 | 55 | return RepositoryFactory::createRepo( 56 | new NullIO(), 57 | $config, 58 | [ 59 | 'type' => $type, 60 | 'url' => $url, 61 | ] 62 | ); 63 | } 64 | 65 | private function createRepositoryConfig(Package $package): Config 66 | { 67 | return $this->createRepositoriesConfig([$package]); 68 | } 69 | 70 | /** 71 | * @param Package[] $packages 72 | */ 73 | private function createRepositoriesConfig(array $packages): Config 74 | { 75 | $config = $this->configFactory->create(); 76 | $config->merge([ 77 | 'repositories' => array_map( 78 | static fn (Package $r): array => $r->getConfig(), 79 | $packages 80 | ), 81 | ]); 82 | 83 | return $config; 84 | } 85 | 86 | private function getConfig(): Config 87 | { 88 | $packages = $this->registry->getRepository(Package::class)->findAll(); 89 | 90 | return $this->createRepositoriesConfig($packages); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Composer/ConfigFactory.php: -------------------------------------------------------------------------------- 1 | composerDirectory = $composerDirectory; 23 | } 24 | 25 | public function create(): Config 26 | { 27 | putenv(sprintf('COMPOSER_HOME=%s', $this->composerDirectory)); 28 | 29 | unset(Config::$defaultRepositories['packagist.org']); 30 | 31 | $fs = new Filesystem(); 32 | if (! $fs->exists($this->composerDirectory)) { 33 | $fs->mkdir($this->composerDirectory); 34 | } 35 | 36 | $cwd = getcwd(); 37 | 38 | if ($cwd === false) { 39 | throw new RuntimeException('cwd() returned false', 1608917718640); 40 | } 41 | 42 | $config = new Config(false, $cwd); 43 | $config->merge([ 44 | 'config' => [ 45 | 'home' => $this->composerDirectory, 46 | ], 47 | 'repositories' => [ 48 | 'packagist.org' => false, 49 | ], 50 | ]); 51 | 52 | return $config; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Controller/DefaultController.php: -------------------------------------------------------------------------------- 1 | projectDir = $projectDir; 24 | } 25 | 26 | /** 27 | * @Route("", name="index") 28 | */ 29 | public function index(): Response 30 | { 31 | return $this->redirectToRoute('app_package_index'); 32 | } 33 | 34 | /** 35 | * @Route("/packages.json", name="packages") 36 | * @Route("/repo/private/packages.json", name="toran_fallback") 37 | * @Route("/repo/private", name="toran_fallback_2") 38 | */ 39 | public function packages(Request $request): Response 40 | { 41 | return $this->redirectToRoute('app_repository_index', $request->query->all()); 42 | } 43 | 44 | /** 45 | * @Route("/how-to", name="howto") 46 | */ 47 | public function howto(): Response 48 | { 49 | $readme = file_get_contents($this->projectDir . '/docs/README.md'); 50 | 51 | return $this->render('default/howto.html.twig', [ 52 | 'readme' => $readme, 53 | ]); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Controller/DownloadController.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 33 | } 34 | 35 | /** 36 | * @Route("/track-downloads", name="track") 37 | */ 38 | public function trackDownloads(Request $request): Response 39 | { 40 | $postData = json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR); 41 | 42 | if (! isset($postData['downloads']) || ! is_array($postData['downloads'])) { 43 | throw new AccessDeniedException(); 44 | } 45 | 46 | $doctrine = $this->registry; 47 | $em = $doctrine->getManager(); 48 | 49 | $packageRepo = $doctrine->getRepository(Package::class); 50 | $versionRepo = $doctrine->getRepository(Version::class); 51 | 52 | foreach ($postData['downloads'] as $p) { 53 | $name = $p['name']; 54 | $versionName = $p['version']; 55 | 56 | $package = $packageRepo->findOneBy([ 57 | 'name' => $name, 58 | ]); 59 | 60 | if ($package === null) { 61 | continue; 62 | } 63 | 64 | $version = $versionRepo->findOneBy([ 65 | 'package' => $package->getId(), 66 | 'name' => $versionName, 67 | ]); 68 | 69 | $download = new Download($package, $versionName); 70 | 71 | if ($version !== null) { 72 | $download->setVersion($version); 73 | } 74 | 75 | $em->persist($download); 76 | } 77 | 78 | $em->flush(); 79 | 80 | return new JsonResponse(['status' => 'success'], 201); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Controller/LoginController.php: -------------------------------------------------------------------------------- 1 | getLastAuthenticationError(); 20 | $lastUsername = $authenticationUtils->getLastUsername(); 21 | 22 | return $this->render('login/login.html.twig', [ 23 | 'last_username' => $lastUsername, 24 | 'error' => $error, 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Controller/ProfileController.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 39 | $this->formFactory = $formFactory; 40 | $this->userPasswordEncoder = $userPasswordEncoder; 41 | $this->flashBagHelper = $flashBagHelper; 42 | } 43 | 44 | /** 45 | * @Route("", name="index") 46 | */ 47 | public function profile(Request $request, User $user): Response 48 | { 49 | $form = $this->formFactory->create(UserProfileType::class, $user); 50 | $form->handleRequest($request); 51 | 52 | if ($form->isSubmitted() && $form->isValid()) { 53 | $this->entityManager->persist($user); 54 | $this->entityManager->flush(); 55 | 56 | $this->flashBagHelper->success('Profile updated'); 57 | 58 | return $this->redirectToRoute($request->get('_route')); 59 | } 60 | 61 | return $this->render('profile/edit.html.twig', [ 62 | 'user' => $this->getUser(), 63 | 'form' => $form->createView(), 64 | ]); 65 | } 66 | 67 | /** 68 | * @Route("/password", name="password") 69 | */ 70 | public function password(Request $request, User $user): Response 71 | { 72 | $form = $this->formFactory->create(UserPasswordType::class); 73 | $form->handleRequest($request); 74 | 75 | if ($form->isSubmitted() && $form->isValid()) { 76 | $password = $form->get('passwordNew')->getData(); 77 | $user->setPassword( 78 | $this->userPasswordEncoder->encodePassword($user, $password) 79 | ); 80 | 81 | $this->entityManager->persist($user); 82 | $this->entityManager->flush(); 83 | 84 | $this->flashBagHelper->success('Password updated'); 85 | 86 | return $this->redirectToRoute($request->get('_route')); 87 | } 88 | 89 | return $this->render('profile/change_password.html.twig', [ 90 | 'user' => $this->getUser(), 91 | 'form' => $form->createView(), 92 | ]); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Controller/RepositoryController.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 34 | $this->packagesDumper = $packagesDumper; 35 | $this->distSync = $distSync; 36 | } 37 | 38 | /** 39 | * @Route("/repo/packages.json", name="index") 40 | */ 41 | public function index(): Response 42 | { 43 | $json = $this->packagesDumper->dumpPackagesJson(); 44 | 45 | return new Response($json); 46 | } 47 | 48 | /** 49 | * @Route("/repo/provider/{vendor}/{project}.json", name="provider") 50 | * @Route("/repo/provider", name="provider_base") 51 | */ 52 | public function provider(string $vendor, string $project): Response 53 | { 54 | $name = $vendor . '/' . $project; 55 | 56 | $repository = $this->registry->getRepository(Package::class); 57 | 58 | $package = $repository->findOneByName($name); 59 | 60 | if ($package === null) { 61 | throw new NotFoundHttpException(); 62 | } 63 | 64 | $json = $this->packagesDumper->dumpPackageJson($package); 65 | 66 | return new Response($json); 67 | } 68 | 69 | /** 70 | * @Route("/repo/dist/{vendor}/{project}/{ref}.{type}", name="dist") 71 | * @Route("/dist/{vendor}/{project}/{ref}.{type}", name="dist_web") 72 | */ 73 | public function dist( 74 | string $vendor, 75 | string $project, 76 | string $ref, 77 | string $type 78 | ): BinaryFileResponse { 79 | $name = $vendor . '/' . $project; 80 | 81 | $repository = $this->registry->getRepository(Package::class); 82 | $package = $repository->findOneByName($name); 83 | 84 | if ($package === null) { 85 | throw $this->createNotFoundException(); 86 | } 87 | 88 | $cacheFile = $this->distSync->getDistFilename($package, $ref); 89 | 90 | if ($cacheFile === null) { 91 | throw $this->createNotFoundException(); 92 | } 93 | 94 | return new BinaryFileResponse($cacheFile, 200, [], false); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Controller/SetupController.php: -------------------------------------------------------------------------------- 1 | userRepository = $userRepository; 34 | $this->userPasswordEncoder = $userPasswordEncoder; 35 | $this->entityManager = $entityManager; 36 | } 37 | 38 | public function __invoke(Request $request): Response 39 | { 40 | if ($this->userRepository->countAll() > 0) { 41 | return $this->redirectToRoute('app_index'); 42 | } 43 | 44 | $form = $this->createForm(UserSetupType::class); 45 | $form->handleRequest($request); 46 | 47 | if ($form->isSubmitted() && $form->isValid()) { 48 | $password = $form->get('password')->getData(); 49 | $user = $form->getData(); 50 | assert($user instanceof User); 51 | 52 | $user->setPassword($this->userPasswordEncoder->encodePassword($user, $password)); 53 | $user->addRole(Role::ADMIN()); 54 | 55 | $this->entityManager->persist($user); 56 | $this->entityManager->flush(); 57 | 58 | return $this->redirectToRoute('app_login'); 59 | } 60 | 61 | return $this->render('setup/index.html.twig', [ 62 | 'form' => $form->createView(), 63 | ]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Domain/Role.php: -------------------------------------------------------------------------------- 1 | name = $name; 35 | $this->token = Uuid::uuid4()->toString(); 36 | $this->password = $this->token; 37 | } 38 | 39 | public function getName(): string 40 | { 41 | return $this->name; 42 | } 43 | 44 | public function setName(string $name): self 45 | { 46 | $this->name = $name; 47 | 48 | return $this; 49 | } 50 | 51 | public function getToken(): string 52 | { 53 | return $this->token; 54 | } 55 | 56 | public function setToken(string $token): self 57 | { 58 | $this->token = $token; 59 | 60 | return $this; 61 | } 62 | 63 | public function isEnable(): bool 64 | { 65 | return $this->enable; 66 | } 67 | 68 | public function setEnable(bool $enable): self 69 | { 70 | $this->enable = $enable; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * @return string[] 77 | */ 78 | public function getRoles(): array 79 | { 80 | return [ 81 | 'ROLE_USER', 82 | 'ROLE_REPO', 83 | ]; 84 | } 85 | 86 | public function getPassword(): string 87 | { 88 | return $this->password; 89 | } 90 | 91 | public function setPassword(string $password): self 92 | { 93 | $this->password = $password; 94 | 95 | return $this; 96 | } 97 | 98 | public function getSalt(): ?string 99 | { 100 | return null; 101 | } 102 | 103 | public function getUsername(): string 104 | { 105 | return $this->getName(); 106 | } 107 | 108 | public function eraseCredentials(): void 109 | { 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Entity/Common/AbstractEntity.php: -------------------------------------------------------------------------------- 1 | created = new DateTime(); 27 | } 28 | 29 | public function getId(): ?int 30 | { 31 | return $this->id; 32 | } 33 | 34 | public function isPersisted(): bool 35 | { 36 | return $this->id !== null; 37 | } 38 | 39 | public function equals(self $self): bool 40 | { 41 | if (! $self instanceof static) { 42 | throw new InvalidArgumentTypeError(static::class, get_debug_type($self)); 43 | } 44 | 45 | return $this->getId() === $self->getId(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Entity/Common/Entity.php: -------------------------------------------------------------------------------- 1 | created; 21 | } 22 | 23 | public function setUpdated(DateTimeInterface $updated): void 24 | { 25 | $this->updated = $updated; 26 | } 27 | 28 | public function getUpdated(): ?DateTimeInterface 29 | { 30 | return $this->updated; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Entity/Common/ModifiedAt.php: -------------------------------------------------------------------------------- 1 | package = $package; 37 | $this->versionName = $versionName; 38 | } 39 | 40 | public function getPackage(): Package 41 | { 42 | return $this->package; 43 | } 44 | 45 | public function getVersion(): ?Version 46 | { 47 | return $this->version; 48 | } 49 | 50 | public function setVersion(Version $version): self 51 | { 52 | $this->version = $version; 53 | 54 | return $this; 55 | } 56 | 57 | public function getVersionName(): ?string 58 | { 59 | return $this->versionName; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Entity/UpdateQueue.php: -------------------------------------------------------------------------------- 1 | package = $package; 35 | $this->lastCalledAt = $lastCalledAt; 36 | } 37 | 38 | public function getPackage(): Package 39 | { 40 | return $this->package; 41 | } 42 | 43 | public function getLockedAt(): ?DateTimeInterface 44 | { 45 | return $this->lockedAt; 46 | } 47 | 48 | public function setLockedAt(?DateTimeInterface $lockedAt): self 49 | { 50 | $this->lockedAt = $lockedAt; 51 | 52 | return $this; 53 | } 54 | 55 | public function getLastCalledAt(): DateTimeInterface 56 | { 57 | return $this->lastCalledAt; 58 | } 59 | 60 | public function setLastCalledAt(DateTimeInterface $lastCalledAt): self 61 | { 62 | $this->lastCalledAt = $lastCalledAt; 63 | 64 | return $this; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Entity/User.php: -------------------------------------------------------------------------------- 1 | email = $email; 46 | } 47 | 48 | public function getUsername(): string 49 | { 50 | return $this->getEmail(); 51 | } 52 | 53 | public function getEmail(): string 54 | { 55 | return $this->email; 56 | } 57 | 58 | public function setEmail(string $email): self 59 | { 60 | $this->email = $email; 61 | 62 | return $this; 63 | } 64 | 65 | public function getPassword(): ?string 66 | { 67 | return $this->password; 68 | } 69 | 70 | public function setPassword(?string $password): void 71 | { 72 | $this->password = $password; 73 | } 74 | 75 | public function isEnable(): bool 76 | { 77 | return $this->enable; 78 | } 79 | 80 | public function setEnable(bool $enable): self 81 | { 82 | $this->enable = $enable; 83 | 84 | return $this; 85 | } 86 | 87 | /** 88 | * @inheritDoc 89 | */ 90 | public function getRoles(): array 91 | { 92 | $roles = $this->roles; 93 | 94 | // guarantee every user at least has ROLE_USER 95 | $roles[] = 'ROLE_USER'; 96 | 97 | return array_unique($roles); 98 | } 99 | 100 | public function addRole(string | Role $role): void 101 | { 102 | $role = (string) $role; 103 | 104 | if (in_array($role, $this->roles, true)) { 105 | return; 106 | } 107 | 108 | if (! Role::isValid($role)) { 109 | throw new InvalidArgumentException(sprintf('%s given but $role has to be one of these values: %s', $role, implode(', ', Role::toArray()))); 110 | } 111 | 112 | $this->roles[] = $role; 113 | } 114 | 115 | public function removeRole(string | Role $role): void 116 | { 117 | $role = (string) $role; 118 | 119 | if (! in_array($role, $this->roles, true)) { 120 | return; 121 | } 122 | 123 | unset($this->roles[array_search($role, $this->roles, true)]); 124 | } 125 | 126 | public function getSalt(): ?string 127 | { 128 | return null; 129 | } 130 | 131 | public function eraseCredentials(): void 132 | { 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Entity/Version.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | private Collection $downloads; 37 | 38 | /** @ORM\Column(type="string") */ 39 | private string $name; 40 | 41 | /** 42 | * @ORM\Column(type="json") 43 | * 44 | * @var mixed[] 45 | */ 46 | private array $data = []; 47 | 48 | public function __construct( 49 | Package $package, 50 | PackageInterface $composerPackage 51 | ) { 52 | parent::__construct(); 53 | 54 | $this->package = $package; 55 | $this->name = $composerPackage->getPrettyVersion(); 56 | $this->downloads = new ArrayCollection(); 57 | } 58 | 59 | public function getPackage(): Package 60 | { 61 | return $this->package; 62 | } 63 | 64 | public function getName(): string 65 | { 66 | return $this->name; 67 | } 68 | 69 | /** 70 | * @return mixed[] 71 | */ 72 | public function getData(): array 73 | { 74 | return $this->data; 75 | } 76 | 77 | /** 78 | * @param mixed[] $data 79 | */ 80 | public function setData(array $data): void 81 | { 82 | $this->data = $data; 83 | } 84 | 85 | public function getPackageInformation(): PackageInterface 86 | { 87 | $loader = new ArrayLoader(); 88 | 89 | $p = $loader->load($this->getData()); 90 | 91 | if ($p instanceof AliasPackage) { 92 | $p = $p->getAliasOf(); 93 | } 94 | 95 | return $p; 96 | } 97 | 98 | /** 99 | * @return Collection 100 | */ 101 | public function getDownloads(): Collection 102 | { 103 | return $this->downloads; 104 | } 105 | 106 | public function __toString(): string 107 | { 108 | return sprintf('%s - %s', $this->getPackage()->getName(), $this->getName()); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/EventListener/EntityListener.php: -------------------------------------------------------------------------------- 1 | getEntity(); 30 | 31 | $this->updateUpdatedAt($entity); 32 | } 33 | 34 | public function preUpdate(LifecycleEventArgs $args): void 35 | { 36 | $entity = $args->getEntity(); 37 | 38 | $this->updateUpdatedAt($entity); 39 | } 40 | 41 | public function postLoad(LifecycleEventArgs $args): void 42 | { 43 | $entity = $args->getEntity(); 44 | 45 | $this->updateUpdatedAt($entity); 46 | 47 | $args->getEntityManager()->persist($entity); 48 | } 49 | 50 | private function updateUpdatedAt(mixed $entity): void 51 | { 52 | if (! ($entity instanceof AbstractEntity)) { 53 | return; 54 | } 55 | 56 | $entity->setUpdated(new DateTime()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/EventListener/ErrorListener.php: -------------------------------------------------------------------------------- 1 | 'onConsoleError', 23 | ]; 24 | } 25 | 26 | public function onConsoleError(ConsoleErrorEvent $event): void 27 | { 28 | $error = $event->getError(); 29 | 30 | if ($error instanceof ExceptionInterface) { 31 | return; 32 | } 33 | 34 | $writer = new Writer(); 35 | $writer->setOutput($event->getOutput()); 36 | $writer->write(new Inspector($error)); 37 | 38 | $event->setExitCode(0); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/EventListener/PackageListener.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 21 | } 22 | 23 | /** 24 | * @inheritDoc 25 | */ 26 | public function getSubscribedEvents(): array 27 | { 28 | return [ 29 | Events::postPersist, 30 | Events::postUpdate, 31 | Events::preRemove, 32 | ]; 33 | } 34 | 35 | public function postPersist(LifecycleEventArgs $event): void 36 | { 37 | $entity = $event->getObject(); 38 | 39 | if ( 40 | ! ($entity instanceof Package) && 41 | ! ($entity instanceof Version) 42 | ) { 43 | return; 44 | } 45 | 46 | $this->cache->clear(); 47 | } 48 | 49 | public function postUpdate(LifecycleEventArgs $event): void 50 | { 51 | $entity = $event->getObject(); 52 | 53 | if ( 54 | ! ($entity instanceof Package) && 55 | ! ($entity instanceof Version) 56 | ) { 57 | return; 58 | } 59 | 60 | $this->cache->clear(); 61 | } 62 | 63 | public function preRemove(LifecycleEventArgs $event): void 64 | { 65 | $entity = $event->getObject(); 66 | 67 | if ( 68 | ! ($entity instanceof Package) && 69 | ! ($entity instanceof Version) 70 | ) { 71 | return; 72 | } 73 | 74 | $this->cache->clear(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/EventListener/SetupListener.php: -------------------------------------------------------------------------------- 1 | userRepository = $userRepository; 23 | $this->urlGenerator = $urlGenerator; 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | public static function getSubscribedEvents(): array 30 | { 31 | return [ 32 | KernelEvents::REQUEST => 'setup', 33 | ]; 34 | } 35 | 36 | public function setup(RequestEvent $event): void 37 | { 38 | if (! $event->isMasterRequest()) { 39 | return; 40 | } 41 | 42 | $request = $event->getRequest(); 43 | 44 | if ($request->get('_route') === 'app_setup') { 45 | return; 46 | } 47 | 48 | if ($this->userRepository->countAll() > 0) { 49 | return; 50 | } 51 | 52 | $response = new RedirectResponse( 53 | $this->urlGenerator->generate('app_setup') 54 | ); 55 | 56 | $event->setResponse($response); 57 | $event->stopPropagation(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Form/Type/Forms/ClientType.php: -------------------------------------------------------------------------------- 1 | add('name', TextType::class, [ 26 | 'label' => 'Name', 27 | 'constraints' => [ 28 | new NotBlank(), 29 | new Regex([ 30 | 'pattern' => '/^[a-z0-9-_]{1,255}$/i', 31 | 'message' => 'Your name must contain only letter, numbers, dash and underscore and must have length between 1 and 255', 32 | ]), 33 | new Length([ 34 | 'min' => 2, 35 | 'max' => 255, 36 | ]), 37 | ], 38 | ]); 39 | 40 | $builder->add('enable', CheckboxType::class, [ 41 | 'required' => false, 42 | ]); 43 | } 44 | 45 | public function configureOptions(OptionsResolver $resolver): void 46 | { 47 | $resolver->setDefaults([ 48 | 'factory' => Client::class, 49 | 'constraints' => [ 50 | new UniqueEntity([ 51 | 'fields' => ['name'], 52 | ]), 53 | ], 54 | ]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Form/Type/Forms/FilterType.php: -------------------------------------------------------------------------------- 1 | add('name', TextType::class, [ 20 | 'label' => false, 21 | 'attr' => [ 22 | 'placeholder' => 'Filter', 23 | ], 24 | ]); 25 | } 26 | 27 | public function configureOptions(OptionsResolver $resolver): void 28 | { 29 | $resolver->setDefaults([ 30 | 'method' => 'GET', 31 | 'csrf_protection' => false, 32 | ]); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Form/Type/Forms/PackageAbandonType.php: -------------------------------------------------------------------------------- 1 | add('replacementPackage', TextType::class, [ 21 | 'label' => 'Replacement package', 22 | 'required' => false, 23 | 'attr' => [ 24 | 'placeholder' => 'optional package name', 25 | ], 26 | ]); 27 | } 28 | 29 | public function configureOptions(OptionsResolver $resolver): void 30 | { 31 | $resolver->setDefaults([ 32 | 'data_class' => Package::class, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Form/Type/Forms/PackageType.php: -------------------------------------------------------------------------------- 1 | add('type', PackageTypeType::class, [ 23 | 'required' => true, 24 | 'label' => 'Typ', 25 | 'constraints' => [ 26 | new NotBlank(), 27 | ], 28 | ]); 29 | 30 | $builder->add('url', TextType::class, [ 31 | 'required' => false, 32 | 'label' => 'Repository URL or path (bitbucket git repositories need the trailing .git)', 33 | 'constraints' => [ 34 | new NotBlank(), 35 | ], 36 | ]); 37 | } 38 | 39 | public function configureOptions(OptionsResolver $resolver): void 40 | { 41 | $resolver->setDefaults([ 42 | 'factory' => Package::class, 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Form/Type/Forms/UserPasswordType.php: -------------------------------------------------------------------------------- 1 | add('password', PasswordType::class, [ 21 | 'label' => 'Current password', 22 | 'attr' => ['placeholder' => 'Current password'], 23 | 'constraints' => [ 24 | new UserPassword(), 25 | ], 26 | ]); 27 | $builder->add('passwordNew', NewPasswordType::class, [ 28 | 'first_options' => [ 29 | 'label' => 'New password', 30 | 'attr' => ['placeholder' => 'New password'], 31 | ], 32 | 'mapped' => false, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Form/Type/Forms/UserProfileType.php: -------------------------------------------------------------------------------- 1 | add('email', EmailType::class, [ 23 | 'label' => 'Email', 24 | 'constraints' => [ 25 | new NotBlank(), 26 | ], 27 | ]); 28 | } 29 | 30 | public function configureOptions(OptionsResolver $resolver): void 31 | { 32 | $resolver->setDefaults([ 33 | 'data_class' => User::class, 34 | 'constraints' => [ 35 | new UniqueEntity([ 36 | 'fields' => ['email'], 37 | ]), 38 | ], 39 | ]); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Form/Type/Forms/UserSetupType.php: -------------------------------------------------------------------------------- 1 | add('email', EmailType::class, [ 26 | 'label' => 'Mail', 27 | 'attr' => ['placeholder' => 'Mail address'], 28 | 'constraints' => [ 29 | new NotBlank(), 30 | new Email(), 31 | ], 32 | ]) 33 | ->add('password', NewPasswordType::class, [ 34 | 'first_options' => [ 35 | 'label' => 'Password', 36 | 'attr' => ['placeholder' => 'Your password'], 37 | ], 38 | 'second_options' => [ 39 | 'label' => false, 40 | 'attr' => ['placeholder' => 'Repeat your password'], 41 | ], 42 | 'mapped' => false, 43 | ]); 44 | } 45 | 46 | public function configureOptions(OptionsResolver $resolver): void 47 | { 48 | $resolver->setDefaults([ 49 | 'factory' => User::class, 50 | 'constraints' => [ 51 | new UniqueEntity([ 52 | 'fields' => ['email'], 53 | ]), 54 | ], 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Form/Type/Forms/UserType.php: -------------------------------------------------------------------------------- 1 | add('email', EmailType::class, [ 27 | 'label' => 'Mail address', 28 | 'constraints' => [ 29 | new NotBlank(), 30 | ], 31 | ]); 32 | 33 | $builder->add('password', PasswordType::class, [ 34 | 'required' => false, 35 | 'mapped' => false, 36 | 'label' => 'Password', 37 | ]); 38 | 39 | $builder->add('roles', ChoiceType::class, [ 40 | 'required' => false, 41 | 'multiple' => true, 42 | 'choices' => [ 43 | 'Admin' => Role::ADMIN, 44 | ], 45 | ]); 46 | 47 | $builder->add('enable', CheckboxType::class, [ 48 | 'required' => false, 49 | ]); 50 | } 51 | 52 | public function configureOptions(OptionsResolver $resolver): void 53 | { 54 | $resolver->setDefaults([ 55 | 'factory' => User::class, 56 | 'constraints' => [ 57 | new UniqueEntity([ 58 | 'fields' => ['email'], 59 | ]), 60 | ], 61 | ]); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Form/Type/Widgets/NewPasswordType.php: -------------------------------------------------------------------------------- 1 | setDefaults([ 19 | 'first_options' => [ 20 | 'label' => false, 21 | 'attr' => ['placeholder' => 'New password'], 22 | ], 23 | 'second_options' => [ 24 | 'label' => false, 25 | 'attr' => ['placeholder' => 'Repeat new password'], 26 | ], 27 | 'type' => PasswordType::class, 28 | 'constraints' => [ 29 | new NotBlank(), 30 | new NotCompromisedPassword(), 31 | ], 32 | ]); 33 | } 34 | 35 | public function getParent(): string 36 | { 37 | return RepeatedType::class; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Form/Type/Widgets/PackageTypeType.php: -------------------------------------------------------------------------------- 1 | setDefaults([ 16 | 'required' => true, 17 | 'label' => 'Type', 18 | 'choices' => [ 19 | 'vcs' => 'vcs', 20 | // 'gitlab' => 'gitlab', 21 | // 'github' => 'github', 22 | // 'git-bitbucket' => 'git-bitbucket', 23 | // 'git' => 'git', 24 | // 'hg' => 'hg', 25 | // 'svn' => 'svn', 26 | // 'artifact' => 'artifact', 27 | // 'pear' => 'pear', 28 | // 'package' => 'package' 29 | ], 30 | 'choices_as_values' => true, 31 | ]); 32 | } 33 | 34 | public function getParent(): string 35 | { 36 | return ChoiceType::class; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Form/Validator/PackageValidator.php: -------------------------------------------------------------------------------- 1 | composerManager = $composerManager; 27 | $this->repositoryHelper = $repositoryHelper; 28 | $this->registry = $registry; 29 | } 30 | 31 | /** 32 | * @param mixed[]|null $payload 33 | */ 34 | public function validateRepository(?Package $object, ExecutionContextInterface $context, ?array $payload): void 35 | { 36 | if ($object === null) { 37 | return; 38 | } 39 | 40 | $url = $object->getUrl(); 41 | $type = $object->getType(); 42 | 43 | $repository = $this->composerManager->createRepositoryByUrl($url, $type); 44 | $info = $this->repositoryHelper->getComposerInformation($repository); 45 | 46 | if ($info !== null) { 47 | return; 48 | } 49 | 50 | $context->addViolation('Url is invalid'); 51 | } 52 | 53 | /** 54 | * @param mixed[]|null $payload 55 | */ 56 | public function validateAddName(?Package $object, ExecutionContextInterface $context, ?array $payload): void 57 | { 58 | if ($object === null) { 59 | return; 60 | } 61 | 62 | $url = $object->getUrl(); 63 | $type = $object->getType(); 64 | 65 | $repository = $this->composerManager->createRepositoryByUrl($url, $type); 66 | $info = $this->repositoryHelper->getComposerInformation($repository); 67 | 68 | if (! isset($info['name'])) { 69 | $context->addViolation('composer.json does not include a valid name.'); 70 | 71 | return; 72 | } 73 | 74 | $package = $this->registry->getRepository(Package::class)->findOneByName($info['name']); 75 | 76 | if ($package === null) { 77 | return; 78 | } 79 | 80 | $context->addViolation('Package name already exists.', [ 81 | 'name' => $package->getName(), 82 | ]); 83 | } 84 | 85 | public function validateEditName(Package $object, ExecutionContextInterface $context, Package $payload): void 86 | { 87 | $url = $object->getUrl(); 88 | $type = $object->getType(); 89 | 90 | $repository = $this->composerManager->createRepositoryByUrl($url, $type); 91 | $info = $this->repositoryHelper->getComposerInformation($repository); 92 | 93 | if (! isset($info['name'])) { 94 | $context->addViolation('composer.json does not include a valid name.'); 95 | 96 | return; 97 | } 98 | 99 | if ($payload->getName() !== $info['name']) { 100 | $context->addViolation('Package name is not accessible with this url.', [ 101 | 'name' => $payload->getName(), 102 | ]); 103 | 104 | return; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Infrastructure/ArgumentResolver/UserValueResolver.php: -------------------------------------------------------------------------------- 1 | security = $security; 20 | } 21 | 22 | public function supports(ParamConverter $configuration): bool 23 | { 24 | return $configuration->getClass() === User::class; 25 | } 26 | 27 | public function apply(Request $request, ParamConverter $configuration): bool 28 | { 29 | if (! $this->security->getUser() instanceof User) { 30 | return false; 31 | } 32 | 33 | if ($request->attributes->has($configuration->getName())) { 34 | return false; 35 | } 36 | 37 | $request->attributes->set( 38 | $configuration->getName(), 39 | $this->security->getUser() 40 | ); 41 | 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Infrastructure/Error/InvalidArgumentTypeError.php: -------------------------------------------------------------------------------- 1 | addSql( 23 | <<<'SQL' 24 | ALTER TABLE user 25 | ADD roles LONGTEXT NOT NULL COMMENT '(DC2Type:json)', 26 | ALGORITHM=INPLACE, LOCK=NONE; 27 | SQL 28 | ); 29 | } 30 | 31 | public function isTransactional(): bool 32 | { 33 | return false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Infrastructure/Migrations/Version20201229113210.php: -------------------------------------------------------------------------------- 1 | addSql( 23 | <<<'SQL' 24 | CREATE TABLE client 25 | ( 26 | id INT UNSIGNED AUTO_INCREMENT NOT NULL, 27 | name VARCHAR(255) NOT NULL, 28 | token VARCHAR(255) DEFAULT NULL, 29 | enable TINYINT(1) DEFAULT 1 NOT NULL, 30 | created DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL COMMENT '(DC2Type:datetimeutc)', 31 | updated DATETIME DEFAULT NULL COMMENT '(DC2Type:datetimeutc)', 32 | UNIQUE INDEX UNIQ_C74404555F37A13B (token), 33 | PRIMARY KEY(id) 34 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB; 35 | SQL 36 | ); 37 | $this->addSql( 38 | <<<'SQL' 39 | DROP INDEX UNIQ_8D93D649F155E556 ON user; 40 | SQL 41 | ); 42 | $this->addSql( 43 | <<<'SQL' 44 | ALTER TABLE user 45 | ADD enable TINYINT(1) DEFAULT 1 NOT NULL, 46 | DROP password_reset_token, 47 | DROP password_reset_token_expire_at, 48 | DROP repository_token, 49 | ALGORITHM=INPLACE, LOCK=NONE; 50 | SQL 51 | ); 52 | } 53 | 54 | public function isTransactional(): bool 55 | { 56 | return false; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Infrastructure/Migrations/Version20210101213326.php: -------------------------------------------------------------------------------- 1 | addSql( 23 | <<<'SQL' 24 | ALTER TABLE client 25 | MODIFY token VARCHAR(255) NOT NULL, 26 | ALGORITHM=INPLACE, LOCK=NONE; 27 | SQL 28 | ); 29 | $this->addSql( 30 | <<<'SQL' 31 | CREATE UNIQUE INDEX UNIQ_C74404555E237E06 ON client (name); 32 | SQL 33 | ); 34 | } 35 | 36 | public function isTransactional(): bool 37 | { 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Infrastructure/Migrations/Version20210102150832.php: -------------------------------------------------------------------------------- 1 | addSql( 23 | <<<'SQL' 24 | ALTER TABLE client 25 | ADD password VARCHAR(255) NOT NULL AFTER token, 26 | ALGORITHM=INPLACE, LOCK=NONE; 27 | SQL 28 | ); 29 | } 30 | 31 | public function isTransactional(): bool 32 | { 33 | return false; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | import('../config/{packages}/*.yaml'); 21 | $container->import('../config/{packages}/' . $this->environment . '/*.yaml'); 22 | $container->import('../config/{packages}/local/*.yaml'); 23 | $container->import('../config/services.yaml'); 24 | } 25 | 26 | protected function configureRoutes(RoutingConfigurator $routes): void 27 | { 28 | $routes->import('../config/{routes}/' . $this->environment . '/*.yaml'); 29 | $routes->import('../config/{routes}/*.yaml'); 30 | $routes->import('../config/routes.yaml'); 31 | } 32 | 33 | protected function getContainerClass(): string 34 | { 35 | $class = static::class; 36 | $class = str_replace('\\', '_', $class) . 'Container'; 37 | 38 | return $class; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Model/PackageAdapter.php: -------------------------------------------------------------------------------- 1 | package = $package; 21 | } 22 | 23 | public function getVendorName(): mixed 24 | { 25 | $split = explode('/', $this->getPackage()->getName()); 26 | 27 | return $split[0]; 28 | } 29 | 30 | public function getProjectName(): mixed 31 | { 32 | $split = explode('/', $this->getPackage()->getName()); 33 | 34 | return $split[1]; 35 | } 36 | 37 | public function getAlias(): ?string 38 | { 39 | $extra = $this->getPackage()->getExtra(); 40 | $version = $this->getPackage()->getPrettyVersion(); 41 | 42 | return $extra['branch-alias'][$version] ?? null; 43 | } 44 | 45 | public function getVersionName(): string 46 | { 47 | $alias = $this->getAlias(); 48 | 49 | return $alias ?? $this->getPackage()->getPrettyVersion(); 50 | } 51 | 52 | public function getPackage(): CompletePackageInterface 53 | { 54 | return $this->package; 55 | } 56 | 57 | public function __get(mixed $id): mixed 58 | { 59 | $propertyAccessor = PropertyAccess::createPropertyAccessor(); 60 | 61 | return $propertyAccessor->getValue($this->getPackage(), $id); 62 | } 63 | 64 | /** 65 | * @param mixed[] $arguments 66 | */ 67 | public function __call(string $name, array $arguments): mixed 68 | { 69 | if (method_exists($this->getPackage(), $name)) { 70 | return call_user_func_array([$this->getPackage(), $name], $arguments); 71 | } 72 | 73 | return $this->__get($name); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Repository/ClientRepository.php: -------------------------------------------------------------------------------- 1 | _em->createQuery( 31 | <<<'DQL' 32 | SELECT u 33 | FROM App\Entity\Client u 34 | WHERE u.token = :token 35 | DQL 36 | ) 37 | ->setParameter('token', $token, Types::STRING) 38 | ->getOneOrNullResult(); 39 | 40 | if ($result === null) { 41 | throw new UsernameNotFoundException(); 42 | } 43 | 44 | return $result; 45 | } 46 | 47 | public function refreshUser(UserInterface $user): UserInterface 48 | { 49 | return $user; 50 | } 51 | 52 | public function supportsClass(string $class): bool 53 | { 54 | return $class === Client::class; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Repository/DownloadRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('p'); 29 | $qb->select('count(p.id)'); 30 | 31 | $qb->where($qb->expr()->in('p.package', ':package')); 32 | $qb->setParameter('package', $package->getId()); 33 | 34 | return (int) $qb->getQuery()->getSingleScalarResult(); 35 | } 36 | 37 | public function countVersionDownloads(Version $version): int 38 | { 39 | $qb = $this->createQueryBuilder('p'); 40 | $qb->select('count(p.id)'); 41 | 42 | $qb->where($qb->expr()->in('p.version', ':version')); 43 | $qb->setParameter('version', $version->getId()); 44 | 45 | return (int) $qb->getQuery()->getSingleScalarResult(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Repository/PackageRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('p'); 31 | $expr = $qb->expr(); 32 | 33 | $qb->andWhere($expr->eq('p.enable', true)); 34 | 35 | $query = $qb->getQuery(); 36 | 37 | return $query->getResult(); 38 | } 39 | 40 | public function findOneByComposerPackage(ComposerPackageInterface $package): ?Package 41 | { 42 | return $this->findOneBy([ 43 | 'name' => $this->findOneByName($package->getName()), 44 | ]); 45 | } 46 | 47 | /** 48 | * @return Package[] 49 | */ 50 | public function findWithRepos(): array 51 | { 52 | $qb = $this->createQueryBuilder('p'); 53 | 54 | $qb->where($qb->expr()->isNotNull('p.repo')); 55 | 56 | return $qb->getQuery()->getResult(); 57 | } 58 | 59 | public function findOneByName(string $name): ?Package 60 | { 61 | return $this->findOneBy([ 62 | 'name' => $name, 63 | ]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Repository/UpdateQueueRepository.php: -------------------------------------------------------------------------------- 1 | findOneBy([ 30 | 'package' => $package->getId(), 31 | ]); 32 | } 33 | 34 | /** 35 | * @return UpdateQueue[] 36 | */ 37 | public function findUnlocked(): array 38 | { 39 | $qb = $this->createQueryBuilder('p'); 40 | $expr = $qb->expr(); 41 | 42 | $qb->where($expr->isNull('p.lockedAt')); 43 | $qb->orWhere($expr->lte('p.lockedAt', ':pastLockedAt')); 44 | 45 | $past = new DateTime('-10 minutes', new DateTimeZone('UTC')); 46 | $qb->setParameter('pastLockedAt', $past); 47 | 48 | return $qb->getQuery()->getResult(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Repository/UserRepository.php: -------------------------------------------------------------------------------- 1 | findOneBy([ 32 | 'username' => $username, 33 | 'apiToken' => $token, 34 | ]); 35 | } 36 | 37 | public function countAll(): int 38 | { 39 | return (int) $this->_em->createQuery( 40 | <<<'DQL' 41 | SELECT COUNT(1) 42 | FROM App\Entity\User u 43 | DQL 44 | ) 45 | ->getSingleScalarResult(); 46 | } 47 | 48 | public function loadUserByUsername(string $username): UserInterface 49 | { 50 | $result = $this->_em->createQuery( 51 | <<<'DQL' 52 | SELECT u 53 | FROM App\Entity\User u 54 | WHERE u.email = :username 55 | DQL 56 | ) 57 | ->setParameter('username', $username, Types::STRING) 58 | ->getOneOrNullResult(); 59 | 60 | if ($result === null) { 61 | throw new UsernameNotFoundException(); 62 | } 63 | 64 | return $result; 65 | } 66 | 67 | public function refreshUser(UserInterface $user): UserInterface 68 | { 69 | return $this->loadUserByUsername($user->getUsername()); 70 | } 71 | 72 | public function supportsClass(string $class): bool 73 | { 74 | return $class === User::class; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Repository/VersionRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('v'); 31 | $expr = $qb->expr(); 32 | 33 | $qb->join('v.package', 'p'); 34 | $qb->andWhere($expr->eq('p.enable', true)); 35 | 36 | if ($package !== null) { 37 | $qb->andWhere($expr->in('p.id', ':package')); 38 | $qb->setParameter('package', $package->getId()); 39 | } 40 | 41 | $query = $qb->getQuery(); 42 | 43 | return $query->getResult(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Security/ApiProvider.php: -------------------------------------------------------------------------------- 1 | isAccountActive()) { 24 | // throw new DisabledException(); 25 | // } 26 | // 27 | // if ($user->isAccountLocked()) { 28 | // throw new LockedException(); 29 | // } 30 | // 31 | // if ($user->isAccountExpired()) { 32 | // throw new AccountExpiredException(); 33 | // } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Security/Guard/Authenticator/ApiAuthenticator.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 26 | } 27 | 28 | public function supports(Request $request): bool 29 | { 30 | return $request->get('token') !== null; 31 | } 32 | 33 | public function getCredentials(Request $request): mixed 34 | { 35 | return $request->get('token'); 36 | } 37 | 38 | public function getUser(mixed $credentials, UserProviderInterface $userProvider): ?UserInterface 39 | { 40 | if ($credentials === null) { 41 | return null; 42 | } 43 | 44 | if ($this->apiKey !== $credentials) { 45 | throw new BadCredentialsException(); 46 | } 47 | 48 | return $userProvider->loadUserByUsername($credentials); 49 | } 50 | 51 | public function checkCredentials(mixed $credentials, UserInterface $user): bool 52 | { 53 | return true; 54 | } 55 | 56 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response 57 | { 58 | return null; 59 | } 60 | 61 | public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response 62 | { 63 | $data = [ 64 | 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()), 65 | ]; 66 | 67 | return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); 68 | } 69 | 70 | public function start(Request $request, ?AuthenticationException $authException = null): Response 71 | { 72 | $data = [ 73 | 'message' => 'Authentication Required', 74 | ]; 75 | 76 | return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); 77 | } 78 | 79 | public function supportsRememberMe(): bool 80 | { 81 | return false; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Security/Guard/Authenticator/LoginFormAuthenticator.php: -------------------------------------------------------------------------------- 1 | urlGenerator = $urlGenerator; 43 | $this->csrfTokenManager = $csrfTokenManager; 44 | $this->passwordEncoder = $passwordEncoder; 45 | } 46 | 47 | public function supports(Request $request): bool 48 | { 49 | return $request->attributes->get('_route') === self::LOGIN_ROUTE 50 | && $request->isMethod('POST'); 51 | } 52 | 53 | /** 54 | * @return array 55 | */ 56 | public function getCredentials(Request $request): array 57 | { 58 | $credentials = [ 59 | 'username' => $request->request->get('username'), 60 | 'password' => $request->request->get('password'), 61 | 'csrf_token' => $request->request->get('_csrf_token'), 62 | ]; 63 | 64 | $request->getSession()->set( 65 | Security::LAST_USERNAME, 66 | $credentials['username'] 67 | ); 68 | 69 | return $credentials; 70 | } 71 | 72 | /** 73 | * @inheritDoc 74 | */ 75 | public function getPassword($credentials): ?string 76 | { 77 | return $credentials['password'] ?? null; 78 | } 79 | 80 | public function getUser(mixed $credentials, UserProviderInterface $userProvider): UserInterface 81 | { 82 | $token = new CsrfToken('authenticate', $credentials['csrf_token']); 83 | if (! $this->csrfTokenManager->isTokenValid($token)) { 84 | throw new InvalidCsrfTokenException(); 85 | } 86 | 87 | try { 88 | $user = $userProvider->loadUserByUsername($credentials['username']); 89 | } catch (UsernameNotFoundException $usernameNotFoundException) { 90 | throw new BadCredentialsException(); 91 | } 92 | 93 | return $user; 94 | } 95 | 96 | public function checkCredentials(mixed $credentials, UserInterface $user): bool 97 | { 98 | return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); 99 | } 100 | 101 | public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response 102 | { 103 | return new RedirectResponse($this->urlGenerator->generate('app_index', ['welcome' => true])); 104 | } 105 | 106 | protected function getLoginUrl(): string 107 | { 108 | return $this->urlGenerator->generate(self::LOGIN_ROUTE); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Service/DistSynchronization.php: -------------------------------------------------------------------------------- 1 | composerManager = $composerManager; 33 | $this->distDir = $distDir; 34 | } 35 | 36 | public function getDistFilename(EntityPackage $dbPackage, string $ref): ?string 37 | { 38 | if ($dbPackage->getVersions()->count() === 0) { 39 | return null; 40 | } 41 | 42 | foreach ($dbPackage->getPackages() as $package) { 43 | if ($package instanceof CompletePackage && $package->getSourceReference() === $ref) { 44 | $cacheFile = $this->getCacheFile($package, $ref); 45 | 46 | if (file_exists($cacheFile)) { 47 | return $cacheFile; 48 | } 49 | 50 | $this->downloadPackage($package); 51 | 52 | return $cacheFile; 53 | } 54 | } 55 | 56 | return null; 57 | } 58 | 59 | private function downloadPackage(PackageInterface $package): string 60 | { 61 | $cacheFile = $this->getCacheFile($package, $package->getSourceReference()); 62 | $cacheDir = dirname($cacheFile); 63 | 64 | $fs = new Filesystem(); 65 | 66 | if (! $fs->exists($cacheDir)) { 67 | $fs->mkdir($cacheDir); 68 | } 69 | 70 | if (! $fs->exists($cacheFile)) { 71 | try { 72 | $downloader = $this->createFileDownloader(); 73 | $path = $downloader->download($package, sys_get_temp_dir()); 74 | 75 | $fs->rename($path, $cacheFile); 76 | 77 | return $cacheFile; 78 | } catch (Throwable $e) { 79 | } 80 | 81 | $composer = $this->composerManager->createComposer(); 82 | $archiveManager = $composer->getArchiveManager(); 83 | $path = $archiveManager->archive($package, $package->getDistType(), sys_get_temp_dir()); 84 | 85 | $fs->rename($path, $cacheFile); 86 | } 87 | 88 | return $cacheFile; 89 | } 90 | 91 | private function getCacheFile(PackageInterface $package, string $ref): string 92 | { 93 | $targetDir = $this->distDir . self::DIST_FORMAT; 94 | 95 | $name = $package->getName(); 96 | $type = $package->getDistType(); 97 | $version = 'placeholder'; 98 | 99 | return ComposerMirror::processUrl($targetDir, $name, $version, $ref, $type); 100 | } 101 | 102 | private function createFileDownloader(): FileDownloader 103 | { 104 | $config = $this->composerManager->createComposer()->getConfig(); 105 | $io = new NullIO(); 106 | 107 | $httpDownloader = new HttpDownloader($io, $config); 108 | 109 | return new FileDownloader($io, $config, $httpDownloader); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Service/FlashBagHelper.php: -------------------------------------------------------------------------------- 1 | flashBag = $flashBag; 16 | } 17 | 18 | public function success(string $message): void 19 | { 20 | $this->flashBag->add('success', $message); 21 | } 22 | 23 | public function warning(string $message): void 24 | { 25 | $this->flashBag->add('warning', $message); 26 | } 27 | 28 | public function alert(string $message): void 29 | { 30 | $this->flashBag->add('danger', $message); 31 | } 32 | 33 | public function info(string $message): void 34 | { 35 | $this->flashBag->add('info', $message); 36 | } 37 | 38 | public function add(string $type, string $message): void 39 | { 40 | $this->flashBag->add($type, $message); 41 | } 42 | 43 | public function set(string $type, string $message): void 44 | { 45 | $this->flashBag->set($type, $message); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Service/PackageSynchronization.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 33 | $this->composerManager = $composerManager; 34 | $this->repositoryHelper = $repositoryHelper; 35 | } 36 | 37 | public function syncAll(?IOInterface $io = null): void 38 | { 39 | $repository = $this->registry->getRepository(Package::class); 40 | $packages = $repository->findAll(); 41 | 42 | foreach ($packages as $package) { 43 | $this->sync($package); 44 | } 45 | } 46 | 47 | public function sync(Package $package): void 48 | { 49 | $repository = $this->composerManager->createRepository($package); 50 | $packages = $repository->getPackages(); 51 | 52 | if (count($packages) === 0) { 53 | return; 54 | } 55 | 56 | $package->setReadme($this->repositoryHelper->getReadme($repository)); 57 | 58 | $this->save($package, ...$packages); 59 | } 60 | 61 | public function save(Package $package, PackageInterface ...$packages): void 62 | { 63 | if (count($packages) === 0) { 64 | return; 65 | } 66 | 67 | $em = $this->registry->getManager(); 68 | $package->setLastUpdate(new DateTime()); 69 | 70 | $em->persist($package); 71 | $em->flush(); 72 | 73 | $this->updateVersions($package, ...$packages); 74 | } 75 | 76 | private function updateVersions(Package $dbPackage, PackageInterface ...$packages): void 77 | { 78 | $versionRepository = $this->registry->getRepository(Version::class); 79 | $em = $this->registry->getManager(); 80 | $dumper = new ArrayDumper(); 81 | 82 | $knownVersions = $versionRepository->findBy([ 83 | 'package' => $dbPackage, 84 | ]); 85 | 86 | $toRemove = []; 87 | foreach ($knownVersions as $knownVersion) { 88 | $name = $knownVersion->getName(); 89 | $toRemove[$name] = $knownVersion; 90 | } 91 | 92 | foreach ($packages as $package) { 93 | if ($package instanceof AliasPackage) { 94 | $package = $package->getAliasOf(); 95 | } 96 | 97 | $dbVersion = $versionRepository->findOneBy([ 98 | 'package' => $dbPackage->getId(), 99 | 'name' => $package->getPrettyVersion(), 100 | ]); 101 | 102 | if ($dbVersion === null) { 103 | $dbVersion = new Version($dbPackage, $package); 104 | } 105 | 106 | $distUrl = $this->repositoryHelper->getComposerDistUrl($package->getPrettyName(), $package->getSourceReference()); 107 | 108 | $package->setDistUrl($distUrl); 109 | $package->setDistType('zip'); 110 | $package->setDistReference($package->getSourceReference()); 111 | 112 | $packageData = $dumper->dump($package); 113 | 114 | $dbVersion->setData($packageData); 115 | 116 | $version = $package->getPrettyVersion(); 117 | 118 | if (isset($toRemove[$version])) { 119 | unset($toRemove[$version]); 120 | } 121 | 122 | $em->persist($dbVersion); 123 | } 124 | 125 | foreach ($toRemove as $item) { 126 | $em->remove($item); 127 | } 128 | 129 | $em->flush(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Service/PackagesDumper.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 36 | $this->router = $router; 37 | $this->repositoryHelper = $repositoryHelper; 38 | $this->cache = $cache; 39 | } 40 | 41 | public function dumpPackageJson(Package $package): string 42 | { 43 | $cacheKey = 'package-' . $package->getId(); 44 | $item = $this->cache->getItem($cacheKey); 45 | 46 | // if ($item->isHit()) { 47 | // return $item->get(); 48 | // } 49 | 50 | $data = []; 51 | $dumper = new ArrayDumper(); 52 | 53 | $repo = $this->registry->getRepository(Version::class); 54 | $versions = $repo->findByPackage($package); 55 | 56 | foreach ($versions as $version) { 57 | $packageData = $dumper->dump($version->getPackageInformation()); 58 | 59 | $packageData['uid'] = $version->getId(); 60 | 61 | if ($package->isAbandoned()) { 62 | $replacement = $package->getReplacementPackage() ?? true; 63 | $packageData['abandoned'] = $replacement; 64 | } 65 | 66 | $data[$version->getName()] = $packageData; 67 | } 68 | 69 | $jsonData = ['packages' => [$package->getName() => $data]]; 70 | $json = json_encode($jsonData, JSON_THROW_ON_ERROR); 71 | 72 | $item->set($json); 73 | $item->expiresAfter(96400); 74 | 75 | $this->cache->save($item); 76 | 77 | return $json; 78 | } 79 | 80 | public function dumpPackagesJson(): string 81 | { 82 | $cacheKey = 'packages'; 83 | $item = $this->cache->getItem($cacheKey); 84 | 85 | // if ($item->isHit()) { 86 | // return $item->get(); 87 | // } 88 | 89 | $repo = $this->registry->getRepository(Package::class); 90 | $packages = $repo->findEnabled(); 91 | 92 | $providers = []; 93 | 94 | foreach ($packages as $package) { 95 | $name = $package->getName(); 96 | 97 | $providers[$name] = [ 98 | 'sha256' => $this->hashPackageJson($package), 99 | ]; 100 | } 101 | 102 | $repo = []; 103 | 104 | $distUrl = $this->repositoryHelper->getComposerDistUrl('%package%', '%reference%', '%type%'); 105 | $mirror = [ 106 | 'dist-url' => $distUrl, 107 | 'preferred' => true, 108 | ]; 109 | 110 | $repo['packages'] = []; 111 | $repo['notify-batch'] = $this->router->generate('app_download_track', [], UrlGeneratorInterface::ABSOLUTE_URL); 112 | $repo['mirrors'] = [$mirror]; 113 | $repo['providers-url'] = $this->router->generate('app_repository_provider_base', [], UrlGeneratorInterface::ABSOLUTE_URL) . '/%package%.json'; 114 | $repo['providers'] = $providers; 115 | 116 | $json = json_encode($repo, JSON_THROW_ON_ERROR); 117 | 118 | $item->set($json); 119 | $item->expiresAfter(96400); 120 | 121 | $this->cache->save($item); 122 | 123 | return $json; 124 | } 125 | 126 | public function hashPackageJson(Package $package): string 127 | { 128 | $json = $this->dumpPackageJson($package); 129 | 130 | return hash('sha256', $json); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Service/RepositoryHelper.php: -------------------------------------------------------------------------------- 1 | router = $router; 26 | } 27 | 28 | public function getReadme(RepositoryInterface $repository): ?string 29 | { 30 | if (! ($repository instanceof VcsRepository)) { 31 | return null; 32 | } 33 | 34 | try { 35 | $driver = $repository->getDriver(); 36 | } catch (Throwable $e) { 37 | return null; 38 | } 39 | 40 | $composerInfo = $this->getComposerInformation($repository); 41 | 42 | $readmes = [ 43 | 'README.md', 44 | 'README', 45 | 'docs/README.md', 46 | 'docs/README', 47 | ]; 48 | 49 | if (isset($composerInfo['readme'])) { 50 | array_unshift($readmes, $composerInfo['readme']); 51 | } 52 | 53 | foreach ($readmes as $readme) { 54 | try { 55 | $source = $this->getReadmeSource($driver, $readme); 56 | 57 | if ($source !== null) { 58 | return $source; 59 | } 60 | } catch (Throwable $e) { 61 | } 62 | } 63 | 64 | return null; 65 | } 66 | 67 | private function getReadmeSource(VcsDriverInterface $driver, string $path): ?string 68 | { 69 | $file = new File($path, false); 70 | $ext = $file->getExtension(); 71 | 72 | if ($ext === 'md') { 73 | $source = $driver->getFileContent($path, $driver->getRootIdentifier()); 74 | } else { 75 | $source = $driver->getFileContent($path, $driver->getRootIdentifier()); 76 | if ($source !== null) { 77 | $source = '
' . htmlspecialchars($source) . '
'; 78 | } 79 | } 80 | 81 | return $source; 82 | } 83 | 84 | /** 85 | * @return mixed[]|null 86 | */ 87 | public function getComposerInformation(RepositoryInterface $repository): ?array 88 | { 89 | if (! ($repository instanceof VcsRepository)) { 90 | return null; 91 | } 92 | 93 | try { 94 | $driver = $repository->getDriver(); 95 | } catch (Throwable $e) { 96 | return null; 97 | } 98 | 99 | if ($driver === null) { 100 | return null; 101 | } 102 | 103 | return $driver->getComposerInformation($driver->getRootIdentifier()); 104 | } 105 | 106 | public function getComposerDistUrl( 107 | string $package, 108 | string $ref, 109 | string $type = 'zip' 110 | ): string { 111 | $distUrl = $this->router->generate('app_repository_dist', [ 112 | 'vendor' => 'PACK', 113 | 'project' => 'AGE', 114 | 'ref' => 'REF', 115 | 'type' => 'TYPE', 116 | ], UrlGeneratorInterface::ABSOLUTE_URL); 117 | 118 | $distUrl = str_replace( 119 | [ 120 | 'PACK/AGE', 121 | 'REF', 122 | 'TYPE', 123 | ], 124 | [ 125 | $package, 126 | $ref, 127 | $type, 128 | ], 129 | $distUrl 130 | ); 131 | 132 | return $distUrl; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Twig/Extension/DevliverExtension.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 30 | $this->router = $router; 31 | } 32 | 33 | /** 34 | * @inheritdoc 35 | */ 36 | public function getFunctions(): array 37 | { 38 | return [ 39 | new TwigFunction('version_downloads', [$this, 'getVersionDownloadsCounter']), 40 | new TwigFunction('package_downloads', [$this, 'getPackageDownloadsCounter']), 41 | new TwigFunction('package_download_url', [$this, 'getPackageDownloadUrl']), 42 | new TwigFunction('package_url', [$this, 'getPackageUrl']), 43 | new TwigFunction('package_adapter', [$this, 'getPackageAdapter']), 44 | ]; 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | */ 50 | public function getFilters(): array 51 | { 52 | return [ 53 | new TwigFilter('sha1', 'sha1'), 54 | new TwigFilter('package_adapter', [$this, 'getPackageAdapter']), 55 | new TwigFilter('gravatar', [$this, 'getGravatarUrl']), 56 | ]; 57 | } 58 | 59 | public function getPackageDownloadsCounter(Package $package): int 60 | { 61 | $repo = $this->registry->getRepository(Download::class); 62 | 63 | return $repo->countPackageDownloads($package); 64 | } 65 | 66 | public function getVersionDownloadsCounter(Version $version): int 67 | { 68 | $repo = $this->registry->getRepository(Download::class); 69 | 70 | return $repo->countVersionDownloads($version); 71 | } 72 | 73 | public function getPackageAdapter(CompletePackageInterface $package): PackageAdapter 74 | { 75 | return new PackageAdapter($package); 76 | } 77 | 78 | public function getPackageUrl(string $name): string 79 | { 80 | $repo = $this->registry->getRepository(Package::class); 81 | 82 | $package = $repo->findOneBy([ 83 | 'name' => $name, 84 | ]); 85 | 86 | if ($package !== null) { 87 | return $this->router->generate('app_package_view', [ 88 | 'package' => $package->getId(), 89 | ]); 90 | } 91 | 92 | return 'https://packagist.org/packages/' . $name; 93 | } 94 | 95 | public function getPackageDownloadUrl(CompletePackageInterface $package): string 96 | { 97 | $adapter = $this->getPackageAdapter($package); 98 | 99 | return $this->router->generate('app_repository_dist_web', [ 100 | 'vendor' => $adapter->getVendorName(), 101 | 'project' => $adapter->getProjectName(), 102 | 'ref' => $package->getSourceReference(), 103 | 'type' => 'zip', 104 | ]); 105 | } 106 | 107 | public function getGravatarUrl(string $email, int $size): string 108 | { 109 | $gravatar = new Gravatar([], true); 110 | 111 | return $gravatar->avatar($email, ['s' => $size]); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicklog/devliver/e086cbc711a54c5c9c836a39ab5ccf00c7589d15/templates/.gitkeep -------------------------------------------------------------------------------- /templates/_partials/footer.html.twig: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /templates/_partials/header.html.twig: -------------------------------------------------------------------------------- 1 | 44 | 45 | {% if is_granted('IS_AUTHENTICATED_REMEMBERED') %} 46 | 76 | {% endif %} -------------------------------------------------------------------------------- /templates/_partials/pagination.html.twig: -------------------------------------------------------------------------------- 1 | {% set classAlign = (align is not defined) ? '' : align=='center' ? ' justify-content-center' : (align=='right' ? ' justify-content-end' : '') %} 2 | {% set classSize = (size is not defined) ? '' : size=='large' ? ' pagination-lg' : (size=='small' ? ' pagination-sm' : '') %} 3 | 70 | -------------------------------------------------------------------------------- /templates/client/add.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | Add client 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {{ form_start(form) }} 9 |
10 |
11 | Client 12 |
13 |
14 | {{ form_widget(form) }} 15 |
16 | 22 |
23 | {{ form_end(form) }} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/client/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'client/add.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | Edit client 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/client/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | Clients 5 | {% endblock %} 6 | 7 | {% block pageActions %} 8 | Add client 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 |
14 |
15 |
16 | Clients {{ pagination.getTotalItemCount }} 17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for client in pagination %} 26 | 27 | 32 | 35 | 47 | 48 | {% endfor %} 49 |
{{ knp_pagination_sortable(pagination, 'Name', ['p.name']) }}Token
28 | 29 | {{ client.name }} 30 | 31 | 33 | {{ client.token }} 34 | 36 |
37 | {% if client.enable != true %} 38 | 39 | {% else %} 40 | 41 | {% endif %} 42 | 43 | 44 | 45 |
46 |
50 |
51 | 54 |
55 |
56 |
57 | {% endblock %} 58 | -------------------------------------------------------------------------------- /templates/default/howto.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | How To 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 | {{ readme|markdown_to_html }} 11 |
12 |
13 | {% endblock %} 14 | 15 | -------------------------------------------------------------------------------- /templates/layout.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block stylesheets %} 10 | {{ encore_entry_link_tags('app') }} 11 | {% endblock %} 12 | 13 | {% block title %}Devliver{% endblock %} 14 | 15 | 16 | 17 |
18 | {% include '_partials/header.html.twig' %} 19 | 20 |
21 |
22 | 45 | 46 | 47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 | {% for type, flashMessages in app.flashes %} 55 | {% for flashMessage in flashMessages %} 56 |
{{ flashMessage|raw }}
57 | {% endfor %} 58 | {% endfor %} 59 |
60 | 61 | {% block content %}{% endblock %} 62 |
63 |
64 |
65 |
66 | 67 | 68 |
69 | {% include '_partials/footer.html.twig' %} 70 |
71 |
72 |
73 | 74 | {% block javascripts %} 75 | {{ encore_entry_script_tags('app') }} 76 | {% endblock %} 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /templates/login/login.html.twig: -------------------------------------------------------------------------------- 1 | {% set bodyClasses = 'page-login' %} 2 | 3 | {% extends 'layout.html.twig' %} 4 | 5 | {% block content %} 6 |
7 |
8 |
9 | Login 10 |
11 | 12 |
13 | 14 |
15 |

Login to your account

16 | 17 | {% if error %} 18 |
{{ error.messageKey|trans(error.messageData, 'security') }}
19 | {% endif %} 20 | 21 |
22 | 23 | 24 |
25 |
26 | 29 |
30 | 31 |
32 |
33 | 36 |
37 |
38 |
39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /templates/package/_details.html.twig: -------------------------------------------------------------------------------- 1 | {% set adapter = package_adapter(version) %} 2 | 3 |
4 |
5 |
6 |
7 | {{ info.prettyVersion }} 8 | {% if adapter.alias is not null %} 9 | / {{ adapter.alias }} 10 | {% endif %} 11 |
12 |
13 | {{ info.releaseDate|date("r") }} 14 |
15 |
16 |
17 |
18 | 19 |
20 | {% if info.authors and info.authors|length > 0 %} 21 | 22 | {% for author in info.authors %} 23 | {% if author.email is defined and author.email is not empty %} 24 | {%- if author.homepage is defined and author.homepage is not empty -%} 25 | 26 | {%- endif -%} 27 | 28 | {% endif -%} 29 | {%- if author.homepage is defined and author.homepage is not empty -%} 30 | 31 | {%- endif -%} 32 | {% endfor %} 33 | 34 | {% endif %} 35 |
36 | 37 |
38 |
39 |
40 | {{ info.sourceReference }} 41 |
42 |
43 | {{ info.license ? info.license|join(', ') : 'Unknown' }} 44 |
45 |
46 | 47 | {% for keyword in info.keywords %} 48 | #{{ keyword }} 49 | {% endfor %} 50 |
51 |
52 |
53 | 54 |
55 |
56 | {% for type in ["requires", "devRequires", "suggests", "provides", "conflicts", "replaces"] %} 57 |
58 |
59 |
{{ ('package_link.' ~ type)|trans }}
60 | {% if attribute(info, type)|length > 0 %} 61 |
    62 | {% for name, version in attribute(info, type) %} 63 | {% if type != 'suggests' %} 64 | {% set version = version.prettyConstraint %} 65 | {% endif %} 66 | 67 |
  • 68 | 69 | {{ name }} 70 | 71 | 72 | {{ version == 'self.version' ? version.version : version }} 73 | 74 |
  • 75 | {% endfor %} 76 |
77 | {% else %} 78 |
79 | n/a 80 |
81 | {% endif %} 82 |
83 |
84 | {% endfor %} 85 |
86 |
-------------------------------------------------------------------------------- /templates/package/abandon.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | {{ 'package.abandon.page_header'|trans }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 | You are about to mark this package as abandoned. This will alert users that this package will no longer be maintained.

10 | If you are handing this project over to a new maintainer or know of a package that replaces it, please use the field below to point users to the new package.
11 | In case you cannot point them to a new package, this one will be tagged as abandoned without a package that replace it. 12 |
13 | 14 | {{ form_start(form) }} 15 |
16 |
17 | Repository 18 |
19 |
20 | {{ form_widget(form) }} 21 |
22 | 28 |
29 | {{ form_end(form) }} 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /templates/package/add.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | {{ 'package.add.page_header'|trans }} 5 | {% endblock %} 6 | 7 | {% block pageActions %} 8 | {% endblock %} 9 | 10 | {% block content %} 11 | {{ form_start(form) }} 12 |
13 |
14 | Repository 15 |
16 |
17 | {{ form_widget(form) }} 18 |
19 | 25 |
26 | {{ form_end(form) }} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/package/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'package/add.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | {{ 'package.edit.page_header'|trans }} 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/profile/base.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | {{ app.user.username }} 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {{ form_start(form) }} 9 | 10 |
11 | 25 |
26 | {% block profile %} 27 | {{ form_widget(form) }} 28 | {% endblock %} 29 |
30 | 35 |
36 | 37 | {{ form_end(form) }} 38 | 39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /templates/profile/change_password.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'profile/base.html.twig' %} 2 | -------------------------------------------------------------------------------- /templates/profile/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'profile/base.html.twig' %} 2 | -------------------------------------------------------------------------------- /templates/setup/index.html.twig: -------------------------------------------------------------------------------- 1 | {% set bodyClasses = 'page-login' %} 2 | 3 | {% extends 'layout.html.twig' %} 4 | 5 | {% block content %} 6 |
7 |
8 | 9 | {{ form_start(form) }} 10 |

Setup

11 | 12 |
13 |
14 |

Create an admin user

15 | {{ form_widget(form) }} 16 | 19 |
20 |
21 | {{ form_end(form) }} 22 |
23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/user/add.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | Add user 5 | {% endblock %} 6 | 7 | {% block content %} 8 | {{ form_start(form) }} 9 |
10 |
11 | User 12 |
13 |
14 | {{ form_widget(form) }} 15 |
16 | 22 |
23 | {{ form_end(form) }} 24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/user/edit.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'user/add.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | Edit user 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /templates/user/index.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html.twig' %} 2 | 3 | {% block pageTitle %} 4 | Users 5 | {% endblock %} 6 | 7 | {% block pageActions %} 8 | Add user 9 | {% endblock %} 10 | 11 | {% block content %} 12 |
13 |
14 |
15 |
16 | Users {{ pagination.getTotalItemCount }} 17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | {% for user in pagination %} 25 | 26 | 31 | 44 | 45 | {% endfor %} 46 |
{{ knp_pagination_sortable(pagination, 'Mail address', ['p.email']) }}
27 | 28 | {{ user.email }} 29 | 30 | 32 |
33 | {% if user.equals(app.user) == false %} 34 | {% if user.enable != true %} 35 | 36 | {% else %} 37 | 38 | {% endif %} 39 | 40 | 41 | {% endif %} 42 |
43 |
47 |
48 | 51 |
52 |
53 |
54 | {% endblock %} 55 | -------------------------------------------------------------------------------- /tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicklog/devliver/e086cbc711a54c5c9c836a39ab5ccf00c7589d15/tests/.gitkeep -------------------------------------------------------------------------------- /tests/Collection/PackageCollectionTest.php: -------------------------------------------------------------------------------- 1 | createMock(PackageInterface::class); 19 | $packageDev->method('isDev')->willReturn(true); 20 | 21 | $packageStable = $this->createMock(PackageInterface::class); 22 | $packageStable->method('isDev')->willReturn(false); 23 | 24 | $collection = PackageCollection::create( 25 | $packageDev, 26 | $packageStable 27 | ); 28 | 29 | self::assertSame($packageStable, $collection->getLastStablePackage()); 30 | } 31 | 32 | public function testLastStableNotFound(): void 33 | { 34 | $packageDev = $this->createMock(PackageInterface::class); 35 | $packageDev->method('isDev')->willReturn(true); 36 | 37 | $packageStable = $this->createMock(PackageInterface::class); 38 | $packageStable->method('isDev')->willReturn(true); 39 | $collection = PackageCollection::create( 40 | $packageDev, 41 | $packageStable 42 | ); 43 | 44 | self::assertNull($collection->getLastStablePackage()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Collection/VersionCollectionTest.php: -------------------------------------------------------------------------------- 1 | createMock(PackageInterface::class); 21 | $packageA->method('isDev')->willReturn(false); 22 | $packageA->method('getReleaseDate')->willReturn(Carbon::parse('2020-05-05 12:00:00')); 23 | 24 | $packageB = $this->createMock(PackageInterface::class); 25 | $packageB->method('isDev')->willReturn(false); 26 | $packageB->method('getReleaseDate')->willReturn(Carbon::parse('2020-05-10 12:00:00')); 27 | 28 | $packageC = $this->createMock(PackageInterface::class); 29 | $packageC->method('isDev')->willReturn(true); 30 | $packageC->method('getReleaseDate')->willReturn(Carbon::parse('2020-05-15 12:00:00')); 31 | 32 | $versionA = $this->createMock(Version::class); 33 | $versionA->method('getPackageInformation')->willReturn($packageA); 34 | 35 | $versionB = $this->createMock(Version::class); 36 | $versionB->method('getPackageInformation')->willReturn($packageB); 37 | 38 | $versionC = $this->createMock(Version::class); 39 | $versionC->method('getPackageInformation')->willReturn($packageC); 40 | 41 | $collection = VersionCollection::create( 42 | $versionA, 43 | $versionB, 44 | $versionC, 45 | ); 46 | 47 | self::assertSame([ 48 | $versionB, 49 | $versionA, 50 | $versionC, 51 | ], $collection->sortByReleaseDate()->toArray()); 52 | } 53 | 54 | public function testToMapCollection(): void 55 | { 56 | $packageA = $this->createMock(PackageInterface::class); 57 | $packageA->method('isDev')->willReturn(false); 58 | $packageA->method('getReleaseDate')->willReturn(Carbon::parse('2020-05-05 12:00:00')); 59 | 60 | $packageB = $this->createMock(PackageInterface::class); 61 | $packageB->method('isDev')->willReturn(false); 62 | $packageB->method('getReleaseDate')->willReturn(Carbon::parse('2020-05-10 12:00:00')); 63 | 64 | $packageC = $this->createMock(PackageInterface::class); 65 | $packageC->method('isDev')->willReturn(true); 66 | $packageC->method('getReleaseDate')->willReturn(Carbon::parse('2020-05-15 12:00:00')); 67 | 68 | $versionA = $this->createMock(Version::class); 69 | $versionA->method('getPackageInformation')->willReturn($packageA); 70 | 71 | $versionB = $this->createMock(Version::class); 72 | $versionB->method('getPackageInformation')->willReturn($packageB); 73 | 74 | $versionC = $this->createMock(Version::class); 75 | $versionC->method('getPackageInformation')->willReturn($packageC); 76 | 77 | $collection = VersionCollection::create( 78 | $versionA, 79 | $versionB, 80 | $versionC, 81 | ); 82 | 83 | self::assertSame([ 84 | $packageA, 85 | $packageB, 86 | $packageC, 87 | ], $collection->toPackageCollection()->toArray()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__) . '/.env'); 13 | } 14 | -------------------------------------------------------------------------------- /translations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicklog/devliver/e086cbc711a54c5c9c836a39ab5ccf00c7589d15/translations/.gitkeep -------------------------------------------------------------------------------- /translations/FOSUserBundle.de.yml: -------------------------------------------------------------------------------- 1 | resetting: 2 | check_email: | 3 | Eine E-Mail wurde verschickt. Sie beinhaltet einen Link zum Zurücksetzen des Passwortes. 4 | 5 | Eventuell wurde diese E-Mail als Spam markiert, wenn sie nicht angekommen ist. 6 | -------------------------------------------------------------------------------- /translations/messages.de.yml: -------------------------------------------------------------------------------- 1 | package_link: 2 | requires: requires 3 | devRequires: requires (dev) 4 | suggests: suggests 5 | provides: provides 6 | conflicts: conflicts 7 | replaces: replaces 8 | 9 | security: 10 | login: 11 | page_header: Anmeldung 12 | headline: Anmelden 13 | 14 | profile: 15 | change_password: 16 | page_header: Passwort ändern 17 | 18 | repository: 19 | singular: Repository 20 | 21 | package: 22 | list: 23 | page_header: Pakete 24 | add: 25 | page_header: Paket hinzufügen 26 | edit: 27 | page_header: Pakete bearbeiten 28 | 29 | singular: Paket 30 | plural: Pakete 31 | add_package: Paket hinzufügen 32 | 33 | action: 34 | add: Hinzufügen 35 | update_all: Alles aktualisieren 36 | update: Aktualisieren 37 | edit: Bearbeiten 38 | remove: Löschen 39 | cancel: Abbrechen 40 | 41 | menu: 42 | admin: Admin 43 | repositories: Repositories 44 | packages: Packages 45 | usage: Anleitung 46 | change_password: Passwort ändern 47 | logout: Abmelden 48 | 49 | resetting: 50 | request: 51 | page_header: Passwort zurücksetzen 52 | headline: Passwort zurücksetzen 53 | reset: 54 | page_header: Passwort setzen 55 | check_email: 56 | page_header: Passwort zurücksetzen 57 | 58 | software: 59 | list: 60 | page_header: Software 61 | plural: Software 62 | -------------------------------------------------------------------------------- /translations/messages.en.yml: -------------------------------------------------------------------------------- 1 | package_link: 2 | requires: requires 3 | devRequires: requires (dev) 4 | suggests: suggests 5 | provides: provides 6 | conflicts: conflicts 7 | replaces: replaces 8 | 9 | security: 10 | login: 11 | page_header: Login 12 | headline: Please sign in 13 | 14 | profile: 15 | change_password: 16 | page_header: Change Password 17 | 18 | repository: 19 | singular: Repository 20 | 21 | package: 22 | list: 23 | page_header: Packages 24 | abandon: 25 | page_header: Abandon Package 26 | add: 27 | page_header: Add Package 28 | edit: 29 | page_header: Edit Package 30 | 31 | singular: Package 32 | plural: Packages 33 | add_package: Add Package 34 | 35 | action: 36 | add: Add 37 | update_all: Update All 38 | update: Update 39 | edit: Edit 40 | remove: Remove 41 | cancel: Cancel 42 | enable: Enable 43 | disable: Disable 44 | abandon: Abandon 45 | unabandon: Unabandon 46 | 47 | menu: 48 | repositories: Repositories 49 | packages: Packages 50 | usage: Usage 51 | change_password: Change password 52 | logout: Logout 53 | 54 | resetting: 55 | request: 56 | page_header: Password reset 57 | headline: Password reset 58 | reset: 59 | page_header: Set Password 60 | check_email: 61 | page_header: Password reset 62 | 63 | software: 64 | list: 65 | page_header: Software 66 | plural: Software 67 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | let Encore = require('@symfony/webpack-encore'); 2 | 3 | if (!Encore.isRuntimeEnvironmentConfigured()) { 4 | Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); 5 | } 6 | 7 | Encore 8 | .setOutputPath('public/build/') 9 | .setPublicPath('/build') 10 | 11 | .addEntry('app', './assets/js/app.js') 12 | 13 | .copyFiles({ 14 | from: './assets/images', 15 | to: 'images/[path][name].[hash:8].[ext]', 16 | pattern: /\.(png|jpg|jpeg|gif|ico|svg)$/ 17 | }) 18 | 19 | .cleanupOutputBeforeBuild() 20 | 21 | .enableSourceMaps(!Encore.isProduction()) 22 | .enableIntegrityHashes(Encore.isProduction()) 23 | .enableVersioning() 24 | .enableSassLoader() 25 | .enableSingleRuntimeChunk() 26 | .enableBuildNotifications() 27 | 28 | // enables @babel/preset-env polyfills 29 | .configureBabelPresetEnv((config) => { 30 | config.useBuiltIns = 'usage'; 31 | config.corejs = 3; 32 | }) 33 | .configureTerserPlugin((config) => { 34 | config.terserOptions = { 35 | format: { 36 | comments: true 37 | } 38 | }; 39 | config.extractComments = true; 40 | }) 41 | ; 42 | 43 | module.exports = Encore.getWebpackConfig(); 44 | --------------------------------------------------------------------------------