diff --git a/.gitattributes b/.gitattributes index b9b652f35..f2b996686 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,10 +7,14 @@ /tests/ export-ignore /docs/ export-ignore -/.editorconfig export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore -/.php-censor.yml export-ignore -/phpunit.xml.dist export-ignore -/phpmd.xml.dist export-ignore -/.php_cs.dist export-ignore +/Makefile export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php-censor.yml export-ignore +/phpunit.xml.dist export-ignore +/phpmd.xml.dist export-ignore +/.php_cs.dist export-ignore +/codecov.yml export-ignore +/infection.json.dist export-ignore +/psalm.xml.dist export-ignore diff --git a/.gitignore b/.gitignore index 7d6d2aca2..64f141baf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /composer.phar /vendor -/.php_cs.cache + /.php-cs-fixer.cache /.phpunit.result.cache diff --git a/.php-censor.yml b/.php-censor.yml index 5895258eb..d111b9e00 100644 --- a/.php-censor.yml +++ b/.php-censor.yml @@ -10,10 +10,15 @@ setup: SKIP_DB_TESTS: '0' POSTGRESQL_USER: '%SECRET:postgres_testdb_user%' POSTGRESQL_PASSWORD: '%SECRET:postgres_testdb_password%' - POSTGRESQL_DBNAME: '%SECRET:postgres_testdb_name%' + POSTGRESQL_DBNAME: '%SECRET:postgres_testdb_name%_%BUILD_ID%' MYSQL_USER: '%SECRET:mysql_testdb_user%' MYSQL_PASSWORD: '%SECRET:mysql_testdb_password%' - MYSQL_DBNAME: '%SECRET:mysql_testdb_name%' + MYSQL_DBNAME: '%SECRET:mysql_testdb_name%_%BUILD_ID%' + shell: + execute_all: true + commands: + - "cd ~ && ./create_test_ci_mysql_db.sh %BUILD_ID%" + - "cd ~ && ./create_test_ci_postgres_db.sh %BUILD_ID%" test: php_unit: @@ -44,6 +49,10 @@ test: allow_failures: false complete: + shell: + commands: + - "cd ~ && ./drop_test_ci_mysql_db.sh %BUILD_ID%" + - "cd ~ && ./drop_test_ci_postgres_db.sh %BUILD_ID%" email_notify: default_mailto_address: poisoncorpsee@gmail.com telegram_notify: diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 91d9f0598..bc368207a 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -14,12 +14,16 @@ return $config ->setRules([ - '@PSR1' => true, - '@PSR12' => true, - 'strict_param' => true, - 'array_syntax' => ['syntax' => 'short'], - 'blank_line_before_statement' => ['statements' => ['return', 'throw']], - 'general_phpdoc_tag_rename' => ['replacements' => [ + '@PSR1' => true, + '@PSR12' => true, + 'strict_param' => true, + 'strict_comparison' => true, + //'declare_strict_types' => true, + 'fully_qualified_strict_types' => true, + 'native_function_invocation' => ['include' => ['@internal'], 'scope' => 'all', 'strict' => true], + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_before_statement' => ['statements' => ['return', 'throw', 'break', 'continue']], + 'general_phpdoc_tag_rename' => ['replacements' => [ 'inheritDocs' => 'inheritDoc', 'inheritdocs' => 'inheritDoc', 'inheritdoc' => 'inheritDoc', diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a3e154fd..dc1b566bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,35 +1,36 @@ -Changelog 2.0 +Changelog 2.1 ============= -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [2.0.14 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.14) (2025-03-15) +## [2.1.6 (Mr. Meeseeks)](https://github.com/php-censor/php-censor/tree/2.1.6) (2025-03-15) -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.13...2.0.14) +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.1.5...2.1.6) ### Fixed -- Security issue CVE-2024-51736: Command execution hijack on Windows with Process class. See: https://symfony.com/cve-2024-51736. +- Security issue with remember me key in auth. See: https://chmod744.super.site/redacted-vulnerability. +- Security issue CVE-2024-50345: CVE-2024-50345: Open redirect via browser-sanitized URLs. See: https://symfony.com/cve-2024-50345. -## [2.0.13 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.13) (2024-05-04) +## [2.1.5 (Mr. Meeseeks)](https://github.com/php-censor/php-censor/tree/2.1.5) (2024-05-04) -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.12...2.0.13) +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.1.4...2.1.5) ### Added -- GitHub Actions pipeline (backport from v2.1) + support of PHP 8.2 and 8.3. +- Support of PHP 8.2 and 8.3 in GitHub Actions. ### Fixed - Security issue with remember me key in auth. See: https://chmod744.super.site/redacted-vulnerability. -## [2.0.12 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.12) (2024-01-11) +## [2.1.4 (Mr. Meeseeks)](https://github.com/php-censor/php-censor/tree/2.1.4) (2024-01-11) -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.11...2.0.12) +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.1.3...2.1.4) ### Fixed @@ -37,201 +38,78 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - `guzzlehttp/psr7` (1.9.0) | CVE-2023-29197: Improper header validation | https://github.com/guzzle/psr7/security/advisories/GHSA-wxmh-65f7-jcvw. -## [2.0.11 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.11) (2023-01-11) - -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.10...2.0.11) - -### Fixed - -- PHP 8.1 deprecation while searching for composer binary. Pull request [#434](https://github.com/php-censor/php-censor/pull/434). -Thanks to [@StudioMaX](https://github.com/StudioMaX). -- PHP 8.1 error with return type of `php_user_filter::filter` function. - - -## [2.0.10 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.10) (2022-06-26) - -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.9...2.0.10) - -### Fixed - -- Updated dependencies. Fixed: - - `guzzlehttp/guzzle` (6.5.7) | CVE-2022-31090: CURLOPT_HTTPAUTH option not cleared on change of origin | https://github.com/guzzle/guzzle/security/advisories/GHSA-25mq-v84q-4j7r - - - `guzzlehttp/guzzle` (6.5.7) | CVE-2022-31091: Change in port should be considered a change in origin https://github.com/guzzle/guzzle/security/advisories/GHSA-q559-8m2m-g699 - - -## [2.0.9 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.9) (2022-06-11) - -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.8...2.0.9) - -### Fixed - -- Updated dependencies. Fixed: - - `guzzlehttp/guzzle` (6.5.6) | CVE-2022-31042: Failure to strip the Cookie header on change in host or HTTP downgrade | https://github.com/guzzle/guzzle/security/advisories/GHSA-f2wf-25xc-69c9 - - - `guzzlehttp/guzzle` (6.5.6) | CVE-2022-31043: Fix failure to strip Authorization header on HTTP downgrade | https://github.com/guzzle/guzzle/security/advisories/GHSA-w248-ffj2-4v5q - -### Changed - -- Added secrets to PHP Censor CI config (`.php-censor.yml`). - - -## [2.0.8 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.8) (2022-06-08) - -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.7...2.0.8) - -### Fixed - -- Updated dependencies. Fixed: - - `guzzlehttp/guzzle` (6.5.5) | CVE-2022-29248: Cross-domain cookie leakage | https://github.com/guzzle/guzzle/security/advisories/GHSA-cwmx-hcrq-mhc3. - - - `guzzlehttp/psr7` (1.8.3) | CVE-2022-24775: Inproper parsing of HTTP headers | https://github.com/guzzle/psr7/security/advisories/GHSA-q7rv-6hp3-vh96. - - -## [2.0.7 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.7) (2022-01-19) +## [2.1.3 (Mr. Meeseeks)](https://github.com/php-censor/php-censor/tree/2.1.3) (2023-01-11) -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.6...2.0.7) +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.1.2...2.1.3) ### Fixed -- **[PhpCsFixer]** Problems with `udiff` option. - - -## [2.0.6 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.6) (2021-12-19) - -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.5...2.0.6) - -### Fixed - -- **[Codeception]** Updated Codeception version (See: [CVE-2021-23420](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-23420)). - -### Changed - -- Several documentation improvements. -- Improved code style. - - -## [2.0.5 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.5) (2021-08-22) - -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.4...2.0.5) - -### Fixed - -- Bug with wrong type when field "access_information" is empty (null). -- **[PhpCsFixer]** Support for version 3.0+. Pull request [#414](https://github.com/php-censor/php-censor/pull/414). +- PHP 8.1 deprecation while searching for composer binary. Pull request [#434](https://github.com/php-censor/php-censor/pull/434). Thanks to [@StudioMaX](https://github.com/StudioMaX). -- **[Mysql, Pgsql, Sqlite]** Variables interpolation for queries. Pull requests - [#415](https://github.com/php-censor/php-censor/pull/415), [#416](https://github.com/php-censor/php-censor/pull/416). - Thanks to [@KieranFYI](https://github.com/KieranFYI). - -### Removed - -- Useless TravisCI and CodeCov configs. +- PHP 8.1 error with return type of `php_user_filter::filter` function. -## [2.0.4 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.4) (2021-06-12) +## [2.1.2 (Mr. Meeseeks)](https://github.com/php-censor/php-censor/tree/2.1.2) (2022-09-01) -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.3...2.0.4) +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.1.1...2.1.2) ### Fixed -- **[PhpStan]** Option `directories` and `directory`. -Issue [#408](https://github.com/php-censor/php-censor/issues/#408). Pull request -[#409](https://github.com/php-censor/php-censor/pull/409). Thanks to [@StudioMaX](https://github.com/StudioMaX). -- **[SecurityChecker]** Option `allowed_warnings`. -- Security issue with old Chart.js version (Chart.js upgraded from version `1.1.1` to `3.3.0`). +- Build logger expression. Pull requests [#431](https://github.com/php-censor/php-censor/pull/431). Thanks to [@StudioMaX](https://github.com/StudioMaX). +- Type of GET param `commitId` in WebhookController. Issue [#432](https://github.com/php-censor/php-censor/pull/432). +- Webhook response type. Issue [#432](https://github.com/php-censor/php-censor/pull/432). +- Secrets security: denied to using secrets in notify plugins content. -## [2.0.3 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.3) (2021-04-20) +## [2.1.1 (Mr. Meeseeks)](https://github.com/php-censor/php-censor/tree/2.1.1) (2022-08-30) -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.2...2.0.3) +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.1.0...2.1.1) ### Fixed -- **[Mage, Mage3, DeployerOrg]** Options `binary_path`, `priority_path` for Mage/Mage3/DeployerOrg plugins. - Pull request [#406](https://github.com/php-censor/php-censor/pull/406). Thanks to [@gnomii](https://github.com/gnomii). +- `php-censor/common` package version. +- Logging with secret variables (Now secrets hides from log). +- Secrets validation. -## [2.0.2 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.2) (2021-03-21) +## [2.1.0 (Mr. Meeseeks)](https://github.com/php-censor/php-censor/tree/2.1.0) (2022-08-18) -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.1...2.0.2) +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.10...2.1.0) -### Fixed +### Added -- SSH keys generating (Removed unwanted symbols). Issue [#403](https://github.com/php-censor/php-censor/issues/#403). -- Environments (Case when you may get environment from another project). Issue -[#405](https://github.com/php-censor/php-censor/issues/#405). -- Localizations for "Notify" plugins. -- Deprecations from PHP 8.0. Pull request [#404](https://github.com/php-censor/php-censor/pull/404). Thanks to -[@ismaail](https://github.com/ismaail). +- Secrets storage with UI and secret variables in build interpolation (you can use it like `%SECRET:secret_name%`. See [documentation](https://github.com/php-censor/php-censor/blob/master/docs/en/interpolation.md)). Issue [#14](https://github.com/php-censor/php-censor/issues/#14). +- Optional logging into database webhook requests payloads (option `php-censor.webhook.log_requests`). Issue [#384](https://github.com/php-censor/php-censor/issues/#384). +- Steps inside stages (`test`, `deploy` etc.) which allow to have several same plugins into one stage. Issue [#91](https://github.com/php-censor/php-censor/issues/#91). Pull request [#417](https://github.com/php-censor/php-censor/pull/417). Thanks to [@KieranFYI](https://github.com/KieranFYI). Usage example: + ```yml + setup: # <--- stage + setup_env: # <--- step 1 + plugin: shell # <--- step 1 plugin name + commands: + - "php -r \"copy('.env.ci', '.env');\"" + - "php artisan key:generate" + - "chmod -R 777 storage bootstrap/cache" + migrate: # <--- step 2 + plugin: shell # <--- step 2 same plugin name + commands: + - "php artisan migrate" + ``` +- `GET`-parameter `environment` for Git webhook. Issue [#407](https://github.com/php-censor/php-censor/issues/#407). +- Cloning/coping projects ability. +- **[PHP Unit]** Coverage trand for builds in the timeline on dashboard. ### Changed -- **[SecurityChecker]** Reimplement the plugin because package `sensiolabs/security-checker` was archived/abandoned -(See [README](https://github.com/sensiolabs/security-checker#sensiolabs-security-checker)). Now plugin uses `symfony` -binary (Symfony CLI) or `fabpot/local-php-security-checker` tool for working. See -[documentation](https://github.com/php-censor/php-censor/blob/release-1.3/docs/en/plugins/security_checker.md). - -### Removed - -- Useless empty doc file about cronjob. -- Useless empty ru doc pages. - - -## [2.0.1 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.1) (2021-01-17) - -[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.0...2.0.1) +- Massive refactoring: added types, dependency injection, new tests, documentation, fixed code style etc. Issue [#413](https://github.com/php-censor/php-censor/issues/#413). Pull requests [#412](https://github.com/php-censor/php-censor/pull/412), [#424](https://github.com/php-censor/php-censor/pull/424) and [#425](https://github.com/php-censor/php-censor/pull/425). Thanks to [@KieranFYI](https://github.com/KieranFYI) and [@Ooypunk](https://github.com/Ooypunk). +- Integrated `symfony/http-foundation` library as a new HTTP part of project. +- Integrate some features from `php-censor/common` library. +- Improved UI: fixed colors and ratio for `Chart.js` charts, added ability to disable AJAX UI reloading (option `php-censor.realtime_ui`), improved error trends view. Pull request [#426](https://github.com/php-censor/php-censor/pull/426). Thanks to [@KieranFYI](https://github.com/KieranFYI). +- Improved Ukrainian localization. Pull request [#419](https://github.com/php-censor/php-censor/pull/419). Thanks to [@oshka](https://github.com/oshka). ### Fixed -- **[PhpCpd]** Param "--names-exclude" for plugin PhpCpd (version 6+). Issue - [#401](https://github.com/php-censor/php-censor/issues/#401). - -### Changed - -- Added `.phpunit.result.cache` file to `.gitignore`. -- Improved `CHANGELOG.md`. -- Improved `.php-censor.yml` config. - - -## [2.0.0 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.0) (2021-01-10) - -[Full Changelog](https://github.com/php-censor/php-censor/compare/1.3.0...2.0.0) - -### [How tp upgrade from v1 to v2](/docs/UPGRADE_2.0.md) - -### Changed - -- **Minimal PHP version increased to 7.4 (from 5.6)**. - -### Removed - -- **Deprecations from versions 1.x**: - - Cronjob worker: `php-censor:run-builds` (Use daemon worker instead: `php-censor:worker`). - - Project configs `phpci.yml` and `.phpci.yml` (use `.php-censor.yml` instead). - - `PHPCI_*` interpolation and env variables (Use `PHP_CENSOR_*` instead). - - Global application config section `b8.database` (Use `php-censor.database` instead). - - Options `authToken`, `api_key`, `api_token`, `token` for several plugins: `CampfireNotify`, -`HipchatNotify`, `FlowdockNotify`, `TelegramNotify`, `SensiolabInsight`, `BitbucketNotify` (Use `auth_token` instead). - - Plugin names: `campfire`, `telegram`, `xmpp`, `email`, `irc`, `phpstan` (Use: `campfire_notify`, `telegram_notify`, -`xmpp_notify`, `email_notify`, `irc_notify`, `php_stan` instead). - - [Codeception] Option `path` (Use option `output_path` instead). - - [Codeception] Option `executable` (Use the options `binary_path` and `binary_name` instead). - - [Grunt] Option `grunt` (Use options `binary_path` and `binary_name` instead). - - [Gulp] Option `gulp` (Use options `binary_path` and `binary_name` instead). - - [PHPCodeSniffer] Option `path` (Use option `directory` instead). - - [PHPCpd] Option `path` (Use option `directory` instead). - - [PHPDocblockChecker] Option `path` (Use option `directory` instead). - - [PHPMessDetector] Option `path` (Use option `directory` instead). - - [PHPUnit] Option `directory` (Use option `directories` instead). - - [SensiolabsInsight] Option `executable` (Use the options `binary_path` and `binary_name` instead). - - [Shell] Option `command` and commands list without any named option. Use option `commands` instead. - - [PackageBuild] Special variables for plugin (`%build.commit%`, `%build.id%`, `%build.branch%`, `%project.title%`, `%date%` and `%time%`). Use interpolated variables instead (`%COMMIT_ID%`, `%BUILD_ID%`, `%BRANCH%`, `%PROJECT_TITLE%`, `%CURRENT_DATE%`, `CURRENT_TIME`). - - [MySQL and PostgreSQL] Options `pass` for plugins MySQL and PostgreSQL. Use option `password` instead. - - [MySQL, PostgreSQL, SQLite] Queries list without option for plugins MySQL, PostgreSQL and SQLite. Use the options `queries` instead. - - [MySQL] Imports list without option for plugin MySQL. Use the options `imports` instead. - - [Mage, Mage3] Section `mage` and `mage3` in the global application config and option `bin`. Use the plugin options `binary_path` and `binary_name` instead. - - [CampfireNotify] Variable `%buildurl%` (Use the variable `%BUILD_LINK%` instead). +- Install command return code. +- **[PHPUnit]** Xdebug settings for coverage option. Pull request [#427](https://github.com/php-censor/php-censor/pull/427). Thanks to [@KieranFYI](https://github.com/KieranFYI). ## Other versions @@ -240,3 +118,4 @@ binary (Symfony CLI) or `fabpot/local-php-security-checker` tool for working. Se - [1.1 Changelog](/docs/CHANGELOG_1.1.md) - [1.2 Changelog](/docs/CHANGELOG_1.2.md) - [1.3 Changelog](/docs/CHANGELOG_1.3.md) +- [2.0 Changelog](/docs/CHANGELOG_2.0.md) diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..ebbb9b0b9 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +PHP?=php7.4 +COMPOSER=/usr/local/bin/composer + +php-info: + @echo "Default PHP version: $(PHP) (Run with custom PHP version: make install PHP=php8.0).\n"; + +list: php-info ## List + @sed -rn 's/^([a-zA-Z_-]+):.*?## (.*)$$/"\1" "\2"/p' < $(MAKEFILE_LIST) | xargs printf "%-20s%s\n" + +install: php-info ## Install dependencies (make install PHP=php8.0) + @if [ ! -d "vendor" ]; then $(PHP) $(COMPOSER) install; fi; + +install-force: php-info ## Force install dependencies (make install PHP=php8.0) + $(PHP) $(COMPOSER) install + +update: php-info ## Update dependencies + @$(PHP) $(COMPOSER) update + +test: php-info install ## Run PHPUnit tests + $(PHP) vendor/bin/phpunit --configuration=phpunit.xml.dist --verbose + +test-coverage: php-info install ## Run PHPUnit tests with coverage report + $(PHP) vendor/bin/phpunit --configuration=phpunit.xml.dist --verbose --coverage-text --coverage-html=tests/runtime/coverage + +mutation-test: php-info install ## Run Infection mutation tests + $(PHP) vendor/bin/infection --threads=4 --show-mutations -vvv + +code-style-fix: php-info install ## Fix code style + $(PHP) vendor/bin/php-cs-fixer fix --allow-risky=yes --diff + +psalm: php-info install ## Run Psalm check + $(PHP) vendor/bin/psalm --config=psalm.xml.dist --threads=4 --show-snippet=true --show-info=true + +rector: php-info install ## Run Rector check + $(PHP) vendor/bin/rector --dry-run --clear-cache + +rector-fix: php-info install ## Run Rector fix + $(PHP) vendor/bin/rector --clear-cache + +run-worker: php-info install ## Run PHP Censor worker + $(PHP) bin/console php-censor:worker -v + +.PHONY: php-info list install install-force update test test-coverage mutation-test code-style-fix psalm rector rector-fix run-worker +.DEFAULT_GOAL := list diff --git a/README.md b/README.md index f268ba0de..89079768f 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,29 @@ -[![PHP Censor](http://ci.php-censor.info/build-status/image/2?branch=master&label=PHPCensor&style=flat-square)](http://ci.php-censor.info/build-status/view/2?branch=master) -[![Latest Version](https://img.shields.io/packagist/v/php-censor/php-censor.svg?label=Version&style=flat-square)](https://packagist.org/packages/php-censor/php-censor) -[![Total downloads](https://img.shields.io/packagist/dt/php-censor/php-censor.svg?label=Downloads&style=flat-square)](https://packagist.org/packages/php-censor/php-censor) -[![License](https://img.shields.io/packagist/l/php-censor/php-censor.svg?label=License&style=flat-square)](https://packagist.org/packages/php-censor/php-censor) +[![Minimum PHP version: 7.4.0](https://img.shields.io/badge/php-7.4.0%2B-blue.svg?label=PHP)](https://packagist.org/packages/php-censor/php-censor) +[![Actions](https://github.com/php-censor/php-censor/actions/workflows/ci.yaml/badge.svg)](https://github.com/php-censor/php-censor/actions) +[![PHP Censor](http://ci.php-censor.info/build-status/image/2?branch=master&label=PHP%20Censor)](http://ci.php-censor.info/build-status/view/2?branch=master) +[![Codecov](https://codecov.io/gh/php-censor/php-censor/branch/master/graph/badge.svg)](https://codecov.io/gh/php-censor/php-censor) +[![Latest Version](https://img.shields.io/packagist/v/php-censor/php-censor.svg?label=Version)](https://packagist.org/packages/php-censor/php-censor) +[![Total downloads](https://img.shields.io/packagist/dt/php-censor/php-censor.svg?label=Downloads)](https://packagist.org/packages/php-censor/php-censor) +[![License](https://img.shields.io/packagist/l/php-censor/php-censor.svg?label=License)](https://packagist.org/packages/php-censor/php-censor)

PHP Censor

- - + **PHP Censor** is an open source, self-hosted, continuous integration server for PHP projects -([PHPCI](https://www.phptesting.org) fork). [Official twitter @php_censor](https://twitter.com/php_censor). +([PHPCI](https://github.com/dancryer/PHPCI) fork). [Official twitter @php_censor](https://twitter.com/php_censor). PHP Censor versions: -| Version | Latest | Branch | Status | Minimal PHP Version | -| :------------------: |:--------:| :-----------: |:---------------------------------------------------------------------:| :-----------------: | -| `1.0` (Morty Smith) | `1.0.16` | `release-1.0` | Old version (**UNSUPPORTED**) | `>=5.6, <8.0` | -| `1.1` (Birdperson) | `1.1.6` | `release-1.1` | Old version (**UNSUPPORTED**) | `>=5.6, <8.0` | -| `1.2` (Summer Smith) | `1.2.4` | `release-1.2` | Old version (**UNSUPPORTED**) | `>=5.6, <8.0` | -| `1.3` (Jerry Smith) | `1.3.6` | `release-1.3` | Old version (**UNSUPPORTED**) | `>=5.6, <8.0` | -| `2.0` (Rick Sanchez) | `2.0.5` | `release-2.0` | Current stable version ([Upgrade from v1 to v2](docs/UPGRADE_2.0.md)) | `>=7.4` | -| `2.1` | WIP | `master` | Feature minor version (WIP) | `>=7.4` | +| Version | Latest | Branch | Status | Minimal PHP Version | +|:---------------------:|:--------:|:-------------:|:------------------------------------------------------------------:| :-----------------: | +| `1.0` (Morty Smith) | `1.0.16` | `release-1.0` | Old version (**UNSUPPORTED**) | `>=5.6, <8.0` | +| `1.1` (Birdperson) | `1.1.6` | `release-1.1` | Old version (**UNSUPPORTED**) | `>=5.6, <8.0` | +| `1.2` (Summer Smith) | `1.2.4` | `release-1.2` | Old version (**UNSUPPORTED**) | `>=5.6, <8.0` | +| `1.3` (Jerry Smith) | `1.3.7` | `release-1.3` | Old version (**UNSUPPORTED**) | `>=5.6, <8.0` | +| `2.0` (Rick Sanchez) | `2.0.14` | `release-2.0` | Last stable version ([Upgrade from v1 to v2](docs/UPGRADE_2.0.md)) | `>=7.4` | +| `2.1` (Mr. Meeseeks) | `2.1.6` | `release-2.1` | Current stable version | `>=7.4` | +| `2.2` | WIP | `master` | Feature minor version (WIP) | `>=7.4` | [![Dashboard](docs/screenshots/dashboard.png)](docs/screenshots/dashboard.png) @@ -70,7 +73,7 @@ PHPMessDetector, PHPTalLint and TechnicalDebt; * Run through any combination of the other [supported plugins](docs/en/README.md#plugins), including Campfire, CleanBuild, CopyBuild, Deployer, Env, Git, Grunt, Gulp, PackageBuild, Phar, Phing, Shell and Wipe; -* Send notifications to Email, XMPP, Slack, IRC, Flowdock, HipChat and +* Send notifications to Email, XMPP, Slack, IRC, Flowdock and [Telegram](docs/en/plugins/telegram_notify.md); * Use your LDAP-server for authentication; @@ -158,11 +161,28 @@ cd /path/to/php-censor ./vendor/bin/phpunit --configuration ./phpunit.xml.dist --coverage-html ./tests/runtime/coverage -vvv --colors=always ``` -For Phar plugin tests set 'phar.readonly' setting to Off (0) in `php.ini` config. Otherwise the tests will be skipped. +For Phar plugin tests set `phar.readonly` setting to Off (`0`) in `php.ini` config. Otherwise the tests will be skipped. + +For database tests create an empty databases on 'localhost' with user/password for MySQL/PostgreSQL and set env +variables from `phpunit.xml.dist` config. For example: + +```shell +#!/usr/bin/env bash -For database tests create an empty 'test_db' database on 'localhost' with user/password: `root/` -for MySQL and with user/password: `postgres/` for PostgreSQL (You can change default test user, password and -database name in `phpunit.xml[.dist]` config constants). If connection failed the tests will be skipped. +psql --username="test" --host="127.0.0.1" --echo-all --command="DROP DATABASE IF EXISTS \"php-censor-test\";" +psql --username="test" --host="127.0.0.1" --echo-all --command="CREATE DATABASE \"php-censor-test\";" + +mysql --user="test" --password="test" --host="127.0.0.1" --verbose --execute="CREATE DATABASE IF NOT EXISTS \`php-censor-test\`;" + +export SKIP_DB_TESTS=0;\ +export POSTGRESQL_DBNAME=php-censor-test;\ +export POSTGRESQL_USER=test;\ +export POSTGRESQL_PASSWORD=test;\ +export MYSQL_DBNAME=php-censor-test;\ +export MYSQL_USER=test;\ +export MYSQL_PASSWORD=test;\ +vendor/bin/phpunit --configuration=phpunit.xml.dist --verbose +``` ## Documentation diff --git a/VERSION.md b/VERSION.md index 3d45b5c65..a74d18b8d 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -2.0.14 +2.2.0-alpha diff --git a/bin/console b/bin/console index 591f7fd43..6e170bd6a 100755 --- a/bin/console +++ b/bin/console @@ -1,11 +1,19 @@ #!/usr/bin/env php run(); +(new Application($configuration, $databaseManager, $storeRegistry))->run(); diff --git a/bootstrap.php b/bootstrap.php index 5a2db93d0..b289db590 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -1,49 +1,30 @@ loadYaml($configFile); -} +$session->start(); -if (!defined('APP_URL') && !empty($config)) { - define('APP_URL', $config->get('php-censor.url', '') . '/'); -} +\define('APP_URL', $configuration->get('php-censor.url', '') . '/'); +\define('REALTIME_UI', $configuration->get('php-censor.realtime_ui', true)); -Lang::init($config); +Lang::init($configuration, $storeRegistry, null, $session->get('php-censor-user-id')); diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..2797a62a7 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,18 @@ +coverage: + round: nearest + precision: 2 + +github_checks: + annotations: false + +comment: false + +ignore: + - "app" + - "docs" + - "public" + - "runtime" + - "tests" + - "vendor" + - "src/Migrations" + - "src/Languages" diff --git a/composer.json b/composer.json index 47ebfc164..fee2d97b0 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,10 @@ "ext-curl": "*", "ext-bcmath": "*", + "php-censor/common": "^1.0", + "swiftmailer/swiftmailer": "^6.2", + "symfony/http-foundation": "^5.4", "symfony/yaml": "^5.4", "symfony/console": "^5.4", "symfony/finder": "^5.4", @@ -79,11 +82,11 @@ "sebastian/diff": "^4.0", "maknz/slack": "^1.7", - "hipchat/hipchat-php": "^1.4", "php-censor/flowdock-client": "^2.0" }, "require-dev": { - "phpunit/phpunit": "^9.0", + "phpunit/phpunit": "^9.5", + "infection/infection": "^0.25", "phpspec/prophecy-phpunit": "^2.0", "squizlabs/php_codesniffer": "^3.5", "sebastian/phpcpd": "^6.0", @@ -92,7 +95,9 @@ "php-parallel-lint/php-parallel-lint": "^1.2", "php-censor/phpdoc-checker": "^3.0", "friendsofphp/php-cs-fixer": "^3.3", - "symfony/var-dumper": "^4.4" + "symfony/var-dumper": "^4.4", + "vimeo/psalm": "^4.23", + "rector/rector": "^0.14" }, "extra": { "platform": { diff --git a/composer.lock b/composer.lock index 34e05bb27..09fe3ffdb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "aacc0500b0411e7aaf41aca25bac439b", + "content-hash": "b021933c25e0840d8c885e184593250d", "packages": [ { "name": "cakephp/core", @@ -728,52 +728,6 @@ ], "time": "2023-04-17T16:00:37+00:00" }, - { - "name": "hipchat/hipchat-php", - "version": "v1.4", - "source": { - "type": "git", - "url": "https://github.com/hipchat/hipchat-php.git", - "reference": "5936c0a48d2d514d94bfc1d774b04c42cd3bc39e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hipchat/hipchat-php/zipball/5936c0a48d2d514d94bfc1d774b04c42cd3bc39e", - "reference": "5936c0a48d2d514d94bfc1d774b04c42cd3bc39e", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "HipChat": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "HipChat", - "email": "support@hipchat.com", - "homepage": "https://www.hipchat.com", - "role": "Company" - } - ], - "description": "PHP library for HipChat", - "homepage": "http://github.com/hipchat/hipchat-php", - "keywords": [ - "hipchat" - ], - "support": { - "issues": "https://github.com/hipchat/hipchat-php/issues", - "source": "https://github.com/hipchat/hipchat-php/tree/master" - }, - "time": "2015-04-28T22:48:40+00:00" - }, { "name": "jasongrimes/paginator", "version": "1.0.3", @@ -1033,6 +987,77 @@ }, "time": "2024-01-11T15:06:06+00:00" }, + { + "name": "php-censor/common", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-censor/common.git", + "reference": "5a3e18158e40b665476606d95c2efca11ec33a5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-censor/common/zipball/5a3e18158e40b665476606d95c2efca11ec33a5c", + "reference": "5a3e18158e40b665476606d95c2efca11ec33a5c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.4.0", + "psr/container": "^1.1", + "symfony/mailer": "^5.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "infection/infection": "^0.25", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phploc/phploc": "^7.0", + "phpmd/phpmd": "^1.5", + "phpunit/phpunit": "^9.5", + "sebastian/phpcpd": "^6.0", + "vimeo/psalm": "^4.16" + }, + "type": "library", + "extra": { + "platform": { + "php": "7.4.*" + } + }, + "autoload": { + "psr-4": { + "PHPCensor\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Dmitry Khomutov", + "email": "poisoncorpsee@gmail.com", + "homepage": "https://corpsee.com", + "role": "PHP Censor developer" + } + ], + "description": "PHP Censor Common Library", + "homepage": "https://php-censor.info", + "keywords": [ + "ci", + "ci-server", + "continuous integration", + "open-source", + "php", + "php-censor", + "self-hosted", + "testing" + ], + "support": { + "issues": "https://github.com/php-censor/common/issues", + "source": "https://github.com/php-censor/common" + }, + "time": "2022-06-29T05:37:35+00:00" + }, { "name": "php-censor/flowdock-client", "version": "2.0.0", @@ -2789,42 +2814,45 @@ "time": "2024-09-28T13:32:08+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "name": "symfony/http-foundation", + "version": "v5.4.48", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "url": "https://github.com/symfony/http-foundation.git", + "reference": "3f38b8af283b830e1363acd79e5bc3412d055341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3f38b8af283b830e1363acd79e5bc3412d055341", + "reference": "3f38b8af283b830e1363acd79e5bc3412d055341", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.16" }, - "provide": { - "ext-ctype": "*" + "require-dev": { + "predis/predis": "^1.0|^2.0", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" }, "suggest": { - "ext-ctype": "For best performance" + "symfony/mime": "To use the file extension guesser" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2832,24 +2860,18 @@ ], "authors": [ { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for ctype functions", + "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/http-foundation/tree/v5.4.48" }, "funding": [ { @@ -2865,45 +2887,48 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-11-13T18:58:02+00:00" }, { - "name": "symfony/polyfill-iconv", - "version": "v1.31.0", + "name": "symfony/mailer", + "version": "v5.4.45", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956" + "url": "https://github.com/symfony/mailer.git", + "reference": "f732e1fafdf0f4a2d865e91f1018aaca174aeed9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/48becf00c920479ca2e910c22a5a39e5d47ca956", - "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956", + "url": "https://api.github.com/repos/symfony/mailer/zipball/f732e1fafdf0f4a2d865e91f1018aaca174aeed9", + "reference": "f732e1fafdf0f4a2d865e91f1018aaca174aeed9", "shasum": "" }, "require": { - "php": ">=7.2" + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=7.2.5", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/mime": "^5.2.6|^6.0", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3" }, - "provide": { - "ext-iconv": "*" + "conflict": { + "symfony/http-kernel": "<4.4" }, - "suggest": { - "ext-iconv": "For best performance" + "require-dev": { + "symfony/http-client": "^4.4|^5.0|^6.0", + "symfony/messenger": "^4.4|^5.0|^6.0" }, "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Iconv\\": "" - } + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2911,25 +2936,18 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Iconv extension", + "description": "Helps sending emails", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "iconv", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-iconv/tree/v1.31.0" + "source": "https://github.com/symfony/mailer/tree/v5.4.45" }, "funding": [ { @@ -2945,42 +2963,53 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "name": "symfony/mime", + "version": "v5.4.45", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "url": "https://github.com/symfony/mime.git", + "reference": "8c1b9b3e5b52981551fc6044539af1d974e39064" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/mime/zipball/8c1b9b3e5b52981551fc6044539af1d974e39064", + "reference": "8c1b9b3e5b52981551fc6044539af1d974e39064", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16" }, - "suggest": { - "ext-intl": "For best performance" + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<4.4", + "symfony/serializer": "<5.4.35|>=6,<6.3.12|>=6.4,<6.4.3" }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/process": "^5.4|^6.4", + "symfony/property-access": "^4.4|^5.1|^6.0", + "symfony/property-info": "^4.4|^5.1|^6.0", + "symfony/serializer": "^5.4.35|~6.3.12|^6.4.3" }, + "type": "library", "autoload": { - "files": [ - "bootstrap.php" - ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2988,26 +3017,22 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's grapheme_* functions", + "description": "Allows manipulating MIME messages", "homepage": "https://symfony.com", "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" + "mime", + "mime-type" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/mime/tree/v5.4.45" }, "funding": [ { @@ -3023,28 +3048,30 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-10-23T20:18:32+00:00" }, { - "name": "symfony/polyfill-intl-idn", + "name": "symfony/polyfill-ctype", "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", - "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.2", - "symfony/polyfill-intl-normalizer": "^1.10" + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" }, "suggest": { - "ext-intl": "For best performance" + "ext-ctype": "For best performance" }, "type": "library", "extra": { @@ -3058,7 +3085,7 @@ "bootstrap.php" ], "psr-4": { - "Symfony\\Polyfill\\Intl\\Idn\\": "" + "Symfony\\Polyfill\\Ctype\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -3067,27 +3094,262 @@ ], "authors": [ { - "name": "Laurent Bassin", - "email": "laurent@bassin.info" - }, - { - "name": "Trevor Rowbotham", - "email": "trevor.rowbotham@pm.me" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "idn", - "intl", + "ctype", "polyfill", - "portable", - "shim" + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/48becf00c920479ca2e910c22a5a39e5d47ca956", + "reference": "48becf00c920479ca2e910c22a5a39e5d47ca956", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-iconv": "*" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Iconv extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "iconv", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/c36586dcf89a12315939e00ec9b4474adcb1d773", + "reference": "c36586dcf89a12315939e00ec9b4474adcb1d773", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" ], "support": { "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.31.0" @@ -4208,31 +4470,44 @@ ], "packages-dev": [ { - "name": "clue/ndjson-react", - "version": "v1.3.0", + "name": "amphp/amp", + "version": "v2.6.4", "source": { "type": "git", - "url": "https://github.com/clue/reactphp-ndjson.git", - "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + "url": "https://github.com/amphp/amp.git", + "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", - "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "url": "https://api.github.com/repos/amphp/amp/zipball/ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", "shasum": "" }, "require": { - "php": ">=5.3", - "react/stream": "^1.2" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", - "react/event-loop": "^1.2" + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^7 | ^8 | ^9", + "react/promise": "^2", + "vimeo/psalm": "^3.12" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, "autoload": { + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ], "psr-4": { - "Clue\\React\\NDJson\\": "src/" + "Amp\\": "lib" } }, "notification-url": "https://packagist.org/downloads/", @@ -4241,70 +4516,218 @@ ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering" - } - ], - "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", - "homepage": "https://github.com/clue/reactphp-ndjson", - "keywords": [ - "NDJSON", - "json", - "jsonlines", - "newline", - "reactphp", - "streaming" + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" ], "support": { - "issues": "https://github.com/clue/reactphp-ndjson/issues", - "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.4" }, "funding": [ { - "url": "https://clue.engineering/support", + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T18:52:26+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v1.8.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.4", + "friendsofphp/php-cs-fixer": "^2.3", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6 || ^7 || ^8", + "psalm/phar": "^3.11.4" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-13T18:00:56+00:00" + }, + { + "name": "composer/package-versions-deprecated", + "version": "1.11.99.5", + "source": { + "type": "git", + "url": "https://github.com/composer/package-versions-deprecated.git", + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/package-versions-deprecated/zipball/b4f54f74ef3453349c24a845d22392cd31e65f1d", + "reference": "b4f54f74ef3453349c24a845d22392cd31e65f1d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1.0 || ^2.0", + "php": "^7 || ^8" + }, + "replace": { + "ocramius/package-versions": "1.11.99" + }, + "require-dev": { + "composer/composer": "^1.9.3 || ^2.0@dev", + "ext-zip": "^1.13", + "phpunit/phpunit": "^6.5 || ^7" + }, + "type": "composer-plugin", + "extra": { + "class": "PackageVersions\\Installer", + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "PackageVersions\\": "src/PackageVersions" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be" + } + ], + "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", + "support": { + "issues": "https://github.com/composer/package-versions-deprecated/issues", + "source": "https://github.com/composer/package-versions-deprecated/tree/1.11.99.5" + }, + "funding": [ + { + "url": "https://packagist.com", "type": "custom" }, { - "url": "https://github.com/clue", + "url": "https://github.com/composer", "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" } ], - "time": "2022-12-23T10:58:28+00:00" + "time": "2022-01-17T14:14:24+00:00" }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/67a32d7d6f9f560b726ab25a061b38ff3a80c560", + "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.10" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "extra": { - "phpstan": { - "includes": [ - "extension.neon" - ] - }, "branch-alias": { - "dev-main": "3.x-dev" + "dev-main": "1.x-dev" } }, "autoload": { @@ -4332,7 +4755,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/1.0.1" }, "funding": [ { @@ -4348,7 +4771,7 @@ "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2022-01-21T20:24:37+00:00" }, { "name": "composer/semver", @@ -4433,27 +4856,27 @@ }, { "name": "composer/xdebug-handler", - "version": "3.0.5", + "version": "2.0.5", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", - "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/9e36aeed4616366d2b690bdce11f71e9178c579a", + "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a", "shasum": "" }, "require": { - "composer/pcre": "^1 || ^2 || ^3", - "php": "^7.2.5 || ^8.0", + "composer/pcre": "^1", + "php": "^5.3.2 || ^7.0 || ^8.0", "psr/log": "^1 || ^2 || ^3" }, "require-dev": { "phpstan/phpstan": "^1.0", "phpstan/phpstan-strict-rules": "^1.1", - "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" }, "type": "library", "autoload": { @@ -4477,9 +4900,9 @@ "performance" ], "support": { - "irc": "ircs://irc.libera.chat:6697/composer", + "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + "source": "https://github.com/composer/xdebug-handler/tree/2.0.5" }, "funding": [ { @@ -4495,7 +4918,120 @@ "type": "tidelift" } ], - "time": "2024-05-06T16:37:16+00:00" + "time": "2022-02-24T20:20:32+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/annotations", + "version": "1.14.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "253dca476f70808a5aeed3a47cc2cc88c5cab915" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/253dca476f70808a5aeed3a47cc2cc88c5cab915", + "reference": "253dca476f70808a5aeed3a47cc2cc88c5cab915", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1 || ^2", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "~1.4.10 || ^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.14.4" + }, + "time": "2024-09-05T10:15:52+00:00" }, { "name": "doctrine/instantiator", @@ -4524,226 +5060,572 @@ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", "vimeo/psalm": "^4.30 || ^5.4" }, - "type": "library", + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:15:36+00:00" + }, + { + "name": "felixfbecker/advanced-json-rpc", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "php": "^7.1 || ^8.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" + }, + "time": "2021-06-11T22:34:44+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.3", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "reference": "a9e113dbc7d849e35b8776da39edaf4313b7b6c9", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "LanguageServerProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", + "keywords": [ + "language", + "microsoft", + "php", + "server" + ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.3" + }, + "time": "2024-04-30T00:40:11+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", + "reference": "47177af1cfb9dab5d1cc4daf91b7179c2efe7fad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/47177af1cfb9dab5d1cc4daf91b7179c2efe7fad", + "reference": "47177af1cfb9dab5d1cc4daf91b7179c2efe7fad", + "shasum": "" + }, + "require": { + "composer/semver": "^3.2", + "composer/xdebug-handler": "^2.0", + "doctrine/annotations": "^1.12", + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.2.5 || ^8.0", + "php-cs-fixer/diff": "^2.0", + "symfony/console": "^4.4.20 || ^5.1.3 || ^6.0", + "symfony/event-dispatcher": "^4.4.20 || ^5.0 || ^6.0", + "symfony/filesystem": "^4.4.20 || ^5.0 || ^6.0", + "symfony/finder": "^4.4.20 || ^5.0 || ^6.0", + "symfony/options-resolver": "^4.4.20 || ^5.0 || ^6.0", + "symfony/polyfill-mbstring": "^1.23", + "symfony/polyfill-php80": "^1.23", + "symfony/polyfill-php81": "^1.23", + "symfony/process": "^4.4.20 || ^5.0 || ^6.0", + "symfony/stopwatch": "^4.4.20 || ^5.0 || ^6.0" + }, + "require-dev": { + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^1.5", + "mikey179/vfsstream": "^1.6.8", + "php-coveralls/php-coveralls": "^2.5.2", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", + "phpspec/prophecy": "^1.15", + "phpspec/prophecy-phpunit": "^1.1 || ^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5", + "phpunitgoodpractices/polyfill": "^1.5", + "phpunitgoodpractices/traits": "^1.9.1", + "symfony/phpunit-bridge": "^5.2.4 || ^6.0", + "symfony/yaml": "^4.4.20 || ^5.0 || ^6.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2021-12-11T16:25:08+00:00" + }, + { + "name": "infection/abstract-testframework-adapter", + "version": "0.5.0", + "source": { + "type": "git", + "url": "https://github.com/infection/abstract-testframework-adapter.git", + "reference": "18925e20d15d1a5995bb85c9dc09e8751e1e069b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/abstract-testframework-adapter/zipball/18925e20d15d1a5995bb85c9dc09e8751e1e069b", + "reference": "18925e20d15d1a5995bb85c9dc09e8751e1e069b", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^2.17", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Infection\\AbstractTestFramework\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" + } + ], + "description": "Abstract Test Framework Adapter for Infection", + "support": { + "issues": "https://github.com/infection/abstract-testframework-adapter/issues", + "source": "https://github.com/infection/abstract-testframework-adapter/tree/0.5.0" + }, + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" + } + ], + "time": "2021-08-17T18:49:12+00:00" + }, + { + "name": "infection/extension-installer", + "version": "0.1.2", + "source": { + "type": "git", + "url": "https://github.com/infection/extension-installer.git", + "reference": "9b351d2910b9a23ab4815542e93d541e0ca0cdcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/infection/extension-installer/zipball/9b351d2910b9a23ab4815542e93d541e0ca0cdcf", + "reference": "9b351d2910b9a23ab4815542e93d541e0ca0cdcf", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1 || ^2.0" + }, + "require-dev": { + "composer/composer": "^1.9 || ^2.0", + "friendsofphp/php-cs-fixer": "^2.18, <2.19", + "infection/infection": "^0.15.2", + "php-coveralls/php-coveralls": "^2.4", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.10", + "phpstan/phpstan-phpunit": "^0.12.6", + "phpstan/phpstan-strict-rules": "^0.12.2", + "phpstan/phpstan-webmozart-assert": "^0.12.2", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.8" + }, + "type": "composer-plugin", + "extra": { + "class": "Infection\\ExtensionInstaller\\Plugin" + }, "autoload": { "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "Infection\\ExtensionInstaller\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", - "keywords": [ - "constructor", - "instantiate" - ], + "description": "Infection Extension Installer", "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.5.0" + "issues": "https://github.com/infection/extension-installer/issues", + "source": "https://github.com/infection/extension-installer/tree/0.1.2" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" - }, - { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" + "url": "https://github.com/infection", + "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" + "url": "https://opencollective.com/infection", + "type": "open_collective" } ], - "time": "2022-12-30T00:15:36+00:00" + "time": "2021-10-20T22:08:34+00:00" }, { - "name": "evenement/evenement", - "version": "v3.0.2", + "name": "infection/include-interceptor", + "version": "0.2.5", "source": { "type": "git", - "url": "https://github.com/igorw/evenement.git", - "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + "url": "https://github.com/infection/include-interceptor.git", + "reference": "0cc76d95a79d9832d74e74492b0a30139904bdf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", - "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "url": "https://api.github.com/repos/infection/include-interceptor/zipball/0cc76d95a79d9832d74e74492b0a30139904bdf7", + "reference": "0cc76d95a79d9832d74e74492b0a30139904bdf7", "shasum": "" }, - "require": { - "php": ">=7.0" - }, "require-dev": { - "phpunit/phpunit": "^9 || ^6" + "friendsofphp/php-cs-fixer": "^2.16", + "infection/infection": "^0.15.0", + "phan/phan": "^2.4 || ^3", + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "^0.12.8", + "phpunit/phpunit": "^8.5", + "vimeo/psalm": "^3.8" }, "type": "library", "autoload": { "psr-4": { - "Evenement\\": "src/" + "Infection\\StreamWrapper\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com" } ], - "description": "Événement is a very simple event dispatching library for PHP", - "keywords": [ - "event-dispatcher", - "event-emitter" - ], + "description": "Stream Wrapper: Include Interceptor. Allows to replace included (autoloaded) file with another one.", "support": { - "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/v3.0.2" + "issues": "https://github.com/infection/include-interceptor/issues", + "source": "https://github.com/infection/include-interceptor/tree/0.2.5" }, - "time": "2023-08-08T05:53:35+00:00" + "funding": [ + { + "url": "https://github.com/infection", + "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" + } + ], + "time": "2021-08-09T10:03:57+00:00" }, { - "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "name": "infection/infection", + "version": "0.25.6", "source": { "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "url": "https://github.com/infection/infection.git", + "reference": "bded7581329766616bf35f1018326c9b3912df0b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/infection/infection/zipball/bded7581329766616bf35f1018326c9b3912df0b", + "reference": "bded7581329766616bf35f1018326c9b3912df0b", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0" + "composer-runtime-api": "^2.0", + "composer/xdebug-handler": "^2.0", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "infection/abstract-testframework-adapter": "^0.5.0", + "infection/extension-installer": "^0.1.0", + "infection/include-interceptor": "^0.2.5", + "justinrainbow/json-schema": "^5.2.10", + "nikic/php-parser": "^4.13", + "ondram/ci-detector": "^3.3.0", + "php": "^7.4.7 || ^8.0", + "sanmai/later": "^0.1.1", + "sanmai/pipeline": "^5.1 || ^6", + "sebastian/diff": "^3.0.2 || ^4.0", + "seld/jsonlint": "^1.7", + "symfony/console": "^3.4.29 || ^4.1.19 || ^5.0 || ^6.0", + "symfony/filesystem": "^3.4.29 || ^4.1.19 || ^5.0 || ^6.0", + "symfony/finder": "^3.4.29 || ^4.1.19 || ^5.0 || ^6.0", + "symfony/process": "^3.4.29 || ^4.1.19 || ^5.0 || ^6.0", + "thecodingmachine/safe": "^1.1.3", + "webmozart/assert": "^1.3", + "webmozart/path-util": "^2.3" + }, + "conflict": { + "dg/bypass-finals": "*", + "phpunit/php-code-coverage": ">9 <9.1.4" }, "require-dev": { - "fidry/makefile": "^0.2.0", - "fidry/php-cs-fixer-config": "^1.1.2", - "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", - "phpunit/phpunit": "^8.5.31 || ^9.5.26", - "webmozarts/strict-phpunit": "^7.5" + "brianium/paratest": "^6.3", + "ext-simplexml": "*", + "helmich/phpunit-json-assert": "^3.0", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.1.0", + "phpstan/phpstan": "^1.2.0", + "phpstan/phpstan-phpunit": "^1.0.0", + "phpstan/phpstan-strict-rules": "^1.1.0", + "phpstan/phpstan-webmozart-assert": "^1.0.2", + "phpunit/phpunit": "^9.3.11", + "symfony/phpunit-bridge": "^4.4.18 || ^5.1.10", + "symfony/yaml": "^5.0", + "thecodingmachine/phpstan-safe-rule": "^1.1.0" }, + "bin": [ + "bin/infection" + ], "type": "library", "autoload": { "psr-4": { - "Fidry\\CpuCoreCounter\\": "src/" + "Infection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause" ], "authors": [ + { + "name": "Maks Rafalko", + "email": "maks.rafalko@gmail.com", + "homepage": "https://twitter.com/maks_rafalko" + }, + { + "name": "Oleg Zhulnev", + "homepage": "https://github.com/sidz" + }, + { + "name": "Gert de Pagter", + "homepage": "https://github.com/BackEndTea" + }, { "name": "Théo FIDRY", - "email": "theo.fidry@gmail.com" + "email": "theo.fidry@gmail.com", + "homepage": "https://twitter.com/tfidry" + }, + { + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com", + "homepage": "https://www.alexeykopytko.com" + }, + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" } ], - "description": "Tiny utility to get the number of CPU cores.", + "description": "Infection is a Mutation Testing framework for PHP. The mutation adequacy score can be used to measure the effectiveness of a test set in terms of its ability to detect faults.", "keywords": [ - "CPU", - "core" + "coverage", + "mutant", + "mutation framework", + "mutation testing", + "testing", + "unit testing" ], "support": { - "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "issues": "https://github.com/infection/infection/issues", + "source": "https://github.com/infection/infection/tree/0.25.6" }, "funding": [ { - "url": "https://github.com/theofidry", + "url": "https://github.com/infection", "type": "github" + }, + { + "url": "https://opencollective.com/infection", + "type": "open_collective" } ], - "time": "2024-08-06T10:04:20+00:00" + "time": "2022-01-08T13:06:28+00:00" }, { - "name": "friendsofphp/php-cs-fixer", - "version": "v3.72.0", + "name": "justinrainbow/json-schema", + "version": "5.3.0", "source": { "type": "git", - "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "900389362c43d116fee1ffc51f7878145fa61b57" + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/900389362c43d116fee1ffc51f7878145fa61b57", - "reference": "900389362c43d116fee1ffc51f7878145fa61b57", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", "shasum": "" }, "require": { - "clue/ndjson-react": "^1.0", - "composer/semver": "^3.4", - "composer/xdebug-handler": "^3.0.3", - "ext-filter": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "fidry/cpu-core-counter": "^1.2", - "php": "^7.4 || ^8.0", - "react/child-process": "^0.6.5", - "react/event-loop": "^1.0", - "react/promise": "^2.0 || ^3.0", - "react/socket": "^1.0", - "react/stream": "^1.0", - "sebastian/diff": "^4.0 || ^5.1 || ^6.0 || ^7.0", - "symfony/console": "^5.4 || ^6.4 || ^7.0", - "symfony/event-dispatcher": "^5.4 || ^6.4 || ^7.0", - "symfony/filesystem": "^5.4 || ^6.4 || ^7.0", - "symfony/finder": "^5.4 || ^6.4 || ^7.0", - "symfony/options-resolver": "^5.4 || ^6.4 || ^7.0", - "symfony/polyfill-mbstring": "^1.31", - "symfony/polyfill-php80": "^1.31", - "symfony/polyfill-php81": "^1.31", - "symfony/process": "^5.4 || ^6.4 || ^7.2", - "symfony/stopwatch": "^5.4 || ^6.4 || ^7.0" + "php": ">=7.1" }, "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.6", - "infection/infection": "^0.29.14", - "justinrainbow/json-schema": "^5.3 || ^6.2", - "keradus/cli-executor": "^2.1", - "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.7", - "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", - "phpunit/phpunit": "^9.6.22 || ^10.5.45 || ^11.5.12", - "symfony/var-dumper": "^5.4.48 || ^6.4.18 || ^7.2.3", - "symfony/yaml": "^5.4.45 || ^6.4.18 || ^7.2.3" - }, - "suggest": { - "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters." + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" }, "bin": [ - "php-cs-fixer" + "bin/validate-json" ], - "type": "application", + "type": "library", "autoload": { "psr-4": { - "PhpCsFixer\\": "src/" - }, - "exclude-from-classmap": [ - "src/Fixer/Internal/*" - ] + "JsonSchema\\": "src/JsonSchema/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4751,32 +5633,33 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" }, { - "name": "Dariusz Rumiński", - "email": "dariusz.ruminski@gmail.com" + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" } ], - "description": "A tool to automatically fix PHP code style", + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", "keywords": [ - "Static code analysis", - "fixer", - "standards", - "static analysis" + "json", + "schema" ], "support": { - "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.72.0" + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" }, - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], - "time": "2025-03-13T11:25:37+00:00" + "time": "2024-07-06T21:00:26+00:00" }, { "name": "myclabs/deep-copy", @@ -4838,6 +5721,57 @@ ], "time": "2025-02-12T12:17:51+00:00" }, + { + "name": "netresearch/jsonmapper", + "version": "v4.5.0", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "reference": "8e76efb98ee8b6afc54687045e1b8dba55ac76e5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.5.0" + }, + "time": "2024-09-08T10:13:13+00:00" + }, { "name": "nikic/php-parser", "version": "v4.19.4", @@ -4894,6 +5828,131 @@ }, "time": "2024-09-29T15:01:53+00:00" }, + { + "name": "ondram/ci-detector", + "version": "3.5.1", + "source": { + "type": "git", + "url": "https://github.com/OndraM/ci-detector.git", + "reference": "594e61252843b68998bddd48078c5058fe9028bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/OndraM/ci-detector/zipball/594e61252843b68998bddd48078c5058fe9028bd", + "reference": "594e61252843b68998bddd48078c5058fe9028bd", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.2", + "lmc/coding-standard": "^1.3 || ^2.0", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/extension-installer": "^1.0.3", + "phpstan/phpstan": "^0.12.0", + "phpstan/phpstan-phpunit": "^0.12.1", + "phpunit/phpunit": "^7.1 || ^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "OndraM\\CiDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ondřej Machulda", + "email": "ondrej.machulda@gmail.com" + } + ], + "description": "Detect continuous integration environment and provide unified access to properties of current build", + "keywords": [ + "CircleCI", + "Codeship", + "Wercker", + "adapter", + "appveyor", + "aws", + "aws codebuild", + "bamboo", + "bitbucket", + "buddy", + "ci-info", + "codebuild", + "continuous integration", + "continuousphp", + "drone", + "github", + "gitlab", + "interface", + "jenkins", + "teamcity", + "travis" + ], + "support": { + "issues": "https://github.com/OndraM/ci-detector/issues", + "source": "https://github.com/OndraM/ci-detector/tree/main" + }, + "time": "2020-09-04T11:21:14+00:00" + }, + { + "name": "openlss/lib-array2xml", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/nullivex/lib-array2xml.git", + "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nullivex/lib-array2xml/zipball/a91f18a8dfc69ffabe5f9b068bc39bb202c81d90", + "reference": "a91f18a8dfc69ffabe5f9b068bc39bb202c81d90", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "LSS": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Bryan Tong", + "email": "bryan@nullivex.com", + "homepage": "https://www.nullivex.com" + }, + { + "name": "Tony Butler", + "email": "spudz76@gmail.com", + "homepage": "https://www.nullivex.com" + } + ], + "description": "Array2XML conversion library credit to lalit.org", + "homepage": "https://www.nullivex.com", + "keywords": [ + "array", + "array conversion", + "xml", + "xml conversion" + ], + "support": { + "issues": "https://github.com/nullivex/lib-array2xml/issues", + "source": "https://github.com/nullivex/lib-array2xml/tree/master" + }, + "time": "2019-03-29T20:06:56+00:00" + }, { "name": "pdepend/pdepend", "version": "2.16.2", @@ -5117,39 +6176,92 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Dmitry Khomutov", + "email": "poisoncorpsee@gmail.com", + "homepage": "http://corpsee.com", + "role": "PHPDoc Checker developer" + }, + { + "name": "Dan Cryer", + "email": "dan.cryer@block8.co.uk", + "homepage": "http://www.block8.co.uk", + "role": "PHP DocBlocks Checker developer" + } + ], + "description": "A simple tool for checking that your PHP classes and methods use PHPDocs (PHP DocBlocks Checker fork).", + "homepage": "https://github.com/php-censor/phpdoc-checker", + "keywords": [ + "checker", + "code quality", + "comment", + "docblock", + "php", + "php-censor", + "phpdoc", + "testing" + ], + "support": { + "issues": "https://github.com/php-censor/phpdoc-checker/issues", + "source": "https://github.com/php-censor/phpdoc-checker" + }, + "time": "2023-10-07T03:44:58+00:00" + }, + { + "name": "php-cs-fixer/diff", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/diff.git", + "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3", + "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0", + "symfony/process": "^3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" ], "authors": [ { - "name": "Dmitry Khomutov", - "email": "poisoncorpsee@gmail.com", - "homepage": "http://corpsee.com", - "role": "PHPDoc Checker developer" + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" }, { - "name": "Dan Cryer", - "email": "dan.cryer@block8.co.uk", - "homepage": "http://www.block8.co.uk", - "role": "PHP DocBlocks Checker developer" + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" } ], - "description": "A simple tool for checking that your PHP classes and methods use PHPDocs (PHP DocBlocks Checker fork).", - "homepage": "https://github.com/php-censor/phpdoc-checker", + "description": "sebastian/diff v3 backport support for PHP 5.6+", + "homepage": "https://github.com/PHP-CS-Fixer", "keywords": [ - "checker", - "code quality", - "comment", - "docblock", - "php", - "php-censor", - "phpdoc", - "testing" + "diff" ], "support": { - "issues": "https://github.com/php-censor/phpdoc-checker/issues", - "source": "https://github.com/php-censor/phpdoc-checker" + "issues": "https://github.com/PHP-CS-Fixer/diff/issues", + "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2" }, - "time": "2023-10-07T03:44:58+00:00" + "abandoned": true, + "time": "2020-10-14T08:32:19+00:00" }, { "name": "php-parallel-lint/php-parallel-lint", @@ -5703,6 +6815,64 @@ }, "time": "2025-02-19T13:28:12+00:00" }, + { + "name": "phpstan/phpstan", + "version": "1.12.21", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "14276fdef70575106a3392a4ed553c06a984df28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/14276fdef70575106a3392a4ed553c06a984df28", + "reference": "14276fdef70575106a3392a4ed553c06a984df28", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2025-03-09T09:24:50+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "9.2.32", @@ -6100,556 +7270,216 @@ "description": "The PHP Unit Testing framework.", "homepage": "https://phpunit.de/", "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" - }, - "funding": [ - { - "url": "https://phpunit.de/sponsors.html", - "type": "custom" - }, - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", - "type": "tidelift" - } - ], - "time": "2024-12-05T13:48:26+00:00" - }, - { - "name": "react/cache", - "version": "v1.2.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/cache.git", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/promise": "^3.0 || ^2.0 || ^1.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, Promise-based cache interface for ReactPHP", - "keywords": [ - "cache", - "caching", - "promise", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.2.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2022-11-30T15:59:55+00:00" - }, - { - "name": "react/child-process", - "version": "v0.6.6", - "source": { - "type": "git", - "url": "https://github.com/reactphp/child-process.git", - "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", - "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/event-loop": "^1.2", - "react/stream": "^1.4" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/socket": "^1.16", - "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\ChildProcess\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Event-driven library for executing child processes with ReactPHP.", - "keywords": [ - "event-driven", - "process", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.6" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2025-01-01T16:37:48+00:00" - }, - { - "name": "react/dns", - "version": "v1.13.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/dns.git", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/cache": "^1.0 || ^0.6 || ^0.5", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.7 || ^1.2.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3 || ^2", - "react/promise-timer": "^1.11" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Dns\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async DNS resolver for ReactPHP", - "keywords": [ - "async", - "dns", - "dns-resolver", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.13.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-06-13T14:18:03+00:00" - }, - { - "name": "react/event-loop", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "suggest": { - "ext-pcntl": "For signal handling support when using the StreamSelectLoop" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\EventLoop\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", - "keywords": [ - "asynchronous", - "event-loop" + "phpunit", + "testing", + "xunit" ], "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" } ], - "time": "2023-11-13T13:48:05+00:00" + "time": "2024-12-05T13:48:26+00:00" }, { - "name": "react/promise", - "version": "v3.2.0", + "name": "rector/rector", + "version": "0.14.8", "source": { "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + "url": "https://github.com/rectorphp/rector.git", + "reference": "46ee9a173a2b2645ca92a75ffc17460139fa226e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/46ee9a173a2b2645ca92a75ffc17460139fa226e", + "reference": "46ee9a173a2b2645ca92a75ffc17460139fa226e", "shasum": "" }, "require": { - "php": ">=7.1.0" + "php": "^7.2|^8.0", + "phpstan/phpstan": "^1.9.0" }, - "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-php-parser": "*", + "rector/rector-phpoffice": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" }, + "bin": [ + "bin/rector" + ], "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.14-dev" + } + }, "autoload": { "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\": "src/" - } + "bootstrap.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/0.14.8" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/tomasvotruba", + "type": "github" } ], - "time": "2024-05-24T10:39:05+00:00" + "time": "2022-11-14T14:09:49+00:00" }, { - "name": "react/socket", - "version": "v1.16.0", + "name": "sanmai/later", + "version": "0.1.4", "source": { "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + "url": "https://github.com/sanmai/later.git", + "reference": "e24c4304a4b1349c2a83151a692cec0c10579f60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "url": "https://api.github.com/repos/sanmai/later/zipball/e24c4304a4b1349c2a83151a692cec0c10579f60", + "reference": "e24c4304a4b1349c2a83151a692cec0c10579f60", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/dns": "^1.13", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.6 || ^1.2.1", - "react/stream": "^1.4" + "php": ">=7.4" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3.3 || ^2", - "react/promise-stream": "^1.4", - "react/promise-timer": "^1.11" + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^3.35.1", + "infection/infection": ">=0.27.6", + "phan/phan": ">=2", + "php-coveralls/php-coveralls": "^2.0", + "phpstan/phpstan": ">=1.4.5", + "phpunit/phpunit": ">=9.5 <10", + "vimeo/psalm": ">=2" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.1.x-dev" + } + }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "React\\Socket\\": "src/" + "Later\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com" } ], - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", - "keywords": [ - "Connection", - "Socket", - "async", - "reactphp", - "stream" - ], + "description": "Later: deferred wrapper object", "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.16.0" + "issues": "https://github.com/sanmai/later/issues", + "source": "https://github.com/sanmai/later/tree/0.1.4" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sanmai", + "type": "github" } ], - "time": "2024-07-26T10:38:09+00:00" + "time": "2023-10-24T00:25:28+00:00" }, { - "name": "react/stream", - "version": "v1.4.0", + "name": "sanmai/pipeline", + "version": "6.12", "source": { "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + "url": "https://github.com/sanmai/pipeline.git", + "reference": "ad7dbc3f773eeafb90d5459522fbd8f188532e25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "url": "https://api.github.com/repos/sanmai/pipeline/zipball/ad7dbc3f773eeafb90d5459522fbd8f188532e25", + "reference": "ad7dbc3f773eeafb90d5459522fbd8f188532e25", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.8", - "react/event-loop": "^1.2" + "php": "^7.4 || ^8.0" }, "require-dev": { - "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + "ergebnis/composer-normalize": "^2.8", + "friendsofphp/php-cs-fixer": "^3.17", + "infection/infection": ">=0.10.5", + "league/pipeline": "^0.3 || ^1.0", + "phan/phan": ">=1.1", + "php-coveralls/php-coveralls": "^2.4.1", + "phpstan/phpstan": ">=0.10", + "phpunit/phpunit": ">=9.4", + "vimeo/psalm": ">=2" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "v6.x-dev" + } + }, "autoload": { + "files": [ + "src/functions.php" + ], "psr-4": { - "React\\Stream\\": "src/" + "Pipeline\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "Apache-2.0" ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Alexey Kopytko", + "email": "alexey@kopytko.com" } ], - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", - "keywords": [ - "event-driven", - "io", - "non-blocking", - "pipe", - "reactphp", - "readable", - "stream", - "writable" - ], + "description": "General-purpose collections pipeline", "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.4.0" + "issues": "https://github.com/sanmai/pipeline/issues", + "source": "https://github.com/sanmai/pipeline/tree/6.12" }, "funding": [ { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" + "url": "https://github.com/sanmai", + "type": "github" } ], - "time": "2024-06-11T12:45:25+00:00" + "time": "2024-10-17T02:22:57+00:00" }, { "name": "sebastian/cli-parser", @@ -7610,6 +8440,70 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "seld/jsonlint", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "reference": "1748aaf847fc731cfad7725aec413ee46f0cc3a2", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.11.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2024-07-11T14:55:45+00:00" + }, { "name": "squizlabs/php_codesniffer", "version": "3.11.3", @@ -7914,6 +8808,145 @@ ], "time": "2022-10-03T15:15:11+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "phpstan/phpstan": "^0.12", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^0.12" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.1-dev" + } + }, + "autoload": { + "files": [ + "deprecated/apc.php", + "deprecated/libevent.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/ingres-ii.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/msql.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/mysqlndMs.php", + "generated/mysqlndQc.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/password.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pdf.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/simplexml.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "psr-4": { + "Safe\\": [ + "lib/", + "deprecated/", + "generated/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" + }, + "time": "2020-10-28T17:51:34+00:00" + }, { "name": "theseer/tokenizer", "version": "1.2.3", @@ -7964,6 +8997,114 @@ ], "time": "2024-03-03T12:36:25+00:00" }, + { + "name": "vimeo/psalm", + "version": "4.30.0", + "source": { + "type": "git", + "url": "https://github.com/vimeo/psalm.git", + "reference": "d0bc6e25d89f649e4f36a534f330f8bb4643dd69" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/d0bc6e25d89f649e4f36a534f330f8bb4643dd69", + "reference": "d0bc6e25d89f649e4f36a534f330f8bb4643dd69", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.4.2", + "amphp/byte-stream": "^1.5", + "composer/package-versions-deprecated": "^1.8.0", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^1.1 || ^2.0 || ^3.0", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "felixfbecker/advanced-json-rpc": "^3.0.3", + "felixfbecker/language-server-protocol": "^1.5", + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "nikic/php-parser": "^4.13", + "openlss/lib-array2xml": "^1.0", + "php": "^7.1|^8", + "sebastian/diff": "^3.0 || ^4.0", + "symfony/console": "^3.4.17 || ^4.1.6 || ^5.0 || ^6.0", + "symfony/polyfill-php80": "^1.25", + "webmozart/path-util": "^2.3" + }, + "provide": { + "psalm/psalm": "self.version" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "brianium/paratest": "^4.0||^6.0", + "ext-curl": "*", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpdocumentor/reflection-docblock": "^5", + "phpmyadmin/sql-parser": "5.1.0||dev-master", + "phpspec/prophecy": ">=1.9.0", + "phpstan/phpdoc-parser": "1.2.* || 1.6.4", + "phpunit/phpunit": "^9.0", + "psalm/plugin-phpunit": "^0.16", + "slevomat/coding-standard": "^7.0", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.3 || ^5.0 || ^6.0", + "weirdan/prophecy-shim": "^1.0 || ^2.0" + }, + "suggest": { + "ext-curl": "In order to send data to shepherd", + "ext-igbinary": "^2.0.5 is required, used to serialize caching data" + }, + "bin": [ + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalter" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-master": "4.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php", + "src/spl_object_id.php" + ], + "psr-4": { + "Psalm\\": "src/Psalm/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Brown" + } + ], + "description": "A static analysis tool for finding errors in PHP applications", + "keywords": [ + "code", + "inspection", + "php" + ], + "support": { + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm/tree/4.30.0" + }, + "time": "2022-11-06T20:37:08+00:00" + }, { "name": "webmozart/assert", "version": "1.11.0", @@ -8021,6 +9162,57 @@ "source": "https://github.com/webmozarts/assert/tree/1.11.0" }, "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "webmozart/path-util", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/path-util.git", + "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/path-util/zipball/d939f7edc24c9a1bb9c0dee5cb05d8e859490725", + "reference": "d939f7edc24c9a1bb9c0dee5cb05d8e859490725", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "webmozart/assert": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\PathUtil\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "A robust cross-platform utility for normalizing, comparing and modifying file paths.", + "support": { + "issues": "https://github.com/webmozart/path-util/issues", + "source": "https://github.com/webmozart/path-util/tree/2.3.0" + }, + "abandoned": "symfony/filesystem", + "time": "2015-12-17T08:42:14+00:00" } ], "aliases": [], diff --git a/docs/CHANGELOG_0.x.md b/docs/CHANGELOG_0.x.md index f9ce08ccd..5b57c35a8 100644 --- a/docs/CHANGELOG_0.x.md +++ b/docs/CHANGELOG_0.x.md @@ -941,7 +941,7 @@ requests as comments on Github (`php-censor.github.comments.commit` and `php-cen ## [0.1.0](https://github.com/php-censor/php-censor/tree/0.1.0) (2017-01-04) -Initial release. Changes from [PHPCI](https://www.phptesting.org/) v1.7.1: +Initial release. Changes from [PHPCI](https://github.com/dancryer/PHPCI) v1.7.1: ### Added diff --git a/docs/CHANGELOG_2.0.md b/docs/CHANGELOG_2.0.md new file mode 100644 index 000000000..069f1291e --- /dev/null +++ b/docs/CHANGELOG_2.0.md @@ -0,0 +1,242 @@ +Changelog 2.0 +============= + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to +[Semantic Versioning](http://semver.org/spec/v2.0.0.html). + + +## [2.0.14 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.14) (2025-03-15) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.13...2.0.14) + +### Fixed + +- Security issue CVE-2024-51736: Command execution hijack on Windows with Process class. See: https://symfony.com/cve-2024-51736. + + +## [2.0.13 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.13) (2024-05-04) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.12...2.0.13) + +### Added + +- GitHub Actions pipeline (backport from v2.1) + support of PHP 8.2 and 8.3. + +### Fixed + +- Security issue with remember me key in auth. See: https://chmod744.super.site/redacted-vulnerability. + + +## [2.0.12 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.12) (2024-01-11) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.11...2.0.12) + +### Fixed + +- Updated dependencies. Fixed: + - `guzzlehttp/psr7` (1.9.0) | CVE-2023-29197: Improper header validation | https://github.com/guzzle/psr7/security/advisories/GHSA-wxmh-65f7-jcvw. + + +## [2.0.11 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.11) (2023-01-11) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.10...2.0.11) + +### Fixed + +- PHP 8.1 deprecation while searching for composer binary. Pull request [#434](https://github.com/php-censor/php-censor/pull/434). + Thanks to [@StudioMaX](https://github.com/StudioMaX). +- PHP 8.1 error with return type of `php_user_filter::filter` function. + + +## [2.0.10 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.10) (2022-06-26) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.9...2.0.10) + +### Fixed + +- Updated dependencies. Fixed: + - `guzzlehttp/guzzle` (6.5.7) | CVE-2022-31090: CURLOPT_HTTPAUTH option not cleared on change of origin | https://github.com/guzzle/guzzle/security/advisories/GHSA-25mq-v84q-4j7r + + - `guzzlehttp/guzzle` (6.5.7) | CVE-2022-31091: Change in port should be considered a change in origin https://github.com/guzzle/guzzle/security/advisories/GHSA-q559-8m2m-g699 + + +## [2.0.9 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.9) (2022-06-11) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.8...2.0.9) + +### Fixed + +- Updated dependencies. Fixed: + - `guzzlehttp/guzzle` (6.5.6) | CVE-2022-31042: Failure to strip the Cookie header on change in host or HTTP downgrade | https://github.com/guzzle/guzzle/security/advisories/GHSA-f2wf-25xc-69c9 + + - `guzzlehttp/guzzle` (6.5.6) | CVE-2022-31043: Fix failure to strip Authorization header on HTTP downgrade | https://github.com/guzzle/guzzle/security/advisories/GHSA-w248-ffj2-4v5q + +### Changed + +- Added secrets to PHP Censor CI config (`.php-censor.yml`). + + +## [2.0.8 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.8) (2022-06-08) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.7...2.0.8) + +### Fixed + +- Updated dependencies. Fixed: + - `guzzlehttp/guzzle` (6.5.5) | CVE-2022-29248: Cross-domain cookie leakage | https://github.com/guzzle/guzzle/security/advisories/GHSA-cwmx-hcrq-mhc3. + + - `guzzlehttp/psr7` (1.8.3) | CVE-2022-24775: Inproper parsing of HTTP headers | https://github.com/guzzle/psr7/security/advisories/GHSA-q7rv-6hp3-vh96. + + +## [2.0.7 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.7) (2022-01-19) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.6...2.0.7) + +### Fixed + +- **[PhpCsFixer]** Problems with `udiff` option. + + +## [2.0.6 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.6) (2021-12-19) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.5...2.0.6) + +### Fixed + +- **[Codeception]** Updated Codeception version (See: [CVE-2021-23420](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-23420)). + +### Changed + +- Several documentation improvements. +- Improved code style. + + +## [2.0.5 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.5) (2021-08-22) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.4...2.0.5) + +### Fixed + +- Bug with wrong type when field "access_information" is empty (null). +- **[PhpCsFixer]** Support for version 3.0+. Pull request [#414](https://github.com/php-censor/php-censor/pull/414). + Thanks to [@StudioMaX](https://github.com/StudioMaX). +- **[Mysql, Pgsql, Sqlite]** Variables interpolation for queries. Pull requests + [#415](https://github.com/php-censor/php-censor/pull/415), [#416](https://github.com/php-censor/php-censor/pull/416). + Thanks to [@KieranFYI](https://github.com/KieranFYI). + +### Removed + +- Useless TravisCI and CodeCov configs. + + +## [2.0.4 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.4) (2021-06-12) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.3...2.0.4) + +### Fixed + +- **[PhpStan]** Option `directories` and `directory`. + Issue [#408](https://github.com/php-censor/php-censor/issues/#408). Pull request + [#409](https://github.com/php-censor/php-censor/pull/409). Thanks to [@StudioMaX](https://github.com/StudioMaX). +- **[SecurityChecker]** Option `allowed_warnings`. +- Security issue with old Chart.js version (Chart.js upgraded from version `1.1.1` to `3.3.0`). + + +## [2.0.3 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.3) (2021-04-20) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.2...2.0.3) + +### Fixed + +- **[Mage, Mage3, DeployerOrg]** Options `binary_path`, `priority_path` for Mage/Mage3/DeployerOrg plugins. + Pull request [#406](https://github.com/php-censor/php-censor/pull/406). Thanks to [@gnomii](https://github.com/gnomii). + + +## [2.0.2 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.2) (2021-03-21) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.1...2.0.2) + +### Fixed + +- SSH keys generating (Removed unwanted symbols). Issue [#403](https://github.com/php-censor/php-censor/issues/#403). +- Environments (Case when you may get environment from another project). Issue + [#405](https://github.com/php-censor/php-censor/issues/#405). +- Localizations for "Notify" plugins. +- Deprecations from PHP 8.0. Pull request [#404](https://github.com/php-censor/php-censor/pull/404). Thanks to + [@ismaail](https://github.com/ismaail). + +### Changed + +- **[SecurityChecker]** Reimplement the plugin because package `sensiolabs/security-checker` was archived/abandoned + (See [README](https://github.com/sensiolabs/security-checker#sensiolabs-security-checker)). Now plugin uses `symfony` + binary (Symfony CLI) or `fabpot/local-php-security-checker` tool for working. See + [documentation](https://github.com/php-censor/php-censor/blob/release-1.3/docs/en/plugins/security_checker.md). + +### Removed + +- Useless empty doc file about cronjob. +- Useless empty ru doc pages. + + +## [2.0.1 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.1) (2021-01-17) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/2.0.0...2.0.1) + +### Fixed + +- **[PhpCpd]** Param "--names-exclude" for plugin PhpCpd (version 6+). Issue + [#401](https://github.com/php-censor/php-censor/issues/#401). + +### Changed + +- Added `.phpunit.result.cache` file to `.gitignore`. +- Improved `CHANGELOG.md`. +- Improved `.php-censor.yml` config. + + +## [2.0.0 (Rick Sanchez)](https://github.com/php-censor/php-censor/tree/2.0.0) (2021-01-10) + +[Full Changelog](https://github.com/php-censor/php-censor/compare/1.3.0...2.0.0) + +### [How tp upgrade from v1 to v2](/docs/UPGRADE_2.0.md) + +### Changed + +- **Minimal PHP version increased to 7.4 (from 5.6)**. + +### Removed + +- **Deprecations from versions 1.x**: + - Cronjob worker: `php-censor:run-builds` (Use daemon worker instead: `php-censor:worker`). + - Project configs `phpci.yml` and `.phpci.yml` (use `.php-censor.yml` instead). + - `PHPCI_*` interpolation and env variables (Use `PHP_CENSOR_*` instead). + - Global application config section `b8.database` (Use `php-censor.database` instead). + - Options `authToken`, `api_key`, `api_token`, `token` for several plugins: `CampfireNotify`, + `HipchatNotify`, `FlowdockNotify`, `TelegramNotify`, `SensiolabInsight`, `BitbucketNotify` (Use `auth_token` instead). + - Plugin names: `campfire`, `telegram`, `xmpp`, `email`, `irc`, `phpstan` (Use: `campfire_notify`, `telegram_notify`, + `xmpp_notify`, `email_notify`, `irc_notify`, `php_stan` instead). + - [Codeception] Option `path` (Use option `output_path` instead). + - [Codeception] Option `executable` (Use the options `binary_path` and `binary_name` instead). + - [Grunt] Option `grunt` (Use options `binary_path` and `binary_name` instead). + - [Gulp] Option `gulp` (Use options `binary_path` and `binary_name` instead). + - [PHPCodeSniffer] Option `path` (Use option `directory` instead). + - [PHPCpd] Option `path` (Use option `directory` instead). + - [PHPDocblockChecker] Option `path` (Use option `directory` instead). + - [PHPMessDetector] Option `path` (Use option `directory` instead). + - [PHPUnit] Option `directory` (Use option `directories` instead). + - [SensiolabsInsight] Option `executable` (Use the options `binary_path` and `binary_name` instead). + - [Shell] Option `command` and commands list without any named option. Use option `commands` instead. + - [PackageBuild] Special variables for plugin (`%build.commit%`, `%build.id%`, `%build.branch%`, `%project.title%`, `%date%` and `%time%`). Use interpolated variables instead (`%COMMIT_ID%`, `%BUILD_ID%`, `%BRANCH%`, `%PROJECT_TITLE%`, `%CURRENT_DATE%`, `CURRENT_TIME`). + - [MySQL and PostgreSQL] Options `pass` for plugins MySQL and PostgreSQL. Use option `password` instead. + - [MySQL, PostgreSQL, SQLite] Queries list without option for plugins MySQL, PostgreSQL and SQLite. Use the options `queries` instead. + - [MySQL] Imports list without option for plugin MySQL. Use the options `imports` instead. + - [Mage, Mage3] Section `mage` and `mage3` in the global application config and option `bin`. Use the plugin options `binary_path` and `binary_name` instead. + - [CampfireNotify] Variable `%buildurl%` (Use the variable `%BUILD_LINK%` instead). + +## Other versions + +- [0.x Changelog](/docs/CHANGELOG_0.x.md) +- [1.0 Changelog](/docs/CHANGELOG_1.0.md) +- [1.1 Changelog](/docs/CHANGELOG_1.1.md) +- [1.2 Changelog](/docs/CHANGELOG_1.2.md) +- [1.3 Changelog](/docs/CHANGELOG_1.3.md) diff --git a/docs/UPGRADE_2.0.md b/docs/UPGRADE_2.0.md index b39529db5..71145c55d 100644 --- a/docs/UPGRADE_2.0.md +++ b/docs/UPGRADE_2.0.md @@ -2,7 +2,7 @@ Upgrade from v1 to v2 ===================== 1. [Upgrade your PHP Censor installation to latest v1 release](https://github.com/php-censor/php-censor/blob/release-1.3/README.md#updating) -(`1.3.0`). +(`1.3.*`). 2. If you use [cronjob worker](https://github.com/php-censor/php-censor/blob/release-1.3/docs/en/workers/cron.md), you should migrate to [daemon worker](en/workers/worker.md). 3. Fix all deprecations from v1 on your installation: diff --git a/docs/en/README.md b/docs/en/README.md index 31b39eb0e..eb00f0a7e 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -29,7 +29,6 @@ Using PHP Censor * [Periodical builds](periodical_builds.md) * [Console commands](commands.md) * [CCMenu/CCTray integration](ccmenu.md) -* [Known issues and workarounds](workarounds.md) Plugins ------- @@ -86,7 +85,6 @@ Plugins * [Campfire](plugins/campfire_notify.md) - `campfire_notify` * [Email](plugins/email_notify.md) - `email_notify` * [FlowDock](plugins/flowdock_notify.md) - `flowdock_notify` -* [HipChat](plugins/hipchat_notify.md) - `hipchat_notify` * [IRC](plugins/irc_notify.md) - `irc_notify` * [Slack](plugins/slack_notify.md) - `slack_notify` * [Telegram](plugins/telegram_notify.md) - `telegram_notify` diff --git a/docs/en/configuring_application.md b/docs/en/configuring_application.md index eed710565..33bd2d4a9 100644 --- a/docs/en/configuring_application.md +++ b/docs/en/configuring_application.md @@ -25,6 +25,9 @@ php-censor: language: en per_page: 10 url: 'http://php-censor.local' + realtime_ui: true # If true dashboard and ui are periodically updated using ajax + webhook: + log_requests: false email_settings: from_address: 'PHP Censor ' smtp_address: null diff --git a/docs/en/configuring_project.md b/docs/en/configuring_project.md index 4fb3b8b17..2a291d0ea 100644 --- a/docs/en/configuring_project.md +++ b/docs/en/configuring_project.md @@ -26,26 +26,37 @@ There are several ways of configuring build in *PHP Censor* project: ignore: - "vendor" setup: - composer: + composer_step: + plugin: composer action: "install" test: - technical_debt: + technical_debt_step: + plugin: technical_debt allowed_errors: -1 - php_code_sniffer: + php_code_sniffer_step: + plugin: php_code_sniffer allowed_warnings: -1 allowed_errors: -1 - php_mess_detector: + php_mess_detector_step: + plugin: php_mess_detector allowed_warnings: -1 - php_docblock_checker: + php_docblock_checker_step: + plugin: php_docblock_checker allowed_warnings: -1 - security_checker: + security_checker_step: + plugin: security_checker allowed_warnings: -1 - php_parallel_lint: + php_parallel_lint_step: + plugin: php_parallel_lint allow_failures: true - php_loc: - php_cpd: - codeception: - php_unit: + php_loc_step: + plugin: php_loc + php_cpd_step: + plugin: php_cpd + codeception_step: + plugin: codeception + php_unit_step: + plugin: php_unit ``` 2. Adding a config `.php-censor.yml` to the root of the project. @@ -53,9 +64,9 @@ There are several ways of configuring build in *PHP Censor* project: 3. Adding a config via web-interface. By default, a config from web-interface replaces a config from repository (`.php-censor.yml`). But if you uncheck - the option "Replace the configuration from file with the configuration from the data base", configurations will be + the option "Replace the configuration from file with the configuration from the database", configurations will be merged (the config from web-interface will have priority over the config from the repository). - + Setting config via web-interface and merging it with the config from the repo may be useful if you want to hide some secret data (passwords, keys) in case of using public repository. The most of the configuration can be stored as a public file in the repo, and passwords and keys may be added via web-interface. @@ -83,15 +94,19 @@ build_settings: pass: "" setup: - mysql: - - "DROP DATABASE IF EXISTS test;" - - "CREATE DATABASE test;" - - "GRANT ALL PRIVILEGES ON test.* TO test@'localhost' IDENTIFIED BY 'test';" - composer: + mysql_step: + plugin: mysql + queries: + - "DROP DATABASE IF EXISTS test;" + - "CREATE DATABASE test;" + - "GRANT ALL PRIVILEGES ON test.* TO test@'localhost' IDENTIFIED BY 'test';" + composer_step: + plugin: composer action: "install" test: - php_unit: + php_unit_step: + plugin: php_unit config: - "PHPUnit-all.xml" - "PHPUnit-ubuntu-fix.xml" @@ -99,34 +114,41 @@ test: - "tests/" run_from: "phpunit/" coverage: "tests/logs/coverage" - php_mess_detector: + php_mess_detector_step: + plugin: php_mess_detector priority_path: binary_path binary_path: /home/user/sbin/ binary_name: phpmd-local - php_code_sniffer: + php_code_sniffer_step: + plugin: php_code_sniffer standard: "PSR2" - php_cpd: + php_cpd_step: + plugin: php_cpd allow_failures: true - grunt: + grunt_step: + plugin: grunt task: "build" deploy: - deployer: + deployer_step: + plugin: deployer webhook_url: "http://deployer.local/deploy/QZaF1bMIUqbMFTmKDmgytUuykRN0cjCgW9SooTnwkIGETAYhDTTYoR8C431t" reason: "PHP Censor Build #%BUILD_ID% - %COMMIT_MESSAGE%" update_only: true complete: - mysql: - host: "localhost" - user: "root" - pass: "" + mysql_step: + plugin: mysql + host: "localhost" + user: "root" + pass: "" - "DROP DATABASE IF EXISTS test;" branch-dev: run-option: replace test: - grunt: + grunt_step: + plugin: grunt task: "build-dev" ``` @@ -136,11 +158,11 @@ Section `build_settings` contents common build settings: * Option `verbose` enable/disable verbosity of plugins output (Default value: `verbose: true`). -* Option `clone_depth: N` allows to clone repository with partial history (Git clone option `--depth=N`). Option +* Option `clone_depth: N` allows cloning repository with partial history (Git clone option `--depth=N`). Option supports Git (GitHub, GitLab, BitBucket, Gogs) and Svn (Subversion) builds. **ATTENTION!:** Option `clone_depth` should be set only from web-interface (Project edit page) because it should - be known before repository cloning. Also you should understand that some features or plugins with the option may + be known before repository cloning. Also, you should understand that some features or plugins with the option may work with unpredictable result. * Option `directory` sets default directory path for all plugins (It may be overloaded by the plugin option @@ -156,7 +178,8 @@ example config returns ignore list: `vendor, tests, docs`: - tests ... test: - example_plugin: + example_step: + plugin: example_plugin ignore: - ./vendor - ./docs @@ -177,11 +200,11 @@ with lower values (like 2). Default value is 1000. Allowed values range: [1, 200 **ATTENTION!:** Option `build_priority` should be set only from web-interface (Project edit page) because it only has an effect before the repository is cloned. -* Also we have global options (Usually connection settings) for some plugins like: ([Campfire](plugins/campfire_notify.md), +* Also, we have global options (Usually connection settings) for some plugins like: ([Campfire](plugins/campfire_notify.md), [Irc](plugins/irc_notify.md), [Mysql](plugins/mysql.md), [Pgsql](plugins/pgsql.md) и [Sqlite](plugins/sqlite.md)). See documentation of the plugins for more details. -* Also we have options for configuring connection parameters for Svn (Subversion) project source type. For example: +* Also, we have options for configuring connection parameters for Svn (Subversion) project source type. For example: ```yml build_settings: @@ -222,7 +245,7 @@ then - in `priority_path`; * `system` - In the first place search among the system utilities (`/bin`, `/usr/bin` etc., use `which`), then - in `local`, then - in `global`, then - in `priority_path`; - * `binary_path` - First of all, look for the specific path specified in the `binary_path` option, then - in `local`, + * `binary_path` - First look for the specific path specified in the `binary_path` option, then - in `local`, then - in `global`, then - in `system`; The `binary_path` option allows you to set a specific path to the directory with the executable plugin file. There @@ -232,7 +255,8 @@ of strings). Example: ```yml setup: - composer: + composer_step: + plugin: composer priority_path: binary_path binary_path: /home/user/bin/ # Search will be by executable file name: composer-1.4, composer-local, composer, composer.phar @@ -249,7 +273,7 @@ this stage deployment plugins should be called ([Shell](plugins/shell.md), [Depl [Mage](plugins/mage.md) и etc.). This stage is very similar to test. * `complete` - Build completion stage. Always executes after the deploy (or after the test, in case deploy is missing), -regardless of whether the buid was successful or failed. In this stage it is possible to send notifications, to clear +regardless of whether the build was successful or failed. In this stage it is possible to send notifications, to clear a database, etc. * `success` - Successful Build Stage. Called only when the build completed successfully. @@ -279,7 +303,7 @@ for the same purposes (For example: `branch-regex:^feature\-\d$` for the branche **If there are several directives `branch-regex:`/`branch-`, the first directive that matches up with the name of the branch will be used.** -The required parameter `run-option` allows to define, to redefine and to complete the configuration and can take +The required parameter `run-option` allows defining, to redefine and to complete the configuration and can take different values: * `replace` - will cause the branch specific plugins to run and the default ones not. @@ -298,17 +322,20 @@ test branch-regex:^feature\-\d$: run-option: replace test: - grunt: - task: "build-feature" + grunt_step: + plugin: grunt + task: build-feature branch-dev: run-option: replace test: - grunt: - task: "build-dev" + grunt_step: + plugin: grunt + task: build-dev branch-codeception: run-option: after test: - codeception: + codeception_step: + plugin: codeception ``` ### How it works @@ -333,24 +360,27 @@ the [specification](http://yaml.org/spec/1.0/#id2563922). ```yml setup: # yaml comment - shell: - - | - echo a long shell command, multiple lines - scriptPath=%BUILD_PATH%/../../hook-path/prepare-test5.sh - if [ -f $scriptPath ] - then - "$scriptPath" '%PROJECT_ID%' '%PROJECT_TITLE%' # script can read its path from $scriptPath - mkdir ../outputs_to_keep/%COMMIT_ID% - fi - - > - echo this is a very long message I must write here, and it is much too long to allow good editing - on only one line, therefore we break it up onto multiple lines, but the result will be on a single - line. - - echo a short command ... + shell_step: + plugin: shell + commands: + - | + echo a long shell command, multiple lines + scriptPath=%BUILD_PATH%/../../hook-path/prepare-test5.sh + if [ -f $scriptPath ] + then + "$scriptPath" '%PROJECT_ID%' '%PROJECT_TITLE%' # script can read its path from $scriptPath + mkdir ../outputs_to_keep/%COMMIT_ID% + fi + - > + echo this is a very long message I must write here, and it is much too long to allow good editing + on only one line, therefore we break it up onto multiple lines, but the result will be on a single + line. + - echo a short command ... branch-master: complete: &xmpp_notify - xmpp_notify: + xmpp_notify_step: + plugin: xmpp_notify username: &userName "login@gmail.com" password: &password "AZERTY123" recipients: @@ -359,7 +389,8 @@ branch-master: alias: "build infos for project" date_format: "%d/%m/%Y" broken: - xmpp_notify: + xmpp_notify_step: + plugin: xmpp_notify username: *userName password: *password recipients: diff --git a/docs/en/environments.md b/docs/en/environments.md index b02d85fe6..db7d0644a 100644 --- a/docs/en/environments.md +++ b/docs/en/environments.md @@ -24,9 +24,9 @@ In this example, there are three environments: ```yml pr: rc: - - feature-A + - feature-A test: - - feature-B + - feature-B ``` When you push commits to `master` branch, three builds will be created - one for each of the environments. @@ -39,14 +39,17 @@ You can use variable `%ENVIRONMENT%` in project config. ```yml setup: - mysql: - - "DROP DATABASE IF EXISTS project_name_%ENVIRONMENT%;" - - "CREATE DATABASE project_name_%ENVIRONMENT%;" + mysql_step: + plugin: mysql + queries: + - "DROP DATABASE IF EXISTS project_name_%ENVIRONMENT%;" + - "CREATE DATABASE project_name_%ENVIRONMENT%;" test: - ... + ... deploy: - mage: - env: %ENVIRONMENT% + mage_step: + plugin: mage + env: %ENVIRONMENT% ``` diff --git a/docs/en/installing.md b/docs/en/installing.md index 0f4b2d214..fd9aa498b 100644 --- a/docs/en/installing.md +++ b/docs/en/installing.md @@ -26,6 +26,10 @@ Or download [latest archive](https://github.com/php-censor/php-censor/releases/l ```bash # For Debian-based aptitude install beanstalkd +# Check if the service is running: +/etc/init.d/beanstalkd status +# If it's not running, start it: +/etc/init.d/beanstalkd start ``` * Install PHP Censor itself: @@ -61,10 +65,10 @@ cd ./php-censor.local --admin-email='admin@php-censor.local' ``` -* [Add a virtual host to your web server](docs/en/virtual_host.md), pointing to the `public` directory within your new +* [Add a virtual host to your web server](virtual_host.md), pointing to the `public` directory within your new PHP Censor directory. You'll need to set up rewrite rules to point all non-existent requests to PHP Censor; -* [Set up the PHP Censor Worker](docs/en/workers/worker.md); +* [Set up the PHP Censor Worker](workers/worker.md); ## Installing via Docker diff --git a/docs/en/interpolation.md b/docs/en/interpolation.md index 64240a656..24c0420f7 100644 --- a/docs/en/interpolation.md +++ b/docs/en/interpolation.md @@ -1,6 +1,9 @@ Injecting variables into messages ================================= +Precompiled variables +--------------------- + Most strings used in the build configuration can have variables related to the build inserted into them with the following syntax: @@ -11,37 +14,34 @@ following syntax: Where `VARIABLE` can be one of the following: * **COMMIT_ID** - The commit hash. - * **SHORT_COMMIT_ID** - The shortened version of the commit hash. - * **COMMITTER_EMAIL** - The Email address of the committer. - * **COMMIT_MESSAGE** - The message written by the committer. - * **COMMIT_LINK** - The URL to the commit. - * **BRANCH** - The name of the branch. - * **BRANCH_LINK** - The URL to the branch. - * **ENVIRONMENT** - Build environment (See [environments](environments.md)). - * **SYSTEM_VERSION** - Version of PHP Censor application. - * **PROJECT_ID** - The ID of the project. - * **BUILD_ID** - The build number. - * **PROJECT_TITLE** - The name of the project. - * **PROJECT_LINK** - The URL to the project in PHP Censor. - * **BUILD_PATH** - The path to the build. - * **BUILD_LINK** - The URL to the build in PHP Censor. -* **CURRENT_DATE** - Current date in format "Y-m-d". +* **CURRENT_DATE** - Current date in format "Y-m-d". * **CURRENT_TIME** - Current time in format "H-i-s". - * **CURRENT_DATETIME** - Current date and time in format "Y-m-d_H-i-s". + +Secret variables +---------------- + +![See "Admin Options" -> "Secrets"](../images/menu-secrets.png "See 'Admin Options' -> 'Secrets'") + +Also, you can create secret variable in admin interface (Open "Admin Options" -> "Secrets" section in admin interface: +`://php-censor.local/secret`) and use it as variable in build configuration like: + +``` +"My important message is about %SECRET:secret-name%" +``` diff --git a/docs/en/plugins/atoum.md b/docs/en/plugins/atoum.md index 1e832f4a5..24a882c95 100644 --- a/docs/en/plugins/atoum.md +++ b/docs/en/plugins/atoum.md @@ -15,7 +15,8 @@ Atom binary). ### Examples ```yml test: - atoum: + atoum_step: + plugin: atoum args: "command line arguments go here" config: "path to config file" directory: "directory to run tests" diff --git a/docs/en/plugins/behat.md b/docs/en/plugins/behat.md index b44c93b55..81917a544 100644 --- a/docs/en/plugins/behat.md +++ b/docs/en/plugins/behat.md @@ -14,7 +14,8 @@ Configuration ```yml test: - behat: + behat_step: + plugin: behat executable: "path to behat binary" features: "command line arguments" ``` diff --git a/docs/en/plugins/bitbucket_notify.md b/docs/en/plugins/bitbucket_notify.md index 8a98e07ed..75ac67ba8 100644 --- a/docs/en/plugins/bitbucket_notify.md +++ b/docs/en/plugins/bitbucket_notify.md @@ -1,5 +1,5 @@ Plugin Bitbucket Notify -========================= +======================= This plugin notify you in Bitbucket about the build status with comments, tasks and build info. @@ -21,13 +21,14 @@ Configuration ```yml complete: - bitbucket_notify: - url: "https://bitbucket.yourhost.de" - auth_token: "123456" - project_key: "test" - repository_slug: "test-service" - create_task_per_fail: true - create_task_if_fail: false - update_build: true - message: "" + bitbucket_notify_step: + plugin: bitbucket_notify + url: "https://bitbucket.yourhost.de" + auth_token: "123456" + project_key: "test" + repository_slug: "test-service" + create_task_per_fail: true + create_task_if_fail: false + update_build: true + message: "" ``` diff --git a/docs/en/plugins/campfire_notify.md b/docs/en/plugins/campfire_notify.md index 2810c8bf9..088f6b023 100644 --- a/docs/en/plugins/campfire_notify.md +++ b/docs/en/plugins/campfire_notify.md @@ -29,7 +29,8 @@ build_settings: url: "campfire URL" success: - campfire_notify: + campfire_notify_step: + plugin: campfire_notify verbose: true message: "Build succeeded!" ``` diff --git a/docs/en/plugins/clean_build.md b/docs/en/plugins/clean_build.md index 1c5a8c9d5..75604c731 100644 --- a/docs/en/plugins/clean_build.md +++ b/docs/en/plugins/clean_build.md @@ -1,4 +1,4 @@ -Plugin Clean build +Plugin Clean Build ================== Works through a list of files to remove from your build. Useful when used in combination with Copy Build or Package @@ -15,7 +15,8 @@ Configuration ```yml complete: - clean_build: + clean_build_step: + plugin: clean_build remove: - composer.json - composer.phar diff --git a/docs/en/plugins/codeception.md b/docs/en/plugins/codeception.md index 50e2eaf90..4871a8e42 100644 --- a/docs/en/plugins/codeception.md +++ b/docs/en/plugins/codeception.md @@ -35,7 +35,8 @@ processing on line 146. ```yml tests: - codeception: + codeception_step: + plugin: codeception config: "codeception.yml" args: "--no-ansi --coverage-html" ``` @@ -44,7 +45,8 @@ Or ```yml tests: - codeception: + codeception_step: + plugin: codeception config: "subdir1/subdir2" args: "report.xml -vv " output_path: diff --git a/docs/en/plugins/composer.md b/docs/en/plugins/composer.md index 343499ef1..2dd8c1b92 100644 --- a/docs/en/plugins/composer.md +++ b/docs/en/plugins/composer.md @@ -23,7 +23,8 @@ flag (default: false). ```yml setup: - composer: + composer_step: + plugin: composer directory: "my/composer/dir" action: "update" prefer_dist: true diff --git a/docs/en/plugins/deployer.md b/docs/en/plugins/deployer.md index 708269706..c2c9b4b92 100644 --- a/docs/en/plugins/deployer.md +++ b/docs/en/plugins/deployer.md @@ -17,7 +17,8 @@ branches matches the one being built (Default: true). ```yml success: - deployer: + deployer_step: + plugin: deployer webhook_url: "https://deployer.example.com/deploy/QZaF1bMIUqbMFTmKDmgytUuykRN0cjCgW9SooTnwkIGETAYhDTTYoR8C431t" reason: "PHP Censor Build #%BUILD_ID% - %COMMIT_MESSAGE%" update_only: true diff --git a/docs/en/plugins/deployer_org.md b/docs/en/plugins/deployer_org.md index a4a974269..3549ab754 100644 --- a/docs/en/plugins/deployer_org.md +++ b/docs/en/plugins/deployer_org.md @@ -31,7 +31,8 @@ Sample configuration ```yml deploy: - deployer_org: + deployer_org_step: + plugin: deployer_org development: # branch name task: sample-task # optional, default task is deploy stage: dev # required, name of stage or server diff --git a/docs/en/plugins/email_notify.md b/docs/en/plugins/email_notify.md index e73e97209..5d028ab94 100644 --- a/docs/en/plugins/email_notify.md +++ b/docs/en/plugins/email_notify.md @@ -25,7 +25,8 @@ Send an email to the committer as well as one@exameple.com if a build fails: ```yml failure: - email_notify: + email_notify_step: + plugin: email_notify committer: true default_mailto_address: one@example.com ``` @@ -34,6 +35,7 @@ Send an email to one@example.com every time a build is run: ```yml complete: - email_notify: + email_notify_step: + plugin: email_notify default_mailto_address: one@example.com ``` diff --git a/docs/en/plugins/env.md b/docs/en/plugins/env.md index e562ac6b7..4c3ea77fd 100644 --- a/docs/en/plugins/env.md +++ b/docs/en/plugins/env.md @@ -12,6 +12,7 @@ None. ```yml setup: - env: + env_step: + plugin: env APPLICATION_ENV: "development" ``` diff --git a/docs/en/plugins/flowdock_notify.md b/docs/en/plugins/flowdock_notify.md index 5ec6fd181..6f80ec69a 100644 --- a/docs/en/plugins/flowdock_notify.md +++ b/docs/en/plugins/flowdock_notify.md @@ -1,5 +1,5 @@ -Plugin FlowdockNotify -===================== +Plugin Flowdock Notify +====================== This plugin joins a [Flowdock](https://www.flowdock.com/) room and sends a user-defined message, for example a "Build Succeeded" message. diff --git a/docs/en/plugins/git.md b/docs/en/plugins/git.md index aff326d5a..0ed63c47a 100644 --- a/docs/en/plugins/git.md +++ b/docs/en/plugins/git.md @@ -10,10 +10,11 @@ Configuration ```yml complete: - git: - master: <-- branch - tag: <-- action - name: "" <-- Action options + git_step: + plugin: git + master: #<-- branch + tag: #<-- action + name: "" #<-- Action options ``` diff --git a/docs/en/plugins/grunt.md b/docs/en/plugins/grunt.md index b017b837f..cdf2f2426 100644 --- a/docs/en/plugins/grunt.md +++ b/docs/en/plugins/grunt.md @@ -15,7 +15,8 @@ Configuration ```yml test: - grunt: + grunt_step: + plugin: grunt directory: "path to run grunt in" gruntfile: "gruntfile.js" task: "css" diff --git a/docs/en/plugins/gulp.md b/docs/en/plugins/gulp.md index 355d32c73..83ebcbcde 100644 --- a/docs/en/plugins/gulp.md +++ b/docs/en/plugins/gulp.md @@ -1,4 +1,4 @@ -Plugin gulp +Plugin Gulp =========== This plugin runs [gulpjs](https://gulpjs.com/) tasks. @@ -15,7 +15,8 @@ Configuration ```yml test: - gulp: + gulp_step: + plugin: gulp directory: "/path/to/run/gulp/from" gulpfile: "gulpfile.js" task: "css" diff --git a/docs/en/plugins/hipchat_notify.md b/docs/en/plugins/hipchat_notify.md deleted file mode 100644 index 10ac96733..000000000 --- a/docs/en/plugins/hipchat_notify.md +++ /dev/null @@ -1,35 +0,0 @@ -Plugin HipchatNotify -==================== - -This plugin joins a [HipChat](https://www.hipchat.com/) room and sends a user-defined message, for example a -"Build Succeeded" message. - -Configuration -------------- - -### Options - -| Field | Required? | Description | -|-------|-----------|-------------| -| `auth_token` | Yes | Your HipChat API authentication token (v1) | -| `room` | Yes | Your Hipchat room name or ID number. This can also be an array of room names or numbers, and the message will be sent to all rooms. | -| `message` | No | The message to send to the room. Default - `%PROJECT_TITLE% built at %BUILD_LINK%` | -| `color` | No | Message color. Valid values: yellow, green, red, purple, gray, random. Default - `yellow`| -| `notify` | No | Whether or not this message should trigger a notification for people in the room (change the tab color, play a sound, etc). Default - `false`. | - -Message can be formatted via HTML. Example: -```html -%PROJECT_TITLE% - build %BUILD_ID% failed! -``` - -### Examples - -```yml -success: - hipchat_notify: - auth_token: 123 - room: 456 - message: '%PROJECT_TITLE% - build %BUILD_ID% failed!' - color: red - notify: true -``` diff --git a/docs/en/plugins/lint.md b/docs/en/plugins/lint.md index 0ecc5c93f..2e49e11b5 100644 --- a/docs/en/plugins/lint.md +++ b/docs/en/plugins/lint.md @@ -16,7 +16,8 @@ to true). ```yml test: - lint: + lint_step: + plugin: lint directory: "single path to lint files" directories: - "directory to lint files" diff --git a/docs/en/plugins/mage.md b/docs/en/plugins/mage.md index 49b89c7e0..0d8803bc4 100644 --- a/docs/en/plugins/mage.md +++ b/docs/en/plugins/mage.md @@ -16,6 +16,7 @@ Mage must be installed locally in your project as it is not provided by PHP Cens ```yml success: - mage: + mage_step: + plugin: mage env: production ``` diff --git a/docs/en/plugins/mage3.md b/docs/en/plugins/mage3.md index e6bebfecf..67ddce9ae 100644 --- a/docs/en/plugins/mage3.md +++ b/docs/en/plugins/mage3.md @@ -17,6 +17,7 @@ Mage must be installed locally in your project as it is not provided by PHP Cens ```yml success: - mage3: + mage3_step: + plugin: mage3 env: production ``` diff --git a/docs/en/plugins/mysql.md b/docs/en/plugins/mysql.md index fdaec9946..5c907b825 100644 --- a/docs/en/plugins/mysql.md +++ b/docs/en/plugins/mysql.md @@ -34,12 +34,14 @@ build_settings: password: '12345678' setup: - mysql: + mysql_step: + plugin: mysql queries: - "CREATE DATABASE my_app_test;" complete: - mysql: + mysql_step: + plugin: mysql queries: - "DROP DATABASE my_app_test;" ``` @@ -47,7 +49,8 @@ complete: Import SQL from file: ```yml setup: - mysql: + mysql_step: + plugin: mysql imports: - database: "foo" # Database name file: "path/dump.sql" # Relative path in build folder diff --git a/docs/en/plugins/pahout.md b/docs/en/plugins/pahout.md index 81fad1a83..eada24eb3 100644 --- a/docs/en/plugins/pahout.md +++ b/docs/en/plugins/pahout.md @@ -16,7 +16,8 @@ Configuration ```yml test: - pahout: ~ + pahout_step: + plugin: pahout ``` ### Additional Options diff --git a/docs/en/plugins/pdepend.md b/docs/en/plugins/pdepend.md index 9b90e73f7..88ae71f1b 100644 --- a/docs/en/plugins/pdepend.md +++ b/docs/en/plugins/pdepend.md @@ -14,7 +14,8 @@ See additional options below. ```yml test: - pdepend: + pdepend_step: + plugin: pdepend directory: "src" ``` diff --git a/docs/en/plugins/pgsql.md b/docs/en/plugins/pgsql.md index e6e5fa92f..3435e0cf0 100644 --- a/docs/en/plugins/pgsql.md +++ b/docs/en/plugins/pgsql.md @@ -30,12 +30,14 @@ build_settings: password: '12345678' setup: - pgsql: + pgsql_step: + plugin: pgsql queries: - "CREATE DATABASE my_app_test;" complete: - pgsql: + pgsql_step: + plugin: pgsql queries: - "DROP DATABASE my_app_test;" ``` diff --git a/docs/en/plugins/phan.md b/docs/en/plugins/phan.md index 1b5a0e50a..40276bcef 100644 --- a/docs/en/plugins/phan.md +++ b/docs/en/plugins/phan.md @@ -15,7 +15,8 @@ Configuration ```yml test: - phan: + phan_step: + plugin: phan allowed_warnings: 10 directory: "app" ignore: diff --git a/docs/en/plugins/phar.md b/docs/en/plugins/phar.md index ce9a25d05..29b4c11c7 100644 --- a/docs/en/plugins/phar.md +++ b/docs/en/plugins/phar.md @@ -17,7 +17,8 @@ Configuration ```yml test: - phar: + phar_step: + plugin: phar directory: /path/to/directory filename: foobar.phar regexp: /\.(php|phtml)$/ diff --git a/docs/en/plugins/phing.md b/docs/en/plugins/phing.md index ef6119309..6f00fb04e 100644 --- a/docs/en/plugins/phing.md +++ b/docs/en/plugins/phing.md @@ -17,7 +17,8 @@ Configuration ```yml setup: - phing: + phing_step: + plugin: phing build_file: 'build.xml' targets: - "build:test" diff --git a/docs/en/plugins/phlint.md b/docs/en/plugins/phlint.md index faa9ada8b..31b027bc4 100644 --- a/docs/en/plugins/phlint.md +++ b/docs/en/plugins/phlint.md @@ -19,7 +19,8 @@ Configuration ```yml test: - phlint: ~ + phlint_step: + plugin: phlint ``` ### Additional Options diff --git a/docs/en/plugins/php_code_sniffer.md b/docs/en/plugins/php_code_sniffer.md index 7bb0547a9..ffe4bd322 100644 --- a/docs/en/plugins/php_code_sniffer.md +++ b/docs/en/plugins/php_code_sniffer.md @@ -27,7 +27,8 @@ validation: ```yml test: - php_code_sniffer: + php_code_sniffer_step: + plugin: php_code_sniffer directory: "app" ignore: - "app/views" @@ -37,7 +38,8 @@ test: For use with an existing project: ```yml test: - php_code_sniffer: + php_code_sniffer_step: + plugin: php_code_sniffer standard: "./phpcs.xml" allowed_errors: -1 # Even a single error will cause the build to fail. -1 = unlimited allowed_warnings: -1 diff --git a/docs/en/plugins/php_cpd.md b/docs/en/plugins/php_cpd.md index 353cb3306..fd78fbf5a 100644 --- a/docs/en/plugins/php_cpd.md +++ b/docs/en/plugins/php_cpd.md @@ -10,7 +10,8 @@ Configuration ```yml test: - php_cpd: + php_cpd_step: + plugin: php_cpd directory: "app" ignore: - "app/my/path" diff --git a/docs/en/plugins/php_cs_fixer.md b/docs/en/plugins/php_cs_fixer.md index 0cbc6875b..f1d6bed8d 100644 --- a/docs/en/plugins/php_cs_fixer.md +++ b/docs/en/plugins/php_cs_fixer.md @@ -23,14 +23,16 @@ Configuration ```yml test: - php_cs_fixer: + php_cs_fixer_step: + plugin: php_cs_fixer directory: "./my/dir/path" # == "%BUILD_PATH%/my/dir/path" args: "--rules=@PSR12 --diff --verbose" ``` ```yml test: - php_cs_fixer: + php_cs_fixer_step: + plugin: php_cs_fixer directory: "%BUILD_PATH%/my/dir/path" verbose: true diff: true @@ -39,7 +41,8 @@ test: ```yml test: - php_cs_fixer: + php_cs_fixer_step: + plugin: php_cs_fixer config: "./my/dir/.php_cs.special" ``` diff --git a/docs/en/plugins/php_docblock_checker.md b/docs/en/plugins/php_docblock_checker.md index 2f64a194b..1c36470f4 100644 --- a/docs/en/plugins/php_docblock_checker.md +++ b/docs/en/plugins/php_docblock_checker.md @@ -18,7 +18,8 @@ Configuration ```yml test: - php_docblock_checker: + php_docblock_checker_step: + plugin: php_docblock_checker allowed_warnings: 10 skip_classes: true ``` diff --git a/docs/en/plugins/php_loc.md b/docs/en/plugins/php_loc.md index 393d0ddec..30ea540a5 100644 --- a/docs/en/plugins/php_loc.md +++ b/docs/en/plugins/php_loc.md @@ -17,7 +17,8 @@ included outside of the app directory. ```yml test: - php_loc: + php_loc_step: + plugin: php_loc directory: "app" ``` diff --git a/docs/en/plugins/php_mess_detector.md b/docs/en/plugins/php_mess_detector.md index 7aa88f3cb..28bdba1df 100644 --- a/docs/en/plugins/php_mess_detector.md +++ b/docs/en/plugins/php_mess_detector.md @@ -19,7 +19,8 @@ details on the rules. (default: ['codesize', 'unusedcode', 'naming']). ```yml test: - php_mess_detector: + php_mess_detector_step: + plugin: php_mess_detector directory: 'app' ignore: - 'vendor' diff --git a/docs/en/plugins/php_parallel_lint.md b/docs/en/plugins/php_parallel_lint.md index 23a64e36a..6edabe916 100644 --- a/docs/en/plugins/php_parallel_lint.md +++ b/docs/en/plugins/php_parallel_lint.md @@ -15,7 +15,8 @@ See additional options below. ```yml test: - php_parallel_lint: + php_parallel_lint_step: + plugin: php_parallel_lint directory: "app" ignore: - "vendor" diff --git a/docs/en/plugins/php_spec.md b/docs/en/plugins/php_spec.md index e3f3e4365..28607206b 100644 --- a/docs/en/plugins/php_spec.md +++ b/docs/en/plugins/php_spec.md @@ -14,7 +14,8 @@ See additional options below. ```yml test: - php_spec: ~ + php_spec_step: + plugin: php_spec ``` ### Additional Options diff --git a/docs/en/plugins/php_stan.md b/docs/en/plugins/php_stan.md index 1522eee33..bae5109b3 100644 --- a/docs/en/plugins/php_stan.md +++ b/docs/en/plugins/php_stan.md @@ -20,7 +20,8 @@ Configuration ```yml test: - php_stan: + php_stan_step: + plugin: php_stan directories: - src - tests diff --git a/docs/en/plugins/php_tal_lint.md b/docs/en/plugins/php_tal_lint.md index 49bf88081..b4cfaf19b 100644 --- a/docs/en/plugins/php_tal_lint.md +++ b/docs/en/plugins/php_tal_lint.md @@ -22,7 +22,8 @@ errors. ```yml test: - php_tal_lint: + php_tal_lint_step: + plugin: php_tal_lint directory: "app" ignore: - "vendor" diff --git a/docs/en/plugins/php_unit.md b/docs/en/plugins/php_unit.md index ec7a9a229..8cdd71698 100644 --- a/docs/en/plugins/php_unit.md +++ b/docs/en/plugins/php_unit.md @@ -33,7 +33,8 @@ Both modes accept: Specify config file and test directory: ```yml test: - php_unit: + php_unit_step: + plugin: php_unit config: - "path/to/phpunit.xml" directories: diff --git a/docs/en/plugins/psalm.md b/docs/en/plugins/psalm.md index 6d3b940c1..e5a7a247a 100644 --- a/docs/en/plugins/psalm.md +++ b/docs/en/plugins/psalm.md @@ -17,7 +17,8 @@ Configuration ```yml test: - psalm: ~ + psalm_step: + plugin: psalm ``` ### Additional Options diff --git a/docs/en/plugins/sensiolabs_insight.md b/docs/en/plugins/sensiolabs_insight.md index c388cb49a..8d456ccaa 100644 --- a/docs/en/plugins/sensiolabs_insight.md +++ b/docs/en/plugins/sensiolabs_insight.md @@ -18,7 +18,8 @@ Configuration ```yml test: - sensiolabs_insight: + sensiolabs_insight_step: + plugin: sensiolabs_insight allow_failures: true user_uuid: 'xxx-xxx-xxx-xxx-xxx' auth_token: 'xxxx' diff --git a/docs/en/plugins/shell.md b/docs/en/plugins/shell.md index 09d7e6136..b18120999 100644 --- a/docs/en/plugins/shell.md +++ b/docs/en/plugins/shell.md @@ -14,7 +14,8 @@ failed. ```yml setup: - shell: + shell_step: + plugin: shell execute_all: true commands: - "[ -d /www ]" diff --git a/docs/en/plugins/slack_notify.md b/docs/en/plugins/slack_notify.md index 642787a3b..ac89825ea 100644 --- a/docs/en/plugins/slack_notify.md +++ b/docs/en/plugins/slack_notify.md @@ -1,5 +1,5 @@ -Plugin SlackNotify -================== +Plugin Slack Notify +=================== This plugin joins a [Slack](https://www.slack.com/) room and sends a user-defined message, for example a "Build Succeeded" message. @@ -26,7 +26,8 @@ Send a message if the build fails: ```yml failure: - slack_notify: + slack_notify_step: + plugin: slack_notify webhook_url: "https://hooks.slack.com/services/R212T827A/G983UY31U/aIp0yuW9u0iTqwAMOEwTg" room: "#php-censor" username: "PHP Censor" @@ -39,7 +40,8 @@ Send a message if the build is successful: ```yml success: - slack_notify: + slack_notify_step: + plugin: slack_notify webhook_url: "https://hooks.slack.com/services/R212T827A/G983UY31U/aIp0yuW9u0iTqwAMOEwTg" room: "#php-censor" username: "PHP Censor" @@ -52,7 +54,8 @@ Send a message every time the build runs: ```yml complete: - slack_notify: + slack_notify_step: + plugin: slack_notify webhook_url: "https://hooks.slack.com/services/R212T827A/G983UY31U/aIp0yuW9u0iTqwAMOEwTg" room: "#php-censor" username: "PHP Censor" diff --git a/docs/en/plugins/sqlite.md b/docs/en/plugins/sqlite.md index f65cce04d..64d1b4643 100644 --- a/docs/en/plugins/sqlite.md +++ b/docs/en/plugins/sqlite.md @@ -24,12 +24,14 @@ build_settings: path: '/path/to/sqlite.sqlite' setup: - sqlite: + sqlite_step: + plugin: sqlite queries: - "CREATE DATABASE my_app_test;" complete: - sqlite: + sqlite_step: + plugin: sqlite queries: - "DROP DATABASE my_app_test;" ``` diff --git a/docs/en/plugins/telegram_notify.md b/docs/en/plugins/telegram_notify.md index 5d86669c8..a89327734 100644 --- a/docs/en/plugins/telegram_notify.md +++ b/docs/en/plugins/telegram_notify.md @@ -17,12 +17,14 @@ In the PHP Censor Project config section add the Telegram trigger ```yml complete: - telegram_notify: + telegram_notify_step: + plugin: telegram_notify auth_token: "" message: "[%ICON_BUILD%] [%PROJECT_TITLE%](%PROJECT_LINK%) - [Build #%BUILD_ID%](%BUILD_LINK%) has finished for commit [%SHORT_COMMIT_ID% (%COMMITTER_EMAIL%)](%COMMIT_LINK%) on branch [%BRANCH%](%BRANCH_LINK%)" recipients: - "" - "-" + - "-/" - "@" send_log: true ``` diff --git a/docs/en/plugins/webhook_notify.md b/docs/en/plugins/webhook_notify.md index f5cdc2ec1..b8d76ffc0 100644 --- a/docs/en/plugins/webhook_notify.md +++ b/docs/en/plugins/webhook_notify.md @@ -1,5 +1,5 @@ -Plugin WebhookNotify -================== +Plugin Webhook Notify +===================== This plugin sends a JSON web hook to a configured URL. @@ -18,7 +18,8 @@ Send a message if the build fails: ```yml failure: - webhook_notify: + webhook_notify_step: + plugin: webhook_notify url: "http://example.com/webhook-handler" ``` @@ -26,7 +27,8 @@ Send a message if the build is successful: ```yml success: - webhook_notify: + webhook_notify_step: + plugin: webhook_notify url: "http://example.com/webhook-handler" ``` @@ -34,6 +36,7 @@ Send a message every time the build runs: ```yml complete: - webhook_notify: + webhook_notify_step: + plugin: webhook_notify url: "http://example.com/webhook-handler" ``` diff --git a/docs/en/plugins/wipe.md b/docs/en/plugins/wipe.md index 4361f98f8..5fee37de4 100644 --- a/docs/en/plugins/wipe.md +++ b/docs/en/plugins/wipe.md @@ -14,6 +14,7 @@ Configuration ```yml complete: - wipe: + wipe_step: + plugin: wipe directory: "/path/to/directory" ``` diff --git a/docs/en/plugins/xmpp_notify.md b/docs/en/plugins/xmpp_notify.md index 912d74b2d..8b4b2c4a5 100644 --- a/docs/en/plugins/xmpp_notify.md +++ b/docs/en/plugins/xmpp_notify.md @@ -29,7 +29,8 @@ Configuration ```yml complete: - xmpp_notify: + xmpp_notify_step: + plugin: xmpp_notify username: "login@gmail.com" password: "AZERTY123" recipients: diff --git a/docs/en/workarounds.md b/docs/en/workarounds.md deleted file mode 100644 index 38209d53e..000000000 --- a/docs/en/workarounds.md +++ /dev/null @@ -1,59 +0,0 @@ -Known issues and workarounds -============================ - -PhpLoc with PHP < 7.2 and XDebug > 3.0 --------------------------------------- - -If you are should support version of PHP < 7.2 with XDebug version > 3.0, you may patch PHPLoc 4.0.1 (Latest PHPLoc -version with PHP < 7.2 support) for support XDebug > 3.0. Do following: - -1. Install plugin `composer-patches` for patching dependencies: - -```bash -composer require cweagans/composer-patches -``` - -2. Configure patching in `composer.json`: - -```json -{ - ... - "config": { - "preferred-install": "source" - }, - "extra": { - "patches": { - "phploc/phploc": { - "PhpLoc4 XDebug3 Fix": "/phploc4-xdebug3-fix.patch" - } - }, - "enable-patching": true - }, - ... -} -``` - -3. Create patch file (For example: `phploc4-xdebug3-fix.patch`): - -``` -diff --git a/src/CLI/Application.php b/src/CLI/Application.php -index faab87f..fa5c87d 100644 ---- a/src/CLI/Application.php -+++ b/src/CLI/Application.php -@@ -109,6 +109,8 @@ private function disableXdebug() - \ini_set('xdebug.show_exception_trace', 0); - \ini_set('xdebug.show_error_trace', 0); - -- \xdebug_disable(); -+ if (\function_exists('xdebug_disable')) { -+ \xdebug_disable(); -+ } - } - } -``` - -4. Update dependencies: - -```bash -composer update -``` diff --git a/docs/images/menu-secrets.png b/docs/images/menu-secrets.png new file mode 100644 index 000000000..56283aa19 Binary files /dev/null and b/docs/images/menu-secrets.png differ diff --git a/infection.json.dist b/infection.json.dist new file mode 100644 index 000000000..83767fb55 --- /dev/null +++ b/infection.json.dist @@ -0,0 +1,18 @@ +{ + "timeout": 10, + "source": { + "directories": [ + "src" + ], + "excludes": [ + "Migrations", + "View" + ] + }, + "logs": { + "text": "tests/runtime/infection.log" + }, + "mutators": { + "@default": true + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index b5f49eb67..7901184b7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,9 +8,20 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" + executionOrder="random" + resolveDependencies="true" bootstrap="./tests/bootstrap.php" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" > + + + + + + + + + ./src diff --git a/psalm.xml.dist b/psalm.xml.dist new file mode 100644 index 000000000..9664617d5 --- /dev/null +++ b/psalm.xml.dist @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/public/assets/css/main.css b/public/assets/css/main.css index d798b1c20..68d5e6ad6 100644 --- a/public/assets/css/main.css +++ b/public/assets/css/main.css @@ -650,3 +650,17 @@ h6, max-height: 1000%; overflow: auto; } + +.blink-item { + animation: blink 1s linear infinite; +} + +@keyframes blink { + 0% {opacity: 0;} + 50% {opacity: 0.5;} + 100% {opacity: 1;} +} + +body .box .small-box h3.dashboard-box-title { + font-size: 28px; +} diff --git a/public/assets/js/app.js b/public/assets/js/app.js index 67ffde7ad..ba1f31eac 100644 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -6,10 +6,12 @@ var PHPCensor = { $(document).ready(function () { // Update latest builds every 5 seconds: PHPCensor.getBuilds(); - PHPCensor.intervals.getBuilds = setInterval(PHPCensor.getBuilds, 5000); + if (REALTIME_UI) { + PHPCensor.intervals.getBuilds = setInterval(PHPCensor.getBuilds, 5000); + } // Update latest project builds every 10 seconds: - if (typeof PROJECT_ID != 'undefined') { + if (REALTIME_UI && typeof PROJECT_ID != 'undefined') { PHPCensor.intervals.getProjectBuilds = setInterval(PHPCensor.getProjectBuilds, 10000); } }); @@ -132,7 +134,6 @@ if (!Function.prototype.bind) { */ function confirmDelete(url, reloadAfter) { - var dialog = new PHPCensorConfirmDialog({ title: Lang.get('confirm_title'), message: Lang.get('confirm_message'), diff --git a/public/assets/js/build-plugins/codeception.js b/public/assets/js/build-plugins/codeception.js index 64600f75c..f4ffb40dc 100644 --- a/public/assets/js/build-plugins/codeception.js +++ b/public/assets/js/build-plugins/codeception.js @@ -43,7 +43,7 @@ var codeceptionPlugin = ActiveBuild.UiPlugin.extend({ }, onUpdateData: function (e) { - if (!e.queryData) { + if (!e.queryData || $.isEmptyObject(e.queryData)) { $('#build-codeception-errors').hide(); return; } @@ -78,7 +78,8 @@ var codeceptionPlugin = ActiveBuild.UiPlugin.extend({ }, onUpdateMeta: function (e) { - if (!e.queryData) { + if (!e.queryData || $.isEmptyObject(e.queryData)) { + $('#build-codeception-errors').hide(); return; } diff --git a/public/assets/js/build-plugins/loc.js b/public/assets/js/build-plugins/loc.js index c5b2c446b..4cbcf0744 100644 --- a/public/assets/js/build-plugins/loc.js +++ b/public/assets/js/build-plugins/loc.js @@ -9,7 +9,7 @@ var locPlugin = ActiveBuild.UiPlugin.extend({ register: function () { var self = this; - var query = ActiveBuild.registerQuery('php_loc-data', -1, {num_builds: 10, key: 'php_loc-data'}); + var query = ActiveBuild.registerQuery('php_loc-data', -1, {num_builds: 20, key: 'php_loc-data'}); $(window).on('php_loc-data', function (data) { self.onUpdate(data); @@ -37,6 +37,11 @@ var locPlugin = ActiveBuild.UiPlugin.extend({ }, onUpdate: function (e) { + if (!e.queryData || $.isEmptyObject(e.queryData)) { + $('#build-lines').hide(); + return; + } + this.lastData = e.queryData; this.displayChart(); }, @@ -50,28 +55,36 @@ var locPlugin = ActiveBuild.UiPlugin.extend({ labels: [], datasets: [ { - label: Lang.get('lines'), - borderColor: "#555299", - color: "#555299", - data: [] + label: Lang.get('lines'), + borderColor: "#555299", + backgroundColor: "#555299", + data: [], + cubicInterpolationMode: 'monotone', + tension: 0.2 }, { - label: Lang.get('logical_lines'), - borderColor: "#00A65A", - color: "#00A65A", - data: [] + label: Lang.get('logical_lines'), + borderColor: "#00A65A", + backgroundColor: "#00A65A", + data: [], + cubicInterpolationMode: 'monotone', + tension: 0.2 }, { - label: Lang.get('comment_lines'), - borderColor: "#8AA4AF", - color: "#8AA4AF", - data: [] + label: Lang.get('comment_lines'), + borderColor: "#8AA4AF", + backgroundColor: "#8AA4AF", + data: [], + cubicInterpolationMode: 'monotone', + tension: 0.2 }, { - label: Lang.get('noncomment_lines'), - borderColor: "#00A7D0", - color: "#00A7D0", - data: [] + label: Lang.get('noncomment_lines'), + borderColor: "#00A7D0", + backgroundColor: "#00A7D0", + data: [], + cubicInterpolationMode: 'monotone', + tension: 0.2 } ] }; @@ -104,7 +117,8 @@ var locPlugin = ActiveBuild.UiPlugin.extend({ data: self.chartData, options: { datasetFill: false, - multiTooltipTemplate: "<%=datasetLabel%>: <%= value %>" + multiTooltipTemplate: "<%=datasetLabel%>: <%= value %>", + maintainAspectRatio: false } }); diff --git a/public/assets/js/build-plugins/phpspec.js b/public/assets/js/build-plugins/phpspec.js index 45dd04e70..cdddc596b 100644 --- a/public/assets/js/build-plugins/phpspec.js +++ b/public/assets/js/build-plugins/phpspec.js @@ -38,7 +38,7 @@ var phpspecPlugin = ActiveBuild.UiPlugin.extend({ }, onUpdate: function (e) { - if (!e.queryData) { + if (!e.queryData || $.isEmptyObject(e.queryData)) { $('#build-php_spec-errors').hide(); return; } diff --git a/public/assets/js/build-plugins/phptallint.js b/public/assets/js/build-plugins/phptallint.js index 40b85c502..3bbfff862 100644 --- a/public/assets/js/build-plugins/phptallint.js +++ b/public/assets/js/build-plugins/phptallint.js @@ -32,7 +32,7 @@ var phptalPlugin = ActiveBuild.UiPlugin.extend({ }, onUpdate: function (e) { - if (!e.queryData) { + if (!e.queryData || $.isEmptyObject(e.queryData)) { $('#build-php_tal_lint').hide(); return; } diff --git a/public/assets/js/build-plugins/phpunit-coverage.js b/public/assets/js/build-plugins/phpunit-coverage.js index f43308849..15a731b9e 100644 --- a/public/assets/js/build-plugins/phpunit-coverage.js +++ b/public/assets/js/build-plugins/phpunit-coverage.js @@ -9,7 +9,7 @@ var coveragePlugin = ActiveBuild.UiPlugin.extend({ register: function () { var self = this; - var query = ActiveBuild.registerQuery('php_unit-coverage', -1, {num_builds: 10, key: 'php_unit-coverage'}); + var query = ActiveBuild.registerQuery('php_unit-coverage', -1, {num_builds: 20, key: 'php_unit-coverage'}); $(window).on('php_unit-coverage', function (data) { self.onUpdate(data); @@ -37,6 +37,11 @@ var coveragePlugin = ActiveBuild.UiPlugin.extend({ }, onUpdate: function (e) { + if (!e.queryData || $.isEmptyObject(e.queryData)) { + $('#build-php_unit-coverage').hide(); + return; + } + this.lastData = e.queryData; this.displayChart(); }, @@ -50,22 +55,28 @@ var coveragePlugin = ActiveBuild.UiPlugin.extend({ labels: [], datasets: [ { - label: Lang.get('classes'), - borderColor: "#555299", - color: "#555299", - data: [] + label: Lang.get('classes'), + borderColor: "#555299", + backgroundColor: "#555299", + data: [], + cubicInterpolationMode: 'monotone', + tension: 0.2 }, { - label: Lang.get('methods'), - borderColor: "#00A65A", - color: "#00A65A", - data: [] + label: Lang.get('methods'), + borderColor: "#00A65A", + backgroundColor: "#00A65A", + data: [], + cubicInterpolationMode: 'monotone', + tension: 0.2 }, { - label: Lang.get('lines'), - borderColor: "#8AA4AF", - color: "#8AA4AF", - data: [] + label: Lang.get('lines'), + borderColor: "#8AA4AF", + backgroundColor: "#8AA4AF", + data: [], + cubicInterpolationMode: 'monotone', + tension: 0.2 } ] }; @@ -101,7 +112,8 @@ var coveragePlugin = ActiveBuild.UiPlugin.extend({ scaleStepWidth : 10, scaleStartValue : 0, datasetFill: false, - multiTooltipTemplate: "<%=datasetLabel%>: <%= value %>" + multiTooltipTemplate: "<%=datasetLabel%>: <%= value %>", + maintainAspectRatio: false } }); diff --git a/public/assets/js/build-plugins/phpunit.js b/public/assets/js/build-plugins/phpunit.js index 211531ca0..6b477e6bd 100644 --- a/public/assets/js/build-plugins/phpunit.js +++ b/public/assets/js/build-plugins/phpunit.js @@ -41,7 +41,7 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({ }, onUpdate: function (e) { - if (!e.queryData) { + if (!e.queryData || $.isEmptyObject(e.queryData)) { $('#build-php_unit-errors').hide(); return; } diff --git a/public/assets/js/build-plugins/summary.js b/public/assets/js/build-plugins/summary.js index ac34acf69..78cb99c6e 100644 --- a/public/assets/js/build-plugins/summary.js +++ b/public/assets/js/build-plugins/summary.js @@ -35,6 +35,7 @@ var SummaryPlugin = ActiveBuild.UiPlugin.extend({ '' + '' + '' + + '' + '' + '' + '' + @@ -43,7 +44,7 @@ var SummaryPlugin = ActiveBuild.UiPlugin.extend({ }, onUpdate: function (e) { - if (!e.queryData) { + if (!e.queryData || $.isEmptyObject(e.queryData)) { $('#build-summary').hide(); return; } @@ -54,9 +55,10 @@ var SummaryPlugin = ActiveBuild.UiPlugin.extend({ tbody.empty(); for (var stage in summary) { - for (var plugin in summary[stage]) { - var data = summary[stage][plugin], - duration = data.started ? ((data.ended || Math.floor(Date.now() / 1000)) - data.started) : '-'; + for (var step in summary[stage]) { + var data = summary[stage][step], + duration = data.started ? ((data.ended || Math.floor(Date.now() / 1000)) - data.started) : '-', + plugin = (typeof data.plugin === "undefined" ? step : data.plugin); var pluginName = Lang.get(plugin); if (0 < data.errors) { @@ -65,6 +67,7 @@ var SummaryPlugin = ActiveBuild.UiPlugin.extend({ tbody.append( '' + '' + + '' + '' + ' diff --git a/src/View/Build/view.phtml b/src/View/Build/view.phtml index cd99a2b2f..670314d78 100644 --- a/src/View/Build/view.phtml +++ b/src/View/Build/view.phtml @@ -4,11 +4,11 @@ use PHPCensor\Helper\Lang; use PHPCensor\Helper\Template; use PHPCensor\Model\Build; use PHPCensor\Model\BuildError; -use PHPCensor\Store\EnvironmentStore; -use PHPCensor\Store\Factory; +use PHPCensor\Model\Environment; /** - * @var Build $build + * @var Build $build + * @var Environment $environment */ ?> @@ -37,7 +37,7 @@ use PHPCensor\Store\Factory; @@ -194,7 +187,7 @@ use PHPCensor\Store\Factory; - + @@ -334,7 +327,7 @@ use PHPCensor\Store\Factory; var BUILD_IS_NEW = ''; var ActiveBuild = new Build(getId(); ?>); - ActiveBuild.setupBuild(, getFileLinkTemplate()); ?>); + ActiveBuild.setupBuild(, getFileLinkTemplate()); ?>); var url = document.location.toString(); if (url.match('#')) { @@ -349,8 +342,8 @@ use PHPCensor\Store\Factory; ' . PHP_EOL; - } + print '' . PHP_EOL; +} ?> - + @@ -23,14 +25,14 @@ use PHPCensor\Store\Factory; switch ($build->getStatus()) { case 0: $cls = 'active'; - $subClass = 'info'; + $subClass = 'info blink-item'; $status = Lang::get('pending'); break; case 1: $cls = 'warning'; - $subClass = 'warning'; + $subClass = 'warning blink-item'; $status = Lang::get('running'); break; @@ -57,7 +59,7 @@ $branches = $build->getExtra('branches'); + + @@ -170,7 +176,7 @@ use PHPCensor\Model\Project; - getType(), [ + getType(), [ Project::TYPE_GITHUB, Project::TYPE_GITLAB, Project::TYPE_BITBUCKET, diff --git a/src/View/Secret/edit.phtml b/src/View/Secret/edit.phtml new file mode 100644 index 000000000..bfd24f58a --- /dev/null +++ b/src/View/Secret/edit.phtml @@ -0,0 +1,14 @@ + +
+
+

+
+ +
+ +
+
diff --git a/src/View/Secret/index.phtml b/src/View/Secret/index.phtml new file mode 100644 index 000000000..4bd04932e --- /dev/null +++ b/src/View/Secret/index.phtml @@ -0,0 +1,56 @@ + +
+ +
+
' + Lang.get('stage') + '' + Lang.get('step') + '' + Lang.get('plugin') + '' + Lang.get('status') + '' + Lang.get('duration') + ' (' + Lang.get('seconds') + ')
' + Lang.get('stage_' + stage) + '' + step + '' + pluginName + '' + this.statusLabels[data.status] + diff --git a/public/assets/js/build-plugins/warnings.js b/public/assets/js/build-plugins/warnings.js index 83d0daa52..d4a03ce06 100644 --- a/public/assets/js/build-plugins/warnings.js +++ b/public/assets/js/build-plugins/warnings.js @@ -24,13 +24,14 @@ var warningsPlugin = ActiveBuild.UiPlugin.extend({ displayOnUpdate: false, rendered: false, chartData: null, + showWidget: false, register: function () { var self = this; var queries = []; for (var key in self.keys) { - queries.push(ActiveBuild.registerQuery(key, -1, {num_builds: 10, key: key})); + queries.push(ActiveBuild.registerQuery(key, -1, {num_builds: 20, key: key})); } $(window).on(Object.keys(self.keys).join(' '), function (data) { @@ -65,6 +66,16 @@ var warningsPlugin = ActiveBuild.UiPlugin.extend({ var self = this; var builds = e.queryData; + if (!self.showWidget) { + if (!e.queryData || $.isEmptyObject(e.queryData)) { + $('#build-warnings').hide(); + + return; + } + } + + self.showWidget = true; + if (!builds || !builds.length) { return; } @@ -122,10 +133,12 @@ var warningsPlugin = ActiveBuild.UiPlugin.extend({ var color = colors.shift(); self.chartData.datasets.push({ - label: self.keys[key], - borderColor: color, - color: color, - data: [] + label: self.keys[key], + borderColor: color, + backgroundColor: color, + data: [], + cubicInterpolationMode: 'monotone', + tension: 0.2 }); for (var build in self.data) { @@ -148,7 +161,7 @@ var warningsPlugin = ActiveBuild.UiPlugin.extend({ var self = this; if ($('#information').hasClass('active') && self.chartData) { - $('#build-warnings-chart').show(); + $('#build-warnings').show(); var ctx = $("#build-warnings-linechart").get(0).getContext("2d"); @@ -160,7 +173,8 @@ var warningsPlugin = ActiveBuild.UiPlugin.extend({ data: self.chartData, options: { datasetFill: false, - multiTooltipTemplate: "<%=datasetLabel%>: <%= value %>" + multiTooltipTemplate: "<%=datasetLabel%>: <%= value %>", + maintainAspectRatio: false } }); diff --git a/public/assets/js/build.js b/public/assets/js/build.js index f9c17b149..dcd585bdb 100644 --- a/public/assets/js/build.js +++ b/public/assets/js/build.js @@ -72,7 +72,7 @@ var Build = Class.extend({ var cb = function () { var fullUri = window.APP_URL + uri; - if (name == 'build-updated') { + if (name === 'build-updated') { fullUri = window.APP_URL + 'build/ajax-data/' + self.buildId + '?per_page=' + PER_PAGE + '&page=' + PAGE + '&plugin=' + BUILD_PLUGIN + '&severity=' + BUILD_SEVERITY + '&is_new=' + BUILD_IS_NEW; } @@ -87,7 +87,7 @@ var Build = Class.extend({ }); }; - if (seconds != -1) { + if (REALTIME_UI && seconds !== -1) { self.queries[name] = setInterval(cb, seconds * 1000); } diff --git a/public/assets/js/dashboard-widgets/all_projects.js b/public/assets/js/dashboard-widgets/all_projects.js index 14af6bf6b..15f9ca01f 100644 --- a/public/assets/js/dashboard-widgets/all_projects.js +++ b/public/assets/js/dashboard-widgets/all_projects.js @@ -13,7 +13,9 @@ PHPCensor.widgets.allProjects = { success: function (data) { $(('#widget-all_projects-container')).html(data); - PHPCensor.widgets.allProjects.interval = setInterval(PHPCensor.widgets.allProjects.update, 10000); + if (REALTIME_UI) { + PHPCensor.widgets.allProjects.interval = setInterval(PHPCensor.widgets.allProjects.update, 10000); + } }, error: PHPCensor.handleFailedAjax diff --git a/public/assets/js/dashboard-widgets/build_errors.js b/public/assets/js/dashboard-widgets/build_errors.js index b4b070d06..7ba7f675f 100644 --- a/public/assets/js/dashboard-widgets/build_errors.js +++ b/public/assets/js/dashboard-widgets/build_errors.js @@ -13,7 +13,9 @@ PHPCensor.widgets.buildErrors = { success: function (data) { $(('#widget-build_errors-container')).html(data); - PHPCensor.widgets.buildErrors.interval = setInterval(PHPCensor.widgets.buildErrors.update, 10000); + if (REALTIME_UI) { + PHPCensor.widgets.buildErrors.interval = setInterval(PHPCensor.widgets.buildErrors.update, 10000); + } }, error: PHPCensor.handleFailedAjax diff --git a/public/assets/js/dashboard-widgets/last_builds.js b/public/assets/js/dashboard-widgets/last_builds.js index b064fb708..924a0f0f8 100644 --- a/public/assets/js/dashboard-widgets/last_builds.js +++ b/public/assets/js/dashboard-widgets/last_builds.js @@ -13,7 +13,9 @@ PHPCensor.widgets.lastBuilds = { success: function (data) { $(('#widget-last_builds-container')).html(data); - PHPCensor.widgets.lastBuilds.interval = setInterval(PHPCensor.widgets.lastBuilds.update, 10000); + if (REALTIME_UI) { + PHPCensor.widgets.lastBuilds.interval = setInterval(PHPCensor.widgets.lastBuilds.update, 10000); + } }, error: PHPCensor.handleFailedAjax diff --git a/public/index.php b/public/index.php index 753adf5e2..5e7564542 100644 --- a/public/index.php +++ b/public/index.php @@ -1,8 +1,19 @@ handleRequest(); +$request = Request::createFromGlobals(); +$application = new Application($configuration, $storeRegistry, $request, $session); + +$application->handleRequest()->send(); diff --git a/rector.php b/rector.php new file mode 100644 index 000000000..a5d4ab021 --- /dev/null +++ b/rector.php @@ -0,0 +1,23 @@ +paths([ + __DIR__ . '/src' + ]); + + // register a single rule + $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); + + $rectorConfig->sets([ + SetList::PHP_74, + //SetList::CODE_QUALITY, + //SetList::TYPE_DECLARATION_STRICT, + //SetList::DEAD_CODE, + ]); +}; diff --git a/src/Application.php b/src/Application.php index 0d8772ae9..a02659c55 100644 --- a/src/Application.php +++ b/src/Application.php @@ -1,57 +1,56 @@ + * @author Dmitry Khomutov */ class Application { - /** - * @var array - */ - protected $route; + private ?array $route; - /** - * @var Controller|WebController - */ - protected $controller; + private Controller $controller; - /** - * @var Request - */ - protected $request; + private Request $request; - /** - * @var Config - */ - protected $config; + private Session $session; - /** - * @var Router - */ - protected $router; + private ConfigurationInterface $configuration; - public function __construct(Config $config, Request $request = null) - { - $this->config = $config; + private StoreRegistry $storeRegistry; - if (!is_null($request)) { - $this->request = $request; - } else { - $this->request = new Request(); - } + private Router $router; + + public function __construct( + ConfigurationInterface $configuration, + StoreRegistry $storeRegistry, + Request $request, + Session $session + ) { + $this->configuration = $configuration; + $this->storeRegistry = $storeRegistry; + $this->request = $request; + $this->session = $session; - $this->router = new Router($this, $this->request, $this->config); + $this->router = new Router($this, $this->request); $this->init(); } @@ -59,16 +58,19 @@ public function __construct(Config $config, Request $request = null) /** * Initialise Application - Handles session verification, routing, etc. */ - public function init() + public function init(): void { $request = & $this->request; $route = '/:controller/:action'; $opts = ['controller' => 'Home', 'action' => 'index']; + $session = $this->session; + // Inlined as a closure to fix "using $this when not in object context" on 5.3 - $validateSession = function () { - if (!empty($_SESSION['php-censor-user-id'])) { - $user = Factory::getStore('User')->getByPrimaryKey($_SESSION['php-censor-user-id']); + $validateSession = function () use ($session) { + $sessionUserId = $session->get('php-censor-user-id'); + if (!empty($sessionUserId)) { + $user = $this->storeRegistry->get('User')->getById((int)$sessionUserId); if ($user) { return true; @@ -81,17 +83,17 @@ public function init() $skipAuth = [$this, 'shouldSkipAuth']; // Handler for the route we're about to register, checks for a valid session where necessary: - $routeHandler = function (&$route, Response &$response) use (&$request, $validateSession, $skipAuth) { - $skipValidation = in_array($route['controller'], ['session', 'webhook', 'build-status'], true); + $routeHandler = function ($route, Response &$response) use (&$request, $validateSession, $skipAuth, $session) { + $skipValidation = \in_array($route['controller'], ['session', 'webhook', 'build-status'], true); - if (!$skipValidation && !$validateSession() && (!is_callable($skipAuth) || !$skipAuth())) { - if ($request->isAjax()) { - $response->setResponseCode(401); - $response->setContent(''); + if (!$skipValidation && !$validateSession() && (!\is_callable($skipAuth) || !$skipAuth())) { + if ($request->isXmlHttpRequest()) { + $response->setStatusCode(Response::HTTP_UNAUTHORIZED); + $response->setContent(null); } else { - $_SESSION['php-censor-login-redirect'] = substr($request->getPath(), 1); - $response = new RedirectResponse($response); - $response->setHeader('Location', APP_URL . 'session/login'); + $session->set('php-censor-login-redirect', \substr($request->getPathInfo(), 1)); + + $response = new RedirectResponse(APP_URL . 'session/login'); } return false; @@ -105,11 +107,9 @@ public function init() } /** - * @return Response - * * @throws NotFoundException */ - protected function handleRequestInner() + protected function handleRequestInner(): Response { $this->route = $this->router->dispatch(); @@ -123,45 +123,64 @@ protected function handleRequestInner() } if (!$this->controllerExists($this->route)) { - throw new NotFoundException('Controller ' . $this->toPhpName($this->route['controller']) . ' does not exist!'); + throw new NotFoundException( + 'Controller ' . $this->toPhpName($this->route['controller']) . ' does not exist!' + ); } - $action = lcfirst($this->toPhpName($this->route['action'])); + $action = \lcfirst($this->toPhpName($this->route['action'])); if (!$this->getController()->hasAction($action)) { - throw new NotFoundException('Controller ' . $this->toPhpName($this->route['controller']) . ' does not have action ' . $action . '!'); + throw new NotFoundException( + 'Controller ' . $this->toPhpName($this->route['controller']) . ' does not have action ' . $action . '!' + ); } return $this->getController()->handleAction($action, $this->route['args']); } + /** + * @throws Common\Exception\RuntimeException + * @throws HttpException + */ + private function getUser(): ?User + { + $sessionUserId = $this->session->get('php-censor-user-id'); + if (empty($sessionUserId)) { + return null; + } + + /** @var UserStore $userStore */ + $userStore = $this->storeRegistry->get('User'); + + return $userStore->getById((int)$sessionUserId); + } + /** * Handle an incoming web request. * - * @return Response + * @throws Common\Exception\RuntimeException + * @throws HttpException */ - public function handleRequest() + public function handleRequest(): Response { try { $response = $this->handleRequestInner(); } catch (HttpException $ex) { - $this->config->set('page_title', 'Error'); - $view = new View('exception'); $view->exception = $ex; + $view->user = $this->getUser(); $response = new Response(); - $response->setResponseCode($ex->getErrorCode()); + $response->setStatusCode($ex->getErrorCode()); $response->setContent($view->render()); - } catch (Exception $ex) { - $this->config->set('page_title', 'Error'); - + } catch (\Throwable $ex) { $view = new View('exception'); $view->exception = $ex; $response = new Response(); - $response->setResponseCode(500); + $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR); $response->setContent($view->render()); } @@ -170,15 +189,11 @@ public function handleRequest() /** * Loads a particular controller, and injects our layout view into it. - * - * @param string $class - * - * @return Controller */ - protected function loadController($class) + protected function loadController(string $class): Controller { /** @var Controller $controller */ - $controller = new $class($this->config, $this->request); + $controller = new $class($this->configuration, $this->storeRegistry, $this->request, $this->session); $controller->init(); @@ -188,16 +203,15 @@ protected function loadController($class) /** * Check whether we should skip auth (because it is disabled) * - * @return bool + * @throws Common\Exception\RuntimeException */ - protected function shouldSkipAuth() + protected function shouldSkipAuth(): bool { - $config = Config::getInstance(); - $disableAuth = (bool)$config->get('php-censor.security.disable_auth', false); - $defaultUserId = (int)$config->get('php-censor.security.default_user_id', 1); + $disableAuth = (bool)$this->configuration->get('php-censor.security.disable_auth', false); + $defaultUserId = (int)$this->configuration->get('php-censor.security.default_user_id', 1); if ($disableAuth && $defaultUserId) { - $user = Factory::getStore('User')->getByPrimaryKey($defaultUserId); + $user = $this->storeRegistry->get('User')->getById($defaultUserId); if ($user) { return true; @@ -207,10 +221,7 @@ protected function shouldSkipAuth() return false; } - /** - * @return Controller - */ - public function getController() + public function getController(): Controller { if (empty($this->controller)) { $controllerClass = $this->getControllerClass($this->route); @@ -220,33 +231,19 @@ public function getController() return $this->controller; } - /** - * @param array $route - * - * @return bool - */ - protected function controllerExists($route) + protected function controllerExists(array $route): bool { - return class_exists($this->getControllerClass($route)); + return \class_exists($this->getControllerClass($route)); } - /** - * @param array $route - * - * @return string - */ - protected function getControllerClass($route) + protected function getControllerClass(array $route): string { - $namespace = $this->toPhpName($route['namespace']); $controller = $this->toPhpName($route['controller']); - return 'PHPCensor\\' . $namespace . '\\' . $controller . 'Controller'; + return 'PHPCensor\Controller\\' . $controller . 'Controller'; } - /** - * @return bool - */ - public function isValidRoute(array $route) + public function isValidRoute(array $route): bool { if ($this->controllerExists($route)) { return true; @@ -255,17 +252,11 @@ public function isValidRoute(array $route) return false; } - /** - * @param string $string - * - * @return string - */ - protected function toPhpName($string) + protected function toPhpName(string $string): string { - $string = str_replace('-', ' ', $string); - $string = ucwords($string); - $string = str_replace(' ', '', $string); + $string = \str_replace('-', ' ', $string); + $string = \ucwords($string); - return $string; + return \str_replace(' ', '', $string); } } diff --git a/src/ArrayConfiguration.php b/src/ArrayConfiguration.php new file mode 100644 index 000000000..78b77d223 --- /dev/null +++ b/src/ArrayConfiguration.php @@ -0,0 +1,22 @@ + + */ +class ArrayConfiguration extends ParameterBag implements ConfigurationInterface +{ + public function load(): void + { + return; + } +} diff --git a/src/BuildFactory.php b/src/BuildFactory.php index cce0531ae..c79bcdd57 100644 --- a/src/BuildFactory.php +++ b/src/BuildFactory.php @@ -1,87 +1,108 @@ + * @author Dmitry Khomutov */ class BuildFactory { + private ConfigurationInterface $configuration; + + private StoreRegistry $storeRegistry; + + public function __construct( + ConfigurationInterface $configuration, + StoreRegistry $storeRegistry + ) { + $this->configuration = $configuration; + $this->storeRegistry = $storeRegistry; + } + /** - * @param int $buildId - * - * @return Build|null - * + * @throws Common\Exception\RuntimeException * @throws Exception\HttpException */ - public static function getBuildById($buildId) + public function getBuildById(int $buildId): ?Build { - $build = Factory::getStore('Build')->getById($buildId); + /** @var Build $build */ + $build = $this->storeRegistry->get('Build')->getById($buildId); - if (empty($build)) { + if (!$build) { return null; } - return self::getBuild($build); + return $this->getBuild($build); } /** * Takes a generic build and returns a type-specific build model. * - * @param Build $build The build from which to get a more specific build type. - * - * @return Build - * * @throws Exception\HttpException */ - public static function getBuild(Build $build) + public function getBuild(Build $build): Build { $project = $build->getProject(); - if (!empty($project)) { switch ($project->getType()) { case Project::TYPE_LOCAL: $type = 'LocalBuild'; + break; case Project::TYPE_GIT: $type = 'GitBuild'; + break; case Project::TYPE_GITHUB: $type = 'GithubBuild'; + break; case Project::TYPE_BITBUCKET: $type = 'BitbucketBuild'; + break; case Project::TYPE_GITLAB: $type = 'GitlabBuild'; + break; case Project::TYPE_GOGS: $type = 'GogsBuild'; + break; case Project::TYPE_HG: $type = 'HgBuild'; + break; case Project::TYPE_BITBUCKET_HG: $type = 'BitbucketHgBuild'; + break; case Project::TYPE_BITBUCKET_SERVER: $type = 'BitbucketServerBuild'; + break; case Project::TYPE_SVN: $type = 'SvnBuild'; + break; default: return $build; } $class = '\\PHPCensor\\Model\\Build\\' . $type; - $build = new $class($build->getDataArray()); + $build = new $class($this->configuration, $this->storeRegistry, $build->getDataArray()); } return $build; diff --git a/src/Builder.php b/src/Builder.php index d0f5ddcc5..075b78095 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -1,158 +1,145 @@ + * @author Dmitry Khomutov */ -class Builder implements LoggerAwareInterface +class Builder { - /** - * @var string - */ - public $buildPath; + public string $buildPath = ''; /** * @var string[] */ - public $ignore = []; + public array $ignore = []; - /** - * @var string[] - */ - public $binaryPath = ''; + public string $binaryPath = ''; - /** - * @var string[] - */ - public $priorityPath = 'local'; + public string $priorityPath = 'local'; - /** - * @var string - */ - public $directory; + public string $directory = ''; - /** - * @var string|null - */ - protected $currentStage = null; + protected ?string $currentStage = null; - /** - * @var bool - */ - protected $verbose = true; + protected bool $verbose = true; - /** - * @var Build - */ - protected $build; + protected Build $build; - /** - * @var LoggerInterface - */ - protected $logger; + protected LoggerInterface $logger; - /** - * @var array - */ - protected $config = []; + protected array $config = []; - /** - * @var string - */ - protected $lastOutput; + protected BuildInterpolator $interpolator; - /** - * @var BuildInterpolator - */ - protected $interpolator; + protected BuildStore $store; - /** - * @var BuildStore - */ - protected $store; + protected Executor $pluginExecutor; - /** - * @var Executor - */ - protected $pluginExecutor; + protected CommandExecutorInterface $commandExecutor; - /** - * @var Helper\CommandExecutorInterface - */ - protected $commandExecutor; + protected BuildLogger $buildLogger; - /** - * @var Logging\BuildLogger - */ - protected $buildLogger; + protected BuildErrorWriter $buildErrorWriter; - /** - * @var BuildErrorWriter - */ - private $buildErrorWriter; + protected ConfigurationInterface $configuration; + + protected DatabaseManager $databaseManager; + + protected StoreRegistry $storeRegistry; + + public function __construct( + ConfigurationInterface $configuration, + DatabaseManager $databaseManager, + StoreRegistry $storeRegistry, + Build $build, + BuildLogger $buildLogger + ) { + $this->configuration = $configuration; + $this->databaseManager = $databaseManager; + $this->storeRegistry = $storeRegistry; + $this->buildLogger = $buildLogger; - /** - * Set up the builder. - * - */ - public function __construct(Build $build, LoggerInterface $logger = null) - { $this->build = $build; - $this->store = Factory::getStore('Build'); - $this->buildLogger = new BuildLogger($logger, $build); - $pluginFactory = $this->buildPluginFactory($build); - $this->pluginExecutor = new Plugin\Util\Executor($pluginFactory, $this->buildLogger); + /** @var BuildStore $buildStore */ + $buildStore = $this->storeRegistry->get('Build'); + /** @var SecretStore $secretStore */ + $secretStore = $this->storeRegistry->get('Secret'); + /** @var EnvironmentStore $environmentStore */ + $environmentStore = $this->storeRegistry->get('Environment'); - $executorClass = 'PHPCensor\Helper\CommandExecutor'; + $this->store = $buildStore; + + $pluginFactory = new PluginFactory($this, $build); + + $this->pluginExecutor = new Plugin\Util\Executor( + $this->storeRegistry, + $pluginFactory, + $this->buildLogger, + $buildStore + ); + + $executorClass = CommandExecutor::class; $this->commandExecutor = new $executorClass( $this->buildLogger, ROOT_DIR, $this->verbose ); - $this->interpolator = new BuildInterpolator(); - $this->buildErrorWriter = new BuildErrorWriter($this->build->getProjectId(), $this->build->getId()); + $this->interpolator = new BuildInterpolator($environmentStore, $secretStore); + $this->buildErrorWriter = new BuildErrorWriter( + $this->configuration, + $this->databaseManager, + $this->storeRegistry, + $this->build->getProjectId(), + $this->build->getId() + ); } - /** - * @return BuildLogger - */ - public function getBuildLogger() + public function getBuildLogger(): BuildLogger { return $this->buildLogger; } - /** - * @return string|null - */ - public function getCurrentStage() + public function getConfiguration(): ConfigurationInterface + { + return $this->configuration; + } + + public function getCurrentStage(): ?string { return $this->currentStage; } /** * Set the config array, as read from .php-censor.yml - * - * @throws Exception */ - public function setConfig(array $config) + public function setConfig(array $config): void { $this->config = $config; } @@ -160,11 +147,9 @@ public function setConfig(array $config) /** * Access a variable from the .php-censor.yml file. * - * @param string $key - * * @return mixed */ - public function getConfig($key = null) + public function getConfig(?string $key = null) { $value = null; if (null === $key) { @@ -176,33 +161,7 @@ public function getConfig($key = null) return $value; } - /** - * Access a variable from the config.yml - * - * @param string $key - * - * @return mixed - */ - public function getSystemConfig($key) - { - return Config::getInstance()->get($key); - } - - /** - * @return string The title of the project being built. - * - * @throws Exception\HttpException - */ - public function getBuildProjectTitle() - { - return $this->build->getProject()->getTitle(); - } - - /** - * @throws Exception\HttpException - * @throws Exception\InvalidArgumentException - */ - public function execute() + public function execute(): void { $this->build->setStatusRunning(); $this->build->setStartDate(new DateTime()); @@ -212,10 +171,10 @@ public function execute() $success = true; $previousBuild = $this->build->getProject()->getPreviousBuild($this->build->getBranch()); - $previousState = Build::STATUS_PENDING; + $previousBuildStatus = Build::STATUS_PENDING; if ($previousBuild) { - $previousState = $previousBuild->getStatus(); + $previousBuildStatus = $previousBuild->getStatus(); } try { @@ -238,7 +197,7 @@ public function execute() } else { $this->build->setStatusFailed(); } - } catch (Exception $ex) { + } catch (\Throwable $ex) { $success = false; $this->build->setStatusFailed(); $this->buildLogger->logFailure('Exception: ' . $ex->getMessage(), $ex); @@ -249,7 +208,7 @@ public function execute() $this->currentStage = Build::STAGE_SUCCESS; $this->pluginExecutor->executePlugins($this->config, Build::STAGE_SUCCESS); - if (Build::STATUS_FAILED === $previousState) { + if (Build::STATUS_FAILED === $previousBuildStatus) { $this->currentStage = Build::STAGE_FIXED; $this->pluginExecutor->executePlugins($this->config, Build::STAGE_FIXED); } @@ -257,12 +216,12 @@ public function execute() $this->currentStage = Build::STAGE_FAILURE; $this->pluginExecutor->executePlugins($this->config, Build::STAGE_FAILURE); - if (Build::STATUS_SUCCESS === $previousState || Build::STATUS_PENDING === $previousState) { + if (Build::STATUS_SUCCESS === $previousBuildStatus || Build::STATUS_PENDING === $previousBuildStatus) { $this->currentStage = Build::STAGE_BROKEN; $this->pluginExecutor->executePlugins($this->config, Build::STAGE_BROKEN); } } - } catch (Exception $ex) { + } catch (\Throwable $ex) { $this->buildLogger->logFailure('Exception: ' . $ex->getMessage(), $ex); } @@ -280,7 +239,7 @@ public function execute() // Complete stage plugins are always run $this->currentStage = Build::STAGE_COMPLETE; $this->pluginExecutor->executePlugins($this->config, Build::STAGE_COMPLETE); - } catch (Exception $ex) { + } catch (\Throwable $ex) { $this->buildLogger->logFailure('Exception: ' . $ex->getMessage()); } @@ -288,7 +247,7 @@ public function execute() $this->build->sendStatusPostback(); $this->build->setFinishDate(new DateTime()); - $removeBuilds = (bool)Config::getInstance()->get('php-censor.build.remove_builds', true); + $removeBuilds = (bool)$this->configuration->get('php-censor.build.remove_builds', true); if ($removeBuilds) { // Clean up: $this->buildLogger->log(''); @@ -299,15 +258,12 @@ public function execute() $this->buildErrorWriter->flush(); $this->setErrorTrend(); + $this->setTestCoverageTrend(); $this->store->save($this->build); } - /** - * @throws Exception\HttpException - * @throws Exception\InvalidArgumentException - */ - protected function setErrorTrend() + protected function setErrorTrend(): void { $this->build->setErrorsTotal($this->store->getErrorsCount($this->build->getId())); @@ -317,15 +273,25 @@ protected function setErrorTrend() $this->build->getBranch() ); - if (isset($trend[1])) { - $previousBuild = $this->store->getById($trend[1]['build_id']); - if ($previousBuild && - !in_array( - $previousBuild->getStatus(), - [Build::STATUS_PENDING, Build::STATUS_RUNNING], - true - )) { - $this->build->setErrorsTotalPrevious((int)$trend[1]['count']); + if (isset($trend[0])) { + $this->build->setErrorsTotalPrevious((int)$trend[0]['count']); + } + } + + protected function setTestCoverageTrend(): void + { + $this->build->setTestCoverage($this->store->getTestCoverage($this->build->getId())); + + $trend = $this->store->getBuildTestCoverageTrend( + $this->build->getId(), + $this->build->getProjectId(), + $this->build->getBranch() + ); + + if (!empty($trend[0]) && !empty($trend[0]['coverage'])) { + $coverage = \json_decode($trend[0]['coverage'], true); + if (isset($coverage['lines'])) { + $this->build->setTestCoveragePrevious($coverage['lines']); } } } @@ -333,31 +299,25 @@ protected function setErrorTrend() /** * Used by this class, and plugins, to execute shell commands. * - * @param string ...$params - * - * @return bool + * @param ...$params */ - public function executeCommand(...$params) + public function executeCommand(...$params): bool { return $this->commandExecutor->executeCommand($params); } /** * Returns the output from the last command run. - * - * @return string */ - public function getLastOutput() + public function getLastOutput(): string { return $this->commandExecutor->getLastOutput(); } /** * Specify whether exec output should be logged. - * - * @param bool $enableLog */ - public function logExecOutput($enableLog = true) + public function logExecOutput(bool $enableLog = true): void { $this->commandExecutor->logExecOutput = $enableLog; } @@ -366,41 +326,35 @@ public function logExecOutput($enableLog = true) * Find a binary required by a plugin. * * @param array|string $binary - * @param string $priorityPath - * @param string $binaryPath - * @param array $binaryName - * @return string * * @throws Exception when no binary has been found. */ - public function findBinary($binary, $priorityPath = 'local', $binaryPath = '', $binaryName = []) - { + public function findBinary( + $binary, + string $priorityPath = 'local', + string $binaryPath = '', + array $binaryName = [] + ): string { return $this->commandExecutor->findBinary($binary, $priorityPath, $binaryPath, $binaryName); } /** * Replace every occurrence of the interpolation vars in the given string * Example: "This is build %BUILD_ID%" => "This is build 182" - * - * @param string $input - * - * @return string */ - public function interpolate($input) + public function interpolate(string $input, bool $useSecrets = false): string { - return $this->interpolator->interpolate($input); + return $this->interpolator->interpolate($input, $useSecrets); } /** * Set up a working copy of the project for building. * * @throws Exception - * - * @return bool */ - protected function setupBuild() + protected function setupBuild(): bool { - $this->buildPath = $this->build->getBuildPath(); + $this->buildPath = (string)$this->build->getBuildPath(); $this->commandExecutor->setBuildPath($this->buildPath); @@ -434,14 +388,14 @@ protected function setupBuild() } if (!empty($this->config['build_settings']['binary_path'])) { - $this->binaryPath = rtrim( - $this->interpolate($this->config['build_settings']['binary_path']), + $this->binaryPath = \rtrim( + $this->interpolate($this->config['build_settings']['binary_path'], true), '/\\' ) . '/'; } if (!empty($this->config['build_settings']['priority_path']) && - in_array( + \in_array( $this->config['build_settings']['priority_path'], Plugin::AVAILABLE_PRIORITY_PATHS, true @@ -456,133 +410,46 @@ protected function setupBuild() $directory = $this->config['build_settings']['directory']; } - $this->directory = rtrim( - $this->interpolate($directory), + $this->directory = \rtrim( + $this->interpolate($directory, true), '/\\' ) . '/'; - $this->buildLogger->logSuccess(sprintf('Working copy created: %s', $this->buildPath)); + $this->buildLogger->logSuccess(\sprintf('Working copy created: %s', $this->buildPath)); if (!$workingCopySuccess) { - throw new Exception('Could not create a working copy.'); + throw new RuntimeException('Could not create a working copy.'); } return true; } - /** - * Sets a logger instance on the object - */ - public function setLogger(LoggerInterface $logger) - { - $this->buildLogger->setLogger($logger); - } - - /** - * Write to the build log. - * - * @param string $message - * @param string $level - * @param array $context - */ - public function log($message, $level = LogLevel::INFO, $context = []) + public function log(string $message, string $level = LogLevel::INFO, array $context = []): void { $this->buildLogger->log($message, $level, $context); } - /** - * Add a warning-coloured message to the log. - * - * @param string $message - */ - public function logWarning($message) + public function logWarning(string $message): void { $this->buildLogger->logWarning($message); } - /** - * Add a success-coloured message to the log. - * - * @param string $message - */ - public function logSuccess($message) + public function logSuccess(string $message): void { $this->buildLogger->logSuccess($message); } - /** - * Add a failure-coloured message to the log. - * - * @param string $message - * @param Exception $exception The exception that caused the error. - */ - public function logFailure($message, Exception $exception = null) + public function logFailure(string $message, ?\Throwable $exception = null): void { $this->buildLogger->logFailure($message, $exception); } - /** - * Add a debug-coloured message to the log. - * - * @param string $message - */ - public function logDebug($message) + public function logDebug(string $message): void { $this->buildLogger->logDebug($message); } - /** - * Returns a configured instance of the plugin factory. - * - * @return PluginFactory - */ - private function buildPluginFactory(Build $build) - { - $pluginFactory = new PluginFactory(); - - $self = $this; - $pluginFactory->registerResource( - function () use ($self) { - return $self; - }, - null, - 'PHPCensor\Builder' - ); - - $pluginFactory->registerResource( - function () use ($build) { - return $build; - }, - null, - 'PHPCensor\Model\Build' - ); - - $logger = $this->logger; - $pluginFactory->registerResource( - function () use ($logger) { - return $logger; - }, - null, - 'Psr\Log\LoggerInterface' - ); - - $pluginFactory->registerResource( - function () use ($self) { - $factory = new MailerFactory($self->getSystemConfig('php-censor')); - - return $factory->getSwiftMailerFromConfig(); - }, - null, - 'Swift_Mailer' - ); - - return $pluginFactory; - } - - /** - * @return BuildErrorWriter - */ - public function getBuildErrorWriter() + public function getBuildErrorWriter(): BuildErrorWriter { return $this->buildErrorWriter; } diff --git a/src/Command/Action/CreateAdmin.php b/src/Command/Action/CreateAdmin.php new file mode 100644 index 000000000..73ad63b47 --- /dev/null +++ b/src/Command/Action/CreateAdmin.php @@ -0,0 +1,118 @@ + + */ +class CreateAdmin +{ + private QuestionHelper $questionHelper; + + private InputInterface $input; + + private OutputInterface $output; + + private StoreRegistry $storeRegistry; + + private UserStore $userStore; + + public function __construct( + QuestionHelper $questionHelper, + InputInterface $input, + OutputInterface $output, + StoreRegistry $storeRegistry, + UserStore $userStore + ) { + $this->questionHelper = $questionHelper; + $this->input = $input; + $this->output = $output; + $this->userStore = $userStore; + $this->storeRegistry = $storeRegistry; + } + + /** + * @throws InvalidArgumentException + */ + public function process(): array + { + $result = []; + $mailValidator = function ($answer) { + if (!\filter_var($answer, FILTER_VALIDATE_EMAIL)) { + throw new InvalidArgumentException('Must be a valid email address.'); + } + + return $answer; + }; + + if ($adminEmail = $this->input->getOption('admin-email')) { + $adminEmail = $mailValidator($adminEmail); + } else { + $questionEmail = new Question('Admin email: '); + $adminEmail = $this->questionHelper->ask($this->input, $this->output, $questionEmail); + } + + if (!$adminName = $this->input->getOption('admin-name')) { + $questionName = new Question('Admin name: '); + $adminName = $this->questionHelper->ask($this->input, $this->output, $questionName); + } + + if (!$adminPassword = $this->input->getOption('admin-password')) { + $questionPassword = new Question('Admin password: '); + $questionPassword->setHidden(true); + $questionPassword->setHiddenFallback(false); + $adminPassword = $this->questionHelper->ask($this->input, $this->output, $questionPassword); + } + + $result['email'] = $adminEmail; + $result['name'] = $adminName; + $result['password'] = $adminPassword; + + return $result; + } + + public function create(array $adminDetails): bool + { + try { + $adminUser = $this->userStore->getByEmail($adminDetails['email']); + if ($adminUser) { + throw new RuntimeException('Admin account already exists!'); + } + + $userService = new UserService($this->storeRegistry, $this->userStore); + + $userService->createUser( + $adminDetails['name'], + $adminDetails['email'], + 'internal', + ['type' => 'internal'], + $adminDetails['password'], + true + ); + + $this->output->writeln('User account created!'); + } catch (\Throwable $ex) { + $this->output->writeln('PHP Censor failed to create your admin account!'); + $this->output->writeln('' . $ex->getMessage() . ''); + + return false; + } + + return true; + } +} diff --git a/src/Command/AddSecretCommand.php b/src/Command/AddSecretCommand.php new file mode 100644 index 000000000..754b3fa7c --- /dev/null +++ b/src/Command/AddSecretCommand.php @@ -0,0 +1,95 @@ + + */ +class AddSecretCommand extends Command +{ + private SecretStore $secretStore; + + public function __construct( + ConfigurationInterface $configuration, + DatabaseManager $databaseManager, + StoreRegistry $storeRegistry, + LoggerInterface $logger, + SecretStore $secretStore, + ?string $name = null + ) { + parent::__construct($configuration, $databaseManager, $storeRegistry, $logger, $name); + + $this->secretStore = $secretStore; + } + + /** + * Configure. + */ + protected function configure(): void + { + $this + ->setName('php-censor:add-secret') + + ->addArgument('secret-name', InputArgument::REQUIRED, 'Secret name') + ->addArgument('secret-value', InputArgument::REQUIRED, 'Secret value') + + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force to update existing values') + + ->setDescription('Update secret'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $secretName = (string)$input->getArgument('secret-name'); + $secretValue = (string)$input->getArgument('secret-value'); + $force = (bool)$input->getOption('force'); + + $secrets = $this->secretStore->getByNames([$secretName]); + if ($secrets && !$force) { + $output->writeln( + \sprintf('Secret with name "%s" already exists! Use flag "-f|--force" if you want update secret.', $secretName) + ); + + return 1; + } + + if (!\preg_match(\sprintf('#%s#', Secret::SECRET_NAME_PATTERN), $secretName)) { + $output->writeln( + \sprintf('Secret name "%s" is invalid! Use only letters, numbers and "-" or "_".', $secretName) + ); + + return 2; + } + + $secret = new Secret($this->storeRegistry); + $secret->setCreateDate(new \DateTime()); + $secret->setUserId(null); + $secret->setName($secretName); + + if ($secrets && $force) { + $secret = $secrets[$secretName]; + } + + $secret->setValue($secretValue); + + $this->secretStore->save($secret); + + return 0; + } +} diff --git a/src/Command/CheckLocalizationCommand.php b/src/Command/CheckLocalizationCommand.php deleted file mode 100644 index 4b5dffef2..000000000 --- a/src/Command/CheckLocalizationCommand.php +++ /dev/null @@ -1,165 +0,0 @@ -basePath = __DIR__.'/../Languages'; - } - - /** - * Configure. - */ - protected function configure() - { - $this - ->setName('php-censor:check-localizations') - ->addOption( - 'same', - 0, - InputOption::VALUE_OPTIONAL, - 'Same than English version (0 = no, 1 = yes)' - ) - ->addOption( - 'langs', - [], - InputOption::VALUE_OPTIONAL, - 'List of languages separated by commas. By default, all languages' - ) - ->setDescription('Check localizations.'); - } - - /** - * Loops through running. - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $output->writeln("\nCheck localizations!"); - - $sameThanEnglish = (null !== $input->getOption('same')) - ? $input->getOption('same') - : false; - - $languagesList = (null !== $input->getOption('langs')) - ? explode(',', $input->getOption('langs')) - : []; - - // Get English version - $english = $this->getTranslations($this->basePath.'/lang.en.php'); - $othersLanguages = $this->getLanguages($languagesList); - $diffs = $this->compareTranslations($english, $othersLanguages); - - foreach ($diffs as $language => $value) { - $output->writeln(sprintf("%s:", $language)); - if (!empty($value['not_present'])) { - $output->writeln("\tNot present:\n\t\t" . implode("\n\t\t", $value['not_present'])); - } - - if ($sameThanEnglish === '1' && !empty($value['same'])) { - $output->writeln("\tSame than English:\n\t\t" . implode("\n\t\t", $value['same'])); - } - } - } - - /** - * Returns array of translations by language. - * - * @param string $language language code - * - * @return array - */ - private function getTranslations($language) - { - return [ - include($language) - ]; - } - - /** - * Returns list of languages. - * - * @return array - */ - private function getLanguages(array $languagesList = []) - { - $files = glob($this->basePath . '/*.php'); - - $languages = array_map(function ($dir) use ($languagesList) { - $name = basename($dir); - - if (in_array($name, $this->excluded, true)) { - return null; - } - - // Check if in list of languages. - if (!empty($languagesList)) { - $languageOfName = explode('.', $name); - - if (null == $languageOfName[1] || !in_array($languageOfName[1], $languagesList, true)) { - return null; - } - } - - return $name; - }, $files); - - return array_filter($languages); - } - - /** - * Compare translations. - * - * @param array $default language by default - * @param array $languages others languages - * - * @return array - */ - private function compareTranslations(array $default, array $languages) - { - $diffs = []; - - // Return diff language by language - foreach ($languages as $language) { - $current = $this->getTranslations($this->basePath.'/'.$language); - - foreach ($default as $key => $values) { - $keyValues = array_keys($values); - - foreach ($keyValues as $key2) { - if (!isset($current[$key][$key2])) { - $diffs[$language]['not_present'][] = $key2; - } elseif ($current[$key][$key2] === $default[$key][$key2]) { - $diffs[$language]['same'][] = $key2; - } - } - } - } - - return $diffs; - } -} diff --git a/src/Command/CheckLocalizationsCommand.php b/src/Command/CheckLocalizationsCommand.php new file mode 100644 index 000000000..e655fa2e3 --- /dev/null +++ b/src/Command/CheckLocalizationsCommand.php @@ -0,0 +1,125 @@ + + */ +class CheckLocalizationsCommand extends Command +{ + protected string $basePath = __DIR__ . '/../Languages'; + + protected array $excluded = ['lang.en.php']; + + protected function configure(): void + { + $this + ->setName('php-censor:check-localizations') + + ->addOption('same', 's', InputOption::VALUE_NONE, 'Same than English version') + ->addOption('languages', 'l', InputOption::VALUE_OPTIONAL, 'List of languages separated by commas. By default, all languages', '') + + ->setDescription('Check localizations'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln("\nCheck localizations!"); + + $sameThanEnglish = (bool)$input->getOption('same'); + + $languages = $input->getOption('languages') + ? \explode(',', $input->getOption('languages')) + : []; + + // Get English version + $english = $this->getTranslations($this->basePath . '/lang.en.php'); + $othersLanguages = $this->getLanguages($languages); + $diffs = $this->compareTranslations($english, $othersLanguages); + + foreach ($diffs as $language => $value) { + $output->writeln(\sprintf("%s:", $language)); + if (!empty($value['not_present'])) { + $output->writeln("\tNot present:\n\t\t" . \implode("\n\t\t", $value['not_present'])); + } + + if ($sameThanEnglish === '1' && !empty($value['same'])) { + $output->writeln("\tSame than English:\n\t\t" . \implode("\n\t\t", $value['same'])); + } + } + + return 0; + } + + private function getTranslations(string $language): array + { + return [ + include $language, + ]; + } + + private function getLanguages(array $languagesList = []): array + { + $files = \glob($this->basePath . '/*.php'); + + $languages = \array_map(function ($dir) use ($languagesList) { + $name = \basename($dir); + + if (\in_array($name, $this->excluded, true)) { + return null; + } + + // Check if in list of languages. + if (!empty($languagesList)) { + $languageOfName = \explode('.', $name); + + if (null === $languageOfName[1] || !\in_array($languageOfName[1], $languagesList, true)) { + return null; + } + } + + return $name; + }, $files); + + return \array_filter($languages); + } + + /** + * Compare translations. + * + * @param array $default Language by default + * @param array $languages Others languages + */ + private function compareTranslations(array $default, array $languages): array + { + $diffs = []; + + // Return diff language by language + foreach ($languages as $language) { + $current = $this->getTranslations($this->basePath.'/'.$language); + + foreach ($default as $key => $values) { + $keyValues = \array_keys($values); + + foreach ($keyValues as $key2) { + if (!isset($current[$key][$key2])) { + $diffs[$language]['not_present'][] = $key2; + } elseif ($current[$key][$key2] === $default[$key][$key2]) { + $diffs[$language]['same'][] = $key2; + } + } + } + } + + return $diffs; + } +} diff --git a/src/Command/Command.php b/src/Command/Command.php new file mode 100644 index 000000000..a45558853 --- /dev/null +++ b/src/Command/Command.php @@ -0,0 +1,93 @@ + + */ +abstract class Command extends BaseCommand +{ + protected ConfigurationInterface $configuration; + + protected DatabaseManager $databaseManager; + + protected StoreRegistry $storeRegistry; + + protected LoggerInterface $logger; + + public function __construct( + ConfigurationInterface $configuration, + DatabaseManager $databaseManager, + StoreRegistry $storeRegistry, + LoggerInterface $logger, + ?string $name = null + ) { + parent::__construct($name); + + $this->configuration = $configuration; + $this->databaseManager = $databaseManager; + $this->storeRegistry = $storeRegistry; + $this->logger = $logger; + } + + protected function configureLogging(OutputInterface $output): void + { + $level = LogLevel::ERROR; + if ($output->isDebug()) { + $level = LogLevel::DEBUG; + } elseif ($output->isVeryVerbose()) { + $level = LogLevel::INFO; + } elseif ($output->isVerbose()) { + $level = LogLevel::NOTICE; + } + + if ($this->logger instanceof Logger) { + $handlers = $this->logger->getHandlers(); + foreach ($handlers as $handler) { + if ($handler instanceof AbstractHandler) { + $handler->setLevel($level); + } + } + + if (!$output->isQuiet()) { + $this->logger->pushHandler( + new OutputLogHandler($output, $level) + ); + } + } + + if ($output->isDebug()) { + $this->logger->notice( + \sprintf('Command "%s" started in debug mode (-vvv).', $this->getName()) + ); + + \define('DEBUG_MODE', true); + } + } + + protected function execute( + InputInterface $input, + OutputInterface $output + ): int { + $this->configureLogging($output); + + return 0; + } +} diff --git a/src/Command/CreateAdminCommand.php b/src/Command/CreateAdminCommand.php index 4550b9f37..78ba1067f 100644 --- a/src/Command/CreateAdminCommand.php +++ b/src/Command/CreateAdminCommand.php @@ -1,38 +1,46 @@ * @author Wogan May (@woganmay) */ class CreateAdminCommand extends Command { - /** - * @var UserStore - */ - protected $userStore; - - public function __construct(UserStore $userStore) - { - parent::__construct(); + protected UserStore $userStore; + + public function __construct( + ConfigurationInterface $configuration, + DatabaseManager $databaseManager, + StoreRegistry $storeRegistry, + LoggerInterface $logger, + UserStore $userStore, + ?string $name = null + ) { + parent::__construct($configuration, $databaseManager, $storeRegistry, $logger, $name); $this->userStore = $userStore; } - protected function configure() + protected function configure(): void { $this ->setName('php-censor:create-admin') @@ -45,52 +53,24 @@ protected function configure() } /** - * Creates an admin user in the existing database - * - * {@inheritDoc} + * @throws InvalidArgumentException */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - /** @var $helper QuestionHelper */ - $helper = $this->getHelperSet()->get('question'); - - // Function to validate email address. - $mailValidator = function ($answer) { - if (!filter_var($answer, FILTER_VALIDATE_EMAIL)) { - throw new InvalidArgumentException('Must be a valid email address.'); - } - - return $answer; - }; - - if ($adminEmail = $input->getOption('admin-email')) { - $adminEmail = $mailValidator($adminEmail); - } else { - $questionEmail = new Question('Admin email: '); - $adminEmail = $helper->ask($input, $output, $questionEmail); - } - - if (!$adminName = $input->getOption('admin-name')) { - $questionName = new Question('Admin name: '); - $adminName = $helper->ask($input, $output, $questionName); - } - - if (!$adminPassword = $input->getOption('admin-password')) { - $questionPassword = new Question('Admin password: '); - $questionPassword->setHidden(true); - $questionPassword->setHiddenFallback(false); - $adminPassword = $helper->ask($input, $output, $questionPassword); - } - - try { - $userService = new UserService($this->userStore); - $userService->createUser($adminName, $adminEmail, 'internal', ['type' => 'internal'], $adminPassword, true); - - $output->writeln('User account created!'); - } catch (Exception $ex) { - $output->writeln('PHP Censor failed to create your admin account!'); - $output->writeln('' . $ex->getMessage() . ''); - } + /** @var $questionHelper QuestionHelper */ + $questionHelper = $this->getHelperSet()->get('question'); + + $createAdmin = new CreateAdmin( + $questionHelper, + $input, + $output, + $this->storeRegistry, + $this->userStore + ); + + $createAdmin->create( + $createAdmin->process() + ); return 0; } diff --git a/src/Command/CreateBuildCommand.php b/src/Command/CreateBuildCommand.php index 1a646e239..b7422cf00 100644 --- a/src/Command/CreateBuildCommand.php +++ b/src/Command/CreateBuildCommand.php @@ -1,74 +1,84 @@ * @author Jérémy DECOOL (@jdecool) */ class CreateBuildCommand extends Command { - /** - * @var ProjectStore - */ - protected $projectStore; + protected ProjectStore $projectStore; - /** - * @var BuildService - */ - protected $buildService; + protected BuildService $buildService; - public function __construct(ProjectStore $projectStore, BuildService $buildService) - { - parent::__construct(); + public function __construct( + ConfigurationInterface $configuration, + DatabaseManager $databaseManager, + StoreRegistry $storeRegistry, + LoggerInterface $logger, + ProjectStore $projectStore, + BuildService $buildService, + ?string $name = null + ) { + parent::__construct($configuration, $databaseManager, $storeRegistry, $logger, $name); $this->projectStore = $projectStore; $this->buildService = $buildService; } - /** - * {@inheritDoc} - */ - protected function configure() + protected function configure(): void { $this ->setName('php-censor:create-build') - ->addArgument('projectId', InputArgument::REQUIRED, 'A project ID') + ->addArgument('project-id', InputArgument::REQUIRED, 'A project ID') ->addOption('commit', null, InputOption::VALUE_OPTIONAL, 'Commit ID to build') ->addOption('branch', null, InputOption::VALUE_OPTIONAL, 'Branch to build') ->addOption('email', null, InputOption::VALUE_OPTIONAL, 'Committer email') ->addOption('message', null, InputOption::VALUE_OPTIONAL, 'Commit message') + ->addOption('environment', null, InputOption::VALUE_OPTIONAL, 'Build environment') ->setDescription('Create a build for a project'); } /** - * {@inheritDoc} + * @throws InvalidArgumentException + * @throws RuntimeException + * @throws HttpException */ - public function execute(InputInterface $input, OutputInterface $output) + public function execute(InputInterface $input, OutputInterface $output): int { - $projectId = $input->getArgument('projectId'); + $projectId = (int)$input->getArgument('project-id'); $commitId = $input->getOption('commit'); $branch = $input->getOption('branch'); - $environment = $input->hasOption('environment') ? $input->getOption('environment') : null; + $environment = $input->getOption('environment'); $ciEmail = $input->getOption('email'); $ciMessage = $input->getOption('message'); + /** @var Project $project */ $project = $this->projectStore->getById($projectId); if (empty($project) || $project->getArchived()) { throw new InvalidArgumentException('Project does not exist: ' . $projectId); @@ -76,8 +86,8 @@ public function execute(InputInterface $input, OutputInterface $output) $environmentId = null; if ($environment) { - /** @var Store\EnvironmentStore $environmentStore */ - $environmentStore = Factory::getStore('Environment'); + /** @var EnvironmentStore $environmentStore */ + $environmentStore = $this->storeRegistry->get('Environment'); $environmentObject = $environmentStore->getByNameAndProjectId($environment, $project->getId()); if ($environmentObject) { $environmentId = $environmentObject->getId(); @@ -99,9 +109,9 @@ public function execute(InputInterface $input, OutputInterface $output) $output->writeln('Build Created'); return 0; - } catch (Exception $e) { + } catch (\Throwable $e) { $output->writeln('Failed'); - $output->writeln(sprintf('%s', $e->getMessage())); + $output->writeln(\sprintf('%s', $e->getMessage())); } return 1; diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 7a370cc29..f6bc9b360 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -1,41 +1,38 @@ * @author Dan Cryer */ class InstallCommand extends Command { - /** - * @var string - */ - protected $configPath = APP_DIR . 'config.yml'; + protected string $configPath = APP_DIR . 'config.yml'; - protected function configure() + protected function configure(): void { $this ->setName('php-censor:install') @@ -92,23 +89,19 @@ protected function configure() ->addOption( 'config-from-file', null, - InputOption::VALUE_OPTIONAL, - 'Take config from file and ignore options', - false + InputOption::VALUE_NONE, + 'Take config from file and ignore options' ) ->setDescription('Install PHP Censor'); } - /** - * Installs PHP Censor - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $configFromFile = (bool)$input->getOption('config-from-file'); if (!$configFromFile && !$this->verifyNotInstalled($output)) { - return; + return 1; } $output->writeln(''); @@ -139,24 +132,27 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->writeConfigFile($conf); } - $this->reloadConfig(); + $this->configuration->load(); + if (!$this->setupDatabase($output)) { - return false; + return 2; } - $admin = $this->getAdminInformation($input, $output); - $this->createAdminUser($admin, $output); + if (!$this->createAdminUser($input, $output)) { + return 3; + } + + if (!$this->createDefaultGroup($output)) { + return 4; + } - $this->createDefaultGroup($output); + return 0; } - /** - * @return bool - */ - protected function verifyNotInstalled(OutputInterface $output) + private function verifyNotInstalled(OutputInterface $output): bool { - if (file_exists($this->configPath)) { - $content = file_get_contents($this->configPath); + if (\file_exists($this->configPath)) { + $content = \file_get_contents($this->configPath); if (!empty($content)) { $output->writeln( @@ -176,23 +172,21 @@ protected function verifyNotInstalled(OutputInterface $output) * * @throws Exception */ - protected function checkRequirements(OutputInterface $output) + private function checkRequirements(OutputInterface $output): void { $output->writeln('Checking requirements...'); $errors = false; - if (!(version_compare(PHP_VERSION, '7.4.0') >= 0)) { + if (!(\version_compare(PHP_VERSION, '7.4.0') >= 0)) { $output->writeln(''); - $output->writeln( - 'PHP Censor requires at least PHP 7.4.0! Installed PHP ' . PHP_VERSION . '' - ); + $output->writeln('PHP Censor requires at least PHP 7.4.0! Installed PHP ' . PHP_VERSION . ''); $errors = true; } $requiredExtensions = ['PDO', 'xml', 'json', 'curl', 'openssl']; foreach ($requiredExtensions as $extension) { - if (!extension_loaded($extension)) { + if (!\extension_loaded($extension)) { $output->writeln(''); $output->writeln('Extension required: ' . $extension . ''); $errors = true; @@ -202,7 +196,7 @@ protected function checkRequirements(OutputInterface $output) $requiredFunctions = ['exec', 'shell_exec', 'proc_open']; foreach ($requiredFunctions as $function) { - if (!function_exists($function)) { + if (!\function_exists($function)) { $output->writeln(''); $output->writeln( 'PHP Censor needs to be able to call the ' . $function . @@ -213,7 +207,7 @@ protected function checkRequirements(OutputInterface $output) } if ($errors) { - throw new Exception( + throw new RuntimeException( 'PHP Censor cannot be installed, as not all requirements are met. ' . 'Please review the errors above before continuing.' ); @@ -223,69 +217,22 @@ protected function checkRequirements(OutputInterface $output) $output->writeln('OK'); } - /** - * Load information for admin user form CLI options or ask info to user. - * - * @return array - */ - protected function getAdminInformation(InputInterface $input, OutputInterface $output) - { - $admin = []; - - /** @var $helper QuestionHelper */ - $helper = $this->getHelperSet()->get('question'); - - // Function to validate email address. - $mailValidator = function ($answer) { - if (!filter_var($answer, FILTER_VALIDATE_EMAIL)) { - throw new InvalidArgumentException('Must be a valid email address.'); - } - - return $answer; - }; - - if ($adminEmail = $input->getOption('admin-email')) { - $adminEmail = $mailValidator($adminEmail); - } else { - $questionEmail = new Question('Admin email: '); - $adminEmail = $helper->ask($input, $output, $questionEmail); - } - - if (!$adminName = $input->getOption('admin-name')) { - $questionName = new Question('Admin name: '); - $adminName = $helper->ask($input, $output, $questionName); - } - - if (!$adminPassword = $input->getOption('admin-password')) { - $questionPassword = new Question('Admin password: '); - $questionPassword->setHidden(true); - $questionPassword->setHiddenFallback(false); - $adminPassword = $helper->ask($input, $output, $questionPassword); - } - - $admin['email'] = $adminEmail; - $admin['name'] = $adminName; - $admin['password'] = $adminPassword; - - return $admin; - } - /** * Load configuration form CLI options or ask info to user. * - * @return array + * @throws Exception */ - protected function getConfigInformation(InputInterface $input, OutputInterface $output) + private function getConfigInformation(InputInterface $input, OutputInterface $output): array { /** @var $helper QuestionHelper */ $helper = $this->getHelperSet()->get('question'); - $urlValidator = function ($answer) { - if (!filter_var($answer, FILTER_VALIDATE_URL)) { - throw new Exception('Must be a valid URL.'); + $urlValidator = function ($domain) { + if (!\filter_var($domain, FILTER_VALIDATE_URL)) { + throw new InvalidArgumentException("${domain} is not a valid URL!"); } - return rtrim($answer, '/'); + return \rtrim($domain, '/'); }; if ($url = $input->getOption('url')) { @@ -302,11 +249,15 @@ protected function getConfigInformation(InputInterface $input, OutputInterface $ $queueConfig = $this->getQueueInformation($input, $output); return [ - 'language' => 'en', - 'per_page' => 10, - 'url' => $url, - 'queue' => $queueConfig, - 'log' => [ + 'language' => 'en', + 'per_page' => 10, + 'url' => $url, + 'queue' => $queueConfig, + 'realtime_ui' => true, + 'webhook' => [ + 'log_requests' => false, + ], + 'log' => [ 'rotate' => false, 'max_files' => 0, ], @@ -371,15 +322,11 @@ protected function getConfigInformation(InputInterface $input, OutputInterface $ /** * If the user wants to use a queue, get the necessary details. - * - * @return array */ - protected function getQueueInformation(InputInterface $input, OutputInterface $output) + private function getQueueInformation(InputInterface $input, OutputInterface $output): array { $queueConfig = [ - 'host' => null, 'port' => Pheanstalk::DEFAULT_PORT, - 'name' => null, 'lifetime' => 600, ]; @@ -417,10 +364,8 @@ protected function getQueueInformation(InputInterface $input, OutputInterface $o /** * Load configuration for database form CLI options or ask info to user. - * - * @return array */ - protected function getDatabaseInformation(InputInterface $input, OutputInterface $output) + private function getDatabaseInformation(InputInterface $input, OutputInterface $output): array { $db = []; @@ -429,7 +374,7 @@ protected function getDatabaseInformation(InputInterface $input, OutputInterface if (!$dbType = $input->getOption('db-type')) { $questionType = new Question('Enter your database type ("mysql" or "pgsql"): '); - $dbType = trim(strtolower($helper->ask($input, $output, $questionType))); + $dbType = \trim(\strtolower($helper->ask($input, $output, $questionType))); } if (!$dbHost = $input->getOption('db-host')) { @@ -437,7 +382,7 @@ protected function getDatabaseInformation(InputInterface $input, OutputInterface 'Enter your database host (default: "localhost"): ', 'localhost' ); - $dbHost = trim($helper->ask($input, $output, $questionHost)); + $dbHost = \trim($helper->ask($input, $output, $questionHost)); } $defaultPort = 3306; @@ -461,10 +406,10 @@ protected function getDatabaseInformation(InputInterface $input, OutputInterface if ( $dbType === 'pgsql' - && !$dbPgsqlSslmode = $input->getOption('db-pgsql-sslmode') + && !$dbPgsqlSslMode = $input->getOption('db-pgsql-sslmode') ) { - $questionSslmode = new Question('Enter your database connection\'s SSL mode (default: prefer): ', 'prefer'); - $dbPgsqlSslmode = $helper->ask($input, $output, $questionSslmode); + $questionSslMode = new Question('Enter your database connection\'s SSL mode (default: prefer): ', 'prefer'); + $dbPgsqlSslMode = $helper->ask($input, $output, $questionSslMode); } if (!$dbName = $input->getOption('db-name')) { @@ -472,7 +417,7 @@ protected function getDatabaseInformation(InputInterface $input, OutputInterface 'Enter your database name (default: "php-censor-db"): ', 'php-censor-db' ); - $dbName = trim($helper->ask($input, $output, $questionDb)); + $dbName = \trim($helper->ask($input, $output, $questionDb)); } if (!$dbUser = $input->getOption('db-user')) { @@ -480,7 +425,7 @@ protected function getDatabaseInformation(InputInterface $input, OutputInterface 'Enter your database user (default: "php-censor-user"): ', 'php-censor-user' ); - $dbUser = trim($helper->ask($input, $output, $questionUser)); + $dbUser = \trim($helper->ask($input, $output, $questionUser)); } if (!$dbPass = $input->getOption('db-password')) { @@ -496,8 +441,8 @@ protected function getDatabaseInformation(InputInterface $input, OutputInterface ] ]; - if ($dbType === 'pgsql' && !empty($dbPgsqlSslmode)) { - $dbServers[0]['pgsql-sslmode'] = $dbPgsqlSslmode; + if ($dbType === 'pgsql' && !empty($dbPgsqlSslMode)) { + $dbServers[0]['pgsql-sslmode'] = $dbPgsqlSslMode; } $dbServers[0]['port'] = $dbPort; @@ -515,10 +460,8 @@ protected function getDatabaseInformation(InputInterface $input, OutputInterface /** * Try and connect to DB using the details provided - * - * @return bool */ - protected function verifyDatabaseDetails(array $db, OutputInterface $output) + private function verifyDatabaseDetails(array $db, OutputInterface $output): bool { $dns = $db['type'] . ':host=' . $db['servers']['write'][0]['host']; @@ -552,7 +495,7 @@ protected function verifyDatabaseDetails(array $db, OutputInterface $output) unset($pdo); return true; - } catch (Exception $ex) { + } catch (\Throwable $ex) { $output->writeln( 'PHP Censor could not connect to database with the details provided. ' . 'Please try again.' @@ -566,27 +509,27 @@ protected function verifyDatabaseDetails(array $db, OutputInterface $output) /** * Write the config.yml file. */ - protected function writeConfigFile(array $config) + private function writeConfigFile(array $config): void { $dumper = new Dumper(); $yaml = $dumper->dump($config, 4); - file_put_contents($this->configPath, $yaml); + \file_put_contents($this->configPath, $yaml); } - protected function setupDatabase(OutputInterface $output) + private function setupDatabase(OutputInterface $output): bool { $output->write('Setting up your database...'); - exec( + \exec( (ROOT_DIR . 'bin/console php-censor-migrations:migrate'), $outputMigration, $status ); $output->writeln(''); - $output->writeln(implode(PHP_EOL, $outputMigration)); - if (0 == $status) { + $output->writeln(\implode(PHP_EOL, $outputMigration)); + if (0 === $status) { $output->writeln('OK'); return true; @@ -600,69 +543,55 @@ protected function setupDatabase(OutputInterface $output) /** * Create admin user using information loaded before. * - * @param array $admin - * @param OutputInterface $output + * @throws InvalidArgumentException + * @throws RuntimeException */ - protected function createAdminUser($admin, $output) + protected function createAdminUser(InputInterface $input, OutputInterface $output): bool { - try { - /** @var UserStore $userStore */ - $userStore = Factory::getStore('User'); - $adminUser = $userStore->getByEmail($admin['email']); - if ($adminUser) { - throw new RuntimeException('Admin account already exists!'); - } + /** @var $questionHelper QuestionHelper */ + $questionHelper = $this->getHelperSet()->get('question'); + + /** @var UserStore $userStore */ + $userStore = $this->storeRegistry->get('User'); + + $createAdmin = new CreateAdmin( + $questionHelper, + $input, + $output, + $this->storeRegistry, + $userStore + ); - $userService = new UserService($userStore); - $userService->createUser( - $admin['name'], - $admin['email'], - 'internal', - ['type' => 'internal'], - $admin['password'], - true - ); + $admin = $createAdmin->process(); - $output->writeln('User account created!'); - } catch (Exception $ex) { - $output->writeln('PHP Censor failed to create your admin account!'); - $output->writeln('' . $ex->getMessage() . ''); - } + return $createAdmin->create($admin); } - /** - * @param OutputInterface $output - */ - protected function createDefaultGroup($output) + protected function createDefaultGroup(OutputInterface $output): bool { try { /** @var ProjectGroupStore $projectGroupStore */ - $projectGroupStore = Factory::getStore('ProjectGroup'); + $projectGroupStore = $this->storeRegistry->get('ProjectGroup'); $projectGroup = $projectGroupStore->getByTitle('Projects'); if ($projectGroup) { throw new RuntimeException('Default project group already exists!'); } - $group = new ProjectGroup(); + $group = new ProjectGroup($this->storeRegistry); $group->setTitle('Projects'); $group->setCreateDate(new DateTime()); $group->setUserId(null); - Factory::getStore('ProjectGroup')->save($group); + $this->storeRegistry->get('ProjectGroup')->save($group); $output->writeln('Default project group created!'); - } catch (Exception $ex) { + } catch (\Throwable $ex) { $output->writeln('PHP Censor failed to create default project group!'); $output->writeln('' . $ex->getMessage() . ''); - } - } - - protected function reloadConfig() - { - $config = Config::getInstance(); - if (file_exists($this->configPath)) { - $config->loadYaml($this->configPath); + return false; } + + return true; } } diff --git a/src/Command/LoggingCommand.php b/src/Command/LoggingCommand.php deleted file mode 100644 index d1f6cac2a..000000000 --- a/src/Command/LoggingCommand.php +++ /dev/null @@ -1,66 +0,0 @@ -logger = $logger; - } - - protected function configureLogging(OutputInterface $output) - { - $level = Logger::ERROR; - if ($output->isDebug()) { - $level = Logger::DEBUG; - } elseif ($output->isVeryVerbose()) { - $level = Logger::INFO; - } elseif ($output->isVerbose()) { - $level = Logger::NOTICE; - } - - $handlers = $this->logger->getHandlers(); - foreach ($handlers as $handler) { - if ($handler instanceof AbstractHandler) { - $handler->setLevel($level); - } - } - - if (!$output->isQuiet()) { - $this->logger->pushHandler( - new OutputLogHandler($output, $level) - ); - } - - if ($output->isDebug()) { - $this->logger->notice( - sprintf('Command "%s" started in debug mode (-vvv).', $this->getName()) - ); - - define('DEBUG_MODE', true); - } - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->configureLogging($output); - } -} diff --git a/src/Command/RebuildQueueCommand.php b/src/Command/RebuildQueueCommand.php index 64bb89597..3e06834ab 100644 --- a/src/Command/RebuildQueueCommand.php +++ b/src/Command/RebuildQueueCommand.php @@ -1,84 +1,76 @@ * @author Dan Cryer */ class RebuildQueueCommand extends Command { - /** - * @var OutputInterface - */ - protected $output; - - /** - * @var Logger - */ - protected $logger; - - /** - * @param string $name - */ - public function __construct(Logger $logger, $name = null) - { - parent::__construct($name); - $this->logger = $logger; - } - - protected function configure() + protected function configure(): void { $this ->setName('php-censor:rebuild-queue') ->setDescription('Rebuilds the PHP Censor worker queue.'); } - protected function execute(InputInterface $input, OutputInterface $output) + /** + * @throws RuntimeException + * @throws HttpException + */ + protected function execute(InputInterface $input, OutputInterface $output): int { - $this->output = $output; - - // For verbose mode we want to output all informational and above - // messages to the symphony output interface. - if ($input->hasOption('verbose') && $input->getOption('verbose')) { - $this->logger->pushHandler( - new OutputLogHandler($this->output, Logger::INFO) - ); - } - /** @var BuildStore $buildStore */ - $buildStore = Factory::getStore('Build'); + $buildStore = $this->storeRegistry->get('Build'); /** @var ProjectStore $projectStore */ - $projectStore = Factory::getStore('Project'); + $projectStore = $this->storeRegistry->get('Project'); $result = $buildStore->getByStatus(0); - $this->logger->addInfo(sprintf('Found %d builds', count($result['items']))); + $this->logger->info(\sprintf('Found %d builds', \count($result['items']))); - $buildService = new BuildService($buildStore, $projectStore); + $buildFactory = new BuildFactory( + $this->configuration, + $this->storeRegistry + ); - while (count($result['items'])) { - $build = array_shift($result['items']); - $build = BuildFactory::getBuild($build); + $buildService = new BuildService( + $this->configuration, + $this->storeRegistry, + $buildFactory, + $buildStore, + $projectStore + ); + + while (\count($result['items'])) { + $build = \array_shift($result['items']); + $build = $buildFactory->getBuild($build); $project = $build->getProject(); - $this->logger->addInfo('Added build #' . $build->getId() . ' to queue.'); + $this->logger->info('Added build #' . $build->getId() . ' to queue.'); $buildService->addBuildToQueue( $build, (null !== $project) ? $project->getBuildPriority() : Project::DEFAULT_BUILD_PRIORITY ); } + + return 0; } } diff --git a/src/Command/RemoveOldBuildsCommand.php b/src/Command/RemoveOldBuildsCommand.php index 3ab764ed0..512828a4d 100644 --- a/src/Command/RemoveOldBuildsCommand.php +++ b/src/Command/RemoveOldBuildsCommand.php @@ -1,46 +1,51 @@ * @author David Sloan */ class RemoveOldBuildsCommand extends Command { - /** - * @var ProjectStore - */ - protected $projectStore; + protected ProjectStore $projectStore; - /** - * @var BuildService - */ - protected $buildService; + protected BuildService $buildService; - public function __construct(ProjectStore $projectStore, BuildService $buildService) - { - parent::__construct(); + public function __construct( + ConfigurationInterface $configuration, + DatabaseManager $databaseManager, + StoreRegistry $storeRegistry, + LoggerInterface $logger, + ProjectStore $projectStore, + BuildService $buildService, + ?string $name = null + ) { + parent::__construct($configuration, $databaseManager, $storeRegistry, $logger, $name); - /** @var ProjectStore $projectStore */ $this->projectStore = $projectStore; - - /** @var BuildStore $buildStore */ $this->buildService = $buildService; } /** * Configure. */ - protected function configure() + protected function configure(): void { $this ->setName('php-censor:remove-old-builds') @@ -48,13 +53,17 @@ protected function configure() } /** - * Loops through projects. - */ - protected function execute(InputInterface $input, OutputInterface $output) + * Loops through projects. + * + * @throws HttpException + */ + protected function execute(InputInterface $input, OutputInterface $output): int { $projects = $this->projectStore->getAll(); foreach ($projects['items'] as $project) { $this->buildService->deleteOldByProject($project->getId()); } + + return 0; } } diff --git a/src/Command/WorkerCommand.php b/src/Command/WorkerCommand.php index 3175b4874..7d70be3b0 100644 --- a/src/Command/WorkerCommand.php +++ b/src/Command/WorkerCommand.php @@ -1,50 +1,57 @@ * @author Dan Cryer */ -class WorkerCommand extends LoggingCommand +class WorkerCommand extends Command { - public const MIN_QUEUE_PRIORITY = 24; - public const MAX_QUEUE_PRIORITY = 2025; + private const MIN_QUEUE_PRIORITY = 24; + private const MAX_QUEUE_PRIORITY = 2025; - /** - * @var BuildService - */ - protected $buildService; + protected BuildService $buildService; + + protected BuildFactory $buildFactory; - /** - * @param string $name - */ public function __construct( - Logger $logger, + ConfigurationInterface $configuration, + DatabaseManager $databaseManager, + StoreRegistry $storeRegistry, + LoggerInterface $logger, BuildService $buildService, - $name = null + BuildFactory $buildFactory, + ?string $name = null ) { - parent::__construct($logger, $name); + parent::__construct($configuration, $databaseManager, $storeRegistry, $logger, $name); $this->buildService = $buildService; + $this->buildFactory = $buildFactory; } - protected function configure() + protected function configure(): void { - $whenHints = 'soon=when next job done (default), done=when current jobs done, idle=when waiting for jobs'; $this ->setName('php-censor:worker') ->addOption( @@ -57,52 +64,83 @@ protected function configure() 'stop-worker', 's', InputOption::VALUE_OPTIONAL, - "Gracefully stop one worker ($whenHints)", - false // default value is used when option not given + "Gracefully stop one worker ('soon' = when next job done, 'done' = when current jobs done, 'idle' = when waiting for jobs)", + '' ) ->setDescription('Runs the PHP Censor build worker.'); } /** - * @throws Exception + * @throws RuntimeException */ - protected function execute(InputInterface $input, OutputInterface $output) + private function checkQueueSettings(array $config): void { - parent::execute($input, $output); - - $config = Config::getInstance()->get('php-censor.queue', []); if (empty($config['host']) || empty($config['name'])) { throw new RuntimeException( 'The worker is not configured. You must set a host and queue in your config.yml file.' ); } + } + + /** + * @throws InvalidArgumentException + */ + private function processWorkerStopFlag(InputInterface $input): bool + { $value = $input->getOption('stop-worker'); - if (false !== $value) { - if ('soon' === $value || null === $value) { + if (!empty($value)) { + if ('soon' === $value) { $priority = self::MIN_QUEUE_PRIORITY; // high priority, stop soon } elseif ('done' === $value) { - $priority = Pheanstalk::DEFAULT_PRIORITY; + $priority = PheanstalkInterface::DEFAULT_PRIORITY; } elseif ('idle' === $value) { $priority = self::MAX_QUEUE_PRIORITY; // low priority, stop late } else { - $msg = sprintf('Invalid value "%s" for --stop-worker, valid are soon, done and idle;', $value); - - throw new InvalidArgumentException($msg); + throw new InvalidArgumentException( + \sprintf('Invalid value "%s" for --stop-worker, valid are soon, done and idle;', $value) + ); } + $jobData = []; $this->buildService->addJobToQueue(BuildWorker::JOB_TYPE_STOP_FLAG, $jobData, $priority); - return; + return true; + } + + return false; + } + + /** + * @throws Exception + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + parent::execute($input, $output); + + $config = $this->configuration->get('php-censor.queue', []); + $this->checkQueueSettings($config); + + $needToStopWorker = $this->processWorkerStopFlag($input); + if ($needToStopWorker) { + return 0; } - (new BuildWorker( + $periodicalWork = (bool)$input->getOption('periodical-work'); + $worker = new BuildWorker( + $this->configuration, + $this->databaseManager, + $this->storeRegistry, $this->logger, $this->buildService, + $this->buildFactory, $config['host'], - Config::getInstance()->get('php-censor.queue.port', Pheanstalk::DEFAULT_PORT), + (int)$this->configuration->get('php-censor.queue.port', PheanstalkInterface::DEFAULT_PORT), $config['name'], - ($input->hasOption('periodical-work') && $input->getOption('periodical-work')) - )) - ->startWorker(); + $periodicalWork + ); + + $worker->startWorker(); + + return 0; } } diff --git a/src/Config.php b/src/Config.php deleted file mode 100644 index 7fdb186a4..000000000 --- a/src/Config.php +++ /dev/null @@ -1,210 +0,0 @@ -setArray($settings); - } elseif (is_string($settings) && file_exists($settings)) { - $this->loadYaml($settings); - } - } - - /** - * @param string $yamlFile - */ - public function loadYaml($yamlFile) - { - // Path to a YAML file. - $parser = new YamlParser(); - $yaml = file_get_contents($yamlFile); - $config = (array)$parser->parse($yaml); - - if (empty($config)) { - return; - } - - $this->setArray($config); - } - - /** - * Get a configuration value by key, returning a default value if not set. - * - * @param string $key - * @param mixed $default - * - * @return mixed - */ - public function get($key, $default = null) - { - $keyParts = explode('.', $key); - $selected = $this->config; - - $i = -1; - $lastPart = count($keyParts) - 1; - while ($part = array_shift($keyParts)) { - $i++; - - if (!array_key_exists($part, $selected)) { - return $default; - } - - if ($i === $lastPart) { - return $selected[$part]; - } else { - $selected = $selected[$part]; - } - } - - return $default; - } - - /** - * Set a value by key. - * - * @param string $key - * @param mixed $value - * - * @return bool - */ - public function set($key, $value = null) - { - $this->config[$key] = $value; - - return true; - } - - /** - * Set an array of values. - * - */ - public function setArray($array) - { - self::deepMerge($this->config, $array); - } - - /** - * Short-hand syntax for get() - * @see Config::get() - * - * @param string $key - * - * @return mixed - */ - public function __get($key) - { - return $this->get($key); - } - - /** - * Short-hand syntax for set() - * @see Config::set() - * - * @param string $key - * @param mixed $value - * - * @return bool - */ - public function __set($key, $value = null) - { - return $this->set($key, $value); - } - - /** - * Is set - * - * @param string $key - * - * @return bool - */ - public function __isset($key) - { - return isset($this->config[$key]); - } - - /** - * Unset - * - * @param string $key - */ - public function __unset($key) - { - unset($this->config[$key]); - } - - /** - * Deeply merge the $target array onto the $source array. The $source array will be modified! - * - * @param array $source - * @param array $target - */ - public static function deepMerge(&$source, $target) - { - if (count($source) === 0) { - $source = $target; - - return; - } - - foreach ($target as $targetKey => $targetValue) { - if (isset($source[$targetKey])) { - if (!is_array($source[$targetKey]) && !is_array($targetValue)) { - // Neither value is an array, overwrite - $source[$targetKey] = $targetValue; - } elseif (is_array($source[$targetKey]) && is_array($targetValue)) { - // Both are arrays, deep merge them - self::deepMerge($source[$targetKey], $targetValue); - } elseif (is_array($source[$targetKey])) { - // Source is the array, push target value - $source[$targetKey][] = $targetValue; - } else { - // Target is the array, push source value and copy back - $targetValue[] = $source[$targetKey]; - $source[$targetKey] = $targetValue; - } - } else { - // No merge required, just set the value - $source[$targetKey] = $targetValue; - } - } - } - - /** - * @return array - */ - public function getArray() - { - return $this->config; - } -} diff --git a/src/Configuration.php b/src/Configuration.php new file mode 100644 index 000000000..a8712cefb --- /dev/null +++ b/src/Configuration.php @@ -0,0 +1,47 @@ + + */ +class Configuration extends ParameterBag implements ConfigurationInterface +{ + private string $configurationPath; + + public function __construct(string $configurationPath) + { + parent::__construct([]); + + $this->configurationPath = $configurationPath; + + $this->load(); + } + + public function load(): void + { + $parameters = []; + if ($this->configurationPath && \file_exists($this->configurationPath)) { + $parameters = $this->loadYaml($this->configurationPath); + } + + $this->parameters = $parameters; + } + + private function loadYaml(string $configurationPath): array + { + $parser = new YamlParser(); + $yaml = \file_get_contents($configurationPath); + + return (array)$parser->parse($yaml); + } +} diff --git a/src/Console/Application.php b/src/Console/Application.php index 000554f66..d4d6098a8 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -1,5 +1,7 @@ */ class Application extends BaseApplication { - public const LOGO = <<<'LOGO' + private const LOGO = <<<'LOGO' ____ __ ______ ______ / __ \/ / / / __ \ / ____/__ ____ _________ _____ / /_/ / /_/ / /_/ / / / / _ \/ __ \/ ___/ __ \/ ___/ @@ -46,12 +54,16 @@ class Application extends BaseApplication LOGO; + private ConfigurationInterface $configuration; + + private DatabaseManager $databaseManager; + + private StoreRegistry $storeRegistry; + /** - * @return Logger - * * @throws Exception */ - protected function initLogger(Config $applicationConfig) + protected function initLogger(ConfigurationInterface $applicationConfig): Logger { $rotate = (bool)$applicationConfig->get('php-censor.log.rotate', false); $maxFiles = (int)$applicationConfig->get('php-censor.log.max_files', 0); @@ -72,24 +84,26 @@ protected function initLogger(Config $applicationConfig) return $logger; } - /** - * @param string $name - * @param string $version - * - * @throws Exception - */ - public function __construct($name = 'PHP Censor', $version = 'UNKNOWN') - { - $version = (string)\trim(\file_get_contents(ROOT_DIR . 'VERSION.md')); + public function __construct( + ConfigurationInterface $configuration, + DatabaseManager $databaseManager, + StoreRegistry $storeRegistry, + string $name = 'PHP Censor', + string $version = 'UNKNOWN' + ) { + $version = \trim(\file_get_contents(ROOT_DIR . 'VERSION.md')); $version = !empty($version) ? $version : '0.0.0 (UNKNOWN)'; parent::__construct($name, $version); - $applicationConfig = Config::getInstance(); - $oldDatabaseSettings = $applicationConfig->get('b8.database', []); - $databaseSettings = $applicationConfig->get('php-censor.database', []); + $this->configuration = $configuration; + $this->databaseManager = $databaseManager; + $this->storeRegistry = $storeRegistry; + + $oldDatabaseSettings = $this->configuration->get('b8.database', []); + $databaseSettings = $this->configuration->get('php-censor.database', []); if ($oldDatabaseSettings && !$databaseSettings) { - throw new \RuntimeException( + throw new InvalidArgumentException( 'Missing database settings in application config "config.yml" (Section: "php-censor.database")' ); } @@ -127,7 +141,7 @@ public function __construct($name = 'PHP Censor', $version = 'UNKNOWN') if (!empty($databaseSettings['type']) && $databaseSettings['type'] === 'pgsql' ) { - if (!array_key_exists('pgsql-sslmode', $databaseSettings['servers']['write'][0])) { + if (!\array_key_exists('pgsql-sslmode', $databaseSettings['servers']['write'][0])) { $databaseSettings['servers']['write'][0]['pgsql-sslmode'] = 'prefer'; } @@ -159,32 +173,43 @@ public function __construct($name = 'PHP Censor', $version = 'UNKNOWN') ); /** @var UserStore $userStore */ - $userStore = Factory::getStore('User'); + $userStore = $this->storeRegistry->get('User'); /** @var ProjectStore $projectStore */ - $projectStore = Factory::getStore('Project'); + $projectStore = $this->storeRegistry->get('Project'); /** @var BuildStore $buildStore */ - $buildStore = Factory::getStore('Build'); - - $buildService = new BuildService($buildStore, $projectStore); - $logger = $this->initLogger($applicationConfig); - - $this->add(new InstallCommand()); - $this->add(new CreateAdminCommand($userStore)); - $this->add(new CreateBuildCommand($projectStore, $buildService)); - $this->add(new RemoveOldBuildsCommand($projectStore, $buildService)); - $this->add(new WorkerCommand($logger, $buildService)); - $this->add(new RebuildQueueCommand($logger)); - $this->add(new CheckLocalizationCommand()); + $buildStore = $this->storeRegistry->get('Build'); + + /** @var SecretStore $secretStore */ + $secretStore = $this->storeRegistry->get('Secret'); + + $buildFactory = new BuildFactory( + $this->configuration, + $this->storeRegistry + ); + + $buildService = new BuildService( + $this->configuration, + $this->storeRegistry, + $buildFactory, + $buildStore, + $projectStore + ); + + $logger = $this->initLogger($this->configuration); + + $this->add(new InstallCommand($this->configuration, $this->databaseManager, $this->storeRegistry, $logger)); + $this->add(new CreateAdminCommand($this->configuration, $this->databaseManager, $this->storeRegistry, $logger, $userStore)); + $this->add(new CreateBuildCommand($this->configuration, $this->databaseManager, $this->storeRegistry, $logger, $projectStore, $buildService)); + $this->add(new RemoveOldBuildsCommand($this->configuration, $this->databaseManager, $this->storeRegistry, $logger, $projectStore, $buildService)); + $this->add(new WorkerCommand($this->configuration, $this->databaseManager, $this->storeRegistry, $logger, $buildService, $buildFactory)); + $this->add(new RebuildQueueCommand($this->configuration, $this->databaseManager, $this->storeRegistry, $logger)); + $this->add(new CheckLocalizationsCommand($this->configuration, $this->databaseManager, $this->storeRegistry, $logger)); + $this->add(new AddSecretCommand($this->configuration, $this->databaseManager, $this->storeRegistry, $logger, $secretStore)); } - /** - * Returns help. - * - * @return string - */ - public function getHelp() + public function getHelp(): string { return self::LOGO . parent::getHelp(); } @@ -194,8 +219,8 @@ public function getHelp() * * @return string The long application version */ - public function getLongVersion() + public function getLongVersion(): string { - return sprintf('%s v%s', $this->getName(), $this->getVersion()); + return \sprintf('%s v%s', $this->getName(), $this->getVersion()); } } diff --git a/src/Controller.php b/src/Controller.php index 8456fa65c..a3c7f783b 100644 --- a/src/Controller.php +++ b/src/Controller.php @@ -1,45 +1,51 @@ + * @author Dmitry Khomutov + */ abstract class Controller { - /** - * @var Request - */ - protected $request; + protected Request $request; - /** - * @var Config - */ - protected $config; + protected Session $session; - public function __construct(Config $config, Request $request) - { - $this->config = $config; - $this->request = $request; + protected ConfigurationInterface $configuration; + + protected StoreRegistry $storeRegistry; + + public function __construct( + ConfigurationInterface $configuration, + StoreRegistry $storeRegistry, + Request $request, + Session $session + ) { + $this->configuration = $configuration; + $this->storeRegistry = $storeRegistry; + $this->request = $request; + $this->session = $session; } /** * Initialise the controller. */ - abstract public function init(); + abstract public function init(): void; - /** - * @param string $name - * - * @return bool - */ - public function hasAction($name) + public function hasAction(string $name): bool { - if (method_exists($this, $name)) { - return true; - } - - if (method_exists($this, '__call')) { + if (\method_exists($this, $name)) { return true; } @@ -49,57 +55,22 @@ public function hasAction($name) /** * Handles an action on this controller and returns a Response object. * - * @param string $action - * @param array $actionParams - * - * @return Response + * @return Response|string */ - public function handleAction($action, $actionParams) + public function handleAction(string $action, array $actionParams) { - return call_user_func_array([$this, $action], $actionParams); - } - - /** - * Get a hash of incoming request parameters ($_GET, $_POST) - * - * @return array - */ - public function getParams() - { - return $this->request->getParams(); + return \call_user_func_array([$this, $action], $actionParams); } /** * Get a specific incoming request parameter. * - * @param string $key * @param mixed $default Default return value (if key does not exist) * * @return mixed */ - public function getParam($key, $default = null) - { - return $this->request->getParam($key, $default); - } - - /** - * Change the value of an incoming request parameter. - * - * @param string $key - * @param mixed $value - */ - public function setParam($key, $value) - { - $this->request->setParam($key, $value); - } - - /** - * Remove an incoming request parameter. - * - * @param string $key - */ - public function unsetParam($key) + public function getParam(string $key, $default = null) { - $this->request->unsetParam($key); + return $this->request->get($key, $default); } } diff --git a/src/Controller/BuildController.php b/src/Controller/BuildController.php index 9934f7558..ebc0ca394 100644 --- a/src/Controller/BuildController.php +++ b/src/Controller/BuildController.php @@ -1,82 +1,87 @@ + * @author Dmitry Khomutov */ class BuildController extends WebController { - /** - * @var string - */ - public $layoutName = 'layout'; + public string $layoutName = 'layout'; - /** - * @var BuildStore - */ - protected $buildStore; + protected BuildStore $buildStore; - /** - * @var ProjectStore - */ - protected $projectStore; + protected ProjectStore $projectStore; - /** - * @var BuildService - */ - protected $buildService; + protected BuildService $buildService; - public function init() + protected BuildFactory $buildFactory; + + public function init(): void { parent::init(); - $this->buildStore = Factory::getStore('Build'); - $this->projectStore = Factory::getStore('Project'); + $this->buildStore = $this->storeRegistry->get('Build'); + $this->projectStore = $this->storeRegistry->get('Project'); + + $this->buildFactory = new BuildFactory( + $this->configuration, + $this->storeRegistry + ); - $this->buildService = new BuildService($this->buildStore, $this->projectStore); + $this->buildService = new BuildService( + $this->configuration, + $this->storeRegistry, + $this->buildFactory, + $this->buildStore, + $this->projectStore + ); } /** * View a specific build. * - * @param int $buildId - * * @throws NotFoundException + * @throws \PHPCensor\Common\Exception\RuntimeException + * @throws \PHPCensor\Exception\HttpException */ - public function view($buildId) + public function view(int $buildId): void { $page = (int)$this->getParam('page', 1); $plugin = $this->getParam('plugin', ''); $isNew = $this->getParam('is_new', ''); - $severity = $this->getParam('severity', null); + $severity = $this->getParam('severity'); if (null !== $severity && '' !== $severity) { $severity = (int)$severity; } else { $severity = null; } - $build = BuildFactory::getBuildById($buildId); + $build = $this->buildFactory->getBuildById($buildId); if (!$build) { throw new NotFoundException(Lang::get('build_x_not_found', $buildId)); @@ -84,28 +89,29 @@ public function view($buildId) /** @var User $user */ $user = $this->getUser(); - $perPage = $user->getFinalPerPage(); + $perPage = $user->getFinalPerPage($this->configuration); $data = $this->getBuildData($build, $plugin, $severity, $isNew, (($page - 1) * $perPage), $perPage); $pages = ($data['errors'] === 0) ? 1 - : (int)ceil($data['errors'] / $perPage); + : (int)\ceil($data['errors'] / $perPage); if ($page > $pages) { $page = $pages; } /** @var BuildErrorStore $errorStore */ - $errorStore = Factory::getStore('BuildError'); + $errorStore = $this->storeRegistry->get('BuildError'); - $this->view->uiPlugins = $this->getUiPlugins(); - $this->view->build = $build; - $this->view->data = $data; + $this->view->uiPlugins = $this->getUiPlugins(); + $this->view->build = $build; + $this->view->data = $data; + $this->view->environment = $this->storeRegistry->get('Environment')->getById((int)$build->getEnvironmentId()); - $this->view->plugin = urldecode($plugin); + $this->view->plugin = \urldecode($plugin); $this->view->plugins = $errorStore->getKnownPlugins($buildId, $severity, $isNew); - $this->view->severity = urldecode(null !== $severity ? $severity : ''); + $this->view->severity = \urldecode(null !== $severity ? (string)$severity : ''); $this->view->severities = $errorStore->getKnownSeverities($buildId, $plugin, $isNew); - $this->view->isNew = urldecode($isNew); + $this->view->isNew = \urldecode($isNew); $this->view->isNews = ['only_new', 'only_old']; $this->view->page = $page; @@ -126,18 +132,22 @@ public function view($buildId) switch ($build->getStatus()) { case 0: $this->layout->skin = 'blue'; + break; case 1: $this->layout->skin = 'yellow'; + break; case 2: $this->layout->skin = 'green'; + break; case 3: $this->layout->skin = 'red'; + break; } @@ -149,7 +159,7 @@ public function view($buildId) $delete = Lang::get('delete_build'); $deleteLink = APP_URL . 'build/delete/' . $build->getId(); - $project = Factory::getStore('Project')->getByPrimaryKey($build->getProjectId()); + $project = $this->storeRegistry->get('Project')->getById((int)$build->getProjectId()); $actions = ''; if (!$project->getArchived()) { @@ -168,16 +178,15 @@ public function view($buildId) /** * Returns an array of the JS plugins to include. - * @return array */ - protected function getUiPlugins() + protected function getUiPlugins(): array { $rtn = []; $path = PUBLIC_DIR . 'assets/js/build-plugins/'; - $dir = opendir($path); + $dir = \opendir($path); - while ($item = readdir($dir)) { - if (substr($item, 0, 1) == '.' || substr($item, -3) != '.js') { + while ($item = \readdir($dir)) { + if (\substr($item, 0, 1) === '.' || \substr($item, -3) !== '.js') { continue; } @@ -190,36 +199,38 @@ protected function getUiPlugins() /** * Get build data from database and json encode it. * - * @param string $plugin - * @param int $severity - * @param string $isNew - * @param int $start - * @param int $perPage - * - * @return array + * @throws \PHPCensor\Common\Exception\InvalidArgumentException + * @throws \PHPCensor\Common\Exception\RuntimeException + * @throws \PHPCensor\Exception\HttpException */ - protected function getBuildData(Build $build, $plugin, $severity, $isNew, $start = 0, $perPage = 10) - { + protected function getBuildData( + Build $build, + string $plugin, + ?int $severity, + string $isNew, + int $start = 0, + int $perPage = 10 + ): array { $data = []; $data['status'] = (int)$build->getStatus(); - $data['log'] = $this->cleanLog($build->getLog()); + $data['log'] = $this->cleanLog((string)$build->getLog()); - $data['create_date'] = !is_null($build->getCreateDate()) + $data['create_date'] = !\is_null($build->getCreateDate()) ? $build->getCreateDate()->format('Y-m-d H:i:s') : null; - $data['start_date'] = !is_null($build->getStartDate()) + $data['start_date'] = !\is_null($build->getStartDate()) ? $build->getStartDate()->format('Y-m-d H:i:s') : null; - $data['finish_date'] = !is_null($build->getFinishDate()) + $data['finish_date'] = !\is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : null; $data['duration'] = $build->getDuration(); /** @var BuildErrorStore $errorStore */ - $errorStore = Factory::getStore('BuildError'); + $errorStore = $this->storeRegistry->get('BuildError'); $errors = $errorStore->getByBuildId($build->getId(), $perPage, $start, $plugin, $severity, $isNew); $errorView = new View('Build/errors'); @@ -233,19 +244,15 @@ protected function getBuildData(Build $build, $plugin, $severity, $isNew, $start return $data; } - /** - * @param int $buildId - * @param string $plugin - * @param int $severity - * @param string $isNew - * @param int $total - * @param int $perPage - * @param int $page - * - * @return string - */ - protected function getPaginatorHtml($buildId, $plugin, $severity, $isNew, $total, $perPage, $page) - { + protected function getPaginatorHtml( + int $buildId, + string $plugin, + ?int $severity, + string $isNew, + int $total, + int $perPage, + int $page + ): string { $view = new View('pagination'); $urlPattern = APP_URL . 'build/view/' . $buildId; @@ -262,10 +269,10 @@ protected function getPaginatorHtml($buildId, $plugin, $severity, $isNew, $total $params['is_new'] = $isNew; } - $urlPattern = $urlPattern . '?' . str_replace( + $urlPattern = $urlPattern . '?' . \str_replace( '%28%3Anum%29', '(:num)', - http_build_query(array_merge($params, ['page' => '(:num)'])) + \http_build_query(\array_merge($params, ['page' => '(:num)'])) ) . '#errors'; $paginator = new Paginator($total, $perPage, $page, $urlPattern); @@ -277,14 +284,14 @@ protected function getPaginatorHtml($buildId, $plugin, $severity, $isNew, $total /** * Create a build using an existing build as a template: * - * - * @return RedirectResponse * @throws NotFoundException + * @throws \PHPCensor\Common\Exception\RuntimeException + * @throws \PHPCensor\Exception\HttpException */ - public function rebuild($buildId) + public function rebuild(int $buildId): Response { - $copy = BuildFactory::getBuildById($buildId); - $project = Factory::getStore('Project')->getByPrimaryKey($copy->getProjectId()); + $copy = $this->buildFactory->getBuildById($buildId); + $project = $this->storeRegistry->get('Project')->getById((int)$copy->getProjectId()); if (!$copy || $project->getArchived()) { throw new NotFoundException(Lang::get('build_x_not_found', $buildId)); @@ -301,51 +308,47 @@ public function rebuild($buildId) $build = $this->buildService->createDuplicateBuild($copy, Build::SOURCE_MANUAL_REBUILD_WEB); if ($this->buildService->queueError) { - $_SESSION['global_error'] = Lang::get('add_to_queue_failed'); + $this->session->set('global_error', Lang::get('add_to_queue_failed')); } - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL.'build/view/' . $build->getId()); - - return $response; + return new RedirectResponse(APP_URL.'build/view/' . $build->getId()); } /** - * Delete a build. - */ - public function delete($buildId) + * Delete a build. + * + * @throws NotFoundException + * @throws \PHPCensor\Common\Exception\RuntimeException + * @throws \PHPCensor\Exception\HttpException + */ + public function delete(int $buildId): Response { $this->requireAdmin(); - $build = BuildFactory::getBuildById($buildId); - + $build = $this->buildFactory->getBuildById($buildId); if (!$build) { throw new NotFoundException(Lang::get('build_x_not_found', $buildId)); } $this->buildService->deleteBuild($build); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL.'project/view/' . $build->getProjectId()); - - return $response; + return new RedirectResponse(APP_URL.'project/view/' . $build->getProjectId()); } /** - * Parse log for unix colours and replace with HTML. - */ - protected function cleanLog($log) + * Parse log for unix colours and replace with HTML. + */ + protected function cleanLog(string $log): string { - return AnsiConverter::convert($log); + $converter = new AnsiToHtmlConverter(null, false); + + return $converter->convert($log); } /** * Formats a list of builds into rows suitable for the dropdowns in the header bar. - * - * - * @return array */ - protected function formatBuilds($builds) + protected function formatBuilds(array $builds): array { $rtn = ['count' => $builds['count'], 'items' => []]; @@ -357,19 +360,19 @@ protected function formatBuilds($builds) $rtn['items'][$build->getId()]['header_row'] = $header->render(); } - ksort($rtn['items']); + \ksort($rtn['items']); return $rtn; } - public function ajaxData($buildId) + public function ajaxData(int $buildId): Response { $page = (int)$this->getParam('page', 1); $perPage = (int)$this->getParam('per_page', 10); - $plugin = $this->getParam('plugin', null); - $isNew = $this->getParam('is_new', null); + $plugin = $this->getParam('plugin'); + $isNew = $this->getParam('is_new'); - $severity = $this->getParam('severity', null); + $severity = $this->getParam('severity'); if (null !== $severity && '' !== $severity) { $severity = (int)$severity; } else { @@ -377,11 +380,11 @@ public function ajaxData($buildId) } $response = new JsonResponse(); - $build = BuildFactory::getBuildById($buildId); + $build = $this->buildFactory->getBuildById($buildId); if (!$build) { - $response->setResponseCode(404); - $response->setContent([]); + $response->setStatusCode(404); + $response->setData([]); return $response; } @@ -405,38 +408,32 @@ public function ajaxData($buildId) $page ); - $response->setContent($data); + $response->setData($data); return $response; } - public function ajaxMeta($buildId) + public function ajaxMeta(int $buildId): Response { - $build = BuildFactory::getBuildById($buildId); - $key = $this->getParam('key', null); - $numBuilds = $this->getParam('num_builds', 1); - $data = null; + $build = $this->buildFactory->getBuildById($buildId); + $key = $this->getParam('key'); + $numBuilds = (int)$this->getParam('num_builds', 1); + $data = []; if ($key && $build) { $data = $this->buildStore->getMeta($key, $build->getProjectId(), $buildId, $build->getBranch(), $numBuilds); } - $response = new JsonResponse(); - $response->setContent($data); - - return $response; + return new JsonResponse($data); } - public function ajaxQueue() + public function ajaxQueue(): Response { $rtn = [ 'pending' => $this->formatBuilds($this->buildStore->getByStatus(Build::STATUS_PENDING)), 'running' => $this->formatBuilds($this->buildStore->getByStatus(Build::STATUS_RUNNING)), ]; - $response = new JsonResponse(); - $response->setContent($rtn); - - return $response; + return new JsonResponse($rtn); } } diff --git a/src/Controller/BuildStatusController.php b/src/Controller/BuildStatusController.php index fc5c442ac..0ccf5d6cc 100644 --- a/src/Controller/BuildStatusController.php +++ b/src/Controller/BuildStatusController.php @@ -1,53 +1,58 @@ + * @author Dmitry Khomutov */ class BuildStatusController extends WebController { - /** - * @var string - */ - public $layoutName = 'layoutPublic'; + public string $layoutName = 'layoutPublic'; - /** - * @var ProjectStore - */ - protected $projectStore; + protected ProjectStore $projectStore; - /** - * @var BuildStore - */ - protected $buildStore; + protected BuildStore $buildStore; + + protected BuildFactory $buildFactory; + + public function init(): void + { + parent::init(); + + $this->buildStore = $this->storeRegistry->get('Build'); + $this->projectStore = $this->storeRegistry->get('Project'); + + $this->buildFactory = new BuildFactory( + $this->configuration, + $this->storeRegistry + ); + } /** * Returns status of the last build - * - * @param string $branch - * - * @return string */ - protected function getStatus(Project $project, $branch) + protected function getStatus(Project $project, string $branch): string { $status = 'passing'; try { @@ -59,7 +64,7 @@ protected function getStatus(Project $project, $branch) if (isset($build) && $build instanceof Build && $build->getStatus() !== Build::STATUS_SUCCESS) { $status = 'failed'; } - } catch (Exception $e) { + } catch (\Throwable $e) { $status = 'error'; } @@ -69,15 +74,12 @@ protected function getStatus(Project $project, $branch) /** * Returns coverage of the last build * - * @param string $branch - * @param string $type - * * @return string */ - protected function getPhpunitCoverage(Project $project, $branch, $type = 'lines') + protected function getPhpunitCoverage(Project $project, string $branch, string $type = 'lines') { $coverage = 0; - if (!in_array($type, ['classes', 'methods', 'lines'], true)) { + if (!\in_array($type, ['classes', 'methods', 'lines'], true)) { $type = 'lines'; } @@ -99,36 +101,27 @@ protected function getPhpunitCoverage(Project $project, $branch, $type = 'lines' $coverage = $coverageMeta[0]['meta_value'][$type]; } } - } catch (Exception $e) { + } catch (\Throwable $e) { } return $coverage; } - /** - * - * @return Response - */ - protected function renderXml(SimpleXMLElement $xml = null) + protected function renderXml(?SimpleXMLElement $xml = null): Response { $response = new Response(); - $response->setHeader('Content-Type', 'text/xml'); + $response->headers->set('Content-Type', 'text/xml'); $response->setContent($xml->asXML()); return $response; } /** - * @param int $projectId - * @param string $branch - * * @throws HttpException * @throws InvalidArgumentException - * - * @return array */ - protected function getLatestBuilds($projectId, $branch) + protected function getLatestBuilds(int $projectId, string $branch): array { $criteria = [ 'project_id' => $projectId, @@ -139,28 +132,20 @@ protected function getLatestBuilds($projectId, $branch) $builds = $this->buildStore->getWhere($criteria, 10, 0, $order); foreach ($builds['items'] as &$build) { - $build = BuildFactory::getBuild($build); + $build = $this->buildFactory->getBuild($build); } return $builds['items']; } - public function init() - { - parent::init(); - - $this->buildStore = Factory::getStore('Build'); - $this->projectStore = Factory::getStore('Project'); - } - /** * Returns the appropriate build PHPUnit coverage image in SVG format for a given project. * - * - * @return Response + * @throws HttpException */ - public function phpunitCoverageImage($projectId) + public function phpunitCoverageImage(int $projectId): Response { + /** @var Project $project */ $project = $this->projectStore->getById($projectId); // plastic|flat|flat-squared|social @@ -177,7 +162,7 @@ public function phpunitCoverageImage($projectId) ]; $coverage = $this->getPhpunitCoverage($project, $branch, $type); - $imageUrl = sprintf( + $imageUrl = \sprintf( 'http://img.shields.io/badge/%s-%s-%s.svg?style=%s', $label, $coverage. '%25', @@ -192,17 +177,17 @@ public function phpunitCoverageImage($projectId) } $cacheDir = RUNTIME_DIR . 'status_cache/'; - $cacheFile = $cacheDir . md5($imageUrl) . '.svg'; - if (!is_file($cacheFile)) { - $image = file_get_contents($imageUrl); - file_put_contents($cacheFile, $image); + $cacheFile = $cacheDir . \md5($imageUrl) . '.svg'; + if (!\is_file($cacheFile)) { + $image = \file_get_contents($imageUrl); + \file_put_contents($cacheFile, $image); } - $image = file_get_contents($cacheFile); + $image = \file_get_contents($cacheFile); $response = new Response(); - $response->setHeader('Content-Type', 'image/svg+xml'); + $response->headers->set('Content-Type', 'image/svg+xml'); $response->setContent($image); return $response; @@ -211,10 +196,9 @@ public function phpunitCoverageImage($projectId) /** * Returns the appropriate build status image in SVG format for a given project. * - * - * @return Response + * @throws HttpException */ - public function image($projectId) + public function image(int $projectId): Response { $project = $this->projectStore->getById($projectId); @@ -232,15 +216,12 @@ public function image($projectId) $status = $this->getStatus($project, $branch); - if (is_null($status)) { - $response = new RedirectResponse(); - $response->setHeader('Location', '/'); - - return $response; + if (\is_null($status)) { + return new RedirectResponse('/'); } - $color = ($status == 'passing') ? 'green' : 'red'; - $imageUrl = sprintf( + $color = ($status === 'passing') ? 'green' : 'red'; + $imageUrl = \sprintf( 'http://img.shields.io/badge/%s-%s-%s.svg?style=%s', $label, $status, @@ -255,17 +236,17 @@ public function image($projectId) } $cacheDir = RUNTIME_DIR . 'status_cache/'; - $cacheFile = $cacheDir . md5($imageUrl) . '.svg'; - if (!is_file($cacheFile)) { - $image = file_get_contents($imageUrl); - file_put_contents($cacheFile, $image); + $cacheFile = $cacheDir . \md5($imageUrl) . '.svg'; + if (!\is_file($cacheFile)) { + $image = \file_get_contents($imageUrl); + \file_put_contents($cacheFile, $image); } - $image = file_get_contents($cacheFile); + $image = \file_get_contents($cacheFile); $response = new Response(); - $response->setHeader('Content-Type', 'image/svg+xml'); + $response->headers->set('Content-Type', 'image/svg+xml'); $response->setContent($image); return $response; @@ -274,15 +255,12 @@ public function image($projectId) /** * View the public status page of a given project, if enabled. * - * @param int $projectId - * - * @return string - * * @throws HttpException * @throws InvalidArgumentException * @throws NotFoundException + * @throws \PHPCensor\Common\Exception\RuntimeException */ - public function view($projectId) + public function view(int $projectId): string { $project = $this->projectStore->getById($projectId); $branch = $this->getParam('branch', $project->getDefaultBranch()); @@ -293,12 +271,13 @@ public function view($projectId) $builds = $this->getLatestBuilds($projectId, $branch); - if (count($builds)) { + if (\count($builds)) { $this->view->latest = $builds[0]; } - $this->view->builds = $builds; - $this->view->project = $project; + $this->view->builds = $builds; + $this->view->project = $project; + $this->view->environmentStore = $this->storeRegistry->get('Environment'); return $this->view->render(); } @@ -306,12 +285,9 @@ public function view($projectId) /** * Displays projects information in ccmenu format * - * - * @return Response - * * @throws Exception */ - public function ccxml($projectId) + public function ccxml(int $projectId): Response { /* @var Project $project */ $project = $this->projectStore->getById($projectId); @@ -337,7 +313,7 @@ public function ccxml($projectId) } } } - } catch (Exception $e) { + } catch (\Throwable $e) { $xml = new SimpleXMLElement(''); } diff --git a/src/Controller/GroupController.php b/src/Controller/GroupController.php index 0efbebeb1..ffa5ea5d6 100644 --- a/src/Controller/GroupController.php +++ b/src/Controller/GroupController.php @@ -1,45 +1,44 @@ + * @author Dmitry Khomutov */ class GroupController extends WebController { - /** - * @var string - */ - public $layoutName = 'layout'; + public string $layoutName = 'layout'; - /** - * @var ProjectGroupStore - */ - protected $groupStore; + protected ProjectGroupStore $groupStore; - public function init() + public function init(): void { parent::init(); - $this->groupStore = Factory::getStore('ProjectGroup'); + $this->groupStore = $this->storeRegistry->get('ProjectGroup'); } /** * List project groups. */ - public function index() + public function index(): void { $this->requireAdmin(); @@ -51,37 +50,40 @@ public function index() 'title' => $group->getTitle(), 'id' => $group->getId(), ]; - $projectsActive = Factory::getStore('Project')->getByGroupId($group->getId(), false); - $projectsArchived = Factory::getStore('Project')->getByGroupId($group->getId(), true); + $projectsActive = $this->storeRegistry->get('Project')->getByGroupId($group->getId(), false); + $projectsArchived = $this->storeRegistry->get('Project')->getByGroupId($group->getId(), true); - $thisGroup['projects'] = array_merge($projectsActive['items'], $projectsArchived['items']); + $thisGroup['projects'] = \array_merge($projectsActive['items'], $projectsArchived['items']); $groups[] = $thisGroup; } $this->layout->title = Lang::get('group_projects'); $this->view->groups = $groups; + $this->view->user = $this->getUser(); } /** * Add or edit a project group. * - * @param null $groupId + * @return Response * - * @return RedirectResponse + * @throws \PHPCensor\Common\Exception\InvalidArgumentException + * @throws \PHPCensor\Common\Exception\RuntimeException + * @throws \PHPCensor\Exception\HttpException */ - public function edit($groupId = null) + public function edit(?int $groupId = null) { $this->requireAdmin(); - if (!is_null($groupId)) { + if (!\is_null($groupId)) { $group = $this->groupStore->getById($groupId); } else { - $group = new ProjectGroup(); + $group = new ProjectGroup($this->storeRegistry); } - if ($this->request->getMethod() == 'POST') { + if ($this->request->getMethod() === 'POST') { $group->setTitle($this->getParam('title')); - if (is_null($groupId)) { + if (\is_null($groupId)) { /** @var User $user */ $user = $this->getUser(); @@ -91,8 +93,7 @@ public function edit($groupId = null) $this->groupStore->save($group); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL.'group'); + $response = new RedirectResponse(APP_URL . 'group'); return $response; } @@ -100,9 +101,9 @@ public function edit($groupId = null) $form = new Form(); $form->setMethod('POST'); - $form->setAction(APP_URL . 'group/edit' . (!is_null($groupId) ? '/' . $groupId : '')); + $form->setAction(APP_URL . 'group/edit' . (!\is_null($groupId) ? '/' . $groupId : '')); - $form->addField(new Form\Element\Csrf('group_form')); + $form->addField(new Csrf($this->session, 'group_form')); $title = new Form\Element\Text('title'); $title->setContainerClass('form-group'); @@ -122,17 +123,18 @@ public function edit($groupId = null) /** * Delete a project group. - * @return RedirectResponse + * + * @throws \PHPCensor\Common\Exception\Exception + * @throws \PHPCensor\Common\Exception\InvalidArgumentException + * @throws \PHPCensor\Exception\HttpException */ - public function delete($groupId) + public function delete(int $groupId): Response { $this->requireAdmin(); $group = $this->groupStore->getById($groupId); $this->groupStore->delete($group); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL.'group'); - return $response; + return new RedirectResponse(APP_URL . 'group'); } } diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index f58b26452..ad91f7cb0 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -1,25 +1,26 @@ */ class HomeController extends WebController { - /** - * @var string - */ - public $layoutName = 'layout'; + public string $layoutName = 'layout'; /** * Display dashboard: */ - public function index() + public function index(): string { $this->layout->title = Lang::get('dashboard'); @@ -28,7 +29,7 @@ public function index() 'right' => [], ]; - $widgetsConfig = Config::getInstance()->get('php-censor.dashboard_widgets', [ + $widgetsConfig = $this->configuration->get('php-censor.dashboard_widgets', [ 'all_projects' => [ 'side' => 'left', ], diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 1f154b19b..ff2342e3e 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -1,8 +1,9 @@ + * @author Dmitry Khomutov */ class ProjectController extends WebController { - /** - * @var string - */ - public $layoutName = 'layout'; + public string $layoutName = 'layout'; - /** - * @var ProjectStore - */ - protected $projectStore; + protected ProjectStore $projectStore; - /** - * @var ProjectService - */ - protected $projectService; + protected ProjectService $projectService; - /** - * @var BuildStore - */ - protected $buildStore; + protected BuildStore $buildStore; - /** - * @var BuildService - */ - protected $buildService; + protected BuildService $buildService; + + protected BuildFactory $buildFactory; /** * Initialise the controller, set up stores and services. */ - public function init() + public function init(): void { parent::init(); - $this->buildStore = Factory::getStore('Build'); - $this->projectStore = Factory::getStore('Project'); - $this->projectService = new ProjectService($this->projectStore); - $this->buildService = new BuildService($this->buildStore, $this->projectStore); + $this->buildStore = $this->storeRegistry->get('Build'); + $this->projectStore = $this->storeRegistry->get('Project'); + + $this->projectService = new ProjectService($this->storeRegistry, $this->projectStore); + + $this->buildFactory = new BuildFactory( + $this->configuration, + $this->storeRegistry + ); + + $this->buildService = new BuildService( + $this->configuration, + $this->storeRegistry, + $this->buildFactory, + $this->buildStore, + $this->projectStore + ); } - /** - * @param int $projectId - * - * @return PHPCensor\Http\Response - */ - public function ajaxBuilds($projectId) + public function ajaxBuilds(int $projectId): Response { $branch = $this->getParam('branch', ''); $environment = $this->getParam('environment', ''); @@ -87,7 +87,7 @@ public function ajaxBuilds($projectId) $perPage ); - $response = new PHPCensor\Http\Response(); + $response = new Response(); $response->setContent($builds[0]); return $response; @@ -96,13 +96,11 @@ public function ajaxBuilds($projectId) /** * View a specific project. * - * @param int $projectId - * * @throws NotFoundException - * - * @return string + * @throws PHPCensor\Exception\HttpException + * @throws RuntimeException */ - public function view($projectId) + public function view(int $projectId): string { $branch = $this->getParam('branch', ''); $environment = $this->getParam('environment', ''); @@ -115,11 +113,11 @@ public function view($projectId) /** @var PHPCensor\Model\User $user */ $user = $this->getUser(); - $perPage = $user->getFinalPerPage(); + $perPage = $user->getFinalPerPage($this->configuration); $builds = $this->getLatestBuildsHtml($projectId, $branch, $environment, (($page - 1) * $perPage), $perPage); $pages = ($builds[1] === 0) ? 1 - : (int)ceil($builds[1] / $perPage); + : (int)\ceil($builds[1] / $perPage); if ($page > $pages) { $page = $pages; @@ -128,9 +126,9 @@ public function view($projectId) $this->view->builds = $builds[0]; $this->view->total = $builds[1]; $this->view->project = $project; - $this->view->branch = urldecode($branch); + $this->view->branch = \urldecode($branch); $this->view->branches = $this->projectStore->getKnownBranches($projectId); - $this->view->environment = urldecode($environment); + $this->view->environment = \urldecode($environment); $this->view->environments = $project->getEnvironmentsNames(); $this->view->page = $page; $this->view->perPage = $perPage; @@ -142,6 +140,7 @@ public function view($projectId) $perPage, $page ); + $this->view->user = $this->getUser(); $this->layout->title = $project->getTitle(); $this->layout->subtitle = ''; @@ -155,18 +154,14 @@ public function view($projectId) return $this->view->render(); } - /** - * @param int $projectId - * @param string $branch - * @param string $environment - * @param int $total - * @param int $perPage - * @param int $page - * - * @return string - */ - protected function getPaginatorHtml($projectId, $branch, $environment, $total, $perPage, $page) - { + protected function getPaginatorHtml( + int $projectId, + string $branch, + string $environment, + int $total, + int $perPage, + int $page + ): string { $view = new View('pagination'); $urlPattern = APP_URL . 'project/view/' . $projectId; @@ -179,10 +174,10 @@ protected function getPaginatorHtml($projectId, $branch, $environment, $total, $ $params['environment'] = $environment; } - $urlPattern = $urlPattern . '?' . str_replace( + $urlPattern = $urlPattern . '?' . \str_replace( '%28%3Anum%29', '(:num)', - http_build_query(array_merge($params, ['page' => '(:num)'])) + \http_build_query(\array_merge($params, ['page' => '(:num)'])) ); $paginator = new Paginator($total, $perPage, $page, $urlPattern); @@ -197,11 +192,8 @@ protected function getPaginatorHtml($projectId, $branch, $environment, $total, $ * @param int $projectId * * @throws NotFoundException - * - * @return RedirectResponse - * */ - public function build($projectId) + public function build($projectId): RedirectResponse { /* @var Project $project */ $project = $this->projectStore->getById($projectId); @@ -219,9 +211,11 @@ public function build($projectId) switch ($type) { case 'environment': $environment = $id; + break; case 'branch': $branch = $id; + break; } @@ -239,7 +233,7 @@ public function build($projectId) $environmentId = null; if ($environment) { /** @var EnvironmentStore $environmentStore */ - $environmentStore = Factory::getStore('Environment'); + $environmentStore = $this->storeRegistry->get('Environment'); $environmentObject = $environmentStore->getByNameAndProjectId($environment, $project->getId()); if ($environmentObject) { $environmentId = $environmentObject->getId(); @@ -262,13 +256,10 @@ public function build($projectId) ); if ($this->buildService->queueError) { - $_SESSION['global_error'] = Lang::get('add_to_queue_failed'); + $this->session->set('global_error', Lang::get('add_to_queue_failed')); } - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL.'build/view/' . $build->getId()); - - return $response; + return new RedirectResponse(APP_URL.'build/view/' . $build->getId()); } /** @@ -282,13 +273,36 @@ public function delete($projectId) { $this->requireAdmin(); + /** @var Project $project */ $project = $this->projectStore->getById($projectId); $this->projectService->deleteProject($project); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL); + return new RedirectResponse(APP_URL); + } - return $response; + /** + * @return RedirectResponse + * + * @throws PHPCensor\Exception\HttpException + * @throws PHPCensor\Exception\HttpException\ForbiddenException + */ + public function clone(int $projectId) + { + $this->requireAdmin(); + + /** @var PHPCensor\Model\User $user */ + $user = $this->getUser(); + + /** @var Project $project */ + $project = $this->projectStore->getById($projectId); + $project->setId(null); + $project->setTitle('CLONE OF: ' . $project->getTitle()); + $project->setCreateDate(new \DateTime()); + $project->setUserId($user->getId()); + + $project = $this->projectStore->save($project); + + return new RedirectResponse(APP_URL.'project/view/' . $project->getId()); } /** @@ -304,10 +318,7 @@ public function deleteAllBuilds($projectId) $this->buildService->deleteAllByProject($projectId); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL . 'project/view/' . $projectId); - - return $response; + return new RedirectResponse(APP_URL . 'project/view/' . $projectId); } /** @@ -323,10 +334,7 @@ public function deleteOldBuilds($projectId) $this->buildService->deleteOldByProject($projectId); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL . 'project/view/' . $projectId); - - return $response; + return new RedirectResponse(APP_URL . 'project/view/' . $projectId); } /** @@ -346,7 +354,7 @@ protected function getLatestBuildsHtml($projectId, $branch = '', $environment = if (!empty($environment)) { /** @var EnvironmentStore $environmentStore */ - $environmentStore = Factory::getStore('Environment'); + $environmentStore = $this->storeRegistry->get('Environment'); $environmentObject = $environmentStore->getByNameAndProjectId($environment, $projectId); if ($environmentObject) { $criteria['environment_id'] = $environmentObject->getId(); @@ -362,10 +370,12 @@ protected function getLatestBuildsHtml($projectId, $branch = '', $environment = $view = new View('Project/ajax-builds'); foreach ($builds['items'] as &$build) { - $build = BuildFactory::getBuild($build); + $build = $this->buildFactory->getBuild($build); } - $view->builds = $builds['items']; + $view->builds = $builds['items']; + $view->environmentStore = $this->storeRegistry->get('Environment'); + $view->user = $this->getUser(); return [ $view->render(), @@ -381,12 +391,13 @@ public function add() $this->layout->title = Lang::get('add_project'); $this->requireAdmin(); - $method = $this->request->getMethod(); - $values = $this->getParams(); + $method = $this->request->getMethod(); + $values = $this->request->request->all(); + $values['default_branch'] = null; if ($method !== 'POST') { - $sshKey = new SshKey(); + $sshKey = new SshKey($this->configuration); $key = $sshKey->generate(); $values['ssh_private_key'] = $key['ssh_private_key']; @@ -425,10 +436,7 @@ public function add() $user = $this->getUser(); $project = $this->projectService->createProject($title, $type, $reference, $user->getId(), $options); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL.'project/view/' . $project->getId()); - - return $response; + return new RedirectResponse(APP_URL.'project/view/' . $project->getId()); } } @@ -452,7 +460,7 @@ public function edit($projectId) $values = $project->getDataArray(); $values['environments'] = $project->getEnvironments(); - if (in_array($values['type'], [ + if (\in_array($values['type'], [ Project::TYPE_GITHUB, Project::TYPE_GITLAB ], true)) { @@ -461,10 +469,10 @@ public function edit($projectId) $values['reference'] = $accessInfo['origin']; } elseif (isset($accessInfo['domain']) && $accessInfo['domain']) { $reference = $accessInfo['user'] . - '@' . $accessInfo['domain'] . ':' . ltrim($project->getReference(), '/') . '.git'; + '@' . $accessInfo['domain'] . ':' . \ltrim($project->getReference(), '/') . '.git'; if (isset($accessInfo['port']) && $accessInfo['port']) { $reference = $accessInfo['user'] . '@' . $accessInfo['domain'] . ':' . $accessInfo['port'] . '/' . - ltrim($project->getReference(), '/') . '.git'; + \ltrim($project->getReference(), '/') . '.git'; } $values['reference'] = $reference; @@ -472,7 +480,7 @@ public function edit($projectId) } if ($method === 'POST') { - $values = $this->getParams(); + $values = $this->request->request->all(); } $form = $this->projectForm($values, 'edit/' . $projectId); @@ -511,10 +519,7 @@ public function edit($projectId) $project = $this->projectService->updateProject($project, $title, $type, $reference, $options); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL.'project/view/' . $project->getId()); - - return $response; + return new RedirectResponse(APP_URL.'project/view/' . $project->getId()); } /** @@ -527,7 +532,7 @@ protected function projectForm($values, $type = 'add') $form->setMethod('POST'); $form->setAction(APP_URL . 'project/' . $type); - $form->addField(new Form\Element\Csrf('project_form')); + $form->addField(new Csrf($this->session, 'project_form')); $form->addField(new Form\Element\Hidden('ssh_public_key')); $options = [ @@ -544,7 +549,7 @@ protected function projectForm($values, $type = 'add') Project::TYPE_SVN => 'Svn (Subversion)', ]; - $sourcesPattern = sprintf('^(%s)', implode('|', Project::$allowedTypes)); + $sourcesPattern = \sprintf('^(%s)', \implode('|', Project::$allowedTypes)); $field = Form\Element\Select::create('type', Lang::get('where_hosted'), true); $field->setPattern($sourcesPattern); @@ -598,22 +603,22 @@ protected function projectForm($values, $type = 'add') $field->setClass('form-control')->setContainerClass('form-group'); $field->setRows(6); $field->setValidator(new Form\Validator\Yaml()); - $field->setDataTransformator(new Form\DataTransformer\Yaml()); + $field->setDataTransformer(new Form\DataTransformer\Yaml()); $form->addField($field); $field = Form\Element\TextArea::create('environments', Lang::get('environments_label'), false); $field->setClass('form-control')->setContainerClass('form-group'); $field->setRows(6); $field->setValidator(new Form\Validator\Yaml()); - $field->setDataTransformator(new Form\DataTransformer\Yaml()); + $field->setDataTransformer(new Form\DataTransformer\Yaml()); $form->addField($field); $field = Form\Element\Select::create('group_id', Lang::get('project_group'), true); $field->setClass('form-control')->setContainerClass('form-group')->setValue(null); - $groups = []; - $groupStore = Factory::getStore('ProjectGroup'); - $groupList = $groupStore->getWhere([], 100, 0, ['title' => 'ASC']); + $groups = []; + $groupStore = $this->storeRegistry->get('ProjectGroup'); + $groupList = $groupStore->getWhere([], 100, 0, ['title' => 'ASC']); foreach ($groupList['items'] as $group) { $groups[$group->getId()] = $group->getTitle(); @@ -690,10 +695,10 @@ protected function getReferenceValidator($values) ], ]; - if (in_array($type, $validators, true) && !preg_match($validators[$type]['regex'], $val)) { - throw new Exception($validators[$type]['message']); - } elseif (Project::TYPE_LOCAL === $type && !is_dir($val)) { - throw new Exception(Lang::get('error_path')); + if (\in_array($type, $validators, true) && !\preg_match($validators[$type]['regex'], $val)) { + throw new RuntimeException($validators[$type]['message']); + } elseif (Project::TYPE_LOCAL === $type && !\is_dir($val)) { + throw new RuntimeException(Lang::get('error_path')); } return true; diff --git a/src/Controller/SecretController.php b/src/Controller/SecretController.php new file mode 100644 index 000000000..3ed942c6d --- /dev/null +++ b/src/Controller/SecretController.php @@ -0,0 +1,138 @@ + + */ +class SecretController extends WebController +{ + public string $layoutName = 'layout'; + + protected SecretStore $secretStore; + + public function init(): void + { + parent::init(); + + $this->secretStore = $this->storeRegistry->get('Secret'); + } + + public function index(): void + { + $this->requireAdmin(); + + $secrets = []; + $secretList = $this->secretStore->getWhere([], 100, 0, ['name' => 'ASC']); + + foreach ($secretList['items'] as $secret) { + $thisSecret = [ + 'name' => $secret->getName(), + 'id' => $secret->getId(), + ]; + $secrets[] = $thisSecret; + } + + $this->layout->title = Lang::get('secrets'); + $this->view->secrets = $secrets; + $this->view->user = $this->getUser(); + } + + /** + * @return Response + * + * @throws \PHPCensor\Common\Exception\InvalidArgumentException + * @throws \PHPCensor\Common\Exception\RuntimeException + * @throws \PHPCensor\Exception\HttpException + */ + public function edit(?int $secretId = null) + { + $this->requireAdmin(); + + if (!\is_null($secretId)) { + $secret = $this->secretStore->getById($secretId); + } else { + $secret = new Secret($this->storeRegistry); + } + + if ($this->request->getMethod() === 'POST') { + $secret->setName($this->getParam('name')); + $secret->setValue($this->getParam('value')); + if (\is_null($secretId)) { + /** @var User $user */ + $user = $this->getUser(); + + $secret->setCreateDate(new DateTime()); + $secret->setUserId($user->getId()); + } + + $this->secretStore->save($secret); + + $response = new RedirectResponse(APP_URL . 'secret'); + + return $response; + } + + $form = new Form(); + + $form->setMethod('POST'); + $form->setAction(APP_URL . 'secret/edit' . (!\is_null($secretId) ? '/' . $secretId : '')); + + $form->addField(new Csrf($this->session, 'secret_form')); + + $field = Form\Element\Text::create('name', Lang::get('secret_name'), true); + $field + ->setClass('form-control') + ->setContainerClass('form-group') + ->setPattern(Secret::SECRET_NAME_PATTERN) + ->setValue($secret->getName()); + $form->addField($field); + + $field = Form\Element\TextArea::create('value', Lang::get('secret_value'), true); + $field + ->setClass('form-control') + ->setContainerClass('form-group') + ->setRows(8) + ->setValue($secret->getValue()); + $form->addField($field); + + $submit = new Form\Element\Submit(); + $submit->setClass('btn btn-success'); + $submit->setValue(Lang::get('secret_save')); + $form->addField($submit); + + $this->view->form = $form; + } + + /** + * @throws \PHPCensor\Common\Exception\Exception + * @throws \PHPCensor\Common\Exception\InvalidArgumentException + * @throws \PHPCensor\Exception\HttpException + */ + public function delete(int $secretId): Response + { + $this->requireAdmin(); + + $group = $this->secretStore->getById($secretId); + + $this->secretStore->delete($group); + + return new RedirectResponse(APP_URL . 'secret'); + } +} diff --git a/src/Controller/SessionController.php b/src/Controller/SessionController.php index 06690ed04..29035b76e 100644 --- a/src/Controller/SessionController.php +++ b/src/Controller/SessionController.php @@ -1,8 +1,9 @@ + * @author Dmitry Khomutov */ class SessionController extends WebController { - /** - * @var string - */ - public $layoutName = 'layoutSession'; + public string $layoutName = 'layoutSession'; - /** - * @var UserStore - */ - protected $userStore; + protected UserStore $userStore; - /** - * @var Service - */ - protected $authentication; + protected Service $authentication; /** * Initialise the controller, set up stores and services. */ - public function init() + public function init(): void { parent::init(); - $this->userStore = Factory::getStore('User'); - $this->authentication = Service::getInstance(); + $this->userStore = $this->storeRegistry->get('User'); + $this->authentication = new Service($this->configuration, $this->storeRegistry); } - protected function loginForm($values) + protected function loginForm(array $values): Form { $form = new Form(); $form->setMethod('POST'); $form->setAction(APP_URL . 'session/login'); - $form->addField(new Csrf('login_form')); + $form->addField(new Csrf($this->session, 'login_form')); $email = new Text('email'); $email->setLabel(Lang::get('login')); @@ -91,25 +86,28 @@ protected function loginForm($values) /** * Handles user login (form and processing) + * + * @return Response|string + * + * @throws HttpException + * @throws \PHPCensor\Common\Exception\InvalidArgumentException */ public function login() { - if (!empty($_COOKIE['remember_key'])) { - $user = $this->userStore->getByRememberKey($_COOKIE['remember_key']); + $rememberKey = $this->request->cookies->get('remember_key'); + if (!empty($rememberKey)) { + $user = $this->userStore->getByRememberKey($rememberKey); if ($user) { - $_SESSION['php-censor-user-id'] = $user->getId(); - - $response = new RedirectResponse(); - $response->setHeader('Location', $this->getLoginRedirect()); + $this->session->set('php-censor-user-id', $user->getId()); - return $response; + return new RedirectResponse($this->getLoginRedirect()); } } $method = $this->request->getMethod(); if ($method === 'POST') { - $values = $this->getParams(); + $values = $this->request->request->all(); } else { $values = []; } @@ -141,34 +139,33 @@ public function login() if ($user && $provider->verifyPassword($user, $password)) { $this->userStore->save($user); $isLoginFailure = false; + break; } } } if (!$isLoginFailure) { - $_SESSION['php-censor-user-id'] = $user->getId(); + $this->session->set('php-censor-user-id', $user->getId()); + $response = new RedirectResponse($this->getLoginRedirect()); if ($rememberMe) { - $rememberKey = md5(random_bytes(64)); + $rememberKey = \md5(\random_bytes(64)); $user->setRememberKey($rememberKey); $this->userStore->save($user); - setcookie( + $response->headers->setCookie(new Cookie( 'remember_key', $rememberKey, - (time() + 60 * 60 * 24 * 30), - null, - null, + (\time() + 60 * 60 * 24 * 30), + '/', null, + false, true - ); + )); } - $response = new RedirectResponse(); - $response->setHeader('Location', $this->getLoginRedirect()); - return $response; } } @@ -181,26 +178,23 @@ public function login() } /** - * Handles user logout. - */ - public function logout() + * Handles user logout. + */ + public function logout(): Response { - unset($_SESSION['php-censor-user-id']); + $this->session->remove('php-censor-user-id'); + $this->session->clear(); - session_destroy(); - - setcookie( + $response = new RedirectResponse(APP_URL); + $response->headers->setCookie(new Cookie( 'remember_key', - null, - (time() - 1), - null, + '', + (\time() - 1), null, null, + false, true - ); - - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL); + )); return $response; } @@ -208,13 +202,11 @@ public function logout() /** * Allows the user to request a password reset email. * - * @return string - * * @throws HttpException */ - public function forgotPassword() + public function forgotPassword(): string { - if ($this->request->getMethod() == 'POST') { + if ($this->request->getMethod() === 'POST') { $email = $this->getParam('email', null); $user = $this->userStore->getByEmail($email); @@ -224,10 +216,10 @@ public function forgotPassword() return $this->view->render(); } - $key = md5(date('Y-m-d') . $user->getHash()); + $key = \md5(\date('Y-m-d') . $user->getHash()); $message = Lang::get('reset_email_body', $user->getName(), APP_URL, $user->getId(), $key); - $email = new Email(Config::getInstance()); + $email = new Email($this->configuration); $email->setEmailTo($user->getEmail(), $user->getName()); $email->setSubject(Lang::get('reset_email_title', $user->getName())); $email->setBody($message); @@ -241,31 +233,32 @@ public function forgotPassword() /** * Allows the user to change their password after a password reset email. - * @return string + * + * @return Response|string + * + * @throws HttpException + * @throws \PHPCensor\Common\Exception\InvalidArgumentException */ - public function resetPassword($userId, $key) + public function resetPassword(int $userId, string $key) { $user = $this->userStore->getById($userId); - $userKey = md5(date('Y-m-d') . $user->getHash()); + $userKey = \md5(\date('Y-m-d') . $user->getHash()); - if (empty($user) || $key != $userKey) { + if (empty($user) || $key !== $userKey) { $this->view->error = Lang::get('reset_invalid'); return $this->view->render(); } - if ($this->request->getMethod() == 'POST') { - $hash = password_hash($this->getParam('password'), PASSWORD_DEFAULT); + if ($this->request->getMethod() === 'POST') { + $hash = \password_hash($this->getParam('password'), PASSWORD_DEFAULT); $user->setHash($hash); $this->userStore->save($user); - $_SESSION['php-censor-user-id'] = $user->getId(); - - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL); + $this->session->set('php-censor-user-id', $user->getId()); - return $response; + return new RedirectResponse(APP_URL); } $this->view->id = $userId; @@ -276,15 +269,16 @@ public function resetPassword($userId, $key) /** * Get the URL the user was trying to go to prior to being asked to log in. - * @return string */ - protected function getLoginRedirect() + protected function getLoginRedirect(): string { $rtn = APP_URL; - if (!empty($_SESSION['php-censor-login-redirect'])) { - $rtn .= $_SESSION['php-censor-login-redirect']; - $_SESSION['php-censor-login-redirect'] = null; + $sessionLoginRedirect = $this->session->get('php-censor-login-redirect'); + if (!empty($sessionLoginRedirect)) { + $rtn .= $sessionLoginRedirect; + + $this->session->remove('php-censor-login-redirect'); } return $rtn; diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 0ba52b84d..a58279489 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -1,75 +1,74 @@ + * @author Dmitry Khomutov */ class UserController extends WebController { - /** - * @var string - */ - public $layoutName = 'layout'; + public string $layoutName = 'layout'; - /** - * @var UserStore - */ - protected $userStore; + protected UserStore $userStore; - /** - * @var UserService - */ - protected $userService; + protected UserService $userService; /** * Initialise the controller, set up stores and services. */ - public function init() + public function init(): void { parent::init(); - $this->userStore = Factory::getStore('User'); - $this->userService = new UserService($this->userStore); + $this->userStore = $this->storeRegistry->get('User'); + $this->userService = new UserService($this->storeRegistry, $this->userStore); } /** * View user list. */ - public function index() + public function index(): string { - $users = $this->userStore->getWhere([], 1000, 0, ['email' => 'ASC']); - $this->view->users = $users; - $this->layout->title = Lang::get('manage_users'); + $users = $this->userStore->getWhere([], 1000, 0, ['email' => 'ASC']); + $this->view->currentUser = $this->getUser(); + $this->view->users = $users; + + $this->layout->title = Lang::get('manage_users'); return $this->view->render(); } /** * Allows the user to edit their profile. - * @return string + * + * @throws \PHPCensor\Common\Exception\RuntimeException + * @throws \PHPCensor\Exception\HttpException */ - public function profile() + public function profile(): string { /** @var User $user */ $user = $this->getUser(); - if ($this->request->getMethod() == 'POST') { + if ($this->request->getMethod() === 'POST') { $name = $this->getParam('name', null); $email = $this->getParam('email', null); $password = $this->getParam('password', null); @@ -97,7 +96,7 @@ public function profile() $form->setAction(APP_URL . 'user/profile'); $form->setMethod('POST'); - $form->addField(new Form\Element\Csrf('profile_form')); + $form->addField(new Csrf($this->session, 'profile_form')); $name = new Form\Element\Text('name'); $name->setClass('form-control'); @@ -129,8 +128,8 @@ public function profile() $language->setLabel(Lang::get('language')); $language->setRequired(true); $language->setOptions( - array_merge( - [null => Lang::get('default') . ' (' . Config::getInstance()->get('php-censor.language') . ')'], + \array_merge( + [null => Lang::get('default') . ' (' . $this->configuration->get('php-censor.language') . ')'], Lang::getLanguageOptions() ) ); @@ -143,7 +142,7 @@ public function profile() $perPage->setLabel(Lang::get('per_page')); $perPage->setRequired(true); $perPage->setOptions([ - null => Lang::get('default') . ' (' . Config::getInstance()->get('php-censor.per_page') . ')', + null => Lang::get('default') . ' (' . $this->configuration->get('php-censor.per_page') . ')', 10 => 10, 25 => 25, 50 => 50, @@ -163,8 +162,12 @@ public function profile() } /** - * Add a user - handles both form and processing. - */ + * Add a user - handles both form and processing. + * + * @return Response|string + * + * @throws \PHPCensor\Exception\HttpException + */ public function add() { $this->requireAdmin(); @@ -174,14 +177,14 @@ public function add() $method = $this->request->getMethod(); if ($method === 'POST') { - $values = $this->getParams(); + $values = $this->request->request->all(); } else { $values = []; } $form = $this->userForm($values); - if ($method !== 'POST' || ($method == 'POST' && !$form->validate())) { + if ($method !== 'POST' || ($method === 'POST' && !$form->validate())) { $view = new View('User/edit'); $view->type = 'add'; $view->user = null; @@ -205,23 +208,27 @@ public function add() $isAdmin ); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL . 'user'); - - return $response; + return new RedirectResponse(APP_URL . 'user'); } /** - * Edit a user - handles both form and processing. - */ - public function edit($userId) + * Edit a user - handles both form and processing. + * + * @return Response|string + * + * @throws ForbiddenException + * @throws NotFoundException + * @throws \PHPCensor\Common\Exception\RuntimeException + * @throws \PHPCensor\Exception\HttpException + */ + public function edit(int $userId) { $currentUser = $this->getUser(); $method = $this->request->getMethod(); $user = $this->userStore->getById($userId); - if (!$currentUser->getIsAdmin() && $currentUser != $user) { + if (!$currentUser->getIsAdmin() && $currentUser !== $user) { throw new ForbiddenException('You do not have permission to do that.'); } @@ -232,10 +239,10 @@ public function edit($userId) $this->layout->title = $user->getName(); $this->layout->subtitle = Lang::get('edit_user'); - $values = array_merge($user->getDataArray(), $this->getParams()); + $values = \array_merge($user->getDataArray(), $this->request->request->all()); $form = $this->userForm($values, 'edit/' . $userId); - if ($method != 'POST' || ($method == 'POST' && !$form->validate())) { + if ($method !== 'POST' || ($method === 'POST' && !$form->validate())) { $view = new View('User/edit'); $view->type = 'edit'; $view->user = $user; @@ -256,16 +263,16 @@ public function edit($userId) $this->userService->updateUser($user, $name, $email, $password, $isAdmin); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL . 'user'); - - return $response; + return new RedirectResponse(APP_URL . 'user'); } /** - * Create user add / edit form. - */ - protected function userForm($values, $type = 'add') + * Create user add / edit form. + * + * @throws \PHPCensor\Common\Exception\RuntimeException + * @throws \PHPCensor\Exception\HttpException + */ + protected function userForm(array $values, string $type = 'add'): Form { $currentUser = $this->getUser(); @@ -274,7 +281,7 @@ protected function userForm($values, $type = 'add') $form->setMethod('POST'); $form->setAction(APP_URL . 'user/' . $type); - $form->addField(new Form\Element\Csrf('user_form')); + $form->addField(new Csrf($this->session, 'user_form')); $field = new Form\Element\Email('email'); $field->setRequired(true); @@ -292,7 +299,7 @@ protected function userForm($values, $type = 'add') $field = new Form\Element\Password('password'); - if ($type == 'add') { + if ($type === 'add') { $field->setRequired(true); $field->setLabel(Lang::get('password')); } else { @@ -324,9 +331,12 @@ protected function userForm($values, $type = 'add') } /** - * Delete a user. - */ - public function delete($userId) + * Delete a user. + * + * @throws NotFoundException + * @throws \PHPCensor\Exception\HttpException + */ + public function delete(int $userId): Response { $this->requireAdmin(); @@ -338,9 +348,6 @@ public function delete($userId) $this->userService->deleteUser($user); - $response = new RedirectResponse(); - $response->setHeader('Location', APP_URL . 'user'); - - return $response; + return new RedirectResponse(APP_URL . 'user'); } } diff --git a/src/Controller/WebhookController.php b/src/Controller/WebhookController.php index f13961277..0b710ff50 100644 --- a/src/Controller/WebhookController.php +++ b/src/Controller/WebhookController.php @@ -1,83 +1,97 @@ * @author Sami Tikka * @author Alex Russell * @author Guillaume Perréal - * + * @author Dmitry Khomutov */ class WebhookController extends Controller { - /** - * @var BuildStore - */ - protected $buildStore; + protected BuildStore $buildStore; - /** - * @var ProjectStore - */ - protected $projectStore; + protected ProjectStore $projectStore; - /** - * @var BuildService - */ - protected $buildService; + protected WebhookRequestStore $webhookRequestStore; + + protected BuildService $buildService; + + protected BuildFactory $buildFactory; + + private bool $logRequests = false; /** * Initialise the controller, set up stores and services. */ - public function init() + public function init(): void { - $this->buildStore = Factory::getStore('Build'); - $this->projectStore = Factory::getStore('Project'); + $this->buildStore = $this->storeRegistry->get('Build'); + $this->projectStore = $this->storeRegistry->get('Project'); + $this->webhookRequestStore = $this->storeRegistry->get('WebhookRequest'); + + $this->buildFactory = new BuildFactory( + $this->configuration, + $this->storeRegistry + ); + + $this->buildService = new BuildService( + $this->configuration, + $this->storeRegistry, + $this->buildFactory, + $this->buildStore, + $this->projectStore + ); - $this->buildService = new BuildService($this->buildStore, $this->projectStore); + $this->logRequests = (bool)$this->configuration->get('php-censor.webhook.log_requests', false); } /** * Handle the action, Ensuring to return a JsonResponse. - * - * @param string $action - * @param array $actionParams - * - * @return Response */ - public function handleAction($action, $actionParams) + public function handleAction(string $action, array $actionParams): Response { - $response = new Response\JsonResponse(); + $response = new JsonResponse(); try { $data = parent::handleAction($action, $actionParams); if (isset($data['responseCode'])) { - $response->setResponseCode($data['responseCode']); + $response->setStatusCode($data['responseCode']); unset($data['responseCode']); } - $response->setContent($data); - } catch (Exception $ex) { - $response->setResponseCode(500); - $response->setContent(['status' => 'failed', 'error' => $ex->getMessage()]); + $response->setData($data); + } catch (\Throwable $ex) { + $response->setStatusCode(500); + $response->setData(['status' => 'failed', 'error' => $ex->getMessage()]); } return $response; @@ -86,27 +100,23 @@ public function handleAction($action, $actionParams) /** * Wrapper for creating a new build. * - * @param int $source - * @param string $commitId - * @param string $branch - * @param string $tag - * @param string $committer - * @param string $commitMessage - * - * @return array + * @return string[] * - * @throws Exception + * @throws NotFoundException + * @throws RuntimeException + * @throws \PHPCensor\Exception\HttpException */ protected function createBuild( - $source, + int $source, Project $project, - $commitId, - $branch, - $tag, - $committer, - $commitMessage, - array $extra = null - ) { + string $commitId, + string $branch, + ?string $tag, + string $committer, + string $commitMessage, + ?array $extra = null, + ?string $environment = null + ): array { if ($project->getArchived()) { throw new NotFoundException(Lang::get('project_x_not_found', $project->getId())); } @@ -134,36 +144,38 @@ protected function createBuild( $environments = $project->getEnvironmentsObjects(); if ($environments['count']) { - $createdBuilds = []; - $environmentIds = $project->getEnvironmentsNamesByBranch($branch); - // use base branch from project - if (!empty($environmentIds)) { - $duplicates = []; - foreach ($environmentIds as $environmentId) { - if (!in_array($environmentId, $ignoreEnvironments, true) || - ($tag && !in_array($tag, $ignoreTags, true))) { - // If not, create a new build job for it: - $build = $this->buildService->createBuild( - $project, - $environmentId, - $commitId, - $project->getDefaultBranch(), - $tag, - $committer, - $commitMessage, - (int)$source, - null, - $extra - ); - - $createdBuilds[] = [ - 'id' => $build->getID(), - 'environment' => $environmentId, - ]; - } else { - $duplicates[] = \array_search($environmentId, $ignoreEnvironments, true); - } + $createdBuilds = []; + + /** @var EnvironmentStore $environmentStore */ + $environmentStore = $this->storeRegistry->get('Environment'); + $environmentObject = $environmentStore->getByNameAndProjectId($environment, $project->getId()); + if ($environment && $environmentObject) { + if ( + !\in_array($environmentObject->getId(), $ignoreEnvironments, true) || + ($tag && !\in_array($tag, $ignoreTags, true)) + ) { + // If not, create a new build job for it: + $build = $this->buildService->createBuild( + $project, + $environmentObject->getId(), + $commitId, + $project->getDefaultBranch(), + $tag, + $committer, + $commitMessage, + (int)$source, + null, + $extra + ); + + $createdBuilds[] = [ + 'id' => $build->getID(), + 'environment' => $environmentObject->getId(), + ]; + } else { + $duplicates[] = \array_search($environmentObject->getId(), $ignoreEnvironments, true); } + if (!empty($createdBuilds)) { if (empty($duplicates)) { return ['status' => 'ok', 'builds' => $createdBuilds]; @@ -171,28 +183,84 @@ protected function createBuild( return [ 'status' => 'ok', 'builds' => $createdBuilds, - 'message' => sprintf( + 'message' => \sprintf( 'For this commit some builds already exists (%s)', - implode(', ', $duplicates) + \implode(', ', $duplicates) ) ]; } } else { return [ 'status' => 'ignored', - 'message' => sprintf( + 'message' => \sprintf( 'For this commit already created builds (%s)', - implode(', ', $duplicates) + \implode(', ', $duplicates) ) ]; } } else { - return ['status' => 'ignored', 'message' => 'Branch not assigned to any environment']; + $environmentIds = $project->getEnvironmentsNamesByBranch($branch); + // use base branch from project + if (!empty($environmentIds)) { + $duplicates = []; + foreach ($environmentIds as $environmentId) { + if ( + !\in_array($environmentId, $ignoreEnvironments, true) || + ($tag && !\in_array($tag, $ignoreTags, true)) + ) { + // If not, create a new build job for it: + $build = $this->buildService->createBuild( + $project, + (int)$environmentId, + $commitId, + $project->getDefaultBranch(), + $tag, + $committer, + $commitMessage, + $source, + null, + $extra + ); + + $createdBuilds[] = [ + 'id' => $build->getID(), + 'environment' => $environmentId, + ]; + } else { + $duplicates[] = \array_search($environmentId, $ignoreEnvironments, true); + } + } + + if (!empty($createdBuilds)) { + if (empty($duplicates)) { + return ['status' => 'ok', 'builds' => $createdBuilds]; + } else { + return [ + 'status' => 'ok', + 'builds' => $createdBuilds, + 'message' => \sprintf( + 'For this commit some builds already exists (%s)', + \implode(', ', $duplicates) + ) + ]; + } + } else { + return [ + 'status' => 'ignored', + 'message' => \sprintf( + 'For this commit already created builds (%s)', + \implode(', ', $duplicates) + ) + ]; + } + } else { + return ['status' => 'ignored', 'message' => 'Branch not assigned to any environment']; + } } } else { $environmentId = null; - if (!in_array($environmentId, $ignoreEnvironments, true) || - ($tag && !in_array($tag, $ignoreTags, true))) { + if (!\in_array($environmentId, $ignoreEnvironments, true) || + ($tag && !\in_array($tag, $ignoreTags, true))) { $build = $this->buildService->createBuild( $project, null, @@ -210,9 +278,9 @@ protected function createBuild( } else { return [ 'status' => 'ignored', - 'message' => sprintf( + 'message' => \sprintf( 'Duplicate of build #%d', - array_search($environmentId, $ignoreEnvironments, true) + \array_search($environmentId, $ignoreEnvironments, true) ), ]; } @@ -222,97 +290,122 @@ protected function createBuild( /** * Fetch a project and check its type. * - * @param int $projectId id or title of project - * - * @return Project - * * @throws Exception If the project does not exist or is not of the expected type. */ - protected function fetchProject($projectId, array $expectedType) + protected function fetchProject(int $projectId, array $expectedType): Project { - if (empty($projectId)) { - throw new Exception('Project does not exist: ' . $projectId); + if (!$projectId) { + throw new NotFoundException('Project does not exist: ' . $projectId); } - if (is_numeric($projectId)) { - $project = $this->projectStore->getById((int)$projectId); - } else { - $projects = $this->projectStore->getByTitle($projectId, 2); - if ($projects['count'] < 1) { - throw new Exception('Project does not found: ' . $projectId); - } - if ($projects['count'] > 1) { - throw new Exception('Project id is ambiguous: ' . $projectId); - } - $project = reset($projects['items']); - } + /** @var Project $project */ + $project = $this->projectStore->getById($projectId); - if (!in_array($project->getType(), $expectedType, true)) { - throw new Exception('Wrong project type: ' . $project->getType()); + if (!\in_array($project->getType(), $expectedType, true)) { + throw new NotFoundException('Wrong project type: ' . $project->getType()); } return $project; } + protected function logWebhookRequest( + int $projectId, + string $webhookType, + string $payload + ): void { + try { + if ($this->logRequests) { + $webhookRequest = new WebhookRequest($this->storeRegistry); + + $webhookRequest->setProjectId($projectId); + $webhookRequest->setWebhookType($webhookType); + $webhookRequest->setPayload($payload); + $webhookRequest->setCreateDate(new \DateTime()); + + $this->webhookRequestStore->save($webhookRequest); + } + } catch (\Throwable $e) { + } + } + /** * Called by POSTing to /webhook/git/?branch=&commit= * - * @param int $projectId - * - * @return array - * * @throws Exception */ - public function git($projectId) + public function git(int $projectId): array { $project = $this->fetchProject($projectId, [ Project::TYPE_LOCAL, Project::TYPE_GIT, + Project::TYPE_GITHUB, ]); - $branch = $this->getParam('branch', $project->getDefaultBranch()); - $commit = $this->getParam('commit'); - $commitMessage = $this->getParam('message'); - $committer = $this->getParam('committer'); + + $payload = [ + 'branch' => $this->getParam('branch', $project->getDefaultBranch()), + 'environment' => $this->getParam('environment'), + 'commit' => (string)$this->getParam('commit', ''), + 'commit_message' => (string)$this->getParam('message', ''), + 'committer' => (string)$this->getParam('committer', ''), + ]; + $payloadJson = \json_encode($payload); + + $this->logWebhookRequest( + $project->getId(), + WebhookRequest::WEBHOOK_TYPE_GIT, + $payloadJson + ); return $this->createBuild( Build::SOURCE_WEBHOOK_PUSH, $project, - $commit, - $branch, + $payload['commit'], + $payload['branch'], null, - $committer, - $commitMessage + $payload['committer'], + $payload['commit_message'], + null, + $payload['environment'] ); } /** * Called by POSTing to /webhook/hg/?branch=&commit= * - * @param int $projectId - * - * @return array - * * @throws Exception */ - public function hg($projectId) + public function hg(int $projectId): array { $project = $this->fetchProject($projectId, [ Project::TYPE_LOCAL, Project::TYPE_HG, ]); - $branch = $this->getParam('branch', $project->getDefaultBranch()); - $commit = $this->getParam('commit'); - $commitMessage = $this->getParam('message'); - $committer = $this->getParam('committer'); + + $payload = [ + 'branch' => $this->getParam('branch', $project->getDefaultBranch()), + 'environment' => $this->getParam('environment'), + 'commit' => (string)$this->getParam('commit', ''), + 'commit_message' => (string)$this->getParam('message', ''), + 'committer' => (string)$this->getParam('committer', ''), + ]; + $payloadJson = \json_encode($payload); + + $this->logWebhookRequest( + $project->getId(), + WebhookRequest::WEBHOOK_TYPE_HG, + $payloadJson + ); return $this->createBuild( Build::SOURCE_WEBHOOK_PUSH, $project, - $commit, - $branch, + $payload['commit'], + $payload['branch'], null, - $committer, - $commitMessage + $payload['committer'], + $payload['commit_message'], + null, + $payload['environment'] ); } @@ -321,43 +414,48 @@ public function hg($projectId) * * @author Sylvain Lévesque * - * @param int $projectId - * - * @return array - * * @throws Exception */ - public function svn($projectId) + public function svn(int $projectId): array { - $project = $this->fetchProject($projectId, [ + $project = $this->fetchProject($projectId, [ Project::TYPE_SVN ]); - $branch = $this->getParam('branch', $project->getDefaultBranch()); - $commit = $this->getParam('commit'); - $commitMessage = $this->getParam('message'); - $committer = $this->getParam('committer'); + + $payload = [ + 'branch' => $this->getParam('branch', $project->getDefaultBranch()), + 'environment' => $this->getParam('environment'), + 'commit' => (string)$this->getParam('commit', ''), + 'commit_message' => (string)$this->getParam('message', ''), + 'committer' => (string)$this->getParam('committer', ''), + ]; + $payloadJson = \json_encode($payload); + + $this->logWebhookRequest( + $project->getId(), + WebhookRequest::WEBHOOK_TYPE_SVN, + $payloadJson + ); return $this->createBuild( Build::SOURCE_WEBHOOK_PUSH, $project, - $commit, - $branch, + $payload['commit'], + $payload['branch'], + null, + $payload['committer'], + $payload['commit_message'], null, - $committer, - $commitMessage + $payload['environment'] ); } /** * Called by Bitbucket. * - * @param int $projectId - * - * @return array - * * @throws Exception */ - public function bitbucket($projectId) + public function bitbucket(int $projectId): array { $project = $this->fetchProject($projectId, [ Project::TYPE_BITBUCKET, @@ -368,11 +466,26 @@ public function bitbucket($projectId) ]); // Support both old services and new webhooks - if ($payload = $this->getParam('payload')) { - return $this->bitbucketService(json_decode($payload, true), $project); + if ($payloadJson = $this->getParam('payload')) { + $this->logWebhookRequest( + $project->getId(), + WebhookRequest::WEBHOOK_TYPE_BITBUCKET, + $payloadJson + ); + + return $this->bitbucketService(\json_decode($payloadJson, true), $project); } - $payload = json_decode(file_get_contents("php://input"), true); + $payloadJson = \file_get_contents("php://input"); + $payload = \json_decode($payloadJson, true); + + if ($payloadJson) { + $this->logWebhookRequest( + $project->getId(), + WebhookRequest::WEBHOOK_TYPE_BITBUCKET, + $payloadJson + ); + } // Handle Pull Request webhooks: if (!empty($payload['pullrequest'])) { @@ -398,10 +511,8 @@ public function bitbucket($projectId) /** * Handle the payload when Bitbucket sends a commit webhook. - * - * @return array */ - protected function bitbucketCommitRequest(Project $project, array $payload) + protected function bitbucketCommitRequest(Project $project, array $payload): array { $results = []; $status = 'failed'; @@ -409,10 +520,10 @@ protected function bitbucketCommitRequest(Project $project, array $payload) if (!empty($commit['new'])) { try { $email = $commit['new']['target']['author']['raw']; - if (strpos($email, '>') !== false) { - // In order not to loose email if it is RAW, w/o "<>" symbols - $email = substr($email, 0, strpos($email, '>')); - $email = substr($email, strpos($email, '<') + 1); + if (\strpos($email, '>') !== false) { + // In order not to lose email if it is RAW, w/o "<>" symbols + $email = \substr($email, 0, \strpos($email, '>')); + $email = \substr($email, \strpos($email, '<') + 1); } $results[$commit['new']['target']['hash']] = $this->createBuild( @@ -425,7 +536,7 @@ protected function bitbucketCommitRequest(Project $project, array $payload) $commit['new']['target']['message'] ); $status = 'ok'; - } catch (Exception $ex) { + } catch (\Throwable $ex) { $results[$commit['new']['target']['hash']] = ['status' => 'failed', 'error' => $ex->getMessage()]; } } @@ -437,15 +548,13 @@ protected function bitbucketCommitRequest(Project $project, array $payload) /** * Handle the payload when Bitbucket sends a Pull Request webhook. * - * @return array - * * @throws Exception */ - protected function bitbucketPullRequest(Project $project, array $payload) + protected function bitbucketPullRequest(Project $project, array $payload): array { - $triggerType = trim($_SERVER['HTTP_X_EVENT_KEY']); + $triggerType = \trim($_SERVER['HTTP_X_EVENT_KEY']); - if (!array_key_exists( + if (!\array_key_exists( $triggerType, BitbucketBuild::$pullrequestTriggersToSources )) { @@ -455,11 +564,11 @@ protected function bitbucketPullRequest(Project $project, array $payload) ]; } - $username = Config::getInstance()->get('php-censor.bitbucket.username'); - $appPassword = Config::getInstance()->get('php-censor.bitbucket.app_password'); + $username = $this->configuration->get('php-censor.bitbucket.username'); + $appPassword = $this->configuration->get('php-censor.bitbucket.app_password'); if (empty($username) || empty($appPassword)) { - throw new Exception('Please provide Username and App Password of your Bitbucket account.'); + throw new ForbiddenException('Please provide Username and App Password of your Bitbucket account.'); } $commitsUrl = $payload['pullrequest']['links']['commits']['href']; @@ -472,16 +581,16 @@ protected function bitbucketPullRequest(Project $project, array $payload) // Check we got a success response: if ($httpStatus < 200 || $httpStatus >= 300) { - throw new Exception('Could not get commits, failed API request.'); + throw new RuntimeException('Could not get commits, failed API request.'); } $results = []; $status = 'failed'; - $commits = json_decode($commitsResponse->getBody(), true)['values']; + $commits = \json_decode((string)$commitsResponse->getBody(), true)['values']; foreach ($commits as $commit) { // Skip all but the current HEAD commit ID: $id = $commit['hash']; - if (strpos($id, $payload['pullrequest']['source']['commit']['hash']) !== 0) { + if (\strpos($id, $payload['pullrequest']['source']['commit']['hash']) !== 0) { $results[$id] = ['status' => 'ignored', 'message' => 'not branch head']; continue; @@ -490,10 +599,10 @@ protected function bitbucketPullRequest(Project $project, array $payload) try { $branch = $payload['pullrequest']['destination']['branch']['name']; $committer = $commit['author']['raw']; - if (strpos($committer, '>') !== false) { - // In order not to loose email if it is RAW, w/o "<>" symbols - $committer = substr($committer, 0, strpos($committer, '>')); - $committer = substr($committer, strpos($committer, '<') + 1); + if (\strpos($committer, '>') !== false) { + // In order not to lose email if it is RAW, w/o "<>" symbols + $committer = \substr($committer, 0, \strpos($committer, '>')); + $committer = \substr($committer, \strpos($committer, '<') + 1); } $message = $commit['message']; @@ -514,7 +623,7 @@ protected function bitbucketPullRequest(Project $project, array $payload) $extra ); $status = 'ok'; - } catch (Exception $ex) { + } catch (\Throwable $ex) { $results[$id] = ['status' => 'failed', 'error' => $ex->getMessage()]; } } @@ -525,15 +634,13 @@ protected function bitbucketPullRequest(Project $project, array $payload) /** * Handle the payload when Bitbucket Server sends a Pull Request webhook. * - * @return array - * * @throws Exception */ - protected function bitbucketSvrPullRequest(Project $project, array $payload) + protected function bitbucketSvrPullRequest(Project $project, array $payload): array { - $triggerType = trim($_SERVER['HTTP_X_EVENT_KEY']); + $triggerType = \trim($_SERVER['HTTP_X_EVENT_KEY']); - if (!array_key_exists( + if (!\array_key_exists( $triggerType, BitbucketServerBuild::$pullrequestTriggersToSources )) { @@ -568,7 +675,7 @@ protected function bitbucketSvrPullRequest(Project $project, array $payload) $extra ); $status = 'ok'; - } catch (Exception $ex) { + } catch (\Throwable $ex) { $results[$id] = ['status' => 'failed', 'error' => $ex->getMessage()]; } @@ -577,18 +684,16 @@ protected function bitbucketSvrPullRequest(Project $project, array $payload) /** * Bitbucket POST service. - * - * @return array */ - protected function bitbucketService(array $payload, Project $project) + protected function bitbucketService(array $payload, Project $project): array { $results = []; $status = 'failed'; foreach ($payload['commits'] as $commit) { try { $email = $commit['raw_author']; - $email = substr($email, 0, strpos($email, '>')); - $email = substr($email, strpos($email, '<') + 1); + $email = \substr($email, 0, \strpos($email, '>')); + $email = \substr($email, \strpos($email, '<') + 1); $results[$commit['raw_node']] = $this->createBuild( Build::SOURCE_WEBHOOK_PUSH, @@ -600,7 +705,7 @@ protected function bitbucketService(array $payload, Project $project) $commit['message'] ); $status = 'ok'; - } catch (Exception $ex) { + } catch (\Throwable $ex) { $results[$commit['raw_node']] = ['status' => 'failed', 'error' => $ex->getMessage()]; } } @@ -609,13 +714,9 @@ protected function bitbucketService(array $payload, Project $project) } /** - * @param int $projectId - * - * @return array - * * @throws Exception */ - public function github($projectId) + public function github(int $projectId): array { $project = $this->fetchProject($projectId, [ Project::TYPE_GITHUB, @@ -624,10 +725,12 @@ public function github($projectId) switch ($_SERVER['CONTENT_TYPE']) { case 'application/json': - $payload = json_decode(file_get_contents('php://input'), true); + $payloadJson = \file_get_contents('php://input'); + break; case 'application/x-www-form-urlencoded': - $payload = json_decode($this->getParam('payload'), true); + $payloadJson = $this->getParam('payload'); + break; default: return [ @@ -637,13 +740,23 @@ public function github($projectId) ]; } + if ($payloadJson) { + $this->logWebhookRequest( + $project->getId(), + WebhookRequest::WEBHOOK_TYPE_GITHUB, + $payloadJson + ); + } + + $payload = \json_decode($payloadJson, true); + // Handle Pull Request webhooks: - if (array_key_exists('pull_request', $payload)) { + if (\array_key_exists('pull_request', $payload)) { return $this->githubPullRequest($project, $payload); } // Handle Push (and Tag) webhooks: - if (array_key_exists('head_commit', $payload)) { + if (\array_key_exists('head_commit', $payload)) { return $this->githubCommitRequest($project, $payload); } @@ -652,19 +765,17 @@ public function github($projectId) /** * Handle the payload when Github sends a commit webhook. - * - * @return array */ - protected function githubCommitRequest(Project $project, array $payload) + protected function githubCommitRequest(Project $project, array $payload): array { // Github sends a payload when you close a pull request with a non-existent commit. We don't want this. - if (array_key_exists('after', $payload) && + if (\array_key_exists('after', $payload) && $payload['after'] === '0000000000000000000000000000000000000000') { return ['status' => 'ignored']; } if (isset($payload['head_commit']) && $payload['head_commit']) { - $isTag = (substr($payload['ref'], 0, 10) == 'refs/tags/') ? true : false; + $isTag = (\substr($payload['ref'], 0, 10) === 'refs/tags/') ? true : false; $commit = $payload['head_commit']; $results = []; $status = 'failed'; @@ -675,11 +786,11 @@ protected function githubCommitRequest(Project $project, array $payload) try { $tag = null; if ($isTag) { - $tag = str_replace('refs/tags/', '', $payload['ref']); - $branch = str_replace('refs/heads/', '', $payload['base_ref']); + $tag = \str_replace('refs/tags/', '', $payload['ref']); + $branch = \str_replace('refs/heads/', '', $payload['base_ref']); $committer = $payload['pusher']['email']; } else { - $branch = str_replace('refs/heads/', '', $payload['ref']); + $branch = \str_replace('refs/heads/', '', $payload['ref']); $committer = $commit['committer']['email']; } @@ -694,7 +805,7 @@ protected function githubCommitRequest(Project $project, array $payload) ); $status = 'ok'; - } catch (Exception $ex) { + } catch (\Throwable $ex) { $results[$commit['id']] = ['status' => 'failed', 'error' => $ex->getMessage()]; } } @@ -708,15 +819,13 @@ protected function githubCommitRequest(Project $project, array $payload) /** * Handle the payload when Github sends a Pull Request webhook. * - * @return array - * * @throws Exception */ - protected function githubPullRequest(Project $project, array $payload) + protected function githubPullRequest(Project $project, array $payload): array { - $triggerType = trim($payload['action']); + $triggerType = \trim($payload['action']); - if (!array_key_exists( + if (!\array_key_exists( $triggerType, GithubBuild::$pullrequestTriggersToSources )) { @@ -727,7 +836,7 @@ protected function githubPullRequest(Project $project, array $payload) } $headers = []; - $token = Config::getInstance()->get('php-censor.github.token'); + $token = $this->configuration->get('php-censor.github.token'); if (!empty($token)) { $headers['Authorization'] = 'token ' . $token; @@ -736,7 +845,7 @@ protected function githubPullRequest(Project $project, array $payload) $url = $payload['pull_request']['commits_url']; //for large pull requests, allow grabbing more then the default number of commits - $customPerPage = Config::getInstance()->get('php-censor.github.per_page'); + $customPerPage = $this->configuration->get('php-censor.github.per_page'); $params = []; if ($customPerPage) { $params['per_page'] = $customPerPage; @@ -747,16 +856,16 @@ protected function githubPullRequest(Project $project, array $payload) 'headers' => $headers, 'query' => $params, ]); - $status = (int)$response->getStatusCode(); + $status = $response->getStatusCode(); // Check we got a success response: if ($status < 200 || $status >= 300) { - throw new Exception('Could not get commits, failed API request.'); + throw new RuntimeException('Could not get commits, failed API request.'); } $results = []; $status = 'failed'; - $commits = json_decode($response->getBody(), true); + $commits = \json_decode((string)$response->getBody(), true); foreach ($commits as $commit) { // Skip all but the current HEAD commit ID: $id = $commit['sha']; @@ -767,7 +876,7 @@ protected function githubPullRequest(Project $project, array $payload) } try { - $branch = str_replace('refs/heads/', '', $payload['pull_request']['base']['ref']); + $branch = \str_replace('refs/heads/', '', $payload['pull_request']['base']['ref']); $committer = $commit['commit']['author']['email']; $message = $commit['commit']['message']; @@ -788,7 +897,7 @@ protected function githubPullRequest(Project $project, array $payload) $extra ); $status = 'ok'; - } catch (Exception $ex) { + } catch (\Throwable $ex) { $results[$id] = ['status' => 'failed', 'error' => $ex->getMessage()]; } } @@ -799,24 +908,29 @@ protected function githubPullRequest(Project $project, array $payload) /** * Called by Gitlab Webhooks: * - * @param int $projectId - * - * @return array - * * @throws Exception */ - public function gitlab($projectId) + public function gitlab(int $projectId): array { $project = $this->fetchProject($projectId, [ Project::TYPE_GITLAB, Project::TYPE_GIT, ]); - $payloadString = file_get_contents("php://input"); - $payload = json_decode($payloadString, true); + $payloadJson = \file_get_contents("php://input"); + + if ($payloadJson) { + $this->logWebhookRequest( + $project->getId(), + WebhookRequest::WEBHOOK_TYPE_GITLAB, + $payloadJson + ); + } + + $payload = \json_decode($payloadJson, true); // build on merge request events - if (isset($payload['object_kind']) && $payload['object_kind'] == 'merge_request') { + if (isset($payload['object_kind']) && $payload['object_kind'] === 'merge_request') { $attributes = $payload['object_attributes']; if ($attributes['state'] === 'opened' || $attributes['state'] === 'reopened') { $branch = $attributes['source_branch']; @@ -836,15 +950,15 @@ public function gitlab($projectId) } // build on push events - if (isset($payload['commits']) && is_array($payload['commits'])) { + if (isset($payload['commits']) && \is_array($payload['commits'])) { // If we have a list of commits, then add them all as builds to be tested: $results = []; $status = 'failed'; - $commit = end($payload['commits']); + $commit = \end($payload['commits']); try { - $branch = str_replace('refs/heads/', '', $payload['ref']); + $branch = \str_replace('refs/heads/', '', $payload['ref']); $committer = $commit['author']['email']; $results[$commit['id']] = $this->createBuild( Build::SOURCE_WEBHOOK_PUSH, @@ -856,7 +970,7 @@ public function gitlab($projectId) $commit['message'] ); $status = 'ok'; - } catch (Exception $ex) { + } catch (\Throwable $ex) { $results[$commit['id']] = ['status' => 'failed', 'error' => $ex->getMessage()]; } @@ -869,11 +983,9 @@ public function gitlab($projectId) /** * @param string $projectId * - * @return array - * * @throws Exception */ - public function gogs($projectId) + public function gogs(int $projectId): array { $project = $this->fetchProject($projectId, [ Project::TYPE_GOGS, @@ -886,19 +998,30 @@ public function gogs($projectId) switch ($contentType) { case 'application/x-www-form-urlencoded': - $payload = json_decode($this->getParam('payload'), true); + $payloadJson = $this->getParam('payload'); + break; case 'application/json': default: - $payload = json_decode(file_get_contents('php://input'), true); + $payloadJson = \file_get_contents('php://input'); } + if ($payloadJson) { + $this->logWebhookRequest( + $project->getId(), + WebhookRequest::WEBHOOK_TYPE_GOGS, + $payloadJson + ); + } + + $payload = \json_decode($payloadJson, true); + // Handle Push web hooks: - if (array_key_exists('commits', $payload)) { + if (\array_key_exists('commits', $payload)) { return $this->gogsCommitRequest($project, $payload); } - if (array_key_exists('pull_request', $payload)) { + if (\array_key_exists('pull_request', $payload)) { return $this->gogsPullRequest($project, $payload); } @@ -907,18 +1030,16 @@ public function gogs($projectId) /** * Handle the payload when Gogs sends a commit webhook. - * - * @return array */ - protected function gogsCommitRequest(Project $project, array $payload) + protected function gogsCommitRequest(Project $project, array $payload): array { - if (isset($payload['commits']) && is_array($payload['commits'])) { + if (isset($payload['commits']) && \is_array($payload['commits'])) { // If we have a list of commits, then add them all as builds to be tested: $results = []; $status = 'failed'; foreach ($payload['commits'] as $commit) { try { - $branch = str_replace('refs/heads/', '', $payload['ref']); + $branch = \str_replace('refs/heads/', '', $payload['ref']); $committer = $commit['author']['email']; $results[$commit['id']] = $this->createBuild( Build::SOURCE_WEBHOOK_PUSH, @@ -930,7 +1051,7 @@ protected function gogsCommitRequest(Project $project, array $payload) $commit['message'] ); $status = 'ok'; - } catch (Exception $ex) { + } catch (\Throwable $ex) { $results[$commit['id']] = ['status' => 'failed', 'error' => $ex->getMessage()]; } } @@ -944,11 +1065,9 @@ protected function gogsCommitRequest(Project $project, array $payload) /** * Handle the payload when Gogs sends a pull request webhook. * - * @return array - * * @throws InvalidArgumentException */ - protected function gogsPullRequest(Project $project, array $payload) + protected function gogsPullRequest(Project $project, array $payload): array { $pullRequest = $payload['pull_request']; $headBranch = $pullRequest['head_branch']; @@ -961,21 +1080,21 @@ protected function gogsPullRequest(Project $project, array $payload) $activeStates = ['open']; $inactiveStates = ['closed']; - if (!in_array($action, $activeActions, true) && !in_array($action, $inactiveActions, true)) { + if (!\in_array($action, $activeActions, true) && !\in_array($action, $inactiveActions, true)) { return ['status' => 'ignored', 'message' => 'Action ' . $action . ' ignored']; } - if (!in_array($state, $activeStates, true) && !in_array($state, $inactiveStates, true)) { + if (!\in_array($state, $activeStates, true) && !\in_array($state, $inactiveStates, true)) { return ['status' => 'ignored', 'message' => 'State ' . $state . ' ignored']; } $envs = []; // Get environment form labels - if (in_array($action, $activeActions, true) && in_array($state, $activeStates, true)) { - if (isset($pullRequest['labels']) && is_array($pullRequest['labels'])) { + if (\in_array($action, $activeActions, true) && \in_array($state, $activeStates, true)) { + if (isset($pullRequest['labels']) && \is_array($pullRequest['labels'])) { foreach ($pullRequest['labels'] as $label) { - if (strpos($label['name'], 'env:') === 0) { - $envs[] = substr($label['name'], 4); + if (\strpos($label['name'], 'env:') === 0) { + $envs[] = \substr($label['name'], 4); } } } @@ -983,11 +1102,11 @@ protected function gogsPullRequest(Project $project, array $payload) $envsUpdated = []; $envObjects = $project->getEnvironmentsObjects(); - $store = Factory::getStore('Environment'); + $store = $this->storeRegistry->get('Environment'); foreach ($envObjects['items'] as $environment) { $branches = $environment->getBranches(); - if (in_array($environment->getName(), $envs, true)) { - if (!in_array($headBranch, $branches, true)) { + if (\in_array($environment->getName(), $envs, true)) { + if (!\in_array($headBranch, $branches, true)) { // Add branch to environment $branches[] = $headBranch; $environment->setBranches($branches); @@ -995,9 +1114,9 @@ protected function gogsPullRequest(Project $project, array $payload) $envsUpdated[] = $environment->getId(); } } else { - if (in_array($headBranch, $branches, true)) { + if (\in_array($headBranch, $branches, true)) { // Remove branch from environment - $branches = array_diff($branches, [$headBranch]); + $branches = \array_diff($branches, [$headBranch]); $environment->setBranches($branches); $store->save($environment); $envsUpdated[] = $environment->getId(); diff --git a/src/Controller/WidgetAllProjectsController.php b/src/Controller/WidgetAllProjectsController.php index b805fb5f3..72e4c2128 100644 --- a/src/Controller/WidgetAllProjectsController.php +++ b/src/Controller/WidgetAllProjectsController.php @@ -1,57 +1,50 @@ */ class WidgetAllProjectsController extends WebController { - /** - * @var BuildStore - */ - protected $buildStore; + protected BuildStore $buildStore; - /** - * @var ProjectStore - */ - protected $projectStore; + protected ProjectStore $projectStore; - /** - * @var ProjectGroupStore - */ - protected $groupStore; + protected ProjectGroupStore $groupStore; /** * Initialise the controller, set up stores and services. */ - public function init() + public function init(): void { parent::init(); - $this->buildStore = Factory::getStore('Build'); - $this->projectStore = Factory::getStore('Project'); - $this->groupStore = Factory::getStore('ProjectGroup'); + $this->buildStore = $this->storeRegistry->get('Build'); + $this->projectStore = $this->storeRegistry->get('Project'); + $this->groupStore = $this->storeRegistry->get('ProjectGroup'); } /** - * @return Response - * * @throws Exception */ - public function index() + public function index(): Response { $this->view->groups = $this->getGroupInfo(); @@ -66,11 +59,9 @@ public function index() * * @param Project[] $projects * - * @return string - * * @throws Exception */ - protected function getSummaryHtml($projects) + protected function getSummaryHtml(array $projects): string { $summaryBuilds = []; $successes = []; @@ -109,11 +100,9 @@ protected function getSummaryHtml($projects) /** * Get a summary of the project groups we have, and what projects they have in them. * - * @return array - * * @throws Exception */ - protected function getGroupInfo() + protected function getGroupInfo(): array { $rtn = []; $groups = $this->groupStore->getWhere([], 100, 0, ['title' => 'ASC']); @@ -132,13 +121,9 @@ protected function getGroupInfo() } /** - * @param int $projectId - * - * @return Response - * * @throws HttpException */ - public function update($projectId) + public function update(int $projectId): Response { $count = $this->buildStore->getWhere( ['project_id' => $projectId], diff --git a/src/Controller/WidgetBuildErrorsController.php b/src/Controller/WidgetBuildErrorsController.php index c3edc7bd6..184c7d664 100644 --- a/src/Controller/WidgetBuildErrorsController.php +++ b/src/Controller/WidgetBuildErrorsController.php @@ -1,45 +1,43 @@ */ class WidgetBuildErrorsController extends WebController { - /** - * @var BuildStore - */ - protected $buildStore; + protected BuildStore $buildStore; - /** - * @var ProjectStore - */ - protected $projectStore; + protected ProjectStore $projectStore; /** * Initialise the controller, set up stores and services. */ - public function init() + public function init(): void { parent::init(); - $this->buildStore = Factory::getStore('Build'); - $this->projectStore = Factory::getStore('Project'); + $this->buildStore = $this->storeRegistry->get('Build'); + $this->projectStore = $this->storeRegistry->get('Project'); } /** * Display dashboard. */ - public function index() + public function index(): Response { $view = new View('WidgetBuildErrors/update'); @@ -52,11 +50,9 @@ public function index() } /** - * @return Response - * * @throws HttpException */ - public function update() + public function update(): Response { $response = new Response(); $response->setContent($this->renderAllProjectsLatestBuilds($this->view)); @@ -65,19 +61,15 @@ public function update() } /** - * @param View $view - * - * @return string - * * @throws HttpException */ - protected function renderAllProjectsLatestBuilds($view) + protected function renderAllProjectsLatestBuilds(View $view): string { $builds = $this->buildStore->getAllProjectsLatestBuilds(); if (!empty($builds['projects'])) { $view->builds = $builds['projects']; - $projects = $this->projectStore->getByIds(array_keys($builds['projects'])); + $projects = $this->projectStore->getByIds(\array_keys($builds['projects'])); $viewProjects = []; foreach ($projects as $id => $project) { diff --git a/src/Controller/WidgetLastBuildsController.php b/src/Controller/WidgetLastBuildsController.php index 26efe1470..58814a661 100644 --- a/src/Controller/WidgetLastBuildsController.php +++ b/src/Controller/WidgetLastBuildsController.php @@ -1,48 +1,57 @@ */ class WidgetLastBuildsController extends WebController { - /** - * @var BuildStore - */ - protected $buildStore; + protected BuildStore $buildStore; + + protected BuildFactory $buildFactory; /** * Initialise the controller, set up stores and services. */ - public function init() + public function init(): void { parent::init(); - $this->buildStore = Factory::getStore('Build'); + $this->buildStore = $this->storeRegistry->get('Build'); + + $this->buildFactory = new BuildFactory( + $this->configuration, + $this->storeRegistry + ); } /** * Display dashboard. */ - public function index() + public function index(): Response { $builds = $this->buildStore->getLatestBuilds(null, 10); foreach ($builds as &$build) { - $build = BuildFactory::getBuild($build); + $build = $this->buildFactory->getBuild($build); } $view = new View('WidgetLastBuilds/update'); $view->builds = $builds; + $view->environmentStore = $this->storeRegistry->get('Environment'); $this->view->timeline = $view->render(); $response = new Response(); @@ -51,18 +60,16 @@ public function index() return $response; } - /** - * @return Response - */ - public function update() + public function update(): Response { $builds = $this->buildStore->getLatestBuilds(null, 10); foreach ($builds as &$build) { - $build = BuildFactory::getBuild($build); + $build = $this->buildFactory->getBuild($build); } - $this->view->builds = $builds; + $this->view->builds = $builds; + $this->view->environmentStore = $this->storeRegistry->get('Environment'); $response = new Response(); $response->setContent($this->view->render()); diff --git a/src/Database.php b/src/Database.php deleted file mode 100644 index 85d1fbd89..000000000 --- a/src/Database.php +++ /dev/null @@ -1,218 +0,0 @@ - [], - 'write' => [] - ]; - - /** - * @var array - */ - protected static $connections = [ - 'read' => null, - 'write' => null - ]; - - /** - * @var array - */ - protected static $dsn = [ - 'read' => '', - 'write' => '' - ]; - - protected static $details = []; - - /** - * @param string $table - * - * @return string - */ - public function lastInsertIdExtended($table = null) - { - if ($table && self::POSTGRESQL_TYPE === $this->getAttribute(self::ATTR_DRIVER_NAME)) { - return parent::lastInsertId('"' . $table . '_id_seq"'); - } - - return parent::lastInsertId(); - } - - protected static function init() - { - $config = Config::getInstance(); - $settings = $config->get('php-censor.database', []); - - self::$servers['read'] = $settings['servers']['read']; - self::$servers['write'] = $settings['servers']['write']; - - self::$details['driver'] = $settings['type']; - self::$details['db'] = $settings['name']; - self::$details['user'] = $settings['username']; - self::$details['pass'] = $settings['password']; - - self::$initialised = true; - } - - /** - * @param string $type - * - * @return Database - * - * @throws Exception - */ - public static function getConnection($type = 'read') - { - if (!self::$initialised) { - self::init(); - } - - if (is_null(self::$connections[$type])) { - // Shuffle, so we pick a random server: - $servers = self::$servers[$type]; - shuffle($servers); - - $connection = null; - - // Loop until we get a working connection: - while (count($servers)) { - // Pull the next server: - $server = array_shift($servers); - - self::$dsn[$type] = self::$details['driver'] . ':host=' . $server['host']; - - if (self::$details['driver'] === 'pgsql') { - if (!array_key_exists('pgsql-sslmode', $server)) { - $server['pgsql-sslmode'] = 'prefer'; - } - - self::$dsn[$type] .= ';sslmode=' . $server['pgsql-sslmode']; - } - - if (isset($server['port'])) { - self::$dsn[$type] .= ';port=' . (int)$server['port']; - } - - self::$dsn[$type] .= ';dbname=' . self::$details['db']; - - $pdoOptions = [ - PDO::ATTR_PERSISTENT => false, - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_TIMEOUT => 2, - ]; - - if (self::MYSQL_TYPE === self::$details['driver']) { - $pdoOptions[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET NAMES 'UTF8'"; - } - - // Try to connect: - try { - $connection = new self( - self::$dsn[$type], - self::$details['user'], - self::$details['pass'], - $pdoOptions - ); - $connection->setType($type); - } catch (PDOException $ex) { - $connection = false; - } - - // Opened a connection? Break the loop: - if ($connection) { - break; - } - } - - // No connection? Oh dear. - if (!$connection && $type === 'read') { - throw new Exception('Could not connect to any ' . $type . ' servers.'); - } - - self::$connections[$type] = $connection; - } - - return self::$connections[$type]; - } - - /** - * @return array - */ - public function getDetails() - { - return self::$details; - } - - /** - * @return string - */ - public function getDsn() - { - return self::$dsn[$this->type]; - } - - /** - * @param string $type - */ - public function setType($type) - { - $this->type = $type; - } - - public static function reset() - { - self::$connections = ['read' => null, 'write' => null]; - self::$initialised = false; - } - - /** - * @param string $statement - * - * @return string - */ - protected function quoteNames($statement) - { - $quote = ''; - if (self::MYSQL_TYPE === self::$details['driver']) { - $quote = '`'; - } elseif (self::POSTGRESQL_TYPE === self::$details['driver']) { - $quote = '"'; - } - - return preg_replace('/{{(.*?)}}/', ($quote . '\1' . $quote), $statement); - } - - /** - * @param string $statement - * - * @return PDOStatement - */ - public function prepareCommon($statement, array $driverOptions = []) - { - return parent::prepare($this->quoteNames($statement), $driverOptions); - } -} diff --git a/src/DatabaseConnection.php b/src/DatabaseConnection.php new file mode 100644 index 000000000..4ae6fd821 --- /dev/null +++ b/src/DatabaseConnection.php @@ -0,0 +1,123 @@ + + */ +class DatabaseConnection +{ + private ?\PDO $pdoConnection = null; + + private string $dsn; + + private ?string $username; + + private ?string $password; + + private ?array $options; + + private string $sequencePattern; + + public function __construct( + string $dsn, + ?string $username = null, + ?string $password = null, + ?array $options = null, + string $sequencePattern = '%s_id_seq' + ) { + $this->sequencePattern = $sequencePattern; + + $this->dsn = $dsn; + $this->username = $username; + $this->password = $password; + $this->options = $options; + } + + public function getPdo(): \PDO + { + if (null === $this->pdoConnection) { + $this->pdoConnection = new \PDO($this->dsn, $this->username, $this->password, $this->options); + } + + return $this->pdoConnection; + } + + public function setPdo(\PDO $pdoConnection): self + { + $this->pdoConnection = $pdoConnection; + + return $this; + } + + public function quoteNames(string $query): string + { + $driver = $this->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME); + $pattern = '\1'; + if (DatabaseManager::MYSQL_TYPE === $driver) { + $pattern = '`\1`'; + } elseif (DatabaseManager::POSTGRESQL_TYPE === $driver) { + $pattern = '"\1"'; + } + + return \preg_replace('#{{(.*?)}}#m', $pattern, $query); + } + + public function lastInsertId(string $tableName): int + { + if (DatabaseManager::POSTGRESQL_TYPE === $this->getPdo()->getAttribute(\PDO::ATTR_DRIVER_NAME)) { + return (int)$this->getPdo()->lastInsertId(\sprintf("\"{$this->sequencePattern}\"", $tableName)); + } + + return (int)$this->getPdo()->lastInsertId(); + } + + /** + * @return false|\PDOStatement + */ + public function prepare(string $query, array $options = []) + { + return $this->getPdo()->prepare($this->quoteNames($query), $options); + } + + /** + * @return false|int + */ + public function exec(string $statement) + { + return $this->getPdo()->exec($this->quoteNames($statement)); + } + + /** + * @return false|\PDOStatement + */ + public function query(string $statement) + { + return $this->getPdo()->query($this->quoteNames($statement)); + } + + public function beginTransaction(): bool + { + return $this->getPdo()->beginTransaction(); + } + + public function commit(): bool + { + return $this->getPdo()->commit(); + } + + public function rollBack(): bool + { + return $this->getPdo()->rollBack(); + } + + public function inTransaction(): bool + { + return $this->getPdo()->inTransaction(); + } +} diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php new file mode 100644 index 000000000..5a74b770a --- /dev/null +++ b/src/DatabaseManager.php @@ -0,0 +1,98 @@ + + */ +class DatabaseManager +{ + public const MYSQL_TYPE = 'mysql'; + public const POSTGRESQL_TYPE = 'pgsql'; + + private ConfigurationInterface $configuration; + + private array $connections = [ + 'read' => null, + 'write' => null + ]; + + public function __construct(ConfigurationInterface $configuration) + { + $this->configuration = $configuration; + } + + /** + * @throws Exception + */ + public function getConnection(string $type = 'read'): DatabaseConnection + { + if (null === $this->connections[$type]) { + $servers = (array)$this->configuration->get("php-censor.database.servers.{$type}", []); + \shuffle($servers); + + $connection = null; + while (\count($servers)) { + $server = \array_shift($servers); + $driver = $this->configuration->get('php-censor.database.type', self::POSTGRESQL_TYPE); + $dsn = $driver . ':host=' . $server['host']; + + if (self::POSTGRESQL_TYPE === $driver) { + if (!\array_key_exists('pgsql-sslmode', $server)) { + $server['pgsql-sslmode'] = 'prefer'; + } + + $dsn .= ';sslmode=' . $server['pgsql-sslmode']; + } + + if (isset($server['port'])) { + $dsn .= ';port=' . (int)$server['port']; + } + + $dsn .= ';dbname=' . $this->configuration->get('php-censor.database.name', ''); + + $pdoOptions = [ + \PDO::ATTR_PERSISTENT => false, + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_TIMEOUT => 2, + ]; + + if (self::MYSQL_TYPE === $driver) { + $pdoOptions[\PDO::MYSQL_ATTR_INIT_COMMAND] = "SET NAMES 'UTF8'"; + } + + try { + $connection = new DatabaseConnection( + $dsn, + $this->configuration->get('php-censor.database.username', ''), + $this->configuration->get('php-censor.database.password', ''), + $pdoOptions + ); + } catch (\PDOException $ex) { + $connection = false; + } + + if ($connection) { + break; + } + } + + if (!$connection) { + throw new RuntimeException('Could not connect to any ' . $type . ' servers.'); + } + + $this->connections[$type] = $connection; + } + + return $this->connections[$type]; + } +} diff --git a/src/Exception/Exception.php b/src/Exception/Exception.php deleted file mode 100644 index 7de397015..000000000 --- a/src/Exception/Exception.php +++ /dev/null @@ -1,7 +0,0 @@ - + */ class HttpException extends Exception { - /** - * @var int - */ - protected $errorCode = 500; - - /** - * @var string - */ - protected $statusMessage = 'Internal Server Error'; - - /** - * @return int - */ - public function getErrorCode() + protected int $errorCode = 500; + + protected string $statusMessage = 'Internal Server Error'; + + public function getErrorCode(): int { return $this->errorCode; } - /** - * @return string - */ - public function getStatusMessage() + public function getStatusMessage(): string { return $this->statusMessage; } - /** - * @return string - */ - public function getHttpHeader() + public function getHttpHeader(): string { return 'HTTP/1.1 ' . $this->errorCode . ' ' . $this->statusMessage; } diff --git a/src/Exception/HttpException/BadRequestException.php b/src/Exception/HttpException/BadRequestException.php index 27b01eee3..a8964f119 100644 --- a/src/Exception/HttpException/BadRequestException.php +++ b/src/Exception/HttpException/BadRequestException.php @@ -1,18 +1,20 @@ + */ class BadRequestException extends HttpException { - /** - * @var int - */ - protected $errorCode = 400; + protected int $errorCode = 400; - /** - * @var string - */ - protected $statusMessage = 'Bad Request'; + protected string $statusMessage = 'Bad Request'; } diff --git a/src/Exception/HttpException/ForbiddenException.php b/src/Exception/HttpException/ForbiddenException.php index bca61f0b7..748b9b0d2 100644 --- a/src/Exception/HttpException/ForbiddenException.php +++ b/src/Exception/HttpException/ForbiddenException.php @@ -1,18 +1,20 @@ + */ class ForbiddenException extends HttpException { - /** - * @var int - */ - protected $errorCode = 403; + protected int $errorCode = 403; - /** - * @var string - */ - protected $statusMessage = 'Forbidden'; + protected string $statusMessage = 'Forbidden'; } diff --git a/src/Exception/HttpException/NotAuthorizedException.php b/src/Exception/HttpException/NotAuthorizedException.php index 67f88f3c5..d41d09a87 100644 --- a/src/Exception/HttpException/NotAuthorizedException.php +++ b/src/Exception/HttpException/NotAuthorizedException.php @@ -1,18 +1,20 @@ + */ class NotAuthorizedException extends HttpException { - /** - * @var int - */ - protected $errorCode = 401; + protected int $errorCode = 401; - /** - * @var string - */ - protected $statusMessage = 'Not Authorized'; + protected string $statusMessage = 'Not Authorized'; } diff --git a/src/Exception/HttpException/NotFoundException.php b/src/Exception/HttpException/NotFoundException.php index 4766cffda..4c3ea537b 100644 --- a/src/Exception/HttpException/NotFoundException.php +++ b/src/Exception/HttpException/NotFoundException.php @@ -1,18 +1,20 @@ + */ class NotFoundException extends HttpException { - /** - * @var int - */ - protected $errorCode = 404; + protected int $errorCode = 404; - /** - * @var string - */ - protected $statusMessage = 'Not Found'; + protected string $statusMessage = 'Not Found'; } diff --git a/src/Exception/HttpException/ServerErrorException.php b/src/Exception/HttpException/ServerErrorException.php index 7568b4b7f..c5cdeee55 100644 --- a/src/Exception/HttpException/ServerErrorException.php +++ b/src/Exception/HttpException/ServerErrorException.php @@ -1,9 +1,17 @@ + */ class ServerErrorException extends HttpException { } diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php deleted file mode 100644 index 17e85a0f4..000000000 --- a/src/Exception/InvalidArgumentException.php +++ /dev/null @@ -1,7 +0,0 @@ - + * @author Dmitry Khomutov + */ class Form extends FieldSet { - /** - * @var string - */ - protected $action = ''; + protected string $action = ''; - /** - * @var string - */ - protected $method = 'POST'; + protected string $method = 'POST'; - /** - * @return string - */ - public function getAction() + public function getAction(): string { return $this->action; } - /** - * @param string $action - */ - public function setAction($action) + public function setAction(string $action): void { $this->action = $action; } - /** - * @return string - */ - public function getMethod() + public function getMethod(): string { return $this->method; } - /** - * @param string $method - */ - public function setMethod($method) + public function setMethod(string $method): void { $this->method = $method; } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { $view->action = $this->getAction(); $view->method = $this->getMethod(); @@ -56,10 +47,7 @@ protected function onPreRender(View &$view) parent::onPreRender($view); } - /** - * @return string - */ - public function __toString() + public function __toString(): string { return $this->render(); } diff --git a/src/Form/ControlGroup.php b/src/Form/ControlGroup.php index 37afe8e6f..9ec0bbc74 100644 --- a/src/Form/ControlGroup.php +++ b/src/Form/ControlGroup.php @@ -1,7 +1,16 @@ + * @author Dmitry Khomutov + */ class ControlGroup extends FieldSet { } diff --git a/src/Form/DataTransformer/DataTransformerInterface.php b/src/Form/DataTransformer/DataTransformerInterface.php index bb54dd0ee..a8bccaeba 100644 --- a/src/Form/DataTransformer/DataTransformerInterface.php +++ b/src/Form/DataTransformer/DataTransformerInterface.php @@ -1,12 +1,20 @@ + */ interface DataTransformerInterface { /** transform when putting the data into a form */ - public function transform($value); + public function transform(string $value): string; /** reversen when getting the data from a form */ - public function reverseTransform($value); + public function reverseTransform(string $value): string; } diff --git a/src/Form/DataTransformer/Yaml.php b/src/Form/DataTransformer/Yaml.php index 7a17d60d2..33413e265 100644 --- a/src/Form/DataTransformer/Yaml.php +++ b/src/Form/DataTransformer/Yaml.php @@ -1,17 +1,25 @@ + */ class Yaml implements DataTransformerInterface { - public function transform($value) + public function transform(string $value): string { /* nothing to do here - only called before displaying values on FE */ return $value; } - public function reverseTransform($value) + public function reverseTransform(string $value): string { - return str_replace("\t", " ", $value); + return \str_replace("\t", " ", $value); } } diff --git a/src/Form/Element.php b/src/Form/Element.php index 5617ba573..c1f1dbc51 100644 --- a/src/Form/Element.php +++ b/src/Form/Element.php @@ -1,173 +1,113 @@ + * @author Dmitry Khomutov + */ abstract class Element { - /** - * @var string - */ - protected $name; - - /** - * @var string - */ - protected $id; - - /** - * @var string - */ - protected $label; - - /** - * @var string - */ - protected $class; - - /** - * @var string - */ - protected $containerClass; - - /** - * @var Element - */ - protected $parent; - - /** - * @param string|null $name - */ - public function __construct($name = null) + protected string $name = ''; + + protected string $id = ''; + + protected string $label = ''; + + protected string $class = ''; + + protected string $containerClass = ''; + + protected ?Element $parent = null; + + public function __construct(?string $name = null) { - if (!is_null($name)) { + if (!\is_null($name)) { $this->setName($name); } } - /** - * @return string - */ - public function getName() + public function getName(): string { return $this->name; } - /** - * @param string $name - * - * @return $this - */ - public function setName($name) + public function setName(string $name): self { - $this->name = strtolower(preg_replace('/([^a-zA-Z0-9_\-%])/', '', $name)); + $this->name = \strtolower(\preg_replace('/([^a-zA-Z0-9_\-%])/', '', $name)); return $this; } - /** - * @return string - */ - public function getId() + public function getId(): string { return !$this->id ? ('element-' . $this->name) : $this->id; } - /** - * @param string $id - * - * @return $this - */ - public function setId($id) + public function setId(string $id): self { $this->id = $id; return $this; } - /** - * @return string - */ - public function getLabel() + public function getLabel(): string { return $this->label; } - /** - * @param string $label - * - * @return $this - */ - public function setLabel($label) + public function setLabel(string $label): self { $this->label = $label; return $this; } - /** - * @return string - */ - public function getClass() + public function getClass(): string { return $this->class; } - /** - * @param string $class - * - * @return $this - */ - public function setClass($class) + public function setClass(string $class): self { $this->class = $class; return $this; } - /** - * @return string - */ - public function getContainerClass() + public function getContainerClass(): string { return $this->containerClass; } - /** - * @param string $class - * - * @return $this - */ - public function setContainerClass($class) + public function setContainerClass(string $class): self { $this->containerClass = $class; return $this; } - /** - * @return $this - */ - public function setParent(Element $parent) + public function setParent(Element $parent): self { $this->parent = $parent; return $this; } - /** - * @param string $viewFile - * - * @return string - */ - public function render($viewFile = null) + public function render(?string $viewFile = null): string { - if (is_null($viewFile)) { - $class = explode('\\', get_called_class()); - $viewFile = end($class); + if (\is_null($viewFile)) { + $class = \explode('\\', \get_called_class()); + $viewFile = \end($class); } $view = new View('Form/' . $viewFile); @@ -184,5 +124,5 @@ public function render($viewFile = null) return $view->render(); } - abstract protected function onPreRender(View &$view); + abstract protected function onPreRender(View &$view): void; } diff --git a/src/Form/Element/Button.php b/src/Form/Element/Button.php index 1e45c0ce3..8bbed1900 100644 --- a/src/Form/Element/Button.php +++ b/src/Form/Element/Button.php @@ -1,21 +1,27 @@ + * @author Dmitry Khomutov + */ class Button extends Input { - /** - * @return bool - */ - public function validate() + public function validate(): bool { return true; } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { parent::onPreRender($view); diff --git a/src/Form/Element/Checkbox.php b/src/Form/Element/Checkbox.php index 7a96921be..f6b432029 100644 --- a/src/Form/Element/Checkbox.php +++ b/src/Form/Element/Checkbox.php @@ -1,16 +1,22 @@ + * @author Dmitry Khomutov + */ class Checkbox extends Input { - /** - * @var bool - */ - protected $checked; + protected bool $checked = false; /** * @var mixed @@ -28,35 +34,39 @@ public function getCheckedValue() /** * @param mixed $value */ - public function setCheckedValue($value) + public function setCheckedValue($value): self { $this->checkedValue = $value; + + return $this; } /** * @param mixed $value */ - public function setValue($value) + public function setValue($value): self { - if (is_bool($value) && $value === true) { + if (\is_bool($value) && $value === true) { $this->value = $this->getCheckedValue(); $this->checked = true; - return; + return $this; } - if ($value == $this->getCheckedValue()) { + if ($value === $this->getCheckedValue()) { $this->value = $this->getCheckedValue(); $this->checked = true; - return; + return $this; } $this->value = $value; $this->checked = false; + + return $this; } - public function onPreRender(View &$view) + protected function onPreRender(View &$view): void { parent::onPreRender($view); diff --git a/src/Form/Element/CheckboxGroup.php b/src/Form/Element/CheckboxGroup.php index 272bfbac2..3e9d4be39 100644 --- a/src/Form/Element/CheckboxGroup.php +++ b/src/Form/Element/CheckboxGroup.php @@ -1,9 +1,18 @@ + * @author Dmitry Khomutov + */ class CheckboxGroup extends FieldSet { } diff --git a/src/Form/Element/Csrf.php b/src/Form/Element/Csrf.php index cf9a78c78..8e02e1c11 100644 --- a/src/Form/Element/Csrf.php +++ b/src/Form/Element/Csrf.php @@ -1,19 +1,34 @@ + * @author Dmitry Khomutov + */ class Csrf extends Hidden { - /** - * @return bool - */ - public function validate() + private Session $session; + + public function __construct(Session $session, ?string $name = null) + { + parent::__construct($name); + + $this->session = $session; + } + + public function validate(): bool { - $sessionToken = isset($_SESSION['csrf_tokens'][$this->getName()]) - ? $_SESSION['csrf_tokens'][$this->getName()] - : null; + $tokens = $this->session->get('csrf_tokens'); + $sessionToken = isset($tokens[$this->getName()]) ? $tokens[$this->getName()] : null; if ($this->value !== $sessionToken) { return false; @@ -22,16 +37,19 @@ public function validate() return true; } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { parent::onPreRender($view); $this->setValue( - rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=') + \rtrim(\strtr(\base64_encode(\random_bytes(32)), '+/', '-_'), '=') ); $view->value = $this->getValue(); - $_SESSION['csrf_tokens'][$this->getName()] = $this->getValue(); + $tokens = $this->session->get('csrf_tokens'); + $tokens[$this->getName()] = $this->getValue(); + + $this->session->set('csrf_tokens', $tokens); } } diff --git a/src/Form/Element/Email.php b/src/Form/Element/Email.php index 4f8dcaff7..f054b84c4 100644 --- a/src/Form/Element/Email.php +++ b/src/Form/Element/Email.php @@ -1,22 +1,26 @@ + * @author Dmitry Khomutov + */ class Email extends Text { - /** - * @param string $viewFile - * - * @return string - */ - public function render($viewFile = null) + public function render(?string $viewFile = null): string { return parent::render(($viewFile ? $viewFile : 'Text')); } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { parent::onPreRender($view); diff --git a/src/Form/Element/Hidden.php b/src/Form/Element/Hidden.php index 086042c14..1644c6ec4 100644 --- a/src/Form/Element/Hidden.php +++ b/src/Form/Element/Hidden.php @@ -1,9 +1,18 @@ + * @author Dmitry Khomutov + */ class Hidden extends Input { } diff --git a/src/Form/Element/Password.php b/src/Form/Element/Password.php index 9135aea13..356f23ec6 100644 --- a/src/Form/Element/Password.php +++ b/src/Form/Element/Password.php @@ -1,22 +1,26 @@ + * @author Dmitry Khomutov + */ class Password extends Text { - /** - * @param string $viewFile - * - * @return string - */ - public function render($viewFile = null) + public function render(?string $viewFile = null): string { return parent::render(($viewFile ? $viewFile : 'Text')); } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { parent::onPreRender($view); diff --git a/src/Form/Element/Radio.php b/src/Form/Element/Radio.php index 6c1e386b5..bd6f1b9a4 100644 --- a/src/Form/Element/Radio.php +++ b/src/Form/Element/Radio.php @@ -1,7 +1,16 @@ + * @author Dmitry Khomutov + */ class Radio extends Select { } diff --git a/src/Form/Element/Select.php b/src/Form/Element/Select.php index 7e0c6c530..a7a1b51fb 100644 --- a/src/Form/Element/Select.php +++ b/src/Form/Element/Select.php @@ -1,23 +1,31 @@ + * @author Dmitry Khomutov + */ class Select extends Input { - /** - * @var array - */ - protected $options = []; + protected array $options = []; - public function setOptions(array $options) + public function setOptions(array $options): self { $this->options = $options; + + return $this; } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { parent::onPreRender($view); diff --git a/src/Form/Element/Submit.php b/src/Form/Element/Submit.php index 38efbaffe..5375acb40 100644 --- a/src/Form/Element/Submit.php +++ b/src/Form/Element/Submit.php @@ -1,9 +1,18 @@ + * @author Dmitry Khomutov + */ class Submit extends Button { /** @@ -11,17 +20,12 @@ class Submit extends Button */ protected $value = 'Submit'; - /** - * @param string $viewFile - * - * @return string - */ - public function render($viewFile = null) + public function render(?string $viewFile = null): string { return parent::render(($viewFile ? $viewFile : 'Button')); } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { parent::onPreRender($view); diff --git a/src/Form/Element/Text.php b/src/Form/Element/Text.php index 14ca6bc38..ccc2bbc28 100644 --- a/src/Form/Element/Text.php +++ b/src/Form/Element/Text.php @@ -1,13 +1,22 @@ + * @author Dmitry Khomutov + */ class Text extends Input { - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { parent::onPreRender($view); diff --git a/src/Form/Element/TextArea.php b/src/Form/Element/TextArea.php index 3ebff2a2d..b603e4fc7 100644 --- a/src/Form/Element/TextArea.php +++ b/src/Form/Element/TextArea.php @@ -1,33 +1,35 @@ + * @author Dmitry Khomutov + */ class TextArea extends Text { - /** - * @var int - */ - protected $rows = 4; - - /** - * @return int - */ - public function getRows() + protected int $rows = 4; + + public function getRows(): int { return $this->rows; } - /** - * @param int $rows - */ - public function setRows($rows) + public function setRows(int $rows): self { $this->rows = $rows; + + return $this; } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { parent::onPreRender($view); diff --git a/src/Form/Element/Url.php b/src/Form/Element/Url.php index 6f192f127..d7e104f01 100644 --- a/src/Form/Element/Url.php +++ b/src/Form/Element/Url.php @@ -1,22 +1,29 @@ + * @author Dmitry Khomutov + */ class Url extends Text { /** * @param string $viewFile - * - * @return string */ - public function render($viewFile = null) + public function render(?string $viewFile = null): string { return parent::render(($viewFile ? $viewFile : 'Text')); } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { parent::onPreRender($view); diff --git a/src/Form/FieldSet.php b/src/Form/FieldSet.php index 40f08adbd..c9951a71a 100644 --- a/src/Form/FieldSet.php +++ b/src/Form/FieldSet.php @@ -1,20 +1,26 @@ + * @author Dmitry Khomutov + */ class FieldSet extends Element { /** * @var Element[] */ - protected $children = []; + protected array $children = []; - /** - * @return array - */ - public function getValues() + public function getValues(): array { $rtn = []; foreach ($this->children as $field) { @@ -22,7 +28,7 @@ public function getValues() $fieldName = $field->getName(); if (empty($fieldName)) { - $rtn = array_merge($rtn, $field->getValues()); + $rtn = \array_merge($rtn, $field->getValues()); } else { $rtn[$fieldName] = $field->getValues(); } @@ -36,7 +42,7 @@ public function getValues() return $rtn; } - public function setValues(array $values) + public function setValues(array $values): self { foreach ($this->children as $field) { if ($field instanceof FieldSet) { @@ -55,18 +61,19 @@ public function setValues(array $values) } } } + + return $this; } - public function addField(Element $field) + public function addField(Element $field): self { $this->children[$field->getName()] = $field; $field->setParent($this); + + return $this; } - /** - * @return bool - */ - public function validate() + public function validate(): bool { $rtn = true; @@ -79,7 +86,7 @@ public function validate() return $rtn; } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { $rendered = []; foreach ($this->children as $child) { @@ -89,20 +96,12 @@ protected function onPreRender(View &$view) $view->children = $rendered; } - /** - * @return Element[] - */ - public function getChildren() + public function getChildren(): array { return $this->children; } - /** - * @param string $fieldName - * - * @return Element - */ - public function getChild($fieldName) + public function getChild(string $fieldName): Element { return $this->children[$fieldName]; } diff --git a/src/Form/Input.php b/src/Form/Input.php index ddba1558a..1cd182217 100644 --- a/src/Form/Input.php +++ b/src/Form/Input.php @@ -1,5 +1,7 @@ + * @author Dmitry Khomutov + */ class Input extends Element { - /** - * @var bool - */ - protected $required = false; + protected bool $required = false; - /** - * @var string - */ - protected $pattern; + protected string $pattern = ''; /** * @var callable @@ -29,29 +32,19 @@ class Input extends Element */ protected $value; - /** - * @var string - */ - protected $error; + protected string $error = ''; - /** - * @var bool - */ - protected $customError = false; + protected bool $customError = false; - /** @var DataTransformerInterface */ - protected $dataTransformator; + protected ?DataTransformerInterface $dataTransformer = null; /** - * @param string $name - * @param string $label - * @param bool $required - * * @return static */ - public static function create($name, $label, $required = false) + public static function create(string $name, string $label, bool $required = false): self { $el = new static(); + $el->setName($name); $el->setLabel($label); $el->setRequired($required); @@ -64,8 +57,8 @@ public static function create($name, $label, $required = false) */ public function getValue() { - if (!empty($this->getDataTransformator())) { - return $this->getDataTransformator()->reverseTransform($this->value); + if (!empty($this->getDataTransformer())) { + return $this->getDataTransformer()->reverseTransform((string)$this->value); } return $this->value; @@ -76,10 +69,10 @@ public function getValue() * * @return $this */ - public function setValue($value) + public function setValue($value): self { - if (!empty($this->getDataTransformator())) { - $this->value = $this->getDataTransformator()->transform($value); + if (!empty($this->getDataTransformer())) { + $this->value = $this->getDataTransformer()->transform($value); } else { $this->value = $value; } @@ -87,22 +80,14 @@ public function setValue($value) return $this; } - /** - * @return bool - */ - public function getRequired() + public function getRequired(): bool { return $this->required; } - /** - * @param bool $required - * - * @return $this - */ - public function setRequired($required) + public function setRequired(bool $required): self { - $this->required = (bool)$required; + $this->required = $required; return $this; } @@ -115,44 +100,28 @@ public function getValidator() return $this->validator; } - /** - * @param callable $validator - * - * @return $this - */ - public function setValidator($validator) + public function setValidator($validator): self { - if (is_callable($validator) || $validator instanceof Closure) { + if (\is_callable($validator) || $validator instanceof Closure) { $this->validator = $validator; } return $this; } - /** - * @return string - */ - public function getPattern() + public function getPattern(): string { return $this->pattern; } - /** - * @param string $pattern - * - * @return $this - */ - public function setPattern($pattern) + public function setPattern(string $pattern): self { $this->pattern = $pattern; return $this; } - /** - * @return bool - */ - public function validate() + public function validate(): bool { if ($this->getRequired() && empty($this->getValue())) { $this->error = $this->getLabel() . ' is required.'; @@ -160,7 +129,7 @@ public function validate() return false; } - if ($this->getPattern() && !preg_match('/' . $this->getPattern() . '/', $this->getValue())) { + if ($this->getPattern() && !\preg_match('#' . $this->getPattern() . '#', (string)$this->getValue())) { $this->error = 'Invalid value entered.'; return false; @@ -168,10 +137,10 @@ public function validate() $validator = $this->getValidator(); - if (is_callable($validator)) { + if (\is_callable($validator)) { try { - call_user_func_array($validator, [$this->getValue()]); - } catch (Exception $ex) { + \call_user_func_array($validator, [$this->getValue()]); + } catch (\Throwable $ex) { $this->error = $ex->getMessage(); return false; @@ -185,12 +154,7 @@ public function validate() return true; } - /** - * @param string $message - * - * @return $this - */ - public function setError($message) + public function setError(string $message): self { $this->customError = true; $this->error = $message; @@ -198,7 +162,7 @@ public function setError($message) return $this; } - protected function onPreRender(View &$view) + protected function onPreRender(View &$view): void { $view->value = $this->getValue(); $view->error = $this->error; @@ -206,16 +170,15 @@ protected function onPreRender(View &$view) $view->required = $this->required; } - /** - * @return DataTransformerInterface - */ - public function getDataTransformator() + public function getDataTransformer(): ?DataTransformerInterface { - return $this->dataTransformator; + return $this->dataTransformer; } - public function setDataTransformator(DataTransformerInterface $dataTransformator) + public function setDataTransformer(DataTransformerInterface $dataTransformer): self { - $this->dataTransformator = $dataTransformator; + $this->dataTransformer = $dataTransformer; + + return $this; } } diff --git a/src/Form/Validator/ValidatorInterface.php b/src/Form/Validator/ValidatorInterface.php index 6e10c277f..270a4247e 100644 --- a/src/Form/Validator/ValidatorInterface.php +++ b/src/Form/Validator/ValidatorInterface.php @@ -1,8 +1,16 @@ + */ interface ValidatorInterface { - public function __invoke($value); + public function __invoke($value): bool; } diff --git a/src/Form/Validator/Yaml.php b/src/Form/Validator/Yaml.php index 469b24554..c60eca074 100644 --- a/src/Form/Validator/Yaml.php +++ b/src/Form/Validator/Yaml.php @@ -1,28 +1,35 @@ + */ class Yaml implements ValidatorInterface { - /** @var Parser */ - protected $parser; + protected ?Parser $parser = null; - public function __invoke($value) + public function __invoke($value): bool { try { $this->getParser()->parse($value); } catch (ParseException $e) { - throw new Exception($e->getMessage()); + throw new RuntimeException($e->getMessage()); } return true; } - public function getParser() + public function getParser(): Parser { if (!$this->parser) { $this->parser = new Parser(); diff --git a/src/Helper/AnsiConverter.php b/src/Helper/AnsiConverter.php deleted file mode 100644 index 850a7aa01..000000000 --- a/src/Helper/AnsiConverter.php +++ /dev/null @@ -1,46 +0,0 @@ -convert($text); - } - - /** - * Do not instantiate this class. - */ - private function __construct() - { - } -} diff --git a/src/Helper/Bitbucket.php b/src/Helper/Bitbucket.php index c20f7192f..2f8467ed6 100644 --- a/src/Helper/Bitbucket.php +++ b/src/Helper/Bitbucket.php @@ -3,13 +3,26 @@ namespace PHPCensor\Helper; use GuzzleHttp\Client; -use PHPCensor\Config; +use PHPCensor\Common\Application\ConfigurationInterface; +use PHPCensor\Model\Build; /** * The Bitbucket Helper class provides some Bitbucket API call functionality. + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class Bitbucket { + private ConfigurationInterface $configuration; + + public function __construct(ConfigurationInterface $configuration) + { + $this->configuration = $configuration; + } + /** * Create a comment on a specific file (and commit) in a Bitbucket Pull Request. * @@ -24,8 +37,8 @@ class Bitbucket */ public function createPullRequestComment($repo, $pullId, $commitId, $file, $line, $comment) { - $username = Config::getInstance()->get('php-censor.bitbucket.username'); - $appPassword = Config::getInstance()->get('php-censor.bitbucket.app_password'); + $username = $this->configuration->get('php-censor.bitbucket.username'); + $appPassword = $this->configuration->get('php-censor.bitbucket.app_password'); if (empty($username) || empty($appPassword)) { return; @@ -35,7 +48,7 @@ public function createPullRequestComment($repo, $pullId, $commitId, $file, $line $data = [ 'content' => $comment, - 'anchor' => substr($commitId, 0, 12), + 'anchor' => \substr($commitId, 0, 12), 'filename' => $file, ]; if ($line > 0) { @@ -60,8 +73,8 @@ public function createPullRequestComment($repo, $pullId, $commitId, $file, $line */ public function createCommitComment($repo, $commitId, $file, $line, $comment) { - $username = Config::getInstance()->get('php-censor.bitbucket.username'); - $appPassword = Config::getInstance()->get('php-censor.bitbucket.app_password'); + $username = $this->configuration->get('php-censor.bitbucket.username'); + $appPassword = $this->configuration->get('php-censor.bitbucket.app_password'); if (empty($username) || empty($appPassword)) { return; @@ -97,8 +110,8 @@ public function createCommitComment($repo, $commitId, $file, $line, $comment) */ public function getPullRequestDiff($repo, $pullRequestId) { - $username = Config::getInstance()->get('php-censor.bitbucket.username'); - $appPassword = Config::getInstance()->get('php-censor.bitbucket.app_password'); + $username = $this->configuration->get('php-censor.bitbucket.username'); + $appPassword = $this->configuration->get('php-censor.bitbucket.app_password'); if (empty($username) || empty($appPassword)) { return; @@ -112,4 +125,20 @@ public function getPullRequestDiff($repo, $pullRequestId) return (string)$response->getBody(); } + + public function getFileLinkTemplate(Build $build): string + { + $reference = $build->getProject()->getReference(); + if (\in_array($build->getSource(), Build::$pullRequestSources, true)) { + $reference = $build->getExtra('remote_reference'); + } + + $link = 'https://bitbucket.org/' . $reference . '/'; + + $link .= 'src/' . $build->getCommitId() . '/'; + $link .= '{FILE}'; + $link .= '#{BASEFILE}-{LINE}'; + + return $link; + } } diff --git a/src/Helper/Branch.php b/src/Helper/Branch.php index 3c988f95b..78ed8b2fb 100644 --- a/src/Helper/Branch.php +++ b/src/Helper/Branch.php @@ -1,12 +1,20 @@ + */ class Branch { - public static function getDefaultBranchName($projectType) + public static function getDefaultBranchName(string $projectType): string { switch ($projectType) { case Project::TYPE_HG: diff --git a/src/Helper/BuildInterpolator.php b/src/Helper/BuildInterpolator.php index ff2889d1c..ef8db63e4 100644 --- a/src/Helper/BuildInterpolator.php +++ b/src/Helper/BuildInterpolator.php @@ -1,14 +1,21 @@ */ class BuildInterpolator { @@ -20,22 +27,30 @@ class BuildInterpolator * * @see setupInterpolationVars() */ - protected $interpolationVars = []; + private array $interpolationVars = []; + + private EnvironmentStore $environmentStore; + private SecretStore $secretStore; + + public function __construct( + EnvironmentStore $environmentStore, + SecretStore $secretStore + ) { + $this->environmentStore = $environmentStore; + $this->secretStore = $secretStore; + } /** * Sets the variables that will be used for interpolation. * - * @param string $url - * @param string $applicationVersion - * * @throws Exception */ - public function setupInterpolationVars(BaseBuild $build, $url, $applicationVersion) + public function setupInterpolationVars(Build $build, string $url, string $applicationVersion): void { $this->interpolationVars = []; $this->interpolationVars['%COMMIT_ID%'] = $build->getCommitId(); - $this->interpolationVars['%SHORT_COMMIT_ID%'] = substr($build->getCommitId(), 0, 7); + $this->interpolationVars['%SHORT_COMMIT_ID%'] = \substr((string)$build->getCommitId(), 0, 7); $this->interpolationVars['%PROJECT_ID%'] = $build->getProjectId(); $this->interpolationVars['%BUILD_ID%'] = $build->getId(); $this->interpolationVars['%COMMITTER_EMAIL%'] = $build->getCommitterEmail(); @@ -51,9 +66,7 @@ public function setupInterpolationVars(BaseBuild $build, $url, $applicationVersi $environmentId = $build->getEnvironmentId(); $environment = null; if ($environmentId) { - /** @var EnvironmentStore $environmentStore */ - $environmentStore = Factory::getStore('Environment'); - $environmentObject = $environmentStore->getById($environmentId); + $environmentObject = $this->environmentStore->getById($environmentId); if ($environmentObject) { $environment = $environmentObject->getName(); } @@ -62,34 +75,45 @@ public function setupInterpolationVars(BaseBuild $build, $url, $applicationVersi $this->interpolationVars['%ENVIRONMENT%'] = $environment; $this->interpolationVars['%SYSTEM_VERSION%'] = $applicationVersion; - putenv('PHP_CENSOR=1'); - putenv('PHP_CENSOR_COMMIT_ID=' . $this->interpolationVars['%COMMIT_ID%']); - putenv('PHP_CENSOR_SHORT_COMMIT_ID=' . $this->interpolationVars['%SHORT_COMMIT_ID%']); - putenv('PHP_CENSOR_COMMITTER_EMAIL=' . $this->interpolationVars['%COMMITTER_EMAIL%']); - putenv('PHP_CENSOR_COMMIT_MESSAGE=' . $this->interpolationVars['%COMMIT_MESSAGE%']); - putenv('PHP_CENSOR_COMMIT_LINK=' . $this->interpolationVars['%COMMIT_LINK%']); - putenv('PHP_CENSOR_PROJECT_ID=' . $this->interpolationVars['%PROJECT_ID%']); - putenv('PHP_CENSOR_PROJECT_TITLE=' . $this->interpolationVars['%PROJECT_TITLE%']); - putenv('PHP_CENSOR_PROJECT_LINK=' . $this->interpolationVars['%PROJECT_LINK%']); - putenv('PHP_CENSOR_BUILD_ID=' . $this->interpolationVars['%BUILD_ID%']); - putenv('PHP_CENSOR_BUILD_PATH=' . $this->interpolationVars['%BUILD_PATH%']); - putenv('PHP_CENSOR_BUILD_LINK=' . $this->interpolationVars['%BUILD_LINK%']); - putenv('PHP_CENSOR_BRANCH=' . $this->interpolationVars['%BRANCH%']); - putenv('PHP_CENSOR_BRANCH_LINK=' . $this->interpolationVars['%BRANCH_LINK%']); - putenv('PHP_CENSOR_ENVIRONMENT=' . $this->interpolationVars['%ENVIRONMENT%']); - putenv('PHP_CENSOR_SYSTEM_VERSION=' . $this->interpolationVars['%SYSTEM_VERSION%']); + \putenv('PHP_CENSOR=1'); + \putenv('PHP_CENSOR_COMMIT_ID=' . $this->interpolationVars['%COMMIT_ID%']); + \putenv('PHP_CENSOR_SHORT_COMMIT_ID=' . $this->interpolationVars['%SHORT_COMMIT_ID%']); + \putenv('PHP_CENSOR_COMMITTER_EMAIL=' . $this->interpolationVars['%COMMITTER_EMAIL%']); + \putenv('PHP_CENSOR_COMMIT_MESSAGE=' . $this->interpolationVars['%COMMIT_MESSAGE%']); + \putenv('PHP_CENSOR_COMMIT_LINK=' . $this->interpolationVars['%COMMIT_LINK%']); + \putenv('PHP_CENSOR_PROJECT_ID=' . $this->interpolationVars['%PROJECT_ID%']); + \putenv('PHP_CENSOR_PROJECT_TITLE=' . $this->interpolationVars['%PROJECT_TITLE%']); + \putenv('PHP_CENSOR_PROJECT_LINK=' . $this->interpolationVars['%PROJECT_LINK%']); + \putenv('PHP_CENSOR_BUILD_ID=' . $this->interpolationVars['%BUILD_ID%']); + \putenv('PHP_CENSOR_BUILD_PATH=' . $this->interpolationVars['%BUILD_PATH%']); + \putenv('PHP_CENSOR_BUILD_LINK=' . $this->interpolationVars['%BUILD_LINK%']); + \putenv('PHP_CENSOR_BRANCH=' . $this->interpolationVars['%BRANCH%']); + \putenv('PHP_CENSOR_BRANCH_LINK=' . $this->interpolationVars['%BRANCH_LINK%']); + \putenv('PHP_CENSOR_ENVIRONMENT=' . $this->interpolationVars['%ENVIRONMENT%']); + \putenv('PHP_CENSOR_SYSTEM_VERSION=' . $this->interpolationVars['%SYSTEM_VERSION%']); } - /** - * @param string $input - * - * @return string - */ - private function realtimeInterpolate($input) + private function realtimeInterpolate(string $input): string { - $input = str_replace('%CURRENT_DATE%', \date('Y-m-d'), $input); - $input = str_replace('%CURRENT_TIME%', \date('H-i-s'), $input); - $input = str_replace('%CURRENT_DATETIME%', \date('Y-m-d_H-i-s'), $input); + $input = \str_replace('%CURRENT_DATE%', \date('Y-m-d'), $input); + $input = \str_replace('%CURRENT_TIME%', \date('H-i-s'), $input); + + return \str_replace('%CURRENT_DATETIME%', \date('Y-m-d_H-i-s'), $input); + } + + private function secretInterpolate(string $input): string + { + \preg_match_all('#%SECRET:([-_\w\d]+)?%#', $input, $matches); + if (!empty($matches[0])) { + $secrets = $this->secretStore->getByNames($matches[1]); + $finalSecrets = []; + + foreach ($matches[0] as $index => $match) { + $finalSecrets[$index] = $secrets[$matches[1][$index]]->getValue(); + } + + $input = \str_replace($matches[0], $finalSecrets, $input); + } return $input; } @@ -97,18 +121,17 @@ private function realtimeInterpolate($input) /** * Replace every occurrence of the interpolation vars in the given string * Example: "This is build %BUILD_ID%" => "This is build 182" - * - * @param string $input - * - * @return string */ - public function interpolate($input) + public function interpolate(string $input, bool $useSecrets = false): string { + if ($useSecrets) { + $input = $this->secretInterpolate($input); + } $input = $this->realtimeInterpolate($input); - $keys = array_keys($this->interpolationVars); - $values = array_values($this->interpolationVars); + $keys = \array_keys($this->interpolationVars); + $values = \array_values($this->interpolationVars); - return str_replace($keys, $values, $input); + return \str_replace($keys, $values, $input); } } diff --git a/src/Helper/CommandExecutor.php b/src/Helper/CommandExecutor.php index fd1f3b031..b240a27ef 100644 --- a/src/Helper/CommandExecutor.php +++ b/src/Helper/CommandExecutor.php @@ -2,12 +2,17 @@ namespace PHPCensor\Helper; -use Exception; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Logging\BuildLogger; use Symfony\Component\Process\Process; /** * Handles running system commands with variables. + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class CommandExecutor implements CommandExecutorInterface { @@ -24,7 +29,7 @@ class CommandExecutor implements CommandExecutorInterface /** * @var array */ - protected $lastOutput; + protected $lastOutput = []; /** * @var string @@ -52,19 +57,15 @@ class CommandExecutor implements CommandExecutorInterface /** * Commands with no proper exit mechanism - * - * @var array */ - private static $noExitCommands = [ + private static array $noExitCommands = [ 'codecept', ]; /** * Environment variables that should not be inherited - * - * @var array */ - private static $blacklistEnvVars = [ + private static array $blacklistEnvVars = [ 'PHP_SELF', 'SCRIPT_NAME', 'SCRIPT_FILENAME', @@ -81,7 +82,6 @@ public function __construct(BuildLogger $logger, $rootDir, $verbose = false) { $this->logger = $logger; $this->verbose = $verbose; - $this->lastOutput = []; $this->rootDir = $rootDir; } @@ -96,7 +96,7 @@ public function executeCommand($args = []) { $this->lastOutput = []; - $this->logger->logDebug('Args: ' . json_encode($args)); + $this->logger->logDebug('Args: ' . \json_encode($args)); $command = \call_user_func_array('sprintf', $args); @@ -104,14 +104,15 @@ public function executeCommand($args = []) $withNoExit = ''; foreach (self::$noExitCommands as $nec) { - if (preg_match("/\b{$nec}\b/", $command)) { + if (\preg_match("/\b{$nec}\b/", $command)) { $withNoExit = $nec; + break; } } $cwd = RUNTIME_DIR . 'builds'; - if ($this->buildPath && file_exists($this->buildPath)) { + if ($this->buildPath && \file_exists($this->buildPath)) { $cwd = $this->buildPath; } @@ -125,14 +126,12 @@ public function executeCommand($args = []) $this->logger->logDebug("Assuming command '{$withNoExit}' does not exit properly"); do { - sleep(15); + \sleep(15); $response = []; - exec("ps auxww | grep '{$withNoExit}' | grep -v grep", $response); - $response = array_filter( + \exec("ps auxww | grep '{$withNoExit}' | grep -v grep", $response); + $response = \array_filter( $response, - function ($a) { - return strpos($a, $this->buildPath) !== false; - } + fn ($a) => \strpos($a, $this->buildPath) !== false ); } while (!empty($response)); $process->stop(); @@ -146,7 +145,7 @@ function ($a) { $lastOutput = $this->replaceIllegalCharacters($process->getOutput()); $lastError = $this->replaceIllegalCharacters($process->getErrorOutput()); - $this->lastOutput = array_filter(explode(PHP_EOL, $lastOutput)); + $this->lastOutput = \array_filter(\explode(PHP_EOL, $lastOutput)); $this->lastError = $lastError; $shouldOutput = ($this->logExecOutput && ($this->verbose || 0 !== $status)); @@ -176,12 +175,12 @@ function ($a) { */ public function replaceIllegalCharacters($utf8String) { - mb_substitute_character(0xFFFD); // is '�' - $legalUtf8String = mb_convert_encoding($utf8String, 'utf8', 'utf8'); + \mb_substitute_character(0xFFFD); // is '�' + $legalUtf8String = \mb_convert_encoding($utf8String, 'utf8', 'utf8'); $regexp = '/[\x00-\x08\x10\x0B\x0C\x0E-\x19\x7F]' . '|[^\x{0}-\x{ffff}]/u'; // more than 3 byte UTF-8 sequences (unsupported in mysql) - return preg_replace($regexp, '�', $legalUtf8String); + return \preg_replace($regexp, '�', $legalUtf8String); } /** @@ -191,7 +190,7 @@ public function replaceIllegalCharacters($utf8String) */ public function getLastOutput() { - return implode(PHP_EOL, $this->lastOutput); + return \implode(PHP_EOL, $this->lastOutput); } /** @@ -212,8 +211,8 @@ public function getLastError() */ protected function findBinaryByPath($binaryPath, $binary) { - if (is_dir($binaryPath) && is_file($binaryPath . '/' . $binary)) { - $this->logger->logDebug(sprintf('Found in %s (binary_path): %s', $binaryPath, $binary)); + if (\is_dir($binaryPath) && \is_file($binaryPath . '/' . $binary)) { + $this->logger->logDebug(\sprintf('Found in %s (binary_path): %s', $binaryPath, $binary)); return $binaryPath . '/' . $binary; } @@ -229,8 +228,8 @@ protected function findBinaryByPath($binaryPath, $binary) */ protected function findBinaryLocal($composerBin, $binary) { - if (is_dir($composerBin) && is_file($composerBin . '/' . $binary)) { - $this->logger->logDebug(sprintf('Found in %s (local): %s', $composerBin, $binary)); + if (\is_dir($composerBin) && \is_file($composerBin . '/' . $binary)) { + $this->logger->logDebug(\sprintf('Found in %s (local): %s', $composerBin, $binary)); return $composerBin . '/' . $binary; } @@ -245,8 +244,8 @@ protected function findBinaryLocal($composerBin, $binary) */ protected function findBinaryGlobal($binary) { - if (is_file($this->rootDir . 'vendor/bin/' . $binary)) { - $this->logger->logDebug(sprintf('Found in %s (global): %s', 'vendor/bin', $binary)); + if (\is_file($this->rootDir . 'vendor/bin/' . $binary)) { + $this->logger->logDebug(\sprintf('Found in %s (global): %s', 'vendor/bin', $binary)); return $this->rootDir . 'vendor/bin/' . $binary; } @@ -263,9 +262,9 @@ protected function findBinaryGlobal($binary) */ protected function findBinarySystem($binary) { - $tempBinary = trim(shell_exec('which ' . $binary)); - if (is_file($tempBinary)) { - $this->logger->logDebug(sprintf('Found in %s (system): %s', '', $binary)); + $tempBinary = \trim(\shell_exec('which ' . $binary)); + if (\is_file($tempBinary)) { + $this->logger->logDebug(\sprintf('Found in %s (system): %s', '', $binary)); return $tempBinary; } @@ -280,16 +279,16 @@ public function findBinary($binary, $priorityPath = 'local', $binaryPath = '', $ { $composerBin = $this->getComposerBinDir($this->buildPath); - if (is_string($binary)) { + if (\is_string($binary)) { $binary = [$binary]; } if ($binaryName) { - array_unshift($binary, ...$binaryName); + \array_unshift($binary, ...$binaryName); } foreach ($binary as $bin) { - $this->logger->logDebug(sprintf('Looking for binary: %s, priority = %s', $bin, $priorityPath)); + $this->logger->logDebug(\sprintf('Looking for binary: %s, priority = %s', $bin, $priorityPath)); if ('binary_path' === $priorityPath) { if ($existedBinary = $this->findBinaryByPath($binaryPath, $bin)) { @@ -358,7 +357,7 @@ public function findBinary($binary, $priorityPath = 'local', $binaryPath = '', $ } } - throw new Exception(sprintf('Could not find %s', implode('/', $binary))); + throw new RuntimeException(\sprintf('Could not find %s', \implode('/', $binary))); } /** @@ -371,14 +370,14 @@ public function findBinary($binary, $priorityPath = 'local', $binaryPath = '', $ */ public function getComposerBinDir($path) { - if (is_dir($path)) { + if (\is_dir($path)) { $composer = $path . '/composer.json'; - if (is_file($composer)) { - $json = json_decode(file_get_contents($composer)); + if (\is_file($composer)) { + $json = \json_decode(\file_get_contents($composer)); if (isset($json->config->{"bin-dir"})) { return $path . '/' . $json->config->{"bin-dir"}; - } elseif (is_dir($path . '/vendor/bin')) { + } elseif (\is_dir($path . '/vendor/bin')) { return $path . '/vendor/bin'; } } @@ -405,41 +404,41 @@ private function getDefaultEnv() $env = []; foreach ($_SERVER as $k => $v) { - if (in_array($k, self::$blacklistEnvVars, true)) { + if (\in_array($k, self::$blacklistEnvVars, true)) { continue; } - if (is_string($v) && false !== $v = getenv($k)) { + if (\is_string($v) && false !== $v = \getenv($k)) { $env[$k] = $v; } } foreach ($_ENV as $k => $v) { - if (in_array($k, self::$blacklistEnvVars, true)) { + if (\in_array($k, self::$blacklistEnvVars, true)) { continue; } - if (is_string($v)) { + if (\is_string($v)) { $env[$k] = $v; } } if (PHP_MAJOR_VERSION >= 7 && PHP_MINOR_VERSION >= 1) { - foreach (getenv() as $k => $v) { - if (in_array($k, self::$blacklistEnvVars, true)) { + foreach (\getenv() as $k => $v) { + if (\in_array($k, self::$blacklistEnvVars, true)) { continue; } - if (is_string($v)) { + if (\is_string($v)) { $env[$k] = $v; } } } else { $output = []; - exec('env', $output); + \exec('env', $output); foreach ($output as $o) { - $keyval = explode('=', $o, 2); - if (count($keyval) < 2 || empty($keyval[1])) { + $keyval = \explode('=', $o, 2); + if (\count($keyval) < 2 || empty($keyval[1])) { continue; } - if (in_array($keyval[0], self::$blacklistEnvVars, true)) { + if (\in_array($keyval[0], self::$blacklistEnvVars, true)) { continue; } $env[$keyval[0]] = $keyval[1]; diff --git a/src/Helper/CommandExecutorInterface.php b/src/Helper/CommandExecutorInterface.php index 379b21b33..3f72078cf 100644 --- a/src/Helper/CommandExecutorInterface.php +++ b/src/Helper/CommandExecutorInterface.php @@ -4,6 +4,12 @@ use Exception; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov + */ interface CommandExecutorInterface { /** diff --git a/src/Helper/Diff.php b/src/Helper/Diff.php deleted file mode 100644 index 97e393ae9..000000000 --- a/src/Helper/Diff.php +++ /dev/null @@ -1,50 +0,0 @@ - */ class Email { @@ -20,7 +25,7 @@ class Email protected $isHtml = false; protected $config; - public function __construct(Config $config) + public function __construct(ConfigurationInterface $config) { $this->config = $config; } @@ -99,13 +104,13 @@ public function getFrom() self::DEFAULT_FROM ); - if (strpos($from, '<') === false) { - return [(string)trim($from) => 'PHP Censor']; + if (\strpos($from, '<') === false) { + return [(string)\trim($from) => 'PHP Censor']; } - preg_match('#^(.*?)<(.*?)>$#ui', $from, $fromParts); + \preg_match('#^(.*?)<(.*?)>$#ui', $from, $fromParts); - return [trim($fromParts[2]) => trim($fromParts[1])]; + return [\trim($fromParts[2]) => \trim($fromParts[1])]; } /** @@ -118,7 +123,7 @@ public function send(Builder $builder = null) { $smtpServer = $this->config->get('php-censor.email_settings.smtp_address'); if (null !== $builder) { - $builder->logDebug(sprintf("SMTP: '%s'", !empty($smtpServer) ? 'true' : 'false')); + $builder->logDebug(\sprintf("SMTP: '%s'", !empty($smtpServer) ? 'true' : 'false')); } $factory = new MailerFactory($this->config->get('php-censor')); @@ -134,15 +139,15 @@ public function send(Builder $builder = null) $message->setContentType('text/html'); } - if (is_array($this->emailCc) && count($this->emailCc)) { + if (\is_array($this->emailCc) && \count($this->emailCc)) { $message->setCc($this->emailCc); } - ob_start(); + \ob_start(); $result = $mailer->send($message); - $rawOutput = ob_get_clean(); + $rawOutput = \ob_get_clean(); if ($rawOutput) { $builder->getBuildLogger()->logWarning($rawOutput); diff --git a/src/Helper/Github.php b/src/Helper/Github.php index b1f09108d..e163e1be9 100644 --- a/src/Helper/Github.php +++ b/src/Helper/Github.php @@ -3,26 +3,38 @@ namespace PHPCensor\Helper; use GuzzleHttp\Client; -use PHPCensor\Config; +use PHPCensor\Common\Application\ConfigurationInterface; /** * The Github Helper class provides some Github API call functionality. + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class Github { + private ConfigurationInterface $configuration; + + public function __construct(ConfigurationInterface $configuration) + { + $this->configuration = $configuration; + } + /** * Create a comment on a specific file (and commit) in a Github Pull Request. * @return null */ public function createPullRequestComment($repo, $pullId, $commitId, $file, $line, $comment) { - $token = Config::getInstance()->get('php-censor.github.token'); + $token = $this->configuration->get('php-censor.github.token'); if (!$token) { return null; } - $url = '/repos/' . strtolower($repo) . '/pulls/' . $pullId . '/comments'; + $url = '/repos/' . \strtolower($repo) . '/pulls/' . $pullId . '/comments'; $params = [ 'body' => $comment, @@ -34,7 +46,7 @@ public function createPullRequestComment($repo, $pullId, $commitId, $file, $line $client = new Client(); $client->post(('https://api.github.com' . $url), [ 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode($token . ':x-oauth-basic'), + 'Authorization' => 'Basic ' . \base64_encode($token . ':x-oauth-basic'), 'Content-Type' => 'application/x-www-form-urlencoded' ], 'json' => $params, @@ -47,13 +59,13 @@ public function createPullRequestComment($repo, $pullId, $commitId, $file, $line */ public function createCommitComment($repo, $commitId, $file, $line, $comment) { - $token = Config::getInstance()->get('php-censor.github.token'); + $token = $this->configuration->get('php-censor.github.token'); if (!$token) { return null; } - $url = '/repos/' . strtolower($repo) . '/commits/' . $commitId . '/comments'; + $url = '/repos/' . \strtolower($repo) . '/commits/' . $commitId . '/comments'; $params = [ 'body' => $comment, @@ -64,7 +76,7 @@ public function createCommitComment($repo, $commitId, $file, $line, $comment) $client = new Client(); $client->post(('https://api.github.com' . $url), [ 'headers' => [ - 'Authorization' => 'Basic ' . base64_encode($token . ':x-oauth-basic'), + 'Authorization' => 'Basic ' . \base64_encode($token . ':x-oauth-basic'), 'Content-Type' => 'application/x-www-form-urlencoded' ], 'json' => $params, diff --git a/src/Helper/Lang.php b/src/Helper/Lang.php index 64526670a..6dde2631a 100644 --- a/src/Helper/Lang.php +++ b/src/Helper/Lang.php @@ -2,12 +2,17 @@ namespace PHPCensor\Helper; -use PHPCensor\Config; -use PHPCensor\Store\Factory; +use PHPCensor\Common\Application\ConfigurationInterface; use PHPCensor\Store\UserStore; +use PHPCensor\StoreRegistry; /** * Languages Helper Class - Handles loading strings files and the strings within them. + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class Lang { @@ -43,14 +48,14 @@ class Lang public static function get(...$params) { $string = $params[0]; - if (array_key_exists($string, self::$strings)) { + if (\array_key_exists($string, self::$strings)) { $params[0] = self::$strings[$string]; - return call_user_func_array('sprintf', $params); - } elseif (self::DEFAULT_LANGUAGE !== self::$language && array_key_exists($string, self::$defaultStrings)) { + return \call_user_func_array('sprintf', $params); + } elseif (self::DEFAULT_LANGUAGE !== self::$language && \array_key_exists($string, self::$defaultStrings)) { $params[0] = self::$defaultStrings[$string]; - return call_user_func_array('sprintf', $params); + return \call_user_func_array('sprintf', $params); } return $string; @@ -74,7 +79,7 @@ public static function getLanguage() */ public static function setLanguage($language) { - if (in_array($language, self::$languages, true)) { + if (\in_array($language, self::$languages, true)) { self::$language = $language; self::$strings = self::loadLanguage(); @@ -114,11 +119,13 @@ public static function getStrings() /** * Initialise the Language helper, try load the language file for the user's browser or the configured default. - * - * @param string $languageForce */ - public static function init(Config $config, $languageForce = null) - { + public static function init( + ConfigurationInterface $config, + StoreRegistry $storeRegistry, + ?string $languageForce = null, + ?int $sessionUserId = null + ) { self::$defaultStrings = self::loadLanguage(self::DEFAULT_LANGUAGE); self::loadAvailableLanguages(); @@ -127,10 +134,10 @@ public static function init(Config $config, $languageForce = null) } $user = null; - if (!empty($_SESSION['php-censor-user-id'])) { + if (!empty($sessionUserId)) { /** @var UserStore $userStore */ - $userStore = Factory::getStore('User'); - $user = $userStore->getById($_SESSION['php-censor-user-id']); + $userStore = $storeRegistry->get('User'); + $user = $userStore->getById($sessionUserId); } if ($user) { @@ -162,12 +169,12 @@ protected static function loadLanguage($language = null) $langFile = SRC_DIR . 'Languages/lang.' . $language . '.php'; - if (!file_exists($langFile)) { + if (!\file_exists($langFile)) { return null; } $strings = include($langFile); - if (is_null($strings) || !is_array($strings) || !count($strings)) { + if (\is_null($strings) || !\is_array($strings) || !\count($strings)) { return null; } @@ -180,8 +187,8 @@ protected static function loadLanguage($language = null) protected static function loadAvailableLanguages() { $matches = []; - foreach (glob(SRC_DIR . 'Languages/lang.*.php') as $file) { - if (preg_match('/lang\.([a-z]{2}\-?[a-z]*)\.php/', $file, $matches)) { + foreach (\glob(SRC_DIR . 'Languages/lang.*.php') as $file) { + if (\preg_match('/lang\.([a-z]{2}\-?[a-z]*)\.php/', $file, $matches)) { self::$languages[] = $matches[1]; } } diff --git a/src/Helper/MailerFactory.php b/src/Helper/MailerFactory.php index 661c0e37d..fdff20a43 100644 --- a/src/Helper/MailerFactory.php +++ b/src/Helper/MailerFactory.php @@ -8,6 +8,11 @@ /** * Class MailerFactory helps to set up and configure a SwiftMailer object. + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class MailerFactory { @@ -22,7 +27,7 @@ class MailerFactory */ public function __construct($config = []) { - if (!is_array($config)) { + if (!\is_array($config)) { $config = []; } @@ -69,14 +74,12 @@ public function getMailConfig($configName) return $this->emailConfig[$configName]; } else { switch ($configName) { - case 'smtp_address': - return ''; case 'default_mailto_address': + case 'smtp_encryption': return null; case 'smtp_port': return '25'; - case 'smtp_encryption': - return null; + case 'smtp_address': default: return ''; } diff --git a/src/Helper/SshKey.php b/src/Helper/SshKey.php index ef74f9ee5..82c812ad4 100644 --- a/src/Helper/SshKey.php +++ b/src/Helper/SshKey.php @@ -2,13 +2,25 @@ namespace PHPCensor\Helper; -use PHPCensor\Config; +use PHPCensor\Common\Application\ConfigurationInterface; /** * Helper class for dealing with SSH keys. + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class SshKey { + protected ConfigurationInterface $configuration; + + public function __construct(ConfigurationInterface $configuration) + { + $this->configuration = $configuration; + } + /** * Uses ssh-keygen to generate a public/private key pair. * @@ -28,8 +40,8 @@ public function generate() 'ssh_public_key' => '' ]; - $sshStrength = Config::getInstance()->get('php-censor.ssh.strength', 2048); - $sshComment = Config::getInstance()->get('php-censor.ssh.comment', 'admin@php-censor'); + $sshStrength = $this->configuration->get('php-censor.ssh.strength', 2048); + $sshComment = $this->configuration->get('php-censor.ssh.comment', 'admin@php-censor'); $output = @\shell_exec( \sprintf( diff --git a/src/Helper/Template.php b/src/Helper/Template.php index e1c9e8c0f..1f8c8eba0 100644 --- a/src/Helper/Template.php +++ b/src/Helper/Template.php @@ -4,6 +4,12 @@ use voku\helper\AntiXSS; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov + */ class Template { /** diff --git a/src/Helper/Xml.php b/src/Helper/Xml.php index 20c5a9d38..307a3d388 100644 --- a/src/Helper/Xml.php +++ b/src/Helper/Xml.php @@ -5,8 +5,15 @@ use DOMDocument; use Exception; use LibXMLError; +use PHPCensor\Helper\Xml\Utf8CleanFilter; use SimpleXMLElement; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov + */ class Xml { /** @@ -15,21 +22,19 @@ class Xml */ public static function loadFromFile($filePath) { - stream_filter_register('xml_utf8_clean', 'PHPCensor\Helper\Xml\Utf8CleanFilter'); + \stream_filter_register('xml_utf8_clean', Utf8CleanFilter::class); try { - $xml = simplexml_load_file('php://filter/read=xml_utf8_clean/resource=' . $filePath); - } catch (Exception $ex) { - $xml = null; - } catch (\Throwable $ex) { // since php7 + $xml = \simplexml_load_file('php://filter/read=xml_utf8_clean/resource=' . $filePath); + } catch (\Throwable $ex) { $xml = null; } if (!$xml) { // from https://stackoverflow.com/questions/7766455/how-to-handle-invalid-unicode-with-simplexml/8092672#8092672 - $oldUse = libxml_use_internal_errors(true); + $oldUse = \libxml_use_internal_errors(true); - libxml_clear_errors(); + \libxml_clear_errors(); $dom = new DOMDocument("1.0", "UTF-8"); @@ -37,15 +42,15 @@ public static function loadFromFile($filePath) $dom->validateOnParse = false; $dom->recover = true; - $dom->loadXML(strtr( - file_get_contents($filePath), + $dom->loadXML(\strtr( + \file_get_contents($filePath), ['"' => "'"] // " in attribute names may mislead the parser )); /** @var LibXMLError $xmlError */ - $xmlError = libxml_get_last_error(); + $xmlError = \libxml_get_last_error(); if ($xmlError) { - $warning = sprintf('L%s C%s: %s', $xmlError->line, $xmlError->column, $xmlError->message); + $warning = \sprintf('L%s C%s: %s', $xmlError->line, $xmlError->column, $xmlError->message); print 'WARNING: ignored errors while reading phpunit result, '.$warning."\n"; } @@ -53,10 +58,10 @@ public static function loadFromFile($filePath) new SimpleXMLElement(''); } - $xml = simplexml_import_dom($dom); + $xml = \simplexml_import_dom($dom); - libxml_clear_errors(); - libxml_use_internal_errors($oldUse); + \libxml_clear_errors(); + \libxml_use_internal_errors($oldUse); } return $xml; diff --git a/src/Helper/Xml/Utf8CleanFilter.php b/src/Helper/Xml/Utf8CleanFilter.php index 5014581ce..e3daafea5 100644 --- a/src/Helper/Xml/Utf8CleanFilter.php +++ b/src/Helper/Xml/Utf8CleanFilter.php @@ -4,6 +4,12 @@ use php_user_filter; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov + */ class Utf8CleanFilter extends php_user_filter { public const PATTERN = '/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u'; @@ -17,11 +23,11 @@ class Utf8CleanFilter extends php_user_filter */ public function filter($in, $out, &$consumed, $closing): int { - while ($bucket = stream_bucket_make_writeable($in)) { - $bucket->data = preg_replace(self::PATTERN, '', $bucket->data); + while ($bucket = \stream_bucket_make_writeable($in)) { + $bucket->data = \preg_replace(self::PATTERN, '', $bucket->data); $consumed += $bucket->datalen; - stream_bucket_append($out, $bucket); + \stream_bucket_append($out, $bucket); } return PSFS_PASS_ON; diff --git a/src/Http/Request.php b/src/Http/Request.php deleted file mode 100644 index 92de8f27c..000000000 --- a/src/Http/Request.php +++ /dev/null @@ -1,147 +0,0 @@ -parseInput(); - - $this->data['path'] = $this->getRequestPath(); - $this->data['parts'] = array_values(array_filter(explode('/', $this->data['path']))); - } - - protected function getRequestPath() - { - $path = ''; - - // Start out with the REQUEST_URI: - if (!empty($_SERVER['REQUEST_URI'])) { - $path = $_SERVER['REQUEST_URI']; - } - - if ($_SERVER['SCRIPT_NAME'] != $_SERVER['REQUEST_URI']) { - $scriptPath = str_replace('/index.php', '', $_SERVER['SCRIPT_NAME']); - $path = str_replace($scriptPath, '', $path); - } - - // Remove index.php from the URL if it is present: - $path = str_replace(['/index.php', 'index.php'], '', $path); - - // Also cut out the query string: - $path = explode('?', $path); - $path = array_shift($path); - - return $path; - } - - /** - * Parse incoming variables, incl. $_GET, $_POST and also reads php://input for PUT/DELETE. - */ - protected function parseInput() - { - $params = $_REQUEST; - - if (!isset($_SERVER['REQUEST_METHOD']) || in_array($_SERVER['REQUEST_METHOD'], ['PUT', 'DELETE'], true)) { - $vars = file_get_contents('php://input'); - - if (!is_string($vars) || strlen(trim($vars)) === 0) { - $vars = ''; - } - - $inputData = []; - parse_str($vars, $inputData); - - $params = array_merge($params, $inputData); - } - - $this->setParams($params); - } - - /** - * Returns all request parameters. - * @return array - */ - public function getParams() - { - return $this->params; - } - - /** - * Return a specific request parameter, or a default value if not set. - */ - public function getParam($key, $default = null) - { - if (isset($this->params[$key])) { - return $this->params[$key]; - } else { - return $default; - } - } - - /** - * Set or override a request parameter. - */ - public function setParam($key, $value = null) - { - $this->params[$key] = $value; - } - - /** - * Set an array of request parameters. - */ - public function setParams(array $params) - { - $this->params = array_merge($this->params, $params); - } - - /** - * Un-set a specific parameter. - */ - public function unsetParam($key) - { - unset($this->params[$key]); - } - - public function getMethod() - { - return strtoupper($_SERVER['REQUEST_METHOD']); - } - - public function getPath() - { - return $this->data['path']; - } - - public function getPathParts() - { - return $this->data['parts']; - } - - public function isAjax() - { - if (!isset($_SERVER['HTTP_X_REQUESTED_WITH'])) { - return false; - } - - if (strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest') { - return true; - } - - return false; - } -} diff --git a/src/Http/Response.php b/src/Http/Response.php deleted file mode 100644 index a22c2390a..000000000 --- a/src/Http/Response.php +++ /dev/null @@ -1,116 +0,0 @@ -data = $createFrom->getData(); - } - } - - public function getData() - { - return $this->data; - } - - public function setResponseCode($code) - { - $this->data['code'] = (int)$code; - } - - public function setHeader($key, $val) - { - $this->data['headers'][$key] = $val; - } - - public function clearHeaders() - { - $this->data['headers'] = []; - } - - public function setContent($content) - { - $this->data['body'] = $content; - } - - public function getContent() - { - return $this->data['body']; - } - - public function flush() - { - $this->sendResponseCode(); - - if (isset($this->data['headers'])) { - foreach ($this->data['headers'] as $header => $val) { - header($header . ': ' . $val, true); - } - } - - return $this->flushBody(); - } - - protected function sendResponseCode() - { - if (!isset($this->data['code'])) { - $this->data['code'] = 200; - } - - switch ($this->data['code']) { - // 300 class - case 301: - $text = 'Moved Permanently'; - break; - case 302: - $text = 'Moved Temporarily'; - break; - - // 400 class errors - case 400: - $text = 'Bad Request'; - break; - case 401: - $text = 'Not Authorized'; - break; - case 403: - $text = 'Forbidden'; - break; - case 404: - $text = 'Not Found'; - break; - - // 500 class errors - case 500: - $text = 'Internal Server Error'; - break; - - // OK - case 200: - default: - $text = 'OK'; - break; - } - - header('HTTP/1.1 ' . $this->data['code'] . ' ' . $text, true, $this->data['code']); - } - - protected function flushBody() - { - if (isset($this->data['body'])) { - return $this->data['body']; - } - - return ''; - } - - public function __toString() - { - return $this->flush(); - } -} diff --git a/src/Http/Response/JsonResponse.php b/src/Http/Response/JsonResponse.php deleted file mode 100644 index 7310710ac..000000000 --- a/src/Http/Response/JsonResponse.php +++ /dev/null @@ -1,25 +0,0 @@ -setContent([]); - $this->setHeader('Content-Type', 'application/json'); - } - - protected function flushBody() - { - if (isset($this->data['body'])) { - return json_encode($this->data['body']); - } - - return json_encode(null); - } -} diff --git a/src/Http/Response/RedirectResponse.php b/src/Http/Response/RedirectResponse.php deleted file mode 100644 index b4c8f3d5b..000000000 --- a/src/Http/Response/RedirectResponse.php +++ /dev/null @@ -1,22 +0,0 @@ -setContent(null); - $this->setResponseCode(302); - } - - public function flush() - { - parent::flush(); - exit(1); - } -} diff --git a/src/Http/Router.php b/src/Http/Router.php index da0427f16..84b8a2884 100644 --- a/src/Http/Router.php +++ b/src/Http/Router.php @@ -3,36 +3,30 @@ namespace PHPCensor\Http; use PHPCensor\Application; -use PHPCensor\Config; -use PHPCensor\Exception\InvalidArgumentException; - +use PHPCensor\Common\Exception\InvalidArgumentException; +use Symfony\Component\HttpFoundation\Request; + +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dan Cryer + * @author Dmitry Khomutov + */ class Router { - /** - * @var Request; - */ - protected $request; + protected Request $request; - /** - * @var Config; - */ - protected $config; + protected Application $application; - /** - * @var Application - */ - protected $application; + protected array $routes = [ + ['route' => '/:controller/:action', 'callback' => null, 'defaults' => []] + ]; - /** - * @var array - */ - protected $routes = [['route' => '/:controller/:action', 'callback' => null, 'defaults' => []]]; - - public function __construct(Application $application, Request $request, Config $config) + public function __construct(Application $application, Request $request) { $this->application = $application; $this->request = $request; - $this->config = $config; } public function clearRoutes() @@ -49,73 +43,69 @@ public function clearRoutes() */ public function register($route, $options = [], $callback = null) { - if (!is_callable($callback)) { + if (!\is_callable($callback)) { throw new InvalidArgumentException('$callback must be callable.'); } - array_unshift($this->routes, ['route' => $route, 'callback' => $callback, 'defaults' => $options]); + \array_unshift($this->routes, ['route' => $route, 'callback' => $callback, 'defaults' => $options]); } public function dispatch() { foreach ($this->routes as $route) { - $pathParts = $this->request->getPathParts(); + $pathParts = $this->request->getPathInfo(); + $pathParts = \array_values(\array_filter(\explode('/', $pathParts))); - //------- - // Set up default values for everything: - //------- - $thisNamespace = 'Controller'; $thisController = null; $thisAction = null; - if (array_key_exists('namespace', $route['defaults'])) { - $thisNamespace = $route['defaults']['namespace']; - } - - if (array_key_exists('controller', $route['defaults'])) { + if (\array_key_exists('controller', $route['defaults'])) { $thisController = $route['defaults']['controller']; } - if (array_key_exists('action', $route['defaults'])) { + if (\array_key_exists('action', $route['defaults'])) { $thisAction = $route['defaults']['action']; } - $routeParts = array_filter(explode('/', $route['route'])); + $routeParts = \array_filter(\explode('/', $route['route'])); $routeMatches = true; - while (count($routeParts)) { - $routePart = array_shift($routeParts); - $pathPart = array_shift($pathParts); + while (\count($routeParts)) { + $routePart = \array_shift($routeParts); + $pathPart = \array_shift($pathParts); switch ($routePart) { - case ':namespace': - $thisNamespace = !is_null($pathPart) ? $pathPart : $thisNamespace; - break; case ':controller': - $thisController = !is_null($pathPart) ? $pathPart : $thisController; + $thisController = !\is_null($pathPart) ? $pathPart : $thisController; + break; case ':action': - $thisAction = !is_null($pathPart) ? $pathPart : $thisAction; + $thisAction = !\is_null($pathPart) ? $pathPart : $thisAction; + break; default: - if ($routePart != $pathPart) { + if ($routePart !== $pathPart) { $routeMatches = false; } } - if (!$routeMatches || !count($pathParts)) { + if (!$routeMatches || !\count($pathParts)) { break; } } - $thisArgs = $pathParts; + foreach ($pathParts as &$pathPart) { + if (\is_numeric($pathPart)) { + $pathPart = (int)$pathPart; + } + } + unset($pathPart); if ($routeMatches) { $route = [ - 'namespace' => $thisNamespace, 'controller' => $thisController, 'action' => $thisAction, - 'args' => $thisArgs, + 'args' => $pathParts, 'callback' => $route['callback'] ]; diff --git a/src/Languages/lang.en.php b/src/Languages/lang.en.php index af7d89ee1..dd7677855 100644 --- a/src/Languages/lang.en.php +++ b/src/Languages/lang.en.php @@ -64,6 +64,7 @@ 'build_now' => 'Build now', 'build_now_debug' => 'Build now with debug', 'edit_project' => 'Edit Project', + 'clone_project' => 'Clone Project', 'delete_project' => 'Delete Project', 'delete_old_builds' => 'Delete old builds', 'delete_all_builds' => 'Delete all builds', @@ -178,6 +179,17 @@ Services section of your Bitbucket repository.', + // Secrets + 'secrets' => 'Secrets', + 'secret' => 'Secret', + 'secret_edit' => 'Edit', + 'secret_delete' => 'Delete', + 'secret_add' => 'Add Secret', + 'secret_add_edit' => 'Add / Edit Secret', + 'secret_name' => 'Secret Name', + 'secret_value' => 'Secret Value', + 'secret_save' => 'Save Secret', + // Project Groups 'group_projects' => 'Project groups', 'project_group' => 'Project group', @@ -199,11 +211,12 @@ 'rebuild_now' => 'Rebuild now', 'rebuild_now_debug' => 'Rebuild now with debug', - 'all_errors' => 'All errors', - 'only_new' => 'Only new errors', - 'only_old' => 'Only old errors', - 'new_errors' => 'New errors', - 'total_errors' => 'Errors', + 'all_errors' => 'All errors', + 'only_new' => 'Only new errors', + 'only_old' => 'Only old errors', + 'new_errors' => 'New errors', + 'total_errors' => 'Errors', + 'test_coverage' => 'Test coverage', 'committed_by_x' => 'Committed by %s', 'commit_id_x' => 'Commit: %s', @@ -347,6 +360,7 @@ // Summary plugin 'build-summary' => 'Summary', 'stage' => 'Stage', + 'step' => 'Step', 'duration' => 'Duration', 'seconds' => 'sec.', 'plugin' => 'Plugin', @@ -417,7 +431,6 @@ 'deployer' => 'Deployer', 'env' => 'Env', 'grunt' => 'Grunt', - 'hipchat_notify' => 'Hipchat Notify', 'irc_notify' => 'IRC Notify', 'lint' => 'Lint', 'mysql' => 'MySQL', diff --git a/src/Languages/lang.es.php b/src/Languages/lang.es.php index abba6e063..e982c42c0 100644 --- a/src/Languages/lang.es.php +++ b/src/Languages/lang.es.php @@ -415,7 +415,6 @@ 'deployer' => 'Deployer', 'env' => 'Env', 'grunt' => 'Grunt', - 'hipchat_notify' => 'Hipchat Notify', 'irc' => 'IRC', 'lint' => 'Lint', 'mysql' => 'MySQL', diff --git a/src/Languages/lang.fr.php b/src/Languages/lang.fr.php index dd1fedb69..d0e23ee79 100644 --- a/src/Languages/lang.fr.php +++ b/src/Languages/lang.fr.php @@ -405,7 +405,6 @@ 'deployer' => 'Deployer', 'env' => 'Env', 'grunt' => 'Grunt', - 'hipchat_notify' => 'Hipchat Notify', 'irc_notify' => 'IRC Notify', 'lint' => 'Lint', 'mysql' => 'MySQL', diff --git a/src/Languages/lang.id.php b/src/Languages/lang.id.php index 7da0b3395..e871d51ce 100644 --- a/src/Languages/lang.id.php +++ b/src/Languages/lang.id.php @@ -412,7 +412,6 @@ 'deployer' => 'Deployer', 'env' => 'Env', 'grunt' => 'Grunt', - 'hipchat_notify' => 'Hipchat Notify', 'irc' => 'IRC', 'lint' => 'Lint', 'mysql' => 'MySQL', diff --git a/src/Languages/lang.my.php b/src/Languages/lang.my.php index 2d8205437..29099f2d2 100644 --- a/src/Languages/lang.my.php +++ b/src/Languages/lang.my.php @@ -86,7 +86,6 @@ 'success' => 'Success', 'failed' => 'gagal', 'failed_allowed' => 'Gagal (Dibolehkan)', - 'error' => 'Error', 'skipped' => 'Dilangkau', 'trace' => 'Stack trace', 'manual_build' => 'Build Manual', @@ -414,7 +413,6 @@ 'deployer' => 'Deployer', 'env' => 'Env', 'grunt' => 'Grunt', - 'hipchat_notify' => 'Hipchat Notify', 'irc' => 'IRC', 'lint' => 'Lint', 'mysql' => 'MySQL', diff --git a/src/Languages/lang.pt-br.php b/src/Languages/lang.pt-br.php index 6f20fad51..b482ff238 100644 --- a/src/Languages/lang.pt-br.php +++ b/src/Languages/lang.pt-br.php @@ -423,7 +423,6 @@ 'deployer' => 'Deployer', 'env' => 'Env', 'grunt' => 'Grunt', - 'hipchat_notify' => 'Hipchat Notify', 'irc' => 'IRC', 'lint' => 'Lint', 'mysql' => 'MySQL', diff --git a/src/Languages/lang.ru.php b/src/Languages/lang.ru.php index b6448ea2b..80a90a529 100644 --- a/src/Languages/lang.ru.php +++ b/src/Languages/lang.ru.php @@ -62,6 +62,7 @@ 'build_now' => 'Собрать', 'build_now_debug' => 'Собрать в режиме отладки', 'edit_project' => 'Редактировать проект', + 'clone_project' => 'Клонировать проект', 'delete_project' => 'Удалить проект', 'delete_old_builds' => 'Удалить старые сборки', 'delete_all_builds' => 'Удалить все сборки', @@ -172,6 +173,17 @@ 'webhooks_help_bitbucket' => 'Чтобы Автоматически собирать этот проект при публикации новых коммитов, добавьте URL ниже как "POST" сервис в разделе Services вашего Bitbucket репозитория.', + // Secrets + 'secrets' => 'Секреты', + 'secret' => 'Секрет', + 'secret_edit' => 'Редактировать', + 'secret_delete' => 'Удалить', + 'secret_add' => 'Добавить секрет', + 'secret_add_edit' => 'Добавить / изменить секрет', + 'secret_name' => 'Название секрета', + 'secret_value' => 'Значение секрета', + 'secret_save' => 'Сохранить секрет', + // Project Groups 'group_projects' => 'Группы проектов', 'project_group' => 'Группа проекта', @@ -193,11 +205,12 @@ 'rebuild_now' => 'Пересобрать', 'rebuild_now_debug' => 'Пересобрать в режиме отладки', - 'all_errors' => 'Все ошибки', - 'only_new' => 'Только новые', - 'only_old' => 'Только старые', - 'new_errors' => 'Новых ошибок', - 'total_errors' => 'Ошибок', + 'all_errors' => 'Все ошибки', + 'only_new' => 'Только новые', + 'only_old' => 'Только старые', + 'new_errors' => 'Новых ошибок', + 'total_errors' => 'Ошибок', + 'test_coverage' => 'Покрытие тестами', 'committed_by_x' => 'Отправил %s', 'commit_id_x' => 'Коммит: %s', @@ -406,7 +419,6 @@ 'deployer' => 'Deployer', 'env' => 'Env', 'grunt' => 'Grunt', - 'hipchat_notify' => 'Hipchat Notify', 'irc_notify' => 'IRC Notify', 'lint' => 'Lint', 'mysql' => 'MySQL', diff --git a/src/Languages/lang.uk.php b/src/Languages/lang.uk.php index 35c9d4a38..bd90a8fce 100644 --- a/src/Languages/lang.uk.php +++ b/src/Languages/lang.uk.php @@ -3,6 +3,8 @@ return [ 'language_name' => 'Українська', 'language' => 'Мова', + 'per_page' => 'Кількість елементів на сторінці', + 'default' => 'За замовчуванням', // Log in: 'log_in_to_app' => 'Увійти до PHP Censor', @@ -34,6 +36,7 @@ 'email_address' => 'Email адреса', 'login' => 'Логин / Email адреса', 'password' => 'Пароль', + 'remember_me' => 'Запам\'ятати мене', 'log_in' => 'Увійти', @@ -46,19 +49,24 @@ 'branch_x' => 'Гілка: %s', 'created_x' => 'Створено: %s', 'started_x' => 'Розпочато: %s', + 'environment_x' => 'Cередовище: %s', // Sidebar 'hello_name' => 'Привіт, %s', 'dashboard' => 'Панель управління', 'admin_options' => 'Меню адміністратора', 'add_project' => 'Додати проект', + 'project_groups' => 'Групи проектів', 'settings' => 'Налаштування', 'manage_users' => 'Управління користувачами', 'plugins' => 'Плагіни', 'view' => 'Переглянути', 'build_now' => 'Зібрати', + 'build_now_debug' => 'Зібрати в режимі налагодження', 'edit_project' => 'Редагувати проект', 'delete_project' => 'Видалити проект', + 'delete_old_builds' => 'Видалити старі збірки', + 'delete_all_builds' => 'Видалити всі збірки', // Project Summary: 'no_builds_yet' => 'Немає збірок!', @@ -69,6 +77,8 @@ 'last_failed_build' => 'Останньою проваленою збіркою була %s.', 'never_failed_build' => 'У цього проекта ніколи не було провалених збірок.', 'view_project' => 'Переглянути проект', + 'projects_with_build_errors' => 'Проекті з помилками збірки', + 'no_build_errors' => 'Немає помилок збірки', // Timeline: 'latest_builds' => 'Останні збірки', @@ -76,7 +86,11 @@ 'running' => 'Виконується', 'success' => 'Успіх', 'failed' => 'Провалена', + 'failed_allowed' => 'Припустимий провал', 'manual_build' => 'Ручна збірка', + 'error' => 'Помилка', + 'skipped' => 'Пропустили', + 'trace' => 'Стек виклику', // Add/Edit Project: 'new_project' => 'Новий проект', @@ -90,7 +104,8 @@ 'gitlab' => 'GitLab', 'git' => 'Git', 'local' => 'Локальний шлях', - 'hg' => 'Mercurial', + 'hg' => 'Mercurial', + 'svn' => 'Subversion', 'where_hosted' => 'Де зберігається ваш проект?', 'choose_github' => 'Оберіть GitHub репозиторій:', @@ -102,6 +117,7 @@ 'build_config' => 'Конфігурація збірки цього проекта для PHP Censor (якщо ви не додали файл .php-censor.yml до репозиторію вашого проекту)', 'default_branch' => 'Назва гілки за замовчуванням', + 'default_branch_only' => 'Тільки гілку за замовчуванням', 'allow_public_status' => 'Увімкнути публічну сторінку статусу та зображення для цього проекта?', 'archived' => 'Архівний', 'archived_menu' => 'Архів', @@ -289,4 +305,100 @@ 'passing_build' => 'Успішно збірка', 'failing_build' => 'Невдала збірка', 'log_output' => 'Вивід лога:', + + 'overwrite_build_config' => 'Перезаписати конфігурацію файлу в сховищі конфігурацією в базі даних? Якщо прапорець +не встановлено, конфігурація в базі даних буде об’єднана з файлом config.', + 'environments_label' => 'Середовище (yaml)', + 'all' => 'Всі', + 'environment' => 'Середовище', + 'build_source' => 'Джерело збірки', + 'source_unknown' => 'Джерело невідомо', + 'source_manual_web' => 'Вручну (з Інтернету)', + 'source_manual_console' => 'Вручну (з CLI)', + 'source_periodical' => 'Періодичний', + 'source_webhook_push' => 'Webhook (Push)', + 'source_webhook_pull_request_created' => 'Webhook (створено pull request)', + 'source_webhook_pull_request_updated' => 'Webhook (оновлено pull request) ', + 'source_webhook_pull_request_approved' => 'Webhook (схвалено pull request) ', + 'source_webhook_pull_request_merged' => 'Webhook (об\'єднано pull request) ', + 'group_projects' => 'Групи проектів', + 'project_group' => 'Група проекту', + 'group_count' => 'Кількість проектів', + 'group_edit' => 'Редагувати групу', + 'group_delete' => 'Видалити групу', + 'group_add' => 'Додати групу', + 'group_add_edit' => 'Додати / редагувати групу', + 'group_title' => 'Назва групи', + 'group_save' => 'Зберігти групу', + 'errors' => 'Помилки', + 'information' => 'Інформаця', + 'is_new' => 'Нова?', + 'new' => 'Нова', + 'rebuild_now_debug' => 'Перезібраті в режимі налагодження', + 'all_errors' => 'Всі помилки', + 'only_new' => 'Тільки нові', + 'only_old' => 'Тількі старі', + 'new_errors' => 'Нові помилки', + 'total_errors' => 'Всього помилок', + 'classes' => 'Класів', + 'methods' => 'Методів', + 'coverage' => 'Покриття коду тестами PHPUnit', + 'phan_warnings' => 'Попередження phan', + 'php_cs_fixer_warnings' => 'Попередження PHP CS Fixer', + 'php_cpd_warnings' => 'Попередження PHP Copy/Paste Detector', + 'php_tal_lint_warnings' => 'Попередження PHP Tal Lint', + 'php_tal_lint_errors' => 'Помилки PHP Tal Lint', + 'behat_warnings' => 'Попередження Behat', + 'sensiolabs_insight_warnings' => 'Попередження Sensiolabs Insight', + 'technical_debt_warnings' => 'Попередження Technical Debt', + 'merged_branches' => 'Гілки, що об’єднуються', + 'codeception_feature' => 'Властивість', + 'codeception_suite' => 'Набір', + 'codeception_time' => 'Час', + 'codeception_synopsis' => 'Тестов виконано: %1$d (за %2$f сек.). +Невдач : %3$d.', + 'suite' => 'Набір', + 'test' => 'Тест', + 'build-summary' => 'Зведення', + 'stage' => 'Етап', + 'duration' => 'Тривалість', + 'seconds' => 'сек.', + 'plugin' => 'Плагін', + 'stage_setup' => 'Встановлення', + 'stage_test' => 'Тестування', + 'stage_deploy' => 'Розгортання', + 'stage_complete' => 'Завершення', + 'stage_success' => 'Успішне завершення', + 'stage_failure' => 'Невдача', + 'stage_broken' => 'Поломка', + 'stage_fixed' => 'Виправлення', + 'severity' => 'Рівень', + 'all_plugins' => 'Всі плагіни', + 'all_severities' => 'Всі рівні', + 'filters' => 'Фільтри', + 'errors_selected' => 'Помилок вибрано', + 'build_details' => 'Інформація про збірку', + 'commit_details' => 'Інформація про комміт', + 'committer' => 'Автор комміту', + 'commit_message' => 'Повідомлення в комміті', + 'timing' => 'Таймінг', + 'created' => 'Створено', + 'started' => 'Почалося', + 'finished' => 'Закінчено', + 'add_to_queue_failed' => 'Збірка успішно створена, але виникла проблема при додаванні збірки в чергу. +Зазвичай таке відбувається, коли PHP Censor намагається працювати з неіснуючим або зупиненим +сервером черг Beanstalkd.', + 'critical' => 'Критичний', + 'high' => 'Високий', + 'normal' => 'Нормальний', + 'low' => 'Низький', + 'confirm_message' => 'Елемент буде видалений назавжди. Ви впевнені?', + 'confirm_title' => 'Підтвердження видалення', + 'confirm_ok' => 'Видалити', + 'confirm_cancel' => 'Відміна', + 'confirm_success' => 'Елемент успішно видалений.', + 'confirm_failed' => 'Видалення провалилося! Відповідь сервера: ', + 'public_status_title' => 'Публічний статус', + 'public_status_image' => 'Іконка статусу', + 'public_status_page' => 'Сторінка публічного статусу', ]; diff --git a/src/Logging/AnsiFormatter.php b/src/Logging/AnsiFormatter.php index c53eee86d..de95098c2 100644 --- a/src/Logging/AnsiFormatter.php +++ b/src/Logging/AnsiFormatter.php @@ -1,9 +1,17 @@ + */ class AnsiFormatter extends LineFormatter { /** @@ -11,7 +19,7 @@ class AnsiFormatter extends LineFormatter */ public function format(array $record): string { - return str_replace( + return \str_replace( ["\033[0;31m", "\033[0m", "\033[0;32m", "\033[0;36m"], '', parent::format($record) diff --git a/src/Logging/BuildDBLogHandler.php b/src/Logging/BuildDBLogHandler.php index 7e3706d83..a0cfbd9a9 100644 --- a/src/Logging/BuildDBLogHandler.php +++ b/src/Logging/BuildDBLogHandler.php @@ -1,52 +1,59 @@ */ class BuildDBLogHandler extends AbstractProcessingHandler { - /** - * @var Build - */ - protected $build; + protected Build $build; + protected BuildStore $buildStore; + private SecretStore $secretStore; - protected $logValue; + protected string $logValue; /** * @var int last flush timestamp */ - protected $flushTimestamp = 0; + protected int $flushTimestamp = 0; /** * @var int flush delay, seconds */ - protected $flushDelay = 1; + protected int $flushDelay = 1; - /** - * @param bool $level - * @param bool $bubble - */ public function __construct( + SecretStore $secretStore, + BuildStore $buildStore, Build $build, - $level = LogLevel::INFO, - $bubble = true + int $level = Logger::INFO, + bool $bubble = true ) { parent::__construct($level, $bubble); - $this->build = $build; + + $this->secretStore = $secretStore; + $this->build = $build; + $this->buildStore = $buildStore; + // We want to add to any existing saved log information. - $this->logValue = $build->getLog(); + $this->logValue = (string)$build->getLog(); } - /** - * Destructor - */ public function __destruct() { $this->flushData(); @@ -55,11 +62,45 @@ public function __destruct() /** * Flush buffered data */ - protected function flushData() + protected function flushData(): void { $this->build->setLog($this->logValue); - Factory::getStore('Build')->save($this->build); - $this->flushTimestamp = time(); + $this->buildStore->save($this->build); + + $this->flushTimestamp = \time(); + } + + private function sanitize(string $message): string + { + return \str_replace([ + '\/', + '//', + $this->build->getBuildPath(), + ROOT_DIR, + ], [ + '/', + '//', + '/', + '/', + ], $message); + } + + private function sanitizeSecrets(string $message): string + { + $replace = []; + $secrets = $this->secretStore->getAll(); + if (\count($secrets['items']) > 0) { + /** @var Secret $secret */ + foreach ($secrets['items'] as $secret) { + $value = $secret->getValue(); + $name = '%' . \sprintf('SECRET:%s', $secret->getName()) . '%'; + if (\trim($value)) { + $replace[$name] = $secret->getValue(); + } + } + } + + return \str_replace($replace, \array_keys($replace), $message); } /** @@ -67,14 +108,13 @@ protected function flushData() */ protected function write(array $record): void { - $message = (string)$record['message']; - $message = str_replace(['\/', '//'], '/', $message); - $message = str_replace($this->build->getBuildPath(), '/', $message); - $message = str_replace(ROOT_DIR, '/', $message); - - $this->logValue .= $message . PHP_EOL; + $this->logValue .= $this->sanitize( + $this->sanitizeSecrets( + (string)$record['message'] + ) + ) . PHP_EOL; - if ($this->flushTimestamp < (time() - $this->flushDelay)) { + if ($this->flushTimestamp < (\time() - $this->flushDelay)) { $this->flushData(); } } diff --git a/src/Logging/BuildLogger.php b/src/Logging/BuildLogger.php index c0e3acef4..521831a78 100644 --- a/src/Logging/BuildLogger.php +++ b/src/Logging/BuildLogger.php @@ -1,27 +1,24 @@ */ -class BuildLogger implements LoggerAwareInterface +class BuildLogger { - /** - * @var LoggerInterface - */ - protected $logger; + private LoggerInterface $logger; - /** - * @var Build - */ - protected $build; + private Build $build; public function __construct(LoggerInterface $logger, Build $build) { @@ -29,26 +26,12 @@ public function __construct(LoggerInterface $logger, Build $build) $this->build = $build; } - /** - * Add an entry to the build log. - * - * @param string|string[] $message - * @param string $level - * @param mixed[] $context - */ - public function log($message, $level = LogLevel::INFO, $context = []) + public function log($message, string $level = LogLevel::INFO, array $context = []): void { - // Skip if no logger has been loaded. - if (!$this->logger) { - return; - } - - if (!is_array($message)) { + if (!\is_array($message)) { $message = [$message]; } - // The build is added to the context so the logger can use - // details from it if required. $context['build'] = $this->build->getId(); foreach ($message as $item) { @@ -56,39 +39,21 @@ public function log($message, $level = LogLevel::INFO, $context = []) } } - /** - * Add a warning-coloured message to the log. - * - * @param string $message - */ - public function logWarning($message) + public function logWarning(string $message): void { $this->log("\033[0;31m" . $message . "\033[0m"); } - /** - * Add a success-coloured message to the log. - * - * @param string $message - */ - public function logSuccess($message) + public function logSuccess(string $message): void { $this->log("\033[0;32m" . $message . "\033[0m"); } - /** - * Add a failure-coloured message to the log. - * - * @param string $message - * @param Exception $exception The exception that caused the error. - */ - public function logFailure($message, Exception $exception = null) + public function logFailure(string $message, ?\Throwable $exception = null): void { $level = LogLevel::INFO; $context = []; - // The psr3 log interface stipulates that exceptions should be passed - // as the exception key in the context array. if ($exception) { $level = LogLevel::ERROR; @@ -99,23 +64,10 @@ public function logFailure($message, Exception $exception = null) $this->log("\033[0;31m" . $message . "\033[0m", $level, $context); } - /** - * Add a debug-coloured message to the log. - * - * @param string $message - */ - public function logDebug($message) + public function logDebug(string $message): void { if ($this->build->isDebug()) { $this->log("\033[0;36m" . $message . "\033[0m", LogLevel::DEBUG); } } - - /** - * Sets a logger instance on the object - */ - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - } } diff --git a/src/Logging/Handler.php b/src/Logging/Handler.php index 1cd975e3e..5ff2a419a 100644 --- a/src/Logging/Handler.php +++ b/src/Logging/Handler.php @@ -1,5 +1,7 @@ */ class Handler { - /** - * @var array - */ - protected $levels = [ + protected array $levels = [ E_WARNING => 'Warning', E_NOTICE => 'Notice', E_USER_ERROR => 'User Error', @@ -26,46 +28,33 @@ class Handler E_USER_DEPRECATED => 'User Deprecated', ]; - /** - * @var LoggerInterface - */ - protected $logger; + protected ?LoggerInterface $logger; - /** - */ - public function __construct(LoggerInterface $logger = null) + public function __construct(?LoggerInterface $logger = null) { $this->logger = $logger; } - /** - * Register a new log handler. - */ - public static function register(LoggerInterface $logger = null) + public static function register(?LoggerInterface $logger = null): void { $handler = new static($logger); - set_error_handler([$handler, 'handleError']); - register_shutdown_function([$handler, 'handleFatalError']); + \set_error_handler([$handler, 'handleError']); + \register_shutdown_function([$handler, 'handleFatalError']); - set_exception_handler([$handler, 'handleException']); + \set_exception_handler([$handler, 'handleException']); } /** - * @param int $level - * @param string $message - * @param string $file - * @param int $line - * * @throws ErrorException */ - public function handleError($level, $message, $file, $line) + public function handleError(int $level, string $message, string $file, int $line): void { - if (error_reporting() & $level) { - $exceptionLevel = isset($this->levels[$level]) ? $this->levels[$level] : $level; + if (\error_reporting() & $level) { + $exceptionLevel = $this->levels[$level] ?? $level; throw new ErrorException( - sprintf('%s: %s in %s line %d', $exceptionLevel, $message, $file, $line), + \sprintf('%s: %s in %s line %d', $exceptionLevel, $message, $file, $line), 0, $level, $file, @@ -74,17 +63,14 @@ public function handleError($level, $message, $file, $line) } } - /** - * @throws ErrorException - */ - public function handleFatalError() + public function handleFatalError(): void { - $fatalError = error_get_last(); + $fatalError = \error_get_last(); try { - if (($error = error_get_last()) !== null) { + if (\error_get_last() !== null) { $error = new ErrorException( - sprintf( + \sprintf( '%s: %s in %s line %d', $fatalError['type'], $fatalError['message'], @@ -98,9 +84,9 @@ public function handleFatalError() ); $this->log($error); } - } catch (Exception $e) { + } catch (\Throwable $e) { $error = new ErrorException( - sprintf( + \sprintf( '%s: %s in %s line %d', $fatalError['type'], $fatalError['message'], @@ -116,22 +102,17 @@ public function handleFatalError() } } - /** - */ - public function handleException($exception) + public function handleException(\Throwable $exception): void { $this->log($exception); } - /** - * Write to the build log. - */ - protected function log($exception) + protected function log(\Throwable $exception): void { if (null !== $this->logger) { - $message = sprintf( + $message = \sprintf( '%s: %s (uncaught exception) at %s line %s', - get_class($exception), + \get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine() diff --git a/src/Logging/OutputLogHandler.php b/src/Logging/OutputLogHandler.php index 593d1fdd3..1b4f0a7f4 100644 --- a/src/Logging/OutputLogHandler.php +++ b/src/Logging/OutputLogHandler.php @@ -1,5 +1,7 @@ */ class OutputLogHandler extends AbstractProcessingHandler { - /** - * @var OutputInterface - */ - protected $output; + protected OutputInterface $output; - /** - * @param bool|string $level - * @param bool $bubble - */ public function __construct( OutputInterface $output, - $level = LogLevel::INFO, - $bubble = true + string $level = LogLevel::INFO, + bool $bubble = true ) { parent::__construct($level, $bubble); diff --git a/src/Migrations/20200505070903_initial_migration_v2.php b/src/Migrations/20200505070903_initial_migration_v2.php index f4f53efe9..19240c445 100644 --- a/src/Migrations/20200505070903_initial_migration_v2.php +++ b/src/Migrations/20200505070903_initial_migration_v2.php @@ -1,10 +1,19 @@ + */ +final class InitialMigrationV2 extends AbstractMigration { private const LATEST_V1_MIGRATION_NAME = 'FixedPhpStanPluginName'; @@ -27,7 +36,7 @@ private function isNewInstallationUp(): bool } if ($isIssetBuild || ($isIssetBuilds && !$this->getLatestV1Migration())) { - throw new \RuntimeException( + throw new RuntimeException( 'You should upgrade your PHP Censor to latest 1.3 release before you can upgrade it to release 2.0' ); } @@ -149,7 +158,7 @@ public function up() ->save(); $projectGroups - ->addColumn('title', 'string', ['limit' => 100, 'null' => false]) + ->addColumn('title', 'string', ['limit' => 100]) ->addColumn('create_date', 'datetime') ->addColumn('user_id', 'integer', ['null' => true]) diff --git a/src/Migrations/20220223061912_webhook_requests_new_table.php b/src/Migrations/20220223061912_webhook_requests_new_table.php new file mode 100644 index 000000000..8c41cca44 --- /dev/null +++ b/src/Migrations/20220223061912_webhook_requests_new_table.php @@ -0,0 +1,52 @@ +table('webhook_requests'); + + $databaseType = $this->getAdapter()->getAdapterType(); + $payloadOptions = ['null' => true]; + if ('mysql' === $databaseType) { + $payloadOptions['limit'] = MysqlAdapter::TEXT_LONG; + } + + $webhookRequests + ->addColumn('project_id', 'integer') + ->addColumn('webhook_type', 'string', ['limit' => 50]) + ->addColumn('payload', 'text', $payloadOptions) + ->addColumn('create_date', 'datetime') + + ->addIndex(['project_id']) + + ->save(); + + $webhookRequests + ->addForeignKey( + 'project_id', + 'projects', + 'id', + ['delete' => 'CASCADE', 'update' => 'CASCADE'] + ) + ->save(); + } + + public function down() + { + $webhookRequests = $this->table('webhook_requests'); + + $webhookRequests + ->dropForeignKey('project_id') + ->save(); + + $webhookRequests + ->drop() + ->save(); + } +} diff --git a/src/Migrations/20220429095102_dashboard_coverage.php b/src/Migrations/20220429095102_dashboard_coverage.php new file mode 100644 index 000000000..9d0ca0c66 --- /dev/null +++ b/src/Migrations/20220429095102_dashboard_coverage.php @@ -0,0 +1,30 @@ +table('builds'); + + $builds + ->addColumn('test_coverage', 'string', ['limit' => 10, 'null' => true]) + ->addColumn('test_coverage_previous', 'string', ['limit' => 10, 'null' => true]) + + ->save(); + } + + public function down() + { + $builds = $this->table('builds'); + + $builds + ->removeColumn('test_coverage') + ->removeColumn('test_coverage_previous') + + ->save(); + } +} diff --git a/src/Migrations/20220604191600_secrets_new_table.php b/src/Migrations/20220604191600_secrets_new_table.php new file mode 100644 index 000000000..e7e426037 --- /dev/null +++ b/src/Migrations/20220604191600_secrets_new_table.php @@ -0,0 +1,48 @@ +table('secrets'); + + $databaseType = $this->getAdapter()->getAdapterType(); + $payloadOptions = []; + if ('mysql' === $databaseType) { + $payloadOptions['limit'] = MysqlAdapter::TEXT_REGULAR; + } + + $secrets + ->addColumn('name', 'string', ['limit' => 100]) + ->addColumn('value', 'text', $payloadOptions) + ->addColumn('create_date', 'datetime') + ->addColumn('user_id', 'integer', ['null' => true]) + + ->addIndex(['name'], ['unique' => true]) + + ->save(); + + $secrets + ->addForeignKey( + 'user_id', + 'users', + 'id', + ['delete' => 'SET NULL', 'update' => 'CASCADE'] + ) + ->save(); + } + + public function down() + { + $secrets = $this->table('secrets'); + + $secrets + ->drop() + ->save(); + } +} diff --git a/src/Model.php b/src/Model.php index d26e0e102..61afbb94f 100644 --- a/src/Model.php +++ b/src/Model.php @@ -4,27 +4,61 @@ namespace PHPCensor; +use DateTime; +use Exception; + +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dan Cryer + * @author Dmitry Khomutov + */ class Model { - /** - * @var array - */ - protected $data = []; + protected array $data = []; - /** - * @var array - */ - protected $modified = []; + protected array $dataTypes = []; + + private array $modified = []; + + protected StoreRegistry $storeRegistry; + + public function __construct( + StoreRegistry $storeRegistry, + array $initialData = [] + ) { + if (!isset($this->dataTypes['id'])) { + $this->dataTypes['id'] = 'integer'; + } + + foreach ($initialData as $column => $value) { + $this->setDataItem($column, $this->castToDataType($this->getDataType($column), $value)); + } + + $this->storeRegistry = $storeRegistry; + } - public function __construct(array $initialData = []) + public function getDataType(string $column): string { - if (is_array($initialData)) { - foreach ($initialData as $index => $item) { - if (array_key_exists($index, $this->data)) { - $this->data[$index] = $item; - } - } + return $this->dataTypes[$column] ?? 'string'; + } + + protected function getDataItem(string $column, $defaultValue = null) + { + return $this->castToDataType($this->getDataType($column), $this->data[$column] ?? $defaultValue); + } + + protected function setDataItem(string $column, $value): bool + { + if (!\array_key_exists($column, $this->data) || $this->data[$column] === $value) { + return false; } + + $this->data[$column] = $value; + $this->modified[$column] = $column; + + return true; } public function getDataArray(): array @@ -37,10 +71,67 @@ public function getModified(): array return $this->modified; } - protected function setModified(string $column): bool + /** + * @return mixed + */ + private function castToDataType(string $type, $value) { - $this->modified[$column] = $column; + if ($value === null) { + return null; + } - return true; + switch ($type) { + case 'int': + case 'integer': + return \is_integer($value) ? $value : \intval($value); + + case 'bool': + case 'boolean': + return \is_bool($value) ? $value : \boolval($value); + + case 'float': + return \is_float($value) ? $value : \floatval($value); + + case 'array': + return \is_array($value) ? $value : \json_decode($value, true); + + case 'datetime': + if (\is_a($value, DateTime::class)) { + return $value; + } + try { + return new DateTime($value); + } catch (\Throwable $e) { + return null; + } + + case 'newline': + if (!\is_string($value)) { + return $value; + } + + return \array_values( + \array_filter( + \array_map( + 'trim', + \explode("\n", $value) + ) + ) + ); + + + default: + return $value; + } + } + + public function getId(): ?int + { + return $this->getDataItem('id'); + } + + public function setId(?int $value): bool + { + return $this->setDataItem('id', $value); } } diff --git a/src/Model/Base/Build.php b/src/Model/Base/Build.php index fbe7d5bdf..4eb10dd30 100644 --- a/src/Model/Base/Build.php +++ b/src/Model/Base/Build.php @@ -6,13 +6,25 @@ use DateTime; use Exception; -use PHPCensor\Exception\InvalidArgumentException; +use PHPCensor\Common\Exception\InvalidArgumentException; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Model; use PHPCensor\Store\BuildStore; -use PHPCensor\Store\Factory; - +use PHPCensor\Traits\Model\HasCreateDateTrait; +use PHPCensor\Traits\Model\HasUserIdTrait; + +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dan Cryer + * @author Dmitry Khomutov + */ class Build extends Model { + use HasUserIdTrait; + use HasCreateDateTrait; + public const STATUS_PENDING = 0; public const STATUS_RUNNING = 1; public const STATUS_SUCCESS = 2; @@ -30,46 +42,55 @@ class Build extends Model public const SOURCE_MANUAL_REBUILD_WEB = 9; public const SOURCE_MANUAL_REBUILD_CONSOLE = 10; - /** - * @var array - */ - protected $data = [ - 'id' => null, - 'parent_id' => null, - 'project_id' => null, - 'commit_id' => null, - 'status' => null, - 'log' => null, - 'branch' => null, - 'tag' => null, - 'create_date' => null, - 'start_date' => null, - 'finish_date' => null, - 'committer_email' => null, - 'commit_message' => null, - 'extra' => null, - 'environment_id' => null, - 'source' => Build::SOURCE_UNKNOWN, - 'user_id' => null, - 'errors_total' => null, - 'errors_total_previous' => null, - 'errors_new' => null, + protected array $data = [ + 'id' => null, + 'parent_id' => null, + 'project_id' => null, + 'commit_id' => null, + 'status' => null, + 'log' => null, + 'branch' => null, + 'tag' => null, + 'create_date' => null, + 'start_date' => null, + 'finish_date' => null, + 'committer_email' => null, + 'commit_message' => null, + 'extra' => [], + 'environment_id' => null, + 'source' => Build::SOURCE_UNKNOWN, + 'user_id' => null, + 'errors_total' => null, + 'errors_total_previous' => null, + 'errors_new' => null, + 'test_coverage' => null, + 'test_coverage_previous' => null, ]; - /** - * @var array - */ - protected $allowedStatuses = [ + protected array $dataTypes = [ + 'project_id' => 'integer', + 'status' => 'integer', + 'create_date' => 'datetime', + 'start_date' => 'datetime', + 'finish_date' => 'datetime', + 'extra' => 'array', + 'environment_id' => 'integer', + 'source' => 'integer', + 'user_id' => 'integer', + 'errors_total' => 'integer', + 'errors_total_previous' => 'integer', + 'errors_new' => 'integer', + 'parent_id' => 'integer', + ]; + + protected array $allowedStatuses = [ self::STATUS_PENDING, self::STATUS_RUNNING, self::STATUS_SUCCESS, self::STATUS_FAILED, ]; - /** - * @var array - */ - protected $allowedSources = [ + protected array $allowedSources = [ self::SOURCE_UNKNOWN, self::SOURCE_MANUAL_WEB, self::SOURCE_MANUAL_CONSOLE, @@ -83,557 +104,322 @@ class Build extends Model self::SOURCE_WEBHOOK_PULL_REQUEST_MERGED, ]; - /** - * @return int - */ - public function getId() + public function getParentId(): ?int { - return (int)$this->data['id']; + return $this->getDataItem('parent_id'); } - /** - * @return bool - */ - public function setId(int $value) + public function setParentId(?int $value): bool { - if ($this->data['id'] === $value) { - return false; - } - - $this->data['id'] = (int)$value; - - return $this->setModified('id'); + return $this->setDataItem('parent_id', $value); } - /** - * @return int|null - */ - public function getParentId() + public function getProjectId(): ?int { - return (null !== $this->data['parent_id']) ? (int)$this->data['parent_id'] : null; + return $this->getDataItem('project_id'); } - /** - * @return bool - */ - public function setParentId(?int $value) + public function setProjectId(int $value): bool { - if ($this->data['parent_id'] === $value) { - return false; - } - - $this->data['parent_id'] = $value; - - return $this->setModified('parent_id'); + return $this->setDataItem('project_id', $value); } - /** - * @return int - */ - public function getProjectId() + public function getCommitId(): ?string { - return (int)$this->data['project_id']; + return $this->getDataItem('commit_id'); } - /** - * @return bool - */ - public function setProjectId(int $value) + public function setCommitId(string $value): bool { - if ($this->data['project_id'] === $value) { - return false; - } - - $this->data['project_id'] = $value; - - return $this->setModified('project_id'); + return $this->setDataItem('commit_id', $value); } - /** - * @return string - */ - public function getCommitId() + public function getStatus(): ?int { - return $this->data['commit_id']; + return $this->getDataItem('status'); } /** - * @return bool - */ - public function setCommitId(string $value) - { - if ($this->data['commit_id'] === $value) { - return false; - } - - $this->data['commit_id'] = $value; - - return $this->setModified('commit_id'); - } - - /** - * @return int - */ - public function getStatus() - { - return (int)$this->data['status']; - } - - /** - * @return bool - * * @throws InvalidArgumentException */ - public function setStatus(int $value) + public function setStatus(int $value): bool { - if (!in_array($value, $this->allowedStatuses, true)) { + if (!\in_array($value, $this->allowedStatuses, true)) { throw new InvalidArgumentException( - 'Column "status" must be one of: ' . join(', ', $this->allowedStatuses) . '.' + 'Column "status" must be one of: ' . \join(', ', $this->allowedStatuses) . '.' ); } - if ($this->data['status'] === $value) { - return false; - } - - $this->data['status'] = $value; - - return $this->setModified('status'); + return $this->setDataItem('status', $value); } - public function setStatusPending() + public function setStatusPending(): bool { - if (self::STATUS_PENDING !== $this->data['status']) { - $this->setModified('status'); - } - - $this->data['status'] = self::STATUS_PENDING; + return $this->setDataItem('status', self::STATUS_PENDING); } - public function setStatusRunning() + public function setStatusRunning(): bool { - if (self::STATUS_RUNNING !== $this->data['status']) { - $this->setModified('status'); - } - - $this->data['status'] = self::STATUS_RUNNING; + return $this->setDataItem('status', self::STATUS_RUNNING); } - public function setStatusSuccess() + public function setStatusSuccess(): bool { - if (self::STATUS_SUCCESS !== $this->data['status']) { - $this->setModified('status'); - } - - $this->data['status'] = self::STATUS_SUCCESS; + return $this->setDataItem('status', self::STATUS_SUCCESS); } - public function setStatusFailed() + public function setStatusFailed(): bool { - if (self::STATUS_FAILED !== $this->data['status']) { - $this->setModified('status'); - } - - $this->data['status'] = self::STATUS_FAILED; + return $this->setDataItem('status', self::STATUS_FAILED); } - /** - * @return string - */ - public function getLog() + public function getLog(): ?string { - return $this->data['log']; + return $this->getDataItem('log'); } - /** - * @return bool - */ - public function setLog(?string $value) + public function setLog(?string $value): bool { - if ($this->data['log'] === $value) { - return false; - } - - $this->data['log'] = $value; - - return $this->setModified('log'); + return $this->setDataItem('log', $value); } - /** - * @return string - */ - public function getBranch() + public function getBranch(): ?string { - return $this->data['branch']; + return $this->getDataItem('branch'); } - /** - * @return bool - */ public function setBranch(string $value) { - if ($this->data['branch'] === $value) { - return false; - } - - $this->data['branch'] = $value; - - return $this->setModified('branch'); + return $this->setDataItem('branch', $value); } - /** - * @return string - */ - public function getTag() + public function getTag(): ?string { - return $this->data['tag']; + return $this->getDataItem('tag'); } - /** - * @return bool - */ - public function setTag(?string $value) + public function setTag(?string $value): bool { - if ($this->data['tag'] === $value) { - return false; - } - - $this->data['tag'] = $value; - - return $this->setModified('tag'); + return $this->setDataItem('tag', $value); } - /** - * @return DateTime|null - * - * @throws Exception - */ - public function getCreateDate() + public function getStartDate(): ?DateTime { - if ($this->data['create_date']) { - return new DateTime($this->data['create_date']); - } - - return null; + return $this->getDataItem('start_date'); } - /** - * @return bool - */ - public function setCreateDate(DateTime $value) + public function setStartDate(DateTime $value): bool { - $stringValue = $value->format('Y-m-d H:i:s'); - - if ($this->data['create_date'] === $stringValue) { - return false; - } - - $this->data['create_date'] = $stringValue; - - return $this->setModified('create_date'); + return $this->setDataItem('start_date', $value); } - /** - * @return DateTime|null - * - * @throws Exception - */ - public function getStartDate() + public function getFinishDate(): ?DateTime { - if ($this->data['start_date']) { - return new DateTime($this->data['start_date']); - } - - return null; + return $this->getDataItem('finish_date'); } - /** - * @return bool - */ - public function setStartDate(DateTime $value) + public function setFinishDate(DateTime $value): bool { - $stringValue = $value->format('Y-m-d H:i:s'); - - if ($this->data['start_date'] === $stringValue) { - return false; - } - - $this->data['start_date'] = $stringValue; - - return $this->setModified('start_date'); + return $this->setDataItem('finish_date', $value); } - /** - * @return DateTime|null - * - * @throws Exception - */ - public function getFinishDate() + public function getCommitterEmail(): ?string { - if ($this->data['finish_date']) { - return new DateTime($this->data['finish_date']); - } - - return null; + return $this->getDataItem('committer_email'); } - /** - * @return bool - */ - public function setFinishDate(DateTime $value) + public function setCommitterEmail(?string $value): bool { - $stringValue = $value->format('Y-m-d H:i:s'); - - if ($this->data['finish_date'] === $stringValue) { - return false; - } - - $this->data['finish_date'] = $stringValue; + return $this->setDataItem('committer_email', $value); + } - return $this->setModified('finish_date'); + public function getCommitMessage(): ?string + { + return $this->getDataItem('commit_message'); } - /** - * @return string - */ - public function getCommitterEmail() + public function setCommitMessage(?string $value): bool { - return $this->data['committer_email']; + return $this->setDataItem('commit_message', $value); } /** - * @return bool + * @return mixed */ - public function setCommitterEmail(?string $value) + public function getExtra(string $key = null) { - if ($this->data['committer_email'] === $value) { - return false; + $data = $this->getDataItem('extra'); + if ($key === null) { + return $data; } - $this->data['committer_email'] = $value; - - return $this->setModified('committer_email'); + return $data[$key] ?? null; } - /** - * @return string - */ - public function getCommitMessage() + public function setExtra(array $value): bool { - return $this->data['commit_message']; + return $this->setDataItem('extra', $value); } /** - * @return bool + * @param mixed $value */ - public function setCommitMessage(?string $value) + public function addExtraValue(string $name, $value): bool { - if ($this->data['commit_message'] === $value) { - return false; + $extra = $this->getExtra(); + if ($extra === null) { + $extra = []; } + $extra[$name] = $value; - $this->data['commit_message'] = $value; - - return $this->setModified('commit_message'); + return $this->setExtra($extra); } - /** - * @param string|null $key - * - * @return array|string|null - */ - public function getExtra($key = null) - { - $data = json_decode($this->data['extra'], true); - $extra = null; - if (is_null($key)) { - $extra = $data; - } elseif (isset($data[$key])) { - $extra = $data[$key]; - } - - return $extra; - } - - /** - * @return bool - * - */ - public function setExtra(array $value) + public function removeExtraValue(string $name): bool { - $extra = json_encode($value); - if ($this->data['extra'] === $extra) { + $extra = $this->getExtra(); + if ($extra === null || !\array_key_exists($name, $extra)) { return false; } + unset($extra[$name]); - $this->data['extra'] = $extra; - - return $this->setModified('extra'); + return $this->setExtra($extra); } - /** - * @return int|null - */ - public function getEnvironmentId() + public function getEnvironmentId(): ?int { - return (null !== $this->data['environment_id']) ? (int)$this->data['environment_id'] : null; + return $this->getDataItem('environment_id'); } - /** - * @return bool - * - * @throws InvalidArgumentException - */ - public function setEnvironmentId(?int $value) + public function setEnvironmentId(?int $value): bool { - if ($this->data['environment_id'] === $value) { - return false; - } - - $this->data['environment_id'] = $value; - - return $this->setModified('environment_id'); + return $this->setDataItem('environment_id', $value); } - /** - * @return int - */ - public function getSource() + public function getSource(): ?int { - return (int)$this->data['source']; + return $this->getDataItem('source'); } /** - * @return bool - * * @throws InvalidArgumentException */ - public function setSource(?int $value) + public function setSource(?int $value): bool { - if (!in_array($value, $this->allowedSources, true)) { + if (!\in_array($value, $this->allowedSources, true)) { throw new InvalidArgumentException( - 'Column "source" must be one of: ' . join(', ', $this->allowedSources) . '.' + 'Column "source" must be one of: ' . \join(', ', $this->allowedSources) . '.' ); } - if ($this->data['source'] === $value) { - return false; - } - - $this->data['source'] = $value; - - return $this->setModified('source'); + return $this->setDataItem('source', $value); } /** - * @return int|null + * @throws InvalidArgumentException|RuntimeException */ - public function getUserId() + public function getErrorsTotal(): ?int { - return (null !== $this->data['user_id']) ? (int)$this->data['user_id'] : null; - } + if ($this->getDataItem('errors_total') === null && + !\in_array($this->getStatus(), [self::STATUS_PENDING, self::STATUS_RUNNING], true)) { + /** @var BuildStore $store */ + $store = $this->storeRegistry->get('Build'); - /** - * @return bool - */ - public function setUserId(?int $value) - { - if ($this->data['user_id'] === $value) { - return false; + $this->setErrorsTotal($store->getErrorsCount($this->getId())); + $store->save($this); } - $this->data['user_id'] = $value; + return $this->getDataItem('errors_total'); + } - return $this->setModified('user_id'); + public function setErrorsTotal(int $value): bool + { + return $this->setDataItem('errors_total', $value); } /** - * @return int - * - * @throws InvalidArgumentException + * @throws Exception */ - public function getErrorsTotal() + public function getErrorsTotalPrevious(): ?int { - if (null === $this->data['errors_total'] && - !in_array($this->getStatus(), [self::STATUS_PENDING, self::STATUS_RUNNING], true)) { + if ($this->getDataItem('errors_total_previous') === null) { /** @var BuildStore $store */ - $store = Factory::getStore('Build'); + $store = $this->storeRegistry->get('Build'); - $this->setErrorsTotal($store->getErrorsCount($this->getId())); - $store->save($this); + $trend = $store->getBuildErrorsTrend($this->getId(), $this->getProjectId(), $this->getBranch()); + + if (isset($trend[0])) { + $this->setErrorsTotalPrevious((int)$trend[0]['count']); + $store->save($this); + } } - return $this->data['errors_total']; + return $this->getDataItem('errors_total_previous'); + } + + public function setErrorsTotalPrevious(int $value): bool + { + return $this->setDataItem('errors_total_previous', $value); } /** - * @return bool + * @throws InvalidArgumentException|RuntimeException */ - public function setErrorsTotal(int $value) + public function getTestCoverage(): ?string { - if ($this->data['errors_total'] === $value) { - return false; + if ($this->getDataItem('test_coverage') === null && + !\in_array($this->getStatus(), [self::STATUS_PENDING, self::STATUS_RUNNING], true)) { + /** @var BuildStore $store */ + $store = $this->storeRegistry->get('Build'); + + $this->setTestCoverage($store->getTestCoverage($this->getId())); + $store->save($this); } - $this->data['errors_total'] = $value; + return $this->getDataItem('test_coverage'); + } - return $this->setModified('errors_total'); + public function setTestCoverage(string $value): bool + { + return $this->setDataItem('test_coverage', $value); } /** - * @return int|null - * * @throws Exception */ - public function getErrorsTotalPrevious() + public function getTestCoveragePrevious(): ?string { - if (null === $this->data['errors_total_previous']) { + if ($this->getDataItem('test_coverage_previous') === null) { /** @var BuildStore $store */ - $store = Factory::getStore('Build'); + $store = $this->storeRegistry->get('Build'); - $trend = $store->getBuildErrorsTrend($this->getId(), $this->getProjectId(), $this->getBranch()); + $trend = $store->getBuildTestCoverageTrend($this->getId(), $this->getProjectId(), $this->getBranch()); - if (isset($trend[1])) { - $previousBuild = $store->getById($trend[1]['build_id']); - if ($previousBuild && - !in_array( - $previousBuild->getStatus(), - [self::STATUS_PENDING, self::STATUS_RUNNING], - true - )) { - $this->setErrorsTotalPrevious((int)$trend[1]['count']); + if (!empty($trend[0]) && !empty($trend[0]['coverage'])) { + $coverage = \json_decode($trend[0]['coverage'], true); + if (isset($coverage['lines'])) { + $this->setTestCoveragePrevious($coverage['lines']); $store->save($this); } } } - return $this->data['errors_total_previous']; + return $this->getDataItem('test_coverage_previous'); } - /** - * @return bool - */ - public function setErrorsTotalPrevious(int $value) + public function setTestCoveragePrevious(string $value): bool { - if ($this->data['errors_total_previous'] === $value) { - return false; - } - - $this->data['errors_total_previous'] = $value; - - return $this->setModified('errors_total_previous'); + return $this->setDataItem('test_coverage_previous', $value); } /** - * @return int - * - * @throws InvalidArgumentException + * @throws InvalidArgumentException|RuntimeException */ - public function getErrorsNew() + public function getErrorsNew(): ?int { - if (null === $this->data['errors_new']) { + if ($this->getDataItem('errors_new') === null) { /** @var BuildStore $errorStore */ - $store = Factory::getStore('Build'); + $store = $this->storeRegistry->get('Build'); $this->setErrorsNew( (int)$store->getNewErrorsCount($this->getId()) @@ -642,36 +428,16 @@ public function getErrorsNew() $store->save($this); } - return $this->data['errors_new']; + return $this->getDataItem('errors_new'); } - /** - * @return bool - */ - public function setErrorsNew(int $value) + public function setErrorsNew(int $value): bool { - if ($this->data['errors_new'] === $value) { - return false; - } - - $this->data['errors_new'] = $value; - - return $this->setModified('errors_new'); + return $this->setDataItem('errors_new', $value); } - /** - * @return bool - */ - public function isDebug() + public function isDebug(): bool { - if (defined('DEBUG_MODE') && DEBUG_MODE) { - return true; - } - - if ($this->getExtra('debug')) { - return true; - } - - return false; + return (\defined('DEBUG_MODE') && DEBUG_MODE) || $this->getExtra('debug'); } } diff --git a/src/Model/Base/BuildError.php b/src/Model/Base/BuildError.php index c4aea954d..f430511e4 100644 --- a/src/Model/Base/BuildError.php +++ b/src/Model/Base/BuildError.php @@ -4,22 +4,26 @@ namespace PHPCensor\Model\Base; -use DateTime; -use Exception; -use PHPCensor\Exception\InvalidArgumentException; use PHPCensor\Model; - +use PHPCensor\Traits\Model\HasCreateDateTrait; + +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dan Cryer + * @author Dmitry Khomutov + */ class BuildError extends Model { + use HasCreateDateTrait; + public const SEVERITY_CRITICAL = 0; public const SEVERITY_HIGH = 1; public const SEVERITY_NORMAL = 2; public const SEVERITY_LOW = 3; - /** - * @var array - */ - protected $data = [ + protected array $data = [ 'id' => null, 'build_id' => null, 'plugin' => null, @@ -33,253 +37,102 @@ class BuildError extends Model 'is_new' => 0, ]; - /** - * @return int - */ - public function getId() - { - return (int)$this->data['id']; - } - - /** - * @return bool - */ - public function setId(int $value) - { - if ($this->data['id'] === $value) { - return false; - } - - $this->data['id'] = $value; - - return $this->setModified('id'); - } - - /** - * @return int - */ - public function getBuildId() - { - return (int)$this->data['build_id']; - } - - /** - * @return bool - */ - public function setBuildId(int $value) - { - if ($this->data['build_id'] === $value) { - return false; - } - - $this->data['build_id'] = $value; - - return $this->setModified('build_id'); - } + protected array $dataTypes = [ + 'build_id' => 'integer', + 'line_start' => 'integer', + 'line_end' => 'integer', + 'severity' => 'integer', + 'create_date' => 'datetime', + 'is_new' => 'boolean' + ]; - /** - * @return string - */ - public function getPlugin() + public function getBuildId(): ?int { - return $this->data['plugin']; + return $this->getDataItem('build_id'); } - /** - * @return bool - */ - public function setPlugin(string $value) + public function setBuildId(int $value): bool { - if ($this->data['plugin'] === $value) { - return false; - } - - $this->data['plugin'] = $value; - - return $this->setModified('plugin'); + return $this->setDataItem('build_id', $value); } - /** - * @return string - */ - public function getFile() + public function getPlugin(): ?string { - return $this->data['file']; + return $this->getDataItem('plugin'); } - /** - * @return bool - */ - public function setFile(?string $value) + public function setPlugin(string $value): bool { - if ($this->data['file'] === $value) { - return false; - } - - $this->data['file'] = $value; - - return $this->setModified('file'); + return $this->setDataItem('plugin', $value); } - /** - * @return int - */ - public function getLineStart() + public function getFile(): ?string { - return (int)$this->data['line_start']; + return $this->getDataItem('file'); } - /** - * @return bool - */ - public function setLineStart(?int $value) + public function setFile(?string $value): bool { - if ($this->data['line_start'] === $value) { - return false; - } - - $this->data['line_start'] = $value; - - return $this->setModified('line_start'); + return $this->setDataItem('file', $value); } - /** - * @return int - */ - public function getLineEnd() + public function getLineStart(): ?int { - return (int)$this->data['line_end']; + return $this->getDataItem('line_start'); } - /** - * @return bool - */ - public function setLineEnd(?int $value) + public function setLineStart(?int $value): bool { - if ($this->data['line_end'] === $value) { - return false; - } - - $this->data['line_end'] = $value; - - return $this->setModified('line_end'); + return $this->setDataItem('line_start', $value); } - /** - * @return int - */ - public function getSeverity() + public function getLineEnd(): ?int { - return (int)$this->data['severity']; + return $this->getDataItem('line_end'); } - /** - * @return bool - */ - public function setSeverity(int $value) + public function setLineEnd(?int $value): bool { - if ($this->data['severity'] === $value) { - return false; - } - - $this->data['severity'] = $value; - - return $this->setModified('severity'); + return $this->setDataItem('line_end', $value); } - /** - * @return string - */ - public function getMessage() + public function getSeverity(): ?int { - return $this->data['message']; + return $this->getDataItem('severity'); } - /** - * @return bool - */ - public function setMessage(string $value) + public function setSeverity(int $value): bool { - if ($this->data['message'] === $value) { - return false; - } - - $this->data['message'] = $value; - - return $this->setModified('message'); + return $this->setDataItem('severity', $value); } - /** - * @return DateTime|null - * - * @throws Exception - */ - public function getCreateDate() + public function getMessage(): ?string { - if ($this->data['create_date']) { - return new DateTime($this->data['create_date']); - } - - return null; + return $this->getDataItem('message'); } - /** - * @return bool - */ - public function setCreateDate(DateTime $value) + public function setMessage(string $value): bool { - $stringValue = $value->format('Y-m-d H:i:s'); - - if ($this->data['create_date'] === $stringValue) { - return false; - } - - $this->data['create_date'] = $stringValue; - - return $this->setModified('create_date'); + return $this->setDataItem('message', $value); } - /** - * @return string - */ - public function getHash() + public function getHash(): ?string { - return $this->data['hash']; + return $this->getDataItem('hash'); } - /** - * @return bool - */ - public function setHash(string $value) + public function setHash(string $value): bool { - if ($this->data['hash'] === $value) { - return false; - } - - $this->data['hash'] = $value; - - return $this->setModified('hash'); + return $this->setDataItem('hash', $value); } - /** - * @return bool - */ - public function getIsNew() + public function getIsNew(): bool { - return (bool)$this->data['is_new']; + return $this->getDataItem('is_new'); } - /** - * @return bool - */ - public function setIsNew(bool $value) + public function setIsNew(bool $value): bool { - if ($this->data['is_new'] === (int)$value) { - return false; - } - - $this->data['is_new'] = (int)$value; - - return $this->setModified('is_new'); + return $this->setDataItem('is_new', $value); } } diff --git a/src/Model/Base/BuildMeta.php b/src/Model/Base/BuildMeta.php index 4fe8bed56..cc5b43536 100644 --- a/src/Model/Base/BuildMeta.php +++ b/src/Model/Base/BuildMeta.php @@ -4,106 +4,55 @@ namespace PHPCensor\Model\Base; -use PHPCensor\Exception\InvalidArgumentException; use PHPCensor\Model; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dan Cryer + * @author Dmitry Khomutov + */ class BuildMeta extends Model { - /** - * @var array - */ - protected $data = [ + protected array $data = [ 'id' => null, 'build_id' => null, 'meta_key' => null, 'meta_value' => null, ]; - /** - * @return int - */ - public function getId() - { - return (int)$this->data['id']; - } - - /** - * @return bool - */ - public function setId(int $value) - { - if ($this->data['id'] === $value) { - return false; - } - - $this->data['id'] = $value; - - return $this->setModified('id'); - } + protected array $dataTypes = [ + 'build_id' => 'integer', + ]; - /** - * @return int - */ - public function getBuildId() + public function getBuildId(): int { - return (int)$this->data['build_id']; + return $this->getDataItem('build_id'); } - /** - * @return bool - */ - public function setBuildId(int $value) + public function setBuildId(int $value): bool { - if ($this->data['build_id'] === $value) { - return false ; - } - - $this->data['build_id'] = $value; - - return $this->setModified('build_id'); + return $this->setDataItem('build_id', $value); } - /** - * @return string - */ - public function getMetaKey() + public function getMetaKey(): ?string { - return $this->data['meta_key']; + return $this->getDataItem('meta_key'); } - /** - * @return bool - */ - public function setMetaKey(string $value) + public function setMetaKey(string $value): bool { - if ($this->data['meta_key'] === $value) { - return false; - } - - $this->data['meta_key'] = $value; - - return $this->setModified('meta_key'); + return $this->setDataItem('meta_key', $value); } - /** - * @return string - */ - public function getMetaValue() + public function getMetaValue(): ?string { - return $this->data['meta_value']; + return $this->getDataItem('meta_value'); } - /** - * @return bool - */ - public function setMetaValue(string $value) + public function setMetaValue(string $value): bool { - if ($this->data['meta_value'] === $value) { - return false; - } - - $this->data['meta_value'] = $value; - - return $this->setModified('meta_value'); + return $this->setDataItem('meta_value', $value); } } diff --git a/src/Model/Base/Environment.php b/src/Model/Base/Environment.php index 7f7d49480..bbfedaf94 100644 --- a/src/Model/Base/Environment.php +++ b/src/Model/Base/Environment.php @@ -4,112 +4,55 @@ namespace PHPCensor\Model\Base; -use PHPCensor\Exception\InvalidArgumentException; use PHPCensor\Model; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov + */ class Environment extends Model { - /** - * @var array - */ - protected $data = [ + protected array $data = [ 'id' => null, 'project_id' => null, 'name' => null, - 'branches' => null, + 'branches' => [], ]; - /** - * @return int - */ - public function getId() - { - return (int)$this->data['id']; - } - - /** - * @return bool - */ - public function setId(int $value) - { - if ($this->data['id'] === $value) { - return false; - } - - $this->data['id'] = $value; - - return $this->setModified('id'); - } + protected array $dataTypes = [ + 'project_id' => 'integer', + 'branches' => 'newline' + ]; - /** - * @return int - */ - public function getProjectId() + public function getProjectId(): ?int { - return (int)$this->data['project_id']; + return $this->getDataItem('project_id'); } - /** - * @return bool - */ - public function setProjectId(int $value) + public function setProjectId(int $value): bool { - if ($this->data['project_id'] === $value) { - return false; - } - - $this->data['project_id'] = $value; - - return $this->setModified('project_id'); + return $this->setDataItem('project_id', $value); } - /** - * @return string - */ - public function getName() + public function getName(): ?string { - return $this->data['name']; + return $this->getDataItem('name'); } - /** - * @return bool - */ - public function setName(string $value) + public function setName(string $value): bool { - if ($this->data['name'] === $value) { - return false; - } - - $this->data['name'] = $value; - - return $this->setModified('name'); + return $this->setDataItem('name', $value); } - /** - * @return array - */ - public function getBranches() + public function getBranches(): array { - return array_filter( - array_map( - 'trim', - explode("\n", $this->data['branches']) - ) - ); + return $this->getDataItem('branches'); } - /** - * @return bool - */ - public function setBranches(array $value) + public function setBranches(array $value): bool { - $branches = implode("\n", $value); - if ($this->data['branches'] === $branches) { - return false; - } - - $this->data['branches'] = $branches; - - return $this->setModified('branches'); + return $this->setDataItem('branches', $value); } } diff --git a/src/Model/Base/Project.php b/src/Model/Base/Project.php index c582e20b4..e82a759e2 100644 --- a/src/Model/Base/Project.php +++ b/src/Model/Base/Project.php @@ -4,33 +4,40 @@ namespace PHPCensor\Model\Base; -use DateTime; -use Exception; -use PHPCensor\Exception\InvalidArgumentException; +use PHPCensor\Common\Exception\InvalidArgumentException; use PHPCensor\Model; - +use PHPCensor\Traits\Model\HasCreateDateTrait; +use PHPCensor\Traits\Model\HasUserIdTrait; + +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dan Cryer + * @author Dmitry Khomutov + */ class Project extends Model { - public const TYPE_LOCAL = 'local'; - public const TYPE_GIT = 'git'; - public const TYPE_GITHUB = 'github'; - public const TYPE_BITBUCKET = 'bitbucket'; - public const TYPE_GITLAB = 'gitlab'; - public const TYPE_GOGS = 'gogs'; - public const TYPE_HG = 'hg'; - public const TYPE_BITBUCKET_HG = 'bitbucket-hg'; + use HasCreateDateTrait; + use HasUserIdTrait; + + public const TYPE_LOCAL = 'local'; + public const TYPE_GIT = 'git'; + public const TYPE_GITHUB = 'github'; + public const TYPE_BITBUCKET = 'bitbucket'; + public const TYPE_GITLAB = 'gitlab'; + public const TYPE_GOGS = 'gogs'; + public const TYPE_HG = 'hg'; + public const TYPE_BITBUCKET_HG = 'bitbucket-hg'; public const TYPE_BITBUCKET_SERVER = 'bitbucket-server'; - public const TYPE_SVN = 'svn'; + public const TYPE_SVN = 'svn'; - public const MIN_BUILD_PRIORITY = 1; - public const MAX_BUILD_PRIORITY = 2000; - public const DEFAULT_BUILD_PRIORITY = 1000; + public const MIN_BUILD_PRIORITY = 1; + public const MAX_BUILD_PRIORITY = 2000; + public const DEFAULT_BUILD_PRIORITY = 1000; public const OFFSET_BETWEEN_BUILD_AND_QUEUE = 24; - /** - * @var array - */ - protected $data = [ + protected array $data = [ 'id' => null, 'title' => null, 'reference' => null, @@ -49,10 +56,18 @@ class Project extends Model 'user_id' => null, ]; - /** - * @var array - */ - public static $allowedTypes = [ + protected array $dataTypes = [ + 'allow_public_status' => 'boolean', + 'archived' => 'boolean', + 'group_id' => 'integer', + 'default_branch_only' => 'boolean', + 'create_date' => 'datetime', + 'user_id' => 'integer', + 'access_information' => 'array', + 'overwrite_build_config' => 'boolean' + ]; + + public static array $allowedTypes = [ self::TYPE_LOCAL, self::TYPE_GIT, self::TYPE_GITHUB, @@ -65,382 +80,150 @@ class Project extends Model self::TYPE_BITBUCKET_SERVER ]; - /** - * @return int - */ - public function getId() - { - return (int)$this->data['id']; - } - - /** - * @return bool - */ - public function setId(int $value) - { - if ($this->data['id'] === $value) { - return false; - } - - $this->data['id'] = $value; - - return $this->setModified('id'); - } - - /** - * @return string - */ - public function getTitle() + public function getTitle(): ?string { - return $this->data['title']; + return $this->getDataItem('title'); } - /** - * @return bool - */ - public function setTitle(string $value) + public function setTitle(string $value): bool { - if ($this->data['title'] === $value) { - return false; - } - - $this->data['title'] = $value; - - return $this->setModified('title'); + return $this->setDataItem('title', $value); } - /** - * @return string - */ - public function getReference() + public function getReference(): ?string { - return $this->data['reference']; + return $this->getDataItem('reference'); } - /** - * @return bool - */ - public function setReference(string $value) + public function setReference(string $value): bool { - if ($this->data['reference'] === $value) { - return false; - } - - $this->data['reference'] = $value; - - return $this->setModified('reference'); + return $this->setDataItem('reference', $value); } - /** - * @return string - */ - public function getDefaultBranch() + public function getDefaultBranch(): ?string { - return $this->data['default_branch']; + return $this->getDataItem('default_branch'); } - /** - * @return bool - */ - public function setDefaultBranch(string $value) + public function setDefaultBranch(string $value): bool { - if ($this->data['default_branch'] === $value) { - return false; - } - - $this->data['default_branch'] = $value; - - return $this->setModified('default_branch'); + return $this->setDataItem('default_branch', $value); } - /** - * @return bool - */ - public function getDefaultBranchOnly() + public function getDefaultBranchOnly(): bool { - return (bool)$this->data['default_branch_only']; + return $this->getDataItem('default_branch_only'); } - /** - * @return bool - */ - public function setDefaultBranchOnly(bool $value) + public function setDefaultBranchOnly(bool $value): bool { - if ($this->data['default_branch_only'] === (int)$value) { - return false; - } - - $this->data['default_branch_only'] = (int)$value; - - return $this->setModified('default_branch_only'); + return $this->setDataItem('default_branch_only', $value); } - /** - * @return string - */ - public function getSshPrivateKey() + public function getSshPrivateKey(): ?string { - return $this->data['ssh_private_key']; + return $this->getDataItem('ssh_private_key'); } - /** - * @return bool - */ - public function setSshPrivateKey(?string $value) + public function setSshPrivateKey(?string $value): bool { - if ($this->data['ssh_private_key'] === $value) { - return false; - } - - $this->data['ssh_private_key'] = $value; - - return $this->setModified('ssh_private_key'); + return $this->setDataItem('ssh_private_key', $value); } - /** - * @return string - */ - public function getSshPublicKey() + public function getSshPublicKey(): ?string { - return $this->data['ssh_public_key']; + return $this->getDataItem('ssh_public_key'); } - /** - * @return bool - */ - public function setSshPublicKey(?string $value) + public function setSshPublicKey(?string $value): bool { - if ($this->data['ssh_public_key'] === $value) { - return false; - } - - $this->data['ssh_public_key'] = $value; - - return $this->setModified('ssh_public_key'); + return $this->setDataItem('ssh_public_key', $value); } - /** - * @return string - */ - public function getType() + public function getType(): ?string { - return $this->data['type']; + return $this->getDataItem('type'); } /** - * @return bool - * * @throws InvalidArgumentException */ - public function setType(string $value) + public function setType(string $value): bool { - if (!in_array($value, static::$allowedTypes, true)) { + if (!\in_array($value, static::$allowedTypes, true)) { throw new InvalidArgumentException( - 'Column "type" must be one of: ' . join(', ', static::$allowedTypes) . '.' + 'Column "type" must be one of: ' . \join(', ', static::$allowedTypes) . '.' ); } - if ($this->data['type'] === $value) { - return false; - } - - $this->data['type'] = $value; - - return $this->setModified('type'); - } - - /** - * @param string|null $key - * - * @return array|string|null - */ - public function getAccessInformation($key = null) - { - $data = \json_decode((string)$this->data['access_information'], true); - $accessInformation = null; - if (is_null($key)) { - $accessInformation = $data; - } elseif (isset($data[$key])) { - $accessInformation = $data[$key]; - } - - return $accessInformation; - } - - /** - * @return bool - */ - public function setAccessInformation(array $value) - { - $accessInformation = \json_encode($value); - if ($this->data['access_information'] === $accessInformation) { - return false; - } - - $this->data['access_information'] = $accessInformation; - - return $this->setModified('access_information'); - } - - /** - * @return string - */ - public function getBuildConfig() - { - return $this->data['build_config']; + return $this->setDataItem('type', $value); } /** - * @return bool + * @return mixed */ - public function setBuildConfig(?string $value) + public function getAccessInformation(string $key = null) { - if ($this->data['build_config'] === $value) { - return false; + $data = $this->getDataItem('access_information'); + if ($key === null) { + return $data; } - $this->data['build_config'] = $value; - - return $this->setModified('build_config'); + return $data[$key] ?? null; } - /** - * @return bool - */ - public function getOverwriteBuildConfig() + public function setAccessInformation(array $value): bool { - return (bool)$this->data['overwrite_build_config']; + return $this->setDataItem('access_information', $value); } - /** - * @return bool - */ - public function setOverwriteBuildConfig(bool $value) + public function getBuildConfig(): ?string { - if ($this->data['overwrite_build_config'] === (int)$value) { - return false; - } - - $this->data['overwrite_build_config'] = (int)$value; - - return $this->setModified('overwrite_build_config'); - } - - /** - * @return bool - */ - public function getAllowPublicStatus() - { - return (bool)$this->data['allow_public_status']; + return $this->getDataItem('build_config'); } - /** - * @return bool - */ - public function setAllowPublicStatus(bool $value) + public function setBuildConfig(?string $value): bool { - if ($this->data['allow_public_status'] === (int)$value) { - return false; - } - - $this->data['allow_public_status'] = (int)$value; - - return $this->setModified('allow_public_status'); + return $this->setDataItem('build_config', $value); } - /** - * @return bool - */ - public function getArchived() + public function getOverwriteBuildConfig(): bool { - return (bool)$this->data['archived']; + return $this->getDataItem('overwrite_build_config'); } - /** - * @return bool - */ - public function setArchived(bool $value) + public function setOverwriteBuildConfig(bool $value): bool { - if ($this->data['archived'] === (int)$value) { - return false; - } - - $this->data['archived'] = (int)$value; - - return $this->setModified('archived'); + return $this->setDataItem('overwrite_build_config', $value); } - /** - * @return int - */ - public function getGroupId() + public function getAllowPublicStatus(): bool { - return (int)$this->data['group_id']; + return $this->getDataItem('allow_public_status'); } - /** - * @return bool - */ - public function setGroupId(int $value) + public function setAllowPublicStatus(bool $value): bool { - if ($this->data['group_id'] === $value) { - return false; - } - - $this->data['group_id'] = $value; - - return $this->setModified('group_id'); + return $this->setDataItem('allow_public_status', $value); } - /** - * @return DateTime|null - * - * @throws Exception - */ - public function getCreateDate() + public function getArchived(): bool { - if ($this->data['create_date']) { - return new DateTime($this->data['create_date']); - } - - return null; + return $this->getDataItem('archived'); } - /** - * @return bool - */ - public function setCreateDate(DateTime $value) + public function setArchived(bool $value): bool { - $stringValue = $value->format('Y-m-d H:i:s'); - - if ($this->data['create_date'] === $stringValue) { - return false; - } - - $this->data['create_date'] = $stringValue; - - return $this->setModified('create_date'); + return $this->setDataItem('archived', $value); } - /** - * @return int|null - */ - public function getUserId() + public function getGroupId(): int { - return (null !== $this->data['user_id']) ? (int)$this->data['user_id'] : null; + return $this->getDataItem('group_id'); } - /** - * @return bool - */ - public function setUserId(?int $value) + public function setGroupId(int $value): bool { - if ($this->data['user_id'] === $value) { - return false; - } - - $this->data['user_id'] = $value; - - return $this->setModified('user_id'); + return $this->setDataItem('group_id', $value); } } diff --git a/src/Model/Base/ProjectGroup.php b/src/Model/Base/ProjectGroup.php index e1a7bc55e..07da33b6d 100644 --- a/src/Model/Base/ProjectGroup.php +++ b/src/Model/Base/ProjectGroup.php @@ -4,116 +4,40 @@ namespace PHPCensor\Model\Base; -use DateTime; -use Exception; -use PHPCensor\Exception\InvalidArgumentException; use PHPCensor\Model; - +use PHPCensor\Traits\Model\HasCreateDateTrait; +use PHPCensor\Traits\Model\HasUserIdTrait; + +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov + */ class ProjectGroup extends Model { - /** - * @var array - */ - protected $data = [ + use HasCreateDateTrait; + use HasUserIdTrait; + + protected array $data = [ 'id' => null, 'title' => null, 'create_date' => null, 'user_id' => null, ]; - /** - * @return int - */ - public function getId() - { - return (int)$this->data['id']; - } - - /** - * @return bool - */ - public function setId(int $value) - { - if ($this->data['id'] === $value) { - return false; - } - - $this->data['id'] = $value; - - return $this->setModified('id'); - } - - /** - * @return string - */ - public function getTitle() - { - return $this->data['title']; - } - - /** - * @return bool - */ - public function setTitle(string $value) - { - if ($this->data['title'] === $value) { - return false; - } - - $this->data['title'] = $value; - - return $this->setModified('title'); - } - - /** - * @return DateTime|null - * - * @throws Exception - */ - public function getCreateDate() - { - if ($this->data['create_date']) { - return new DateTime($this->data['create_date']); - } - - return null; - } - - /** - * @return bool - */ - public function setCreateDate(DateTime $value) - { - $stringValue = $value->format('Y-m-d H:i:s'); - - if ($this->data['create_date'] === $stringValue) { - return false; - } - - $this->data['create_date'] = $stringValue; - - return $this->setModified('create_date'); - } + protected array $dataTypes = [ + 'user_id' => 'integer', + 'create_date' => 'datetime' + ]; - /** - * @return string|null - */ - public function getUserId() + public function getTitle(): ?string { - return (null !== $this->data['user_id']) ? (int)$this->data['user_id'] : null; + return $this->getDataItem('title'); } - /** - * @return bool - */ - public function setUserId(?int $value) + public function setTitle(string $value): bool { - if ($this->data['user_id'] === $value) { - return false; - } - - $this->data['user_id'] = $value; - - return $this->setModified('user_id'); + return $this->setDataItem('title', $value); } } diff --git a/src/Model/Base/Secret.php b/src/Model/Base/Secret.php new file mode 100644 index 000000000..8f021ff1b --- /dev/null +++ b/src/Model/Base/Secret.php @@ -0,0 +1,54 @@ + + */ +class Secret extends Model +{ + use HasCreateDateTrait; + use HasUserIdTrait; + + protected array $data = [ + 'id' => null, + 'name' => null, + 'value' => null, + 'create_date' => null, + 'user_id' => null, + ]; + + protected array $dataTypes = [ + 'create_date' => 'datetime', + 'user_id' => 'integer', + ]; + + public function getName(): ?string + { + return $this->getDataItem('name'); + } + + public function setName(string $value): bool + { + return $this->setDataItem('name', $value); + } + + public function getValue(): ?string + { + return $this->getDataItem('value'); + } + + public function setValue(string $value): bool + { + return $this->setDataItem('value', $value); + } +} diff --git a/src/Model/Base/User.php b/src/Model/Base/User.php index de58b128f..b35cc0d26 100644 --- a/src/Model/Base/User.php +++ b/src/Model/Base/User.php @@ -4,15 +4,18 @@ namespace PHPCensor\Model\Base; -use PHPCensor\Exception\InvalidArgumentException; use PHPCensor\Model; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dan Cryer + * @author Dmitry Khomutov + */ class User extends Model { - /** - * @var array - */ - protected $data = [ + protected array $data = [ 'id' => null, 'email' => null, 'hash' => null, @@ -25,236 +28,107 @@ class User extends Model 'remember_key' => null, ]; - /** - * @return int - */ - public function getId() - { - return (int)$this->data['id']; - } - - /** - * @return bool - */ - public function setId(int $value) - { - if ($this->data['id'] === $value) { - return false; - } - - $this->data['id'] = $value; - - return $this->setModified('id'); - } + protected array $dataTypes = [ + 'is_admin' => 'boolean', + 'per_page' => 'integer', + 'provider_data' => 'array' + ]; - /** - * @return string - */ - public function getEmail() + public function getEmail(): ?string { - return $this->data['email']; + return $this->getDataItem('email'); } - /** - * @return bool - */ - public function setEmail(string $value) + public function setEmail(string $value): bool { - if ($this->data['email'] === $value) { - return false; - } - - $this->data['email'] = $value; - - return $this->setModified('email'); + return $this->setDataItem('email', $value); } - /** - * @return string - */ - public function getHash() + public function getHash(): ?string { - return $this->data['hash']; + return $this->getDataItem('hash'); } - /** - * @return bool - */ - public function setHash(string $value) + public function setHash(string $value): bool { - if ($this->data['hash'] === $value) { - return false; - } - - $this->data['hash'] = $value; - - return $this->setModified('hash'); + return $this->setDataItem('hash', $value); } - /** - * @return bool - */ - public function getIsAdmin() + public function getIsAdmin(): bool { - return (bool)$this->data['is_admin']; + return $this->getDataItem('is_admin'); } - /** - * @return bool - */ - public function setIsAdmin(bool $value) + public function setIsAdmin(bool $value): bool { - if ($this->data['is_admin'] === (int)$value) { - return false; - } - - $this->data['is_admin'] = (int)$value; - - return $this->setModified('is_admin'); + return $this->setDataItem('is_admin', $value); } - /** - * @return string - */ - public function getName() + public function getName(): ?string { - return $this->data['name']; + return $this->getDataItem('name'); } - /** - * @return bool - */ - public function setName(string $value) + public function setName(string $value): bool { - if ($this->data['name'] === $value) { - return false; - } - - $this->data['name'] = $value; - - return $this->setModified('name'); + return $this->setDataItem('name', $value); } - /** - * @return string - */ - public function getLanguage() + public function getLanguage(): ?string { - return $this->data['language']; + return $this->getDataItem('language'); } - /** - * @param string $value - * - * @return bool - */ - public function setLanguage($value) + public function setLanguage(?string $value): bool { - if ($this->data['language'] === $value) { - return false; - } - - $this->data['language'] = $value; - - return $this->setModified('language'); + return $this->setDataItem('language', $value); } - /** - * @return int - */ - public function getPerPage() + public function getPerPage(): ?int { - return (int)$this->data['per_page']; + return $this->getDataItem('per_page'); } - /** - * @return bool - */ - public function setPerPage(?int $value) + public function setPerPage(?int $value): bool { - if ($this->data['per_page'] === $value) { - return false; - } - - $this->data['per_page'] = $value; - - return $this->setModified('per_page'); + return $this->setDataItem('per_page', $value); } - /** - * @return string - */ - public function getProviderKey() + public function getProviderKey(): ?string { - return $this->data['provider_key']; + return $this->getDataItem('provider_key'); } - /** - * @return bool - */ - public function setProviderKey(string $value) + public function setProviderKey(string $value): bool { - if ($this->data['provider_key'] === $value) { - return false; - } - - $this->data['provider_key'] = $value; - - return $this->setModified('provider_key'); + return $this->setDataItem('provider_key', $value); } /** - * @param string|null $key - * - * @return array|string|null + * @return mixed */ - public function getProviderData($key = null) + public function getProviderData(string $key = null) { - $data = json_decode($this->data['provider_data'], true); - $providerData = null; - if (is_null($key)) { - $providerData = $data; - } elseif (isset($data[$key])) { - $providerData = $data[$key]; + $data = $this->getDataItem('provider_data'); + if ($key === null) { + return $data; } - return $providerData; + return $data[$key] ?? null; } - /** - * @return bool - */ - public function setProviderData(array $value) + public function setProviderData(array $value): bool { - $providerData = json_encode($value); - if ($this->data['provider_data'] === $providerData) { - return false; - } - - $this->data['provider_data'] = $providerData; - - return $this->setModified('provider_data'); + return $this->setDataItem('provider_data', $value); } - /** - * @return string - */ - public function getRememberKey() + public function getRememberKey(): ?string { - return $this->data['remember_key']; + return $this->getDataItem('remember_key'); } - /** - * @return bool - */ - public function setRememberKey(?string $value) + public function setRememberKey(?string $value): bool { - if ($this->data['remember_key'] === $value) { - return false; - } - - $this->data['remember_key'] = $value; - - return $this->setModified('remember_key'); + return $this->setDataItem('remember_key', $value); } } diff --git a/src/Model/Base/WebhookRequest.php b/src/Model/Base/WebhookRequest.php new file mode 100644 index 000000000..01b4cdb3f --- /dev/null +++ b/src/Model/Base/WebhookRequest.php @@ -0,0 +1,70 @@ + + */ +class WebhookRequest extends Model +{ + use HasCreateDateTrait; + + public const WEBHOOK_TYPE_GIT = 'git'; + public const WEBHOOK_TYPE_GITHUB = 'github'; + public const WEBHOOK_TYPE_BITBUCKET = 'bitbucket'; + public const WEBHOOK_TYPE_GITLAB = 'gitlab'; + public const WEBHOOK_TYPE_GOGS = 'gogs'; + public const WEBHOOK_TYPE_HG = 'hg'; + public const WEBHOOK_TYPE_SVN = 'svn'; + + protected array $data = [ + 'id' => null, + 'project_id' => null, + 'webhook_type' => null, + 'payload' => null, + 'create_date' => null, + ]; + + protected array $dataTypes = [ + 'project_id' => 'integer', + 'create_date' => 'datetime', + ]; + + public function getProjectId(): ?int + { + return $this->getDataItem('project_id'); + } + + public function setProjectId(int $value): bool + { + return $this->setDataItem('project_id', $value); + } + + public function getWebhookType(): ?string + { + return $this->getDataItem('webhook_type'); + } + + public function setWebhookType(string $value): bool + { + return $this->setDataItem('webhook_type', $value); + } + + public function getPayload(): ?string + { + return $this->getDataItem('payload'); + } + + public function setPayload(?string $value): bool + { + return $this->setDataItem('payload', $value); + } +} diff --git a/src/Model/Build.php b/src/Model/Build.php index 53817a4b3..145ae8c52 100644 --- a/src/Model/Build.php +++ b/src/Model/Build.php @@ -1,5 +1,7 @@ + * @author Dmitry Khomutov */ class Build extends BaseBuild { @@ -33,20 +39,14 @@ class Build extends BaseBuild public const STAGE_FIXED = 'fixed'; public const STAGE_BROKEN = 'broken'; - /** - * @var array - */ - public static $pullRequestSources = [ + public static array $pullRequestSources = [ self::SOURCE_WEBHOOK_PULL_REQUEST_CREATED, self::SOURCE_WEBHOOK_PULL_REQUEST_UPDATED, self::SOURCE_WEBHOOK_PULL_REQUEST_APPROVED, self::SOURCE_WEBHOOK_PULL_REQUEST_MERGED, ]; - /** - * @var array - */ - public static $webhookSources = [ + public static array $webhookSources = [ self::SOURCE_WEBHOOK_PUSH, self::SOURCE_WEBHOOK_PULL_REQUEST_CREATED, self::SOURCE_WEBHOOK_PULL_REQUEST_UPDATED, @@ -54,20 +54,11 @@ class Build extends BaseBuild self::SOURCE_WEBHOOK_PULL_REQUEST_MERGED, ]; - /** - * @var array - */ - protected $totalErrorsCount = []; + protected array $totalErrorsCount = []; - /** - * @var string - */ - protected $buildDirectory; + protected string $buildDirectory; - /** - * @var string - */ - protected $buildBranchDirectory; + protected string $buildBranchDirectory; /** * @return Project|null @@ -82,39 +73,11 @@ public function getProject() } /** @var ProjectStore $projectStore */ - $projectStore = Factory::getStore('Project'); + $projectStore = $this->storeRegistry->get('Project'); return $projectStore->getById($projectId); } - /** - * @param string $name - * @param mixed $value - */ - public function addExtraValue($name, $value) - { - $extra = json_decode($this->data['extra'], true); - if ($extra === false) { - $extra = []; - } - $extra[$name] = $value; - - $this->setExtra($extra); - } - - /** - * @param string $name - */ - public function removeExtraValue($name) - { - $extra = $this->getExtra(); - if (!empty($extra[$name])) { - unset($extra[$name]); - } - - $this->setExtra($extra); - } - /** * Get BuildError models by BuildId for this Build. * @@ -122,7 +85,7 @@ public function removeExtraValue($name) */ public function getBuildBuildErrors() { - return Factory::getStore('BuildError')->getByBuildId($this->getId()); + return $this->storeRegistry->get('BuildError')->getByBuildId($this->getId()); } /** @@ -132,7 +95,7 @@ public function getBuildBuildErrors() */ public function getBuildBuildMetas() { - return Factory::getStore('BuildMeta')->getByBuildId($this->getId()); + return $this->storeRegistry->get('BuildMeta')->getByBuildId($this->getId()); } /** @@ -213,9 +176,9 @@ public function getProjectTitle() */ public function storeMeta($key, $value) { - $value = json_encode($value); + $value = \json_encode($value); - Factory::getStore('Build')->setMeta($this->getId(), $key, $value); + $this->storeRegistry->get('Build')->setMeta($this->getId(), $key, $value); } /** @@ -239,8 +202,8 @@ public function handleConfigBeforeClone(Builder $builder) $yamlParser = new YamlParser(); $buildConfig = $yamlParser->parse($buildConfig); - if ($buildConfig && is_array($buildConfig)) { - $builder->logDebug('Config before repository clone (DB): ' . json_encode($buildConfig)); + if ($buildConfig && \is_array($buildConfig)) { + $builder->logDebug('Config before repository clone (DB): ' . \json_encode($buildConfig)); $builder->setConfig($buildConfig); } @@ -250,25 +213,21 @@ public function handleConfigBeforeClone(Builder $builder) } /** - * @param string $buildPath - * - * @return bool - * * @throws Exception */ - protected function handleConfig(Builder $builder, $buildPath) + protected function handleConfig(Builder $builder, string $buildPath): bool { $yamlParser = new YamlParser(); $overwriteBuildConfig = $this->getProject()->getOverwriteBuildConfig(); $buildConfig = $builder->getConfig(); - $repositoryConfig = $this->getZeroConfigPlugins($builder); + $repositoryConfig = $this->getZeroConfigPlugins(); $repositoryConfigFrom = ''; - if (file_exists($buildPath . '/.php-censor.yml')) { + if (\file_exists($buildPath . '/.php-censor.yml')) { $repositoryConfigFrom = '.php-censor.yml'; $repositoryConfig = $yamlParser->parse( - file_get_contents($buildPath . '/.php-censor.yml') + \file_get_contents($buildPath . '/.php-censor.yml') ); } @@ -281,22 +240,22 @@ protected function handleConfig(Builder $builder, $buildPath) if (!$buildConfig) { $builder->logDebug( - sprintf('Build config from repository (%s)', $repositoryConfigFrom) + \sprintf('Build config from repository (%s)', $repositoryConfigFrom) ); $buildConfig = $repositoryConfig; } elseif ($buildConfig && !$overwriteBuildConfig) { $builder->logDebug( - sprintf('Build config from project (DB) + config from repository (%s)', $repositoryConfigFrom) + \sprintf('Build config from project (DB) + config from repository (%s)', $repositoryConfigFrom) ); - $buildConfig = array_replace_recursive($repositoryConfig, $buildConfig); + $buildConfig = \array_replace_recursive($repositoryConfig, $buildConfig); } elseif ($buildConfig) { $builder->logDebug('Build config from project (DB)'); } - if ($buildConfig && is_array($buildConfig)) { - $builder->logDebug('Final config: ' . json_encode($buildConfig)); + if ($buildConfig && \is_array($buildConfig)) { + $builder->logDebug('Final config: ' . \json_encode($buildConfig)); $builder->setConfig($buildConfig); } @@ -311,7 +270,7 @@ protected function handleConfig(Builder $builder, $buildPath) * * @throws ReflectionException */ - protected function getZeroConfigPlugins(Builder $builder) + protected function getZeroConfigPlugins() { $pluginDir = SRC_DIR . 'Plugin/'; $dir = new DirectoryIterator($pluginDir); @@ -333,29 +292,33 @@ protected function getZeroConfigPlugins(Builder $builder) continue; } - if ($item->getExtension() != 'php') { + if ($item->getExtension() !== 'php') { continue; } - $className = '\PHPCensor\Plugin\\' . $item->getBasename('.php'); + $className = 'PHPCensor\Plugin\\' . $item->getBasename('.php'); $reflectedPlugin = new ReflectionClass($className); - if (!$reflectedPlugin->implementsInterface('\PHPCensor\ZeroConfigPluginInterface')) { + if (!$reflectedPlugin->implementsInterface(ZeroConfigPluginInterface::class)) { continue; } foreach ([Build::STAGE_SETUP, Build::STAGE_TEST] as $stage) { if ($className::canExecuteOnStage($stage, $this)) { + $pluginName = $className::pluginName(); + $stepName = \sprintf('%s_step', $pluginName); + $pluginConfig = [ + 'plugin' => $pluginName, 'zero_config' => true, ]; - if (PhpParallelLint::pluginName() === $className::pluginName()) { + if (PhpParallelLint::pluginName() === $pluginName) { $pluginConfig['allow_failures'] = true; } - $config[$stage][$className::pluginName()] = $pluginConfig; + $config[$stage][$stepName] = $pluginConfig; } } } @@ -365,22 +328,15 @@ protected function getZeroConfigPlugins(Builder $builder) /** * Allows specific build types (e.g. Github) to report violations back to their respective services. - * - * @param string $plugin - * @param string $message - * @param int $severity - * @param string $file - * @param int $lineStart - * @param int $lineEnd */ public function reportError( Builder $builder, - $plugin, - $message, - $severity = BuildError::SEVERITY_NORMAL, - $file = null, - $lineStart = null, - $lineEnd = null + string $plugin, + string $message, + int $severity = BuildError::SEVERITY_NORMAL, + ?string $file = null, + ?int $lineStart = null, + ?int $lineEnd = null ) { $writer = $builder->getBuildErrorWriter(); $writer->write( @@ -404,8 +360,8 @@ public function getBuildDirectory() $createDate = $this->getCreateDate(); if (empty($this->buildDirectory)) { - $this->buildDirectory = $this->getProjectId() . '/' . $this->getId() . '_' . substr( - md5( + $this->buildDirectory = $this->getProjectId() . '/' . $this->getId() . '_' . \substr( + \md5( ($this->getId() . '_' . ($createDate ? $createDate->format('Y-m-d H:i:s') : null)) ), 0, @@ -427,8 +383,8 @@ public function getBuildBranchDirectory() $createDate = $this->getCreateDate(); if (empty($this->buildBranchDirectory)) { - $this->buildBranchDirectory = $this->getProjectId() . '/' . $this->getBranch() . '_' . substr( - md5( + $this->buildBranchDirectory = $this->getProjectId() . '/' . $this->getBranch() . '_' . \substr( + \md5( ($this->getBranch() . '_' . ($createDate ? $createDate->format('Y-m-d H:i:s') : null)) ), 0, @@ -448,33 +404,31 @@ public function getBuildPath() return null; } - return rtrim( - realpath(RUNTIME_DIR . 'builds'), + return \rtrim( + \realpath(RUNTIME_DIR . 'builds'), '/\\' ) . '/' . $this->getBuildDirectory() . '/'; } /** * Removes the build directory. - * - * @param bool $withArtifacts */ - public function removeBuildDirectory($withArtifacts = false) + public function removeBuildDirectory(bool $withArtifacts = false) { // Get the path and remove the trailing slash as this may prompt PHP // to see this as a directory even if it's a link. - $buildPath = rtrim($this->getBuildPath(), '/'); + $buildPath = \rtrim($this->getBuildPath(), '/'); - if (!$buildPath || !is_dir($buildPath)) { + if (!$buildPath || !\is_dir($buildPath)) { return; } try { $fileSystem = new Filesystem(); - if (is_link($buildPath)) { + if (\is_link($buildPath)) { // Remove the symlink without using recursive. - exec(sprintf('rm "%s"', $buildPath)); + \exec(\sprintf('rm "%s"', $buildPath)); } else { $fileSystem->remove($buildPath); } @@ -485,7 +439,7 @@ public function removeBuildDirectory($withArtifacts = false) $fileSystem->remove(PUBLIC_DIR . 'artifacts/pdepend/' . $buildDirectory); $fileSystem->remove(PUBLIC_DIR . 'artifacts/phpunit/' . $buildDirectory); } - } catch (Exception $e) { + } catch (\Throwable $e) { } } @@ -527,15 +481,15 @@ public function getPrettyDuration() $end = new DateTime(); } - $diff = date_diff($start, $end); + $diff = \date_diff($start, $end); $parts = []; foreach (['y', 'm', 'd', 'h', 'i', 's'] as $timePart) { - if ($diff->{$timePart} != 0) { - $parts[] = $diff->{$timePart} . ($timePart == 'i' ? 'm' : $timePart); + if ($diff->{$timePart} !== 0) { + $parts[] = $diff->{$timePart} . ($timePart === 'i' ? 'm' : $timePart); } } - return implode(" ", $parts); + return \implode(" ", $parts); } /** @@ -559,9 +513,9 @@ public function createWorkingCopy(Builder $builder, $buildPath) */ protected function writeSshKey() { - $tempKeyFile = tempnam(sys_get_temp_dir(), 'key_'); + $tempKeyFile = \tempnam(\sys_get_temp_dir(), 'key_'); - file_put_contents($tempKeyFile, $this->getProject()->getSshPrivateKey()); + \file_put_contents($tempKeyFile, $this->getProject()->getSshPrivateKey()); return $tempKeyFile; } @@ -583,10 +537,10 @@ protected function writeSshWrapper($keyFile) ssh {$sshFlags} -o IdentityFile={$keyFile} $* OUT; - $tempShFile = tempnam(sys_get_temp_dir(), 'sh_'); + $tempShFile = \tempnam(\sys_get_temp_dir(), 'sh_'); - file_put_contents($tempShFile, $script); - shell_exec('chmod +x "' . $tempShFile . '"'); + \file_put_contents($tempShFile, $script); + \shell_exec('chmod +x "' . $tempShFile . '"'); return $tempShFile; } @@ -650,7 +604,7 @@ public function getTotalErrorsCount($plugin = null, $severity = null, $isNew = n if (!isset($this->totalErrorsCount[$key])) { /** @var BuildErrorStore $store */ - $store = Factory::getStore('BuildError'); + $store = $this->storeRegistry->get('BuildError'); $this->totalErrorsCount[$key] = (int)$store->getErrorTotalForBuild( $this->getId(), @@ -664,27 +618,52 @@ public function getTotalErrorsCount($plugin = null, $severity = null, $isNew = n } /** - * @return int - * * @throws InvalidArgumentException * @throws HttpException */ - public function getErrorsTrend() + public function getErrorsTrend(): array { $total = (int)$this->getErrorsTotal(); - $previous = $this->getErrorsTotalPrevious(); + $previous = (int)$this->getErrorsTotalPrevious(); - if (null === $previous) { - return 0; - } - - $previous = (int)$previous; if ($previous > $total) { - return 1; + return [ + 'trend' => 1, + 'delta' => $total - $previous, + ]; } elseif ($previous < $total) { - return -1; + return [ + 'trend' => -1, + 'delta' => $total - $previous, + ]; + } + + return [ + 'trend' => 0, + 'delta' => 0, + ]; + } + + /** + * @throws InvalidArgumentException + * @throws HttpException + */ + public function getTestCoverageTrend(): array + { + $total = $this->getTestCoverage(); + $previous = $this->getTestCoveragePrevious(); + + if (null === $total) { + $total = '0.00'; + } + + if (null === $previous) { + $previous = '0.00'; } - return 0; + return [ + 'trend' => \bccomp($previous, $total, 2), + 'delta' => \bcsub($total, $previous, 2), + ]; } } diff --git a/src/Model/Build/BitbucketBuild.php b/src/Model/Build/BitbucketBuild.php index c76787094..3a4918be5 100644 --- a/src/Model/Build/BitbucketBuild.php +++ b/src/Model/Build/BitbucketBuild.php @@ -5,26 +5,29 @@ use Exception; use GuzzleHttp\Client; use PHPCensor\Builder; -use PHPCensor\Config; use PHPCensor\Helper\Bitbucket; use PHPCensor\Helper\Diff; use PHPCensor\Model\Build; use PHPCensor\Model\BuildError; +use PHPCensor\Traits\Model\Build\GitGetDiffLineNumberTrait; /** * BitBucket Build Model * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class BitbucketBuild extends GitBuild { - /** - * @var array - */ - public static $pullrequestTriggersToSources = [ - 'pullrequest:created' => Build::SOURCE_WEBHOOK_PULL_REQUEST_CREATED, - 'pullrequest:updated' => Build::SOURCE_WEBHOOK_PULL_REQUEST_UPDATED, - 'pullrequest:approved' => Build::SOURCE_WEBHOOK_PULL_REQUEST_APPROVED, + use GitGetDiffLineNumberTrait; + + public static array $pullrequestTriggersToSources = [ + 'pullrequest:created' => Build::SOURCE_WEBHOOK_PULL_REQUEST_CREATED, + 'pullrequest:updated' => Build::SOURCE_WEBHOOK_PULL_REQUEST_UPDATED, + 'pullrequest:approved' => Build::SOURCE_WEBHOOK_PULL_REQUEST_APPROVED, 'pullrequest:fulfilled' => Build::SOURCE_WEBHOOK_PULL_REQUEST_MERGED, ]; @@ -55,7 +58,7 @@ public function getBranchLink() */ public function getRemoteBranchLink() { - $remoteBranch = $this->getExtra('remote_branch'); + $remoteBranch = $this->getExtra('remote_branch'); $remoteReference = $this->getExtra('remote_reference'); return 'https://bitbucket.org/' . $remoteReference . '/src/?at=' . $remoteBranch; @@ -78,7 +81,7 @@ public function getTagLink() */ public function sendStatusPostback() { - if (!in_array($this->getSource(), Build::$webhookSources, true)) { + if (!\in_array($this->getSource(), Build::$webhookSources, true)) { return false; } @@ -87,14 +90,14 @@ public function sendStatusPostback() return false; } - $username = Config::getInstance()->get('php-censor.bitbucket.username'); - $appPassword = Config::getInstance()->get('php-censor.bitbucket.app_password'); + $username = $this->configuration->get('php-censor.bitbucket.username'); + $appPassword = $this->configuration->get('php-censor.bitbucket.app_password'); - if (empty($username) || empty($appPassword) || empty($this->data['id'])) { + if (empty($username) || empty($appPassword) || empty($this->getId())) { return false; } - $allowStatusCommit = (bool)Config::getInstance()->get( + $allowStatusCommit = (bool)$this->configuration->get( 'php-censor.bitbucket.status.commit', false ); @@ -108,50 +111,54 @@ public function sendStatusPostback() case 1: $status = 'INPROGRESS'; $description = 'PHP Censor build running.'; + break; case 2: $status = 'SUCCESSFUL'; $description = 'PHP Censor build passed.'; + break; case 3: $status = 'FAILED'; $description = 'PHP Censor build failed.'; + break; default: $status = 'STOPPED'; $description = 'PHP Censor build failed to complete.'; + break; } - $phpCensorUrl = Config::getInstance()->get('php-censor.url'); + $phpCensorUrl = $this->configuration->get('php-censor.url'); - $url = sprintf( + $url = \sprintf( '/2.0/repositories/%s/commit/%s/statuses/build', - (in_array($this->getSource(), Build::$pullRequestSources, true) + (\in_array($this->getSource(), Build::$pullRequestSources, true) ? $this->getExtra('remote_reference') : $project->getReference()), $this->getCommitId() ); $client = new Client([ - 'base_uri' => 'https://api.bitbucket.org', + 'base_uri' => 'https://api.bitbucket.org', 'http_errors' => false, ]); $response = $client->post($url, [ - 'auth' => [$username, $appPassword], + 'auth' => [$username, $appPassword], 'headers' => [ 'Content-Type' => 'application/json', ], 'json' => [ - 'state' => $status, - 'key' => 'PHP-CENSOR', - 'url' => $phpCensorUrl . '/build/view/' . $this->getId(), - 'name' => 'PHP Censor Build #' . $this->getId(), + 'state' => $status, + 'key' => 'PHP-CENSOR', + 'url' => $phpCensorUrl . '/build/view/' . $this->getId(), + 'name' => 'PHP Censor Build #' . $this->getId(), 'description' => $description, ], ]); - $status = (int)$response->getStatusCode(); + $status = $response->getStatusCode(); return ($status >= 200 && $status < 300); } @@ -163,7 +170,7 @@ public function sendStatusPostback() */ protected function getCloneUrl() { - $key = trim($this->getProject()->getSshPrivateKey()); + $key = \trim($this->getProject()->getSshPrivateKey()); if (!empty($key)) { return 'git@bitbucket.org:' . $this->getProject()->getReference() . '.git'; @@ -175,22 +182,13 @@ protected function getCloneUrl() /** * Get a template to use for generating links to files. * - * @return string|null + * @return string */ public function getFileLinkTemplate() { - $reference = $this->getProject()->getReference(); - - if (in_array($this->getSource(), Build::$pullRequestSources, true)) { - $reference = $this->getExtra('remote_reference'); - } - - $link = 'https://bitbucket.org/' . $reference . '/'; - $link .= 'src/' . $this->getCommitId() . '/'; - $link .= '{FILE}'; - $link .= '#{BASEFILE}-{LINE}'; + $bitbucket = new Bitbucket($this->configuration); - return $link; + return $bitbucket->getFileLinkTemplate($this); } /** @@ -198,12 +196,12 @@ public function getFileLinkTemplate() */ protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = null) { - $success = true; + $success = true; $skipGitFinalization = false; try { - if (in_array($this->getSource(), Build::$pullRequestSources, true)) { - $helper = new Bitbucket(); + if (\in_array($this->getSource(), Build::$pullRequestSources, true)) { + $helper = new Bitbucket($this->configuration); $diff = $helper->getPullRequestDiff( $this->getProject()->getReference(), $this->getExtra('pull_request_number') @@ -215,10 +213,10 @@ protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = nul $success = $builder->executeCommand($cmd, $cloneTo, $diffFile); - unlink($diffFile); + \unlink($diffFile); $skipGitFinalization = true; } - } catch (Exception $ex) { + } catch (\Throwable $ex) { $success = false; } @@ -239,11 +237,11 @@ protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = nul */ protected function writeDiff($cloneTo, $diff) { - $filePath = dirname($cloneTo . '/temp'); + $filePath = \dirname($cloneTo . '/temp'); $diffFile = $filePath . '.patch'; - file_put_contents($diffFile, $diff); - chmod($diffFile, 0600); + \file_put_contents($diffFile, $diff); + \chmod($diffFile, 0600); return $diffFile; } @@ -263,12 +261,12 @@ public function reportError( parent::reportError($builder, $plugin, $message, $severity, $file, $lineStart, $lineEnd); try { - $allowCommentCommit = (bool)Config::getInstance()->get( + $allowCommentCommit = (bool)$this->configuration->get( 'php-censor.bitbucket.comments.commit', false ); - $allowCommentPullRequest = (bool)Config::getInstance()->get( + $allowCommentPullRequest = (bool)$this->configuration->get( 'php-censor.bitbucket.comments.pull_request', false ); @@ -277,12 +275,12 @@ public function reportError( if ($file) { $diffLineNumber = $this->getDiffLineNumber($builder, $file, $lineStart); - if (!is_null($diffLineNumber)) { - $helper = new Bitbucket(); + if (!\is_null($diffLineNumber)) { + $helper = new Bitbucket($this->configuration); - $repo = $this->getProject()->getReference(); + $repo = $this->getProject()->getReference(); $prNumber = $this->getExtra('pull_request_number'); - $commit = $this->getCommitId(); + $commit = $this->getCommitId(); if (!empty($prNumber)) { if ($allowCommentPullRequest) { @@ -298,43 +296,6 @@ public function reportError( } } catch (\Throwable $e) { $builder->getBuildLogger()->logFailure('Exception: ' . $e->getMessage(), $e); - } catch (\Exception $e) { - $builder->getBuildLogger()->logFailure('Exception: ' . $e->getMessage(), $e); } } - - /** - * Uses git diff to figure out what the diff line position is, based on the error line number. - * - * @param string $file - * @param int $line - * - * @return int|null - */ - protected function getDiffLineNumber(Builder $builder, $file, $line) - { - $builder->logExecOutput(false); - - $line = (int)$line; - $prNumber = $this->getExtra('pull_request_number'); - $path = $builder->buildPath; - - if (!empty($prNumber)) { - $builder->executeCommand('cd "%s" && git diff "origin/%s" "%s"', $path, $this->getBranch(), $file); - } else { - $commitId = $this->getCommitId(); - $compare = empty($commitId) ? 'HEAD' : $commitId; - - $builder->executeCommand('cd "%s" && git diff "%s^^" "%s"', $path, $compare, $file); - } - - $builder->logExecOutput(true); - - $diff = $builder->getLastOutput(); - - $helper = new Diff(); - $lines = $helper->getLinePositions($diff); - - return isset($lines[$line]) ? $lines[$line] : null; - } } diff --git a/src/Model/Build/BitbucketHgBuild.php b/src/Model/Build/BitbucketHgBuild.php index 0ccdc0320..6ef6348b4 100644 --- a/src/Model/Build/BitbucketHgBuild.php +++ b/src/Model/Build/BitbucketHgBuild.php @@ -2,12 +2,16 @@ namespace PHPCensor\Model\Build; -use PHPCensor\Model\Build; +use PHPCensor\Helper\Bitbucket; /** * BitbucketHgBuild Build Model * + * @package PHP Censor + * @subpackage Application + * * @author Artem Bochkov + * @author Dmitry Khomutov */ class BitbucketHgBuild extends HgBuild { @@ -61,7 +65,7 @@ public function getTagLink() */ protected function getCloneUrl() { - $key = trim($this->getProject()->getSshPrivateKey()); + $key = \trim($this->getProject()->getSshPrivateKey()); if (!empty($key)) { return 'ssh://hg@bitbucket.org/' . $this->getProject()->getReference(); @@ -77,17 +81,8 @@ protected function getCloneUrl() */ public function getFileLinkTemplate() { - $reference = $this->getProject()->getReference(); - - if (in_array($this->getSource(), Build::$pullRequestSources, true)) { - $reference = $this->getExtra('remote_reference'); - } - - $link = 'https://bitbucket.org/' . $reference . '/'; - $link .= 'src/' . $this->getCommitId() . '/'; - $link .= '{FILE}'; - $link .= '#{BASEFILE}-{LINE}'; + $bitbucket = new Bitbucket($this->configuration); - return $link; + return $bitbucket->getFileLinkTemplate($this); } } diff --git a/src/Model/Build/BitbucketServerBuild.php b/src/Model/Build/BitbucketServerBuild.php index 378073dc8..2c0707c1d 100644 --- a/src/Model/Build/BitbucketServerBuild.php +++ b/src/Model/Build/BitbucketServerBuild.php @@ -4,14 +4,18 @@ use Exception; use PHPCensor\Builder; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Model\Build; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov + */ class BitbucketServerBuild extends GitBuild { - /** - * @var array - */ - public static $pullrequestTriggersToSources = [ + public static array $pullrequestTriggersToSources = [ 'pr:opened' => Build::SOURCE_WEBHOOK_PULL_REQUEST_CREATED, 'pr:updated' => Build::SOURCE_WEBHOOK_PULL_REQUEST_UPDATED, 'pr:approved' => Build::SOURCE_WEBHOOK_PULL_REQUEST_APPROVED, @@ -92,7 +96,7 @@ public function getFileLinkTemplate() { $reference = $this->getProject()->getReference(); - if (in_array($this->getSource(), Build::$pullRequestSources, true)) { + if (\in_array($this->getSource(), Build::$pullRequestSources, true)) { $reference = $this->getExtra('remote_reference'); } @@ -113,7 +117,7 @@ protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = nul $skipGitFinalization = false; try { - if (in_array($this->getSource(), Build::$pullRequestSources, true)) { + if (\in_array($this->getSource(), Build::$pullRequestSources, true)) { $diff = $this->getPullRequestDiff($builder, $cloneTo, $extra['remote_branch']); $diffFile = $this->writeDiff($builder->buildPath, $diff); @@ -127,10 +131,9 @@ protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = nul $success = $builder->executeCommand($applyCmd, $diffFile); } - //unlink($diffFile); $skipGitFinalization = true; } - } catch (Exception $ex) { + } catch (\Throwable $ex) { $success = false; } @@ -156,7 +159,7 @@ protected function getPullRequestDiff(Builder $builder, $cloneTo, $targetBranch) return $builder->getLastOutput(); } - throw new Exception('Unable to create diff patch.'); + throw new RuntimeException('Unable to create diff patch.'); } /** @@ -169,11 +172,11 @@ protected function getPullRequestDiff(Builder $builder, $cloneTo, $targetBranch) */ protected function writeDiff($cloneTo, $diff) { - $filePath = dirname($cloneTo . '/temp'); + $filePath = \dirname($cloneTo . '/temp'); $diffFile = $filePath . '.patch'; - file_put_contents($diffFile, $diff); - chmod($diffFile, 0600); + \file_put_contents($diffFile, $diff); + \chmod($diffFile, 0600); return $diffFile; } diff --git a/src/Model/Build/GitBuild.php b/src/Model/Build/GitBuild.php index 3bcd320f6..65528935e 100644 --- a/src/Model/Build/GitBuild.php +++ b/src/Model/Build/GitBuild.php @@ -4,15 +4,21 @@ use Exception; use PHPCensor\Builder; +use PHPCensor\Common\Application\ConfigurationInterface; use PHPCensor\Model\Build; +use PHPCensor\StoreRegistry; use Psr\Log\LogLevel; /** * Remote Git Build Model * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ -class GitBuild extends Build +class GitBuild extends TypedBuild { /** * Get the URL to be used to clone this remote repository. @@ -35,7 +41,7 @@ protected function getCloneUrl() */ public function createWorkingCopy(Builder $builder, $buildPath) { - $key = trim($this->getProject()->getSshPrivateKey()); + $key = \trim($this->getProject()->getSshPrivateKey()); if (!empty($key)) { $success = $this->cloneBySsh($builder, $buildPath); @@ -93,11 +99,11 @@ protected function cloneByHttp(Builder $builder, $cloneTo) $buildSettings = $builder->getConfig('build_settings'); if ($buildSettings && isset($buildSettings['clone_depth']) && (0 < (int)$buildSettings['clone_depth'])) { - $cmd .= ' --depth ' . intval($buildSettings['clone_depth']) . ' '; + $cmd .= ' --depth ' . \intval($buildSettings['clone_depth']) . ' '; } - $cmd .= ' -b "%s" "%s" "%s"'; - $success = $builder->executeCommand($cmd, $this->getBranch(), $this->getCloneUrl(), $cloneTo); + $cmd .= ' -b %s "%s" "%s"'; + $success = $builder->executeCommand($cmd, \escapeshellarg($this->getBranch()), $this->getCloneUrl(), $cloneTo); if ($success) { $success = $this->postCloneSetup($builder, $cloneTo); @@ -123,13 +129,13 @@ protected function cloneBySsh(Builder $builder, $cloneTo) $buildSettings = $builder->getConfig('build_settings'); if ($buildSettings && isset($buildSettings['clone_depth']) && (0 < (int)$buildSettings['clone_depth'])) { - $cmd .= ' --depth ' . intval($buildSettings['clone_depth']) . ' '; + $cmd .= ' --depth ' . \intval($buildSettings['clone_depth']) . ' '; } - $cmd .= ' -b "%s" "%s" "%s"'; + $cmd .= ' -b %s "%s" "%s"'; $cmd = 'export GIT_SSH="' . $gitSshWrapper . '" && ' . $cmd; - $success = $builder->executeCommand($cmd, $this->getBranch(), $this->getCloneUrl(), $cloneTo); + $success = $builder->executeCommand($cmd, \escapeshellarg($this->getBranch()), $this->getCloneUrl(), $cloneTo); if ($success) { $extra = [ @@ -140,8 +146,8 @@ protected function cloneBySsh(Builder $builder, $cloneTo) } // Remove the key file and git wrapper: - unlink($keyFile); - unlink($gitSshWrapper); + \unlink($keyFile); + \unlink($gitSshWrapper); return $success; } @@ -161,21 +167,21 @@ protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = nul if (empty($this->getEnvironmentId()) && !empty($commitId)) { $cmd = $chdir . ' && git checkout %s --quiet'; - $success = $builder->executeCommand($cmd, $cloneTo, $commitId); + $success = $builder->executeCommand($cmd, $cloneTo, \escapeshellarg($commitId)); } // Always update the commit hash with the actual HEAD hash if ($builder->executeCommand($chdir . ' && git rev-parse HEAD', $cloneTo)) { - $commitId = trim($builder->getLastOutput()); + $commitId = \trim($builder->getLastOutput()); $this->setCommitId($commitId); - if ($builder->executeCommand($chdir . ' && git log -1 --pretty=format:%%s %s', $cloneTo, $commitId)) { - $this->setCommitMessage(trim($builder->getLastOutput())); + if ($builder->executeCommand($chdir . ' && git log -1 --pretty=format:%%s %s', $cloneTo, \escapeshellarg($commitId))) { + $this->setCommitMessage(\trim($builder->getLastOutput())); } - if ($builder->executeCommand($chdir . ' && git log -1 --pretty=format:%%ae %s', $cloneTo, $commitId)) { - $this->setCommitterEmail(trim($builder->getLastOutput())); + if ($builder->executeCommand($chdir . ' && git log -1 --pretty=format:%%ae %s', $cloneTo, \escapeshellarg($commitId))) { + $this->setCommitterEmail(\trim($builder->getLastOutput())); } } diff --git a/src/Model/Build/GithubBuild.php b/src/Model/Build/GithubBuild.php index 30ad6dc96..1b537b975 100644 --- a/src/Model/Build/GithubBuild.php +++ b/src/Model/Build/GithubBuild.php @@ -5,23 +5,25 @@ use Exception; use GuzzleHttp\Client; use PHPCensor\Builder; -use PHPCensor\Config; -use PHPCensor\Helper\Diff; use PHPCensor\Helper\Github; use PHPCensor\Model\Build; use PHPCensor\Model\BuildError; +use PHPCensor\Traits\Model\Build\GitGetDiffLineNumberTrait; /** * Github Build Model * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class GithubBuild extends GitBuild { - /** - * @var array - */ - public static $pullrequestTriggersToSources = [ + use GitGetDiffLineNumberTrait; + + public static array $pullrequestTriggersToSources = [ 'opened' => Build::SOURCE_WEBHOOK_PULL_REQUEST_CREATED, 'synchronize' => Build::SOURCE_WEBHOOK_PULL_REQUEST_UPDATED, 'reopened' => Build::SOURCE_WEBHOOK_PULL_REQUEST_UPDATED, @@ -91,7 +93,7 @@ public function getTagLink() */ public function sendStatusPostback() { - if (!in_array($this->getSource(), Build::$webhookSources, true)) { + if (!\in_array($this->getSource(), Build::$webhookSources, true)) { return false; } @@ -100,12 +102,12 @@ public function sendStatusPostback() return false; } - $token = Config::getInstance()->get('php-censor.github.token'); - if (empty($token) || empty($this->data['id'])) { + $token = $this->configuration->get('php-censor.github.token'); + if (empty($token) || empty($this->getId())) { return false; } - $allowStatusCommit = (bool)Config::getInstance()->get( + $allowStatusCommit = (bool)$this->configuration->get( 'php-censor.github.status.commit', false ); @@ -119,22 +121,26 @@ public function sendStatusPostback() case 1: $status = 'pending'; $description = 'PHP Censor build running.'; + break; case 2: $status = 'success'; $description = 'PHP Censor build passed.'; + break; case 3: $status = 'failure'; $description = 'PHP Censor build failed.'; + break; default: $status = 'error'; $description = 'PHP Censor build failed to complete.'; + break; } - $phpCensorUrl = Config::getInstance()->get('php-censor.url'); + $phpCensorUrl = $this->configuration->get('php-censor.url'); $url = '/repos/' . $project->getReference() . '/statuses/' . $this->getCommitId(); $client = new Client([ @@ -166,7 +172,7 @@ public function sendStatusPostback() */ protected function getCloneUrl() { - $key = trim($this->getProject()->getSshPrivateKey()); + $key = \trim($this->getProject()->getSshPrivateKey()); $port = $this->getProject()->getAccessInformation('port'); @@ -185,19 +191,17 @@ protected function getCloneUrl() /** * Get a parsed version of the commit message, with links to issues and commits. - * - * @return string */ - public function getCommitMessage() + public function getCommitMessage(): ?string { $message = parent::getCommitMessage(); $project = $this->getProject(); - if (!is_null($project)) { + if (!\is_null($project)) { $reference = $project->getReference(); $commitLink = '#$1'; - $message = preg_replace('/\#([0-9]+)/', $commitLink, $message); - $message = preg_replace( + $message = \preg_replace('/\#([0-9]+)/', $commitLink, $message); + $message = \preg_replace( '/\@([a-zA-Z0-9_]+)/', '@$1', $message @@ -215,7 +219,7 @@ public function getCommitMessage() public function getFileLinkTemplate() { $reference = $this->getProject()->getReference(); - if (in_array($this->getSource(), Build::$pullRequestSources, true)) { + if (\in_array($this->getSource(), Build::$pullRequestSources, true)) { $reference = $this->getExtra('remote_reference'); } @@ -235,7 +239,7 @@ protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = nul $success = true; try { - if (in_array($this->getSource(), Build::$pullRequestSources, true)) { + if (\in_array($this->getSource(), Build::$pullRequestSources, true)) { $pullRequestId = $this->getExtra('pull_request_number'); $cmd = 'cd "%s" && git checkout -b php-censor/' @@ -248,7 +252,7 @@ protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = nul $success = $builder->executeCommand($cmd, $cloneTo, $this->getBranch(), $pullRequestId); } - } catch (Exception $ex) { + } catch (\Throwable $ex) { $success = false; } @@ -264,22 +268,22 @@ protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = nul */ public function reportError( Builder $builder, - $plugin, - $message, - $severity = BuildError::SEVERITY_NORMAL, - $file = null, - $lineStart = null, - $lineEnd = null + string $plugin, + string $message, + int $severity = BuildError::SEVERITY_NORMAL, + ?string $file = null, + ?int $lineStart = null, + ?int $lineEnd = null ) { parent::reportError($builder, $plugin, $message, $severity, $file, $lineStart, $lineEnd); try { - $allowCommentCommit = (bool)Config::getInstance()->get( + $allowCommentCommit = (bool)$this->configuration->get( 'php-censor.github.comments.commit', false ); - $allowCommentPullRequest = (bool)Config::getInstance()->get( + $allowCommentPullRequest = (bool)$this->configuration->get( 'php-censor.github.comments.pull_request', false ); @@ -288,8 +292,8 @@ public function reportError( if ($file) { $diffLineNumber = $this->getDiffLineNumber($builder, $file, $lineStart); - if (!is_null($diffLineNumber)) { - $helper = new Github(); + if (!\is_null($diffLineNumber)) { + $helper = new Github($this->configuration); $repo = $this->getProject()->getReference(); $prNumber = $this->getExtra('pull_request_number'); @@ -309,43 +313,6 @@ public function reportError( } } catch (\Throwable $e) { $builder->getBuildLogger()->logFailure('Exception: ' . $e->getMessage(), $e); - } catch (\Exception $e) { - $builder->getBuildLogger()->logFailure('Exception: ' . $e->getMessage(), $e); } } - - /** - * Uses git diff to figure out what the diff line position is, based on the error line number. - * - * @param string $file - * @param int $line - * - * @return int|null - */ - protected function getDiffLineNumber(Builder $builder, $file, $line) - { - $builder->logExecOutput(false); - - $line = (int)$line; - $prNumber = $this->getExtra('pull_request_number'); - $path = $builder->buildPath; - - if (!empty($prNumber)) { - $builder->executeCommand('cd "%s" && git diff "origin/%s" "%s"', $path, $this->getBranch(), $file); - } else { - $commitId = $this->getCommitId(); - $compare = empty($commitId) ? 'HEAD' : $commitId; - - $builder->executeCommand('cd "%s" && git diff "%s^^" "%s"', $path, $compare, $file); - } - - $builder->logExecOutput(true); - - $diff = $builder->getLastOutput(); - - $helper = new Diff(); - $lines = $helper->getLinePositions($diff); - - return isset($lines[$line]) ? $lines[$line] : null; - } } diff --git a/src/Model/Build/GitlabBuild.php b/src/Model/Build/GitlabBuild.php index dbeb86454..b37b8f911 100644 --- a/src/Model/Build/GitlabBuild.php +++ b/src/Model/Build/GitlabBuild.php @@ -5,7 +5,11 @@ /** * Gitlab Build Model * + * @package PHP Censor + * @subpackage Application + * * @author André Cianfarani + * @author Dmitry Khomutov */ class GitlabBuild extends GitBuild { @@ -40,7 +44,7 @@ public function getBranchLink() */ public function getFileLinkTemplate() { - return sprintf( + return \sprintf( '//%s/%s/blob/%s/{FILE}#L{LINE}', $this->getProject()->getAccessInformation('domain'), $this->getProject()->getReference(), @@ -53,7 +57,7 @@ public function getFileLinkTemplate() */ protected function getCloneUrl() { - $key = trim($this->getProject()->getSshPrivateKey()); + $key = \trim($this->getProject()->getSshPrivateKey()); $user = $this->getProject()->getAccessInformation('user'); $domain = $this->getProject()->getAccessInformation('domain'); diff --git a/src/Model/Build/GogsBuild.php b/src/Model/Build/GogsBuild.php index 7a478cd47..0788529d5 100644 --- a/src/Model/Build/GogsBuild.php +++ b/src/Model/Build/GogsBuild.php @@ -4,6 +4,11 @@ /** * GogsBuild Build Model + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class GogsBuild extends GitBuild { @@ -14,7 +19,7 @@ class GogsBuild extends GitBuild */ protected function getCleanedReferenceForLink() { - return preg_replace('/\.git$/i', '', $this->getProject()->getReference()); + return \preg_replace('/\.git$/i', '', $this->getProject()->getReference()); } /** @@ -44,7 +49,7 @@ public function getBranchLink() */ public function getFileLinkTemplate() { - return sprintf( + return \sprintf( '%s/src/%s/{FILE}#L{LINE}', $this->getCleanedReferenceForLink(), $this->getCommitId() diff --git a/src/Model/Build/HgBuild.php b/src/Model/Build/HgBuild.php index 5a2f7c001..046dc8241 100644 --- a/src/Model/Build/HgBuild.php +++ b/src/Model/Build/HgBuild.php @@ -4,15 +4,33 @@ use Exception; use PHPCensor\Builder; +use PHPCensor\Common\Application\ConfigurationInterface; use PHPCensor\Model\Build; +use PHPCensor\StoreRegistry; /** * Mercurial Build Model * + * @package PHP Censor + * @subpackage Application + * * @author Pavel Gopanenko + * @author Dmitry Khomutov */ class HgBuild extends Build { + protected ConfigurationInterface $configuration; + + public function __construct( + ConfigurationInterface $configuration, + StoreRegistry $storeRegistry, + array $initialData = [] + ) { + parent::__construct($storeRegistry, $initialData); + + $this->configuration = $configuration; + } + /** * Get the URL to be used to clone this remote repository. * @@ -34,7 +52,7 @@ protected function getCloneUrl() */ public function createWorkingCopy(Builder $builder, $buildPath) { - $key = trim($this->getProject()->getSshPrivateKey()); + $key = \trim($this->getProject()->getSshPrivateKey()); if (!empty($key)) { $success = $this->cloneBySsh($builder, $buildPath); @@ -60,7 +78,7 @@ public function createWorkingCopy(Builder $builder, $buildPath) */ protected function cloneByHttp(Builder $builder, $cloneTo) { - return $builder->executeCommand('hg clone %s "%s" -r %s', $this->getCloneUrl(), $cloneTo, $this->getBranch()); + return $builder->executeCommand('hg clone %s "%s" -r %s', $this->getCloneUrl(), $cloneTo, \escapeshellarg($this->getBranch())); } /** @@ -76,14 +94,14 @@ protected function cloneBySsh(Builder $builder, $cloneTo) // Do the hg clone: $cmd = 'hg clone --ssh "ssh -i ' . $keyFile . '" %s "%s" -r %s'; - $success = $builder->executeCommand($cmd, $this->getCloneUrl(), $cloneTo, $this->getBranch()); + $success = $builder->executeCommand($cmd, $this->getCloneUrl(), $cloneTo, \escapeshellarg($this->getBranch())); if ($success) { $success = $this->postCloneSetup($builder, $cloneTo); } // Remove the key file: - unlink($keyFile); + \unlink($keyFile); return $success; } @@ -103,7 +121,7 @@ protected function postCloneSetup(Builder $builder, $cloneTo, array $extra = nul // Allow switching to a specific branch: if (!empty($commitId)) { $cmd = 'cd "%s" && hg checkout %s'; - $success = $builder->executeCommand($cmd, $cloneTo, $this->getBranch()); + $success = $builder->executeCommand($cmd, $cloneTo, \escapeshellarg($this->getBranch())); } return $success; diff --git a/src/Model/Build/LocalBuild.php b/src/Model/Build/LocalBuild.php index 7a82fd9af..a129ec73b 100644 --- a/src/Model/Build/LocalBuild.php +++ b/src/Model/Build/LocalBuild.php @@ -9,9 +9,13 @@ /** * Local Build Model * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ -class LocalBuild extends Build +class LocalBuild extends TypedBuild { /** * Create a working copy by cloning, copying, or similar. @@ -25,12 +29,12 @@ class LocalBuild extends Build public function createWorkingCopy(Builder $builder, $buildPath) { $reference = $this->getProject()->getReference(); - $reference = substr($reference, -1) == '/' ? substr($reference, 0, -1) : $reference; - $buildPath = substr($buildPath, 0, -1); + $reference = \substr($reference, -1) === '/' ? \substr($reference, 0, -1) : $reference; + $buildPath = \substr($buildPath, 0, -1); // If there's a /config file in the reference directory, it is probably a bare repository // which we'll extract into our build path directly. - if (is_file($reference . '/config') && + if (\is_file($reference . '/config') && true === $this->handleBareRepository($builder, $reference, $buildPath)) { return $this->handleConfig($builder, $buildPath); } @@ -63,7 +67,7 @@ public function createWorkingCopy(Builder $builder, $buildPath) */ protected function handleBareRepository(Builder $builder, $reference, $buildPath) { - $gitConfig = parse_ini_file($reference.'/config', true); + $gitConfig = \parse_ini_file($reference.'/config', true); // If it is indeed a bare repository, then extract it into our build path: if ($gitConfig['core']['bare']) { @@ -86,13 +90,13 @@ protected function handleBareRepository(Builder $builder, $reference, $buildPath */ protected function handleSymlink(Builder $builder, $reference, $buildPath) { - if (is_link($buildPath) && is_file($buildPath)) { - unlink($buildPath); + if (\is_link($buildPath) && \is_file($buildPath)) { + \unlink($buildPath); } - $builder->log(sprintf('Symlinking: %s to %s', $reference, $buildPath)); + $builder->log(\sprintf('Symlinking: %s to %s', $reference, $buildPath)); - if (!symlink($reference, $buildPath)) { + if (!\symlink($reference, $buildPath)) { $builder->logFailure('Failed to symlink.'); return false; diff --git a/src/Model/Build/SvnBuild.php b/src/Model/Build/SvnBuild.php index a7ac774cc..a1ab34981 100644 --- a/src/Model/Build/SvnBuild.php +++ b/src/Model/Build/SvnBuild.php @@ -9,11 +9,15 @@ /** * Remote Subversion Build Model * + * @package PHP Censor + * @subpackage Application + * * @author Nadir Dzhilkibaev + * @author Dmitry Khomutov */ -class SvnBuild extends Build +class SvnBuild extends TypedBuild { - protected $svnCommand = 'svn export -q --non-interactive '; + protected string $svnCommand = 'svn export -q --non-interactive '; /** * Get the URL to be used to clone this remote repository. @@ -22,15 +26,15 @@ class SvnBuild extends Build */ protected function getCloneUrl() { - $url = rtrim($this->getProject()->getReference(), '/') . '/'; - $branch = ltrim($this->getBranch(), '/'); + $url = \rtrim($this->getProject()->getReference(), '/') . '/'; + $branch = \ltrim($this->getBranch(), '/'); // For empty default branch or default branch name like "/trunk" or "trunk" (-> "trunk") - if (empty($branch) || $branch == 'trunk') { + if (empty($branch) || $branch === 'trunk') { $url .= 'trunk'; - // For default branch with standard default branch directory ("branches") like "/branch-1" or "branch-1" - // (-> "branches/branch-1") - } elseif (false === strpos($branch, '/')) { + // For default branch with standard default branch directory ("branches") like "/branch-1" or "branch-1" + // (-> "branches/branch-1") + } elseif (false === \strpos($branch, '/')) { $url .= 'branches/' . $branch; // For default branch with non-standard branch directory like "/branch/branch-1" or "branch/branch-1" // (-> "branch/branch-1") @@ -50,14 +54,14 @@ protected function extendSvnCommandFromConfig(Builder $builder) $buildSettings = $builder->getConfig('build_settings'); if ($buildSettings) { - if (isset($buildSettings['svn']) && is_array($buildSettings['svn'])) { + if (isset($buildSettings['svn']) && \is_array($buildSettings['svn'])) { foreach ($buildSettings['svn'] as $key => $value) { $cmd .= " --${key} ${value} "; } } if (isset($buildSettings['clone_depth']) && 0 < (int)$buildSettings['clone_depth']) { - $cmd .= ' --depth ' . intval($buildSettings['clone_depth']) . ' '; + $cmd .= ' --depth ' . \intval($buildSettings['clone_depth']) . ' '; } } @@ -77,7 +81,7 @@ public function createWorkingCopy(Builder $builder, $buildPath) { $this->extendSvnCommandFromConfig($builder); - $key = trim($this->getProject()->getSshPrivateKey()); + $key = \trim($this->getProject()->getSshPrivateKey()); if (!empty($key)) { $success = $this->cloneBySsh($builder, $buildPath); @@ -107,7 +111,7 @@ protected function cloneByHttp(Builder $builder, $cloneTo) if (!empty($this->getCommitId())) { $cmd .= ' -r %s %s "%s"'; - $success = $builder->executeCommand($cmd, $this->getCommitId(), $this->getCloneUrl(), $cloneTo); + $success = $builder->executeCommand($cmd, \escapeshellarg($this->getCommitId()), $this->getCloneUrl(), $cloneTo); } else { $cmd .= ' %s "%s"'; $success = $builder->executeCommand($cmd, $this->getCloneUrl(), $cloneTo); @@ -133,8 +137,8 @@ protected function cloneBySsh(Builder $builder, $cloneTo) $success = $builder->executeCommand($cmd, $this->getCloneUrl(), $cloneTo); // Remove the key file and svn wrapper: - unlink($keyFile); - unlink($sshWrapper); + \unlink($keyFile); + \unlink($sshWrapper); return $success; } diff --git a/src/Model/Build/TypedBuild.php b/src/Model/Build/TypedBuild.php new file mode 100644 index 000000000..410f3a497 --- /dev/null +++ b/src/Model/Build/TypedBuild.php @@ -0,0 +1,30 @@ + + */ +class TypedBuild extends Build +{ + protected ConfigurationInterface $configuration; + + public function __construct( + ConfigurationInterface $configuration, + StoreRegistry $storeRegistry, + array $initialData = [] + ) { + parent::__construct($storeRegistry, $initialData); + + $this->configuration = $configuration; + } +} diff --git a/src/Model/BuildError.php b/src/Model/BuildError.php index 63c5ed892..b8e1496b0 100644 --- a/src/Model/BuildError.php +++ b/src/Model/BuildError.php @@ -1,11 +1,19 @@ + * @author Dmitry Khomutov + */ class BuildError extends BaseBuildError { /** @@ -19,7 +27,7 @@ public function getBuild() } /** @var BuildStore $buildStore */ - $buildStore = Factory::getStore('Build'); + $buildStore = $this->storeRegistry->get('Build'); return $buildStore->getById($buildId); } @@ -82,7 +90,7 @@ public static function getSeverityName($severity) */ public static function generateHash($plugin, $file, $lineStart, $lineEnd, $severity, $message) { - return md5($plugin . $file . $lineStart . $lineEnd . $severity . $message); + return \md5($plugin . $file . $lineStart . $lineEnd . $severity . $message); } /** diff --git a/src/Model/BuildMeta.php b/src/Model/BuildMeta.php index b6e09dd78..8dbc8a4ea 100644 --- a/src/Model/BuildMeta.php +++ b/src/Model/BuildMeta.php @@ -1,11 +1,19 @@ + * @author Dmitry Khomutov + */ class BuildMeta extends BaseBuildMeta { /** @@ -19,7 +27,7 @@ public function getBuild() } /** @var BuildStore $buildStore */ - $buildStore = Factory::getStore('Build'); + $buildStore = $this->storeRegistry->get('Build'); return $buildStore->getById($buildId); } diff --git a/src/Model/Environment.php b/src/Model/Environment.php index acac89084..745b4d671 100644 --- a/src/Model/Environment.php +++ b/src/Model/Environment.php @@ -1,9 +1,17 @@ + */ class Environment extends BaseEnvironment { } diff --git a/src/Model/Project.php b/src/Model/Project.php index 5c2b4e252..ff377e942 100644 --- a/src/Model/Project.php +++ b/src/Model/Project.php @@ -1,16 +1,21 @@ + * @author Dmitry Khomutov */ class Project extends BaseProject { @@ -25,7 +30,7 @@ public function getGroup() } /** @var ProjectGroupStore $groupStore */ - $groupStore = Factory::getStore('ProjectGroup'); + $groupStore = $this->storeRegistry->get('ProjectGroup'); return $groupStore->getById($groupId); } @@ -37,7 +42,7 @@ public function getGroup() */ public function getProjectBuilds() { - return Factory::getStore('Build')->getByProjectId($this->getId()); + return $this->storeRegistry->get('Build')->getByProjectId($this->getId()); } /** @@ -60,10 +65,10 @@ public function getLatestBuild($branch, $status = null) } $order = ['id' => 'DESC']; - $builds = Factory::getStore('Build')->getWhere($criteria, 1, 0, $order); + $builds = $this->storeRegistry->get('Build')->getWhere($criteria, 1, 0, $order); - if (is_array($builds['items']) && count($builds['items'])) { - $latest = array_shift($builds['items']); + if (\is_array($builds['items']) && \count($builds['items'])) { + $latest = \array_shift($builds['items']); if (isset($latest) && $latest instanceof Build) { return $latest; @@ -84,13 +89,13 @@ public function getPreviousBuild($branch) { $criteria = [ 'branch' => $branch, - 'project_id' => $this->getId() + 'project_id' => $this->getId(), ]; $order = ['id' => 'DESC']; - $builds = Factory::getStore('Build')->getWhere($criteria, 1, 1, $order); + $builds = $this->storeRegistry->get('Build')->getWhere($criteria, 1, 1, $order); - if (is_array($builds['items']) && count($builds['items'])) { - $previous = array_shift($builds['items']); + if (\is_array($builds['items']) && \count($builds['items'])) { + $previous = \array_shift($builds['items']); if (isset($previous) && $previous instanceof Build) { return $previous; @@ -110,12 +115,14 @@ public function getIcon() switch ($this->getType()) { case Project::TYPE_GITHUB: $icon = 'github'; + break; case Project::TYPE_BITBUCKET: case Project::TYPE_BITBUCKET_HG: case Project::TYPE_BITBUCKET_SERVER: $icon = 'bitbucket'; + break; case Project::TYPE_GIT: @@ -125,6 +132,7 @@ public function getIcon() case Project::TYPE_SVN: default: $icon = 'code-fork'; + break; } @@ -136,10 +144,7 @@ public function getIcon() */ protected function getEnvironmentStore() { - /** @var EnvironmentStore $store */ - $store = Factory::getStore('Environment'); - - return $store; + return $this->storeRegistry->get('Environment'); } /** @@ -150,7 +155,6 @@ protected function getEnvironmentStore() public function getEnvironmentsObjects() { $projectId = $this->getId(); - if (empty($projectId)) { return null; } @@ -167,9 +171,11 @@ public function getEnvironmentsNames() { $environments = $this->getEnvironmentsObjects(); $environmentsNames = []; - foreach ($environments['items'] as $environment) { - /** @var Environment $environment */ - $environmentsNames[] = $environment->getName(); + if ($environments) { + foreach ($environments['items'] as $environment) { + /** @var Environment $environment */ + $environmentsNames[] = $environment->getName(); + } } return $environmentsNames; @@ -184,15 +190,16 @@ public function getEnvironments() { $environments = $this->getEnvironmentsObjects(); $environmentsConfig = []; - foreach ($environments['items'] as $environment) { - /** @var Environment $environment */ - $environmentsConfig[$environment->getName()] = $environment->getBranches(); + if ($environments) { + foreach ($environments['items'] as $environment) { + /** @var Environment $environment */ + $environmentsConfig[$environment->getName()] = $environment->getBranches(); + } } $yamlDumper = new YamlDumper(); - $value = $yamlDumper->dump($environmentsConfig, 10, 0, true, false); - return $value; + return $yamlDumper->dump($environmentsConfig, 10, 0); } /** @@ -204,27 +211,32 @@ public function setEnvironments($value) { $yamlParser = new YamlParser(); $environmentsConfig = $yamlParser->parse($value); - $environmentsNames = !empty($environmentsConfig) ? array_keys($environmentsConfig) : []; + $environmentsNames = (!empty($environmentsConfig) && \is_array($environmentsConfig)) ? \array_keys($environmentsConfig) : []; $currentEnvironments = $this->getEnvironmentsObjects(); $store = $this->getEnvironmentStore(); - foreach ($currentEnvironments['items'] as $environment) { - /** @var Environment $environment */ - $key = array_search($environment->getName(), $environmentsNames, true); - if ($key !== false) { - // already exist - unset($environmentsNames[$key]); - $environment->setBranches(!empty($environmentsConfig[$environment->getName()]) ? $environmentsConfig[$environment->getName()] : []); - $store->save($environment); - } else { - // remove - $store->delete($environment); + if (!empty($currentEnvironments['items'])) { + foreach ($currentEnvironments['items'] as $environment) { + /** @var Environment $environment */ + $key = \array_search($environment->getName(), $environmentsNames, true); + if ($key !== false) { + // already exist + unset($environmentsNames[$key]); + $branches = !empty($environmentsConfig[$environment->getName()]) + ? $environmentsConfig[$environment->getName()] + : []; + $environment->setBranches($branches); + $store->save($environment); + } else { + // remove + $store->delete($environment); + } } } if (!empty($environmentsNames)) { // add foreach ($environmentsNames as $environmentName) { - $environment = new Environment(); + $environment = new Environment($this->storeRegistry); $environment->setProjectId($this->getId()); $environment->setName($environmentName); $environment->setBranches(!empty($environmentsConfig[$environment->getName()]) ? $environmentsConfig[$environment->getName()] : []); @@ -236,7 +248,7 @@ public function setEnvironments($value) /** * @param string $branch * - * @return string[] + * @return int[] */ public function getEnvironmentsNamesByBranch($branch) { @@ -245,7 +257,7 @@ public function getEnvironmentsNamesByBranch($branch) $defaultBranch = ($branch === $this->getDefaultBranch()); foreach ($environments['items'] as $environment) { /** @var Environment $environment */ - if ($defaultBranch || in_array($branch, $environment->getBranches(), true)) { + if ($defaultBranch || \in_array($branch, $environment->getBranches(), true)) { $environmentsIds[] = $environment->getId(); } } @@ -264,7 +276,7 @@ public function getBranchesByEnvironment($environmentId) $environments = $this->getEnvironmentsObjects(); foreach ($environments['items'] as $environment) { /** @var Environment $environment */ - if ($environmentId == $environment->getId()) { + if ($environmentId === $environment->getId()) { return $environment->getBranches(); } } diff --git a/src/Model/ProjectGroup.php b/src/Model/ProjectGroup.php index 14b6cf707..35885d9b8 100644 --- a/src/Model/ProjectGroup.php +++ b/src/Model/ProjectGroup.php @@ -1,11 +1,18 @@ + */ class ProjectGroup extends BaseProjectGroup { /** @@ -14,7 +21,7 @@ class ProjectGroup extends BaseProjectGroup public function getGroupProjects() { /** @var ProjectStore $projectStore */ - $projectStore = Factory::getStore('Project'); + $projectStore = $this->storeRegistry->get('Project'); return $projectStore->getByGroupId($this->getId(), false); } diff --git a/src/Model/Secret.php b/src/Model/Secret.php new file mode 100644 index 000000000..e0e241d46 --- /dev/null +++ b/src/Model/Secret.php @@ -0,0 +1,18 @@ + + */ +class Secret extends BaseSecret +{ + public const SECRET_NAME_PATTERN = '^[-_\w\d]+$'; +} diff --git a/src/Model/User.php b/src/Model/User.php index ddeaef3a7..ee091953b 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -1,25 +1,28 @@ + * @author Dmitry Khomutov */ class User extends BaseUser { - /** - * @return int - */ - public function getFinalPerPage() + public function getFinalPerPage(ConfigurationInterface $configuration): ?int { $perPage = $this->getPerPage(); if ($perPage) { return $perPage; } - return (int)Config::getInstance()->get('php-censor.per_page', 10); + return (int)$configuration->get('php-censor.per_page', 10); } } diff --git a/src/Model/WebhookRequest.php b/src/Model/WebhookRequest.php new file mode 100644 index 000000000..2a5dae163 --- /dev/null +++ b/src/Model/WebhookRequest.php @@ -0,0 +1,17 @@ + + */ +class WebhookRequest extends BaseWebhookRequest +{ +} diff --git a/src/Plugin.php b/src/Plugin.php index 202abcc02..adc7f278f 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -4,10 +4,13 @@ use Exception; use PHPCensor\Model\Build; -use PHPCensor\Plugin\Codeception; /** + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ abstract class Plugin { @@ -73,6 +76,8 @@ abstract class Plugin */ protected $binaryName = []; + protected ?StoreRegistry $storeRegistry = null; + public function __construct(Builder $builder, Build $build, array $options = []) { $this->builder = $builder; @@ -85,21 +90,21 @@ public function __construct(Builder $builder, Build $build, array $options = []) // Plugin option overwrite builder options for priority_path and binary_path if (!empty($options['priority_path']) && - in_array($options['priority_path'], self::AVAILABLE_PRIORITY_PATHS, true)) { + \in_array($options['priority_path'], self::AVAILABLE_PRIORITY_PATHS, true)) { $this->priorityPath = $options['priority_path']; } else { $this->priorityPath = $this->builder->priorityPath; } if (!empty($options['binary_name'])) { - if (is_array($options['binary_name'])) { + if (\is_array($options['binary_name'])) { $this->binaryName = $options['binary_name']; } else { $this->binaryName = [(string)$options['binary_name']]; } } - $this->builder->logDebug('Plugin options: ' . json_encode($options)); + $this->builder->logDebug('Plugin options: ' . \json_encode($options)); } /** @@ -109,21 +114,21 @@ public function __construct(Builder $builder, Build $build, array $options = []) */ protected function normalizePath($rawPath) { - $normalizedPath = $this->builder->interpolate($rawPath); + $normalizedPath = $this->builder->interpolate($rawPath, true); - if ('/' !== substr($rawPath, 0, 1)) { + if ('/' !== \substr($rawPath, 0, 1)) { $normalizedPath = $this->build->getBuildPath() . $normalizedPath; } - $realPath = realpath($normalizedPath); + $realPath = \realpath($normalizedPath); return (false !== $realPath) - ? rtrim($realPath, '/\\') . '/' - : rtrim( - str_replace( + ? \rtrim($realPath, '/\\') . '/' + : \rtrim( + \str_replace( '//', '/', - str_replace('/./', '/', $normalizedPath) + \str_replace('/./', '/', $normalizedPath) ), '/\\' ) . '/'; @@ -136,9 +141,9 @@ protected function normalizeBinaryPath() { $binaryPath = ''; if (!empty($this->options['binary_path'])) { - $optionBinaryPath = $this->builder->interpolate($this->options['binary_path']); + $optionBinaryPath = $this->builder->interpolate($this->options['binary_path'], true); - if ('/' !== substr($optionBinaryPath, 0, 1)) { + if ('/' !== \substr($optionBinaryPath, 0, 1)) { $binaryPath = $this->build->getBuildPath(); } @@ -147,15 +152,15 @@ protected function normalizeBinaryPath() $binaryPath = $this->builder->binaryPath; } - $realPath = realpath($binaryPath); + $realPath = \realpath($binaryPath); return (false !== $realPath) - ? rtrim($realPath, '/\\') . '/' - : rtrim( - str_replace( + ? \rtrim($realPath, '/\\') . '/' + : \rtrim( + \str_replace( '//', '/', - str_replace('/./', '/', $binaryPath) + \str_replace('/./', '/', $binaryPath) ), '/\\' ) . '/'; @@ -166,15 +171,15 @@ protected function normalizeBinaryPath() */ protected function normalizeDirectory() { - if (!empty($this->options['directory']) && is_array($this->options['directory'])) { + if (!empty($this->options['directory']) && \is_array($this->options['directory'])) { return $this->builder->directory; } $directory = ''; if (!empty($this->options['directory'])) { - $optionDirectory = $this->builder->interpolate($this->options['directory']); + $optionDirectory = $this->builder->interpolate($this->options['directory'], true); - if ('/' !== substr($optionDirectory, 0, 1)) { + if ('/' !== \substr($optionDirectory, 0, 1)) { $directory = $this->build->getBuildPath(); } @@ -183,15 +188,15 @@ protected function normalizeDirectory() $directory = $this->builder->directory; } - $realPath = realpath($directory); + $realPath = \realpath($directory); $finalDirectory = (false !== $realPath) - ? rtrim($realPath, '/\\') . '/' - : rtrim( - str_replace( + ? \rtrim($realPath, '/\\') . '/' + : \rtrim( + \str_replace( '//', '/', - str_replace('/./', '/', $directory) + \str_replace('/./', '/', $directory) ), '/\\' ) . '/'; @@ -209,37 +214,37 @@ protected function normalizeIgnore() $ignore = $this->builder->ignore; if (!empty($this->options['ignore'])) { - $ignore = array_merge($ignore, $this->options['ignore']); + $ignore = \array_merge($ignore, $this->options['ignore']); } $baseDirectory = $this->builder->buildPath; - array_walk($ignore, function (&$value) use ($baseDirectory) { - $value = $this->builder->interpolate($value); + \array_walk($ignore, function (&$value) use ($baseDirectory) { + $value = $this->builder->interpolate($value, true); - if ('/' !== substr($value, 0, 1)) { + if ('/' !== \substr($value, 0, 1)) { $value = $baseDirectory . $value; } - clearstatcache(true); - $realPath = realpath($value); + \clearstatcache(true); + $realPath = \realpath($value); $value = (false !== $realPath) ? $realPath : $value; - $value = str_replace("/./", '/', $value); - $value = rtrim( - str_replace( + $value = \str_replace("/./", '/', $value); + $value = \rtrim( + \str_replace( '//', '/', - str_replace($baseDirectory, '', $value) + \str_replace($baseDirectory, '', $value) ), '/\\' ); }); - return array_unique($ignore); + return \array_unique($ignore); } /** @@ -292,4 +297,9 @@ public static function pluginName() { return ''; } + + public function setStoreRegistry(StoreRegistry $storeRegistry) + { + $this->storeRegistry = $storeRegistry; + } } diff --git a/src/Plugin/Atoum.php b/src/Plugin/Atoum.php index 3a516f211..571ea265d 100644 --- a/src/Plugin/Atoum.php +++ b/src/Plugin/Atoum.php @@ -8,6 +8,11 @@ /** * Atoum plugin, runs Atoum tests within a project. + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class Atoum extends Plugin { diff --git a/src/Plugin/Behat.php b/src/Plugin/Behat.php index e8e21573c..bfff90a48 100644 --- a/src/Plugin/Behat.php +++ b/src/Plugin/Behat.php @@ -10,14 +10,18 @@ /** * Behat BDD Plugin * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class Behat extends Plugin { /** * @var string */ - protected $features; + protected $features = ''; /** * @return string @@ -36,8 +40,6 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->executable = $this->findBinary(['behat', 'behat.phar']); - $this->features = ''; - if (!empty($options['features'])) { $this->features = $options['features']; } @@ -49,7 +51,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) public function execute() { if (!$this->executable) { - $this->builder->logFailure(sprintf('Could not find %s', 'behat')); + $this->builder->logFailure(\sprintf('Could not find %s', 'behat')); return false; } @@ -73,30 +75,31 @@ public function parseBehatOutput() { $output = $this->builder->getLastOutput(); - $parts = explode('---', $output); + $parts = \explode('---', $output); - if (count($parts) <= 1) { + if (\count($parts) <= 1) { return [0, []]; } - $lines = explode(PHP_EOL, $parts[1]); + $lines = \explode(PHP_EOL, $parts[1]); $storeFailures = false; $data = []; foreach ($lines as $line) { - $line = trim($line); - if ('Failed scenarios:' == $line) { + $line = \trim($line); + if ('Failed scenarios:' === $line) { $storeFailures = true; + continue; } - if (strpos($line, ':') === false) { + if (\strpos($line, ':') === false) { $storeFailures = false; } if ($storeFailures) { - $lineParts = explode(':', $line); + $lineParts = \explode(':', $line); $data[] = [ 'file' => $lineParts[0], 'line' => $lineParts[1], @@ -108,12 +111,12 @@ public function parseBehatOutput() 'Behat scenario failed.', BuildError::SEVERITY_HIGH, $lineParts[0], - $lineParts[1] + (int)$lineParts[1] ); } } - $errorCount = count($data); + $errorCount = \count($data); return [$errorCount, $data]; } diff --git a/src/Plugin/BitbucketNotify.php b/src/Plugin/BitbucketNotify.php index f962b7da1..862d53c64 100644 --- a/src/Plugin/BitbucketNotify.php +++ b/src/Plugin/BitbucketNotify.php @@ -5,15 +5,20 @@ use Exception; use GuzzleHttp\Client; use PHPCensor\Builder; -use PHPCensor\Database; +use PHPCensor\Common\Exception\InvalidArgumentException; use PHPCensor\Model\Build; use PHPCensor\Plugin; use PHPCensor\Plugin\Util\BitbucketNotifyPluginResult; use PHPCensor\Store\BuildErrorStore; use PHPCensor\Store\BuildMetaStore; use PHPCensor\Store\BuildStore; -use PHPCensor\Store\Factory; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov + */ class BitbucketNotify extends Plugin { /** @var string */ @@ -45,11 +50,6 @@ class BitbucketNotify extends Plugin */ protected $httpClient; - /** - * @var Database - */ - protected $pdo; - /** * @return string */ @@ -67,8 +67,6 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->pdo = Database::getConnection('read'); - $this->httpClient = new Client(); $this->url = \trim($options['url']); $this->message = isset($options['message']) ? $options['message'] : ''; @@ -79,7 +77,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->updateBuild = $options['update_build']; if (\array_key_exists('auth_token', $options)) { - $this->authToken = $options['auth_token']; + $this->authToken = $this->builder->interpolate($options['auth_token'], true); } if (empty($this->message)) { @@ -103,8 +101,8 @@ public function __construct(Builder $builder, Build $build, array $options = []) $chart = APP_URL . 'artifacts/pdepend/' . $buildDirectory . '/chart.svg'; $pyramid = APP_URL . 'artifacts/pdepend/' . $buildDirectory . '/pyramid.svg'; - $this->message .= sprintf('![Chart](%s "Pdepend Chart")', $chart); - $this->message .= sprintf('![Pyramid](%s "Pdepend Pyramid")', $pyramid) . PHP_EOL; + $this->message .= \sprintf('![Chart](%s "Pdepend Chart")', $chart); + $this->message .= \sprintf('![Pyramid](%s "Pdepend Pyramid")', $pyramid) . PHP_EOL; $this->message .= $summary . PHP_EOL; } } @@ -115,7 +113,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) empty($this->projectKey) || empty($this->repositorySlug) ) { - throw new Exception('Please define the url for bitbucket plugin!'); + throw new InvalidArgumentException('Please define the url for bitbucket plugin!'); } } @@ -158,9 +156,9 @@ public function execute() */ protected function findPullRequestsByBranch() { - $endpoint = sprintf('/projects/%s/repos/%s/pull-requests', $this->projectKey, $this->repositorySlug); + $endpoint = \sprintf('/projects/%s/repos/%s/pull-requests', $this->projectKey, $this->repositorySlug); $response = $this->apiRequest($endpoint)->getBody(); - $response = json_decode($response, true); + $response = \json_decode($response, true); foreach ($response['values'] as $pullRequest) { if ($pullRequest['fromRef']['displayId'] === $this->getBuild()->getBranch()) { @@ -173,7 +171,7 @@ protected function findPullRequestsByBranch() protected function getTargetBranchForPullRequest($pullRequestId) { - $endpoint = sprintf( + $endpoint = \sprintf( '/projects/%s/repos/%s/pull-requests/%d', $this->projectKey, $this->repositorySlug, @@ -181,7 +179,7 @@ protected function getTargetBranchForPullRequest($pullRequestId) ); $response = $this->apiRequest($endpoint)->getBody(); - $response = json_decode($response, true); + $response = \json_decode($response, true); return $response['toRef']['displayId']; } @@ -193,7 +191,7 @@ protected function getTargetBranchForPullRequest($pullRequestId) */ protected function createCommentInPullRequest($pullRequestId, $message) { - $endpoint = sprintf( + $endpoint = \sprintf( '/projects/%s/repos/%s/pull-requests/%s/comments', $this->projectKey, $this->repositorySlug, @@ -201,7 +199,7 @@ protected function createCommentInPullRequest($pullRequestId, $message) ); $response = $this->apiRequest($endpoint, 'post', ['text' => $message])->getBody(); - $response = json_decode($response, true); + $response = \json_decode($response, true); return (int)$response['id']; } @@ -224,7 +222,7 @@ protected function createTaskForCommentInPullRequest($commentId, $message) protected function updateBuild() { - $endpoint = sprintf( + $endpoint = \sprintf( '/commits/%s', $this->getBuild()->getCommitId() ); @@ -232,9 +230,11 @@ protected function updateBuild() switch ($this->getBuild()->getStatus()) { case Build::STATUS_SUCCESS: $state = 'SUCCESSFUL'; + break; case Build::STATUS_FAILED: $state = 'FAILED'; + break; default: $state = 'INPROGRESS'; @@ -257,7 +257,7 @@ protected function updateBuild() protected function prepareResult($targetBranch) { /** @var BuildErrorStore $buildErrorStore */ - $buildErrorStore = Factory::getStore('BuildError'); + $buildErrorStore = $this->storeRegistry->get('BuildError'); $targetBranchBuildStats = $buildErrorStore->getErrorAmountPerPluginForBuild( $this->findLatestBuild($targetBranch) @@ -269,8 +269,8 @@ protected function prepareResult($targetBranch) return []; } - $plugins = array_unique(array_merge(array_keys($targetBranchBuildStats), array_keys($currentBranchBuildStats))); - sort($plugins); + $plugins = \array_unique([...\array_keys($targetBranchBuildStats), ...\array_keys($currentBranchBuildStats)]); + \sort($plugins); $result = []; foreach ($plugins as $plugin) { @@ -294,9 +294,8 @@ protected function prepareResult($targetBranch) public function getPhpUnitCoverage($targetBranch) { /** @var BuildMetaStore $buildMetaStore */ - $buildMetaStore = Factory::getStore('BuildMeta'); - $latestTargeBuildId = $this->findLatestBuild($targetBranch); - $latestCurrentBuildId = $this->findLatestBuild($this->build->getBranch()); + $buildMetaStore = $this->storeRegistry->get('BuildMeta'); + $latestTargetBuildId = $this->findLatestBuild($targetBranch); $targetMetaData = $buildMetaStore->getByKey( $this->findLatestBuild($targetBranch), @@ -308,13 +307,13 @@ public function getPhpUnitCoverage($targetBranch) ); $targetBranchCoverage = []; - if (!is_null($latestTargeBuildId) && !is_null($targetMetaData)) { - $targetBranchCoverage = json_decode($targetMetaData->getMetaValue(), true); + if (!\is_null($latestTargetBuildId) && !\is_null($targetMetaData)) { + $targetBranchCoverage = \json_decode($targetMetaData->getMetaValue(), true); } $currentBranchCoverage = []; - if (!is_null($currentMetaData)) { - $currentBranchCoverage = json_decode($currentMetaData->getMetaValue(), true); + if (!\is_null($currentMetaData)) { + $currentBranchCoverage = \json_decode($currentMetaData->getMetaValue(), true); } return new Plugin\Util\BitbucketNotifyPhpUnitResult( @@ -332,7 +331,7 @@ protected function buildResultComparator(array $plugins) { $maxPluginNameLength = 20; if (!empty($plugins)) { - $maxPluginNameLength = max(array_map('strlen', $plugins)); + $maxPluginNameLength = \max(\array_map('strlen', $plugins)); } $lines = []; @@ -345,12 +344,12 @@ protected function buildResultComparator(array $plugins) protected function reportGenerator(array $stats) { - $statsString = trim(implode(PHP_EOL, $stats)); + $statsString = \trim(\implode(PHP_EOL, $stats)); if (empty($stats)) { $statsString = 'no changes between your branch and target branch'; } - $message = str_replace(['%STATS%'], [$statsString], $this->message); + $message = \str_replace(['%STATS%'], [$statsString], $this->message); return $this->builder->interpolate($message); } @@ -362,7 +361,7 @@ protected function reportGenerator(array $stats) protected function findLatestBuild($branchName) { /** @var BuildStore $buildStore */ - $buildStore = Factory::getStore('Build'); + $buildStore = $this->storeRegistry->get('Build'); $build = $buildStore->getLatestBuildByProjectAndBranch($this->getBuild()->getProjectId(), $branchName); diff --git a/src/Plugin/CampfireNotify.php b/src/Plugin/CampfireNotify.php index 32b7bfa09..60d5b4503 100644 --- a/src/Plugin/CampfireNotify.php +++ b/src/Plugin/CampfireNotify.php @@ -2,22 +2,26 @@ namespace PHPCensor\Plugin; -use Exception; use PHPCensor\Builder; +use PHPCensor\Common\Exception\InvalidArgumentException; use PHPCensor\Model\Build; use PHPCensor\Plugin; /** * Campfire Plugin - Allows Campfire API actions. Strongly based on icecube (http://labs.mimmin.com/icecube) * + * @package PHP Censor + * @subpackage Application + * * @author André Cianfarani + * @author Dmitry Khomutov */ class CampfireNotify extends Plugin { protected $url; protected $authToken; protected $userAgent; - protected $cookie; + protected $cookie = 'php-censor-cookie'; protected $verbose = false; protected $room; protected $message; @@ -40,7 +44,6 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->message = $options['message']; $version = $this->builder->interpolate('%SYSTEM_VERSION%'); $this->userAgent = 'PHP Censor/' . $version; - $this->cookie = "php-censor-cookie"; if (isset($options['verbose']) && $options['verbose']) { $this->verbose = true; @@ -53,14 +56,14 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->url = $campfire['url']; if (\array_key_exists('auth_token', $campfire)) { - $this->authToken = $campfire['auth_token']; + $this->authToken = $this->builder->interpolate($campfire['auth_token'], true); } if (\array_key_exists('room', $campfire)) { $this->room = $campfire['room']; } } else { - throw new Exception('No connection parameters given for Campfire plugin'); + throw new InvalidArgumentException('No connection parameters given for Campfire plugin'); } } diff --git a/src/Plugin/CleanBuild.php b/src/Plugin/CleanBuild.php index 8f6e4390d..738bb0cc4 100644 --- a/src/Plugin/CleanBuild.php +++ b/src/Plugin/CleanBuild.php @@ -10,7 +10,11 @@ * Clean build removes Composer related files and allows users to clean up their build directory. * Useful as a precursor to copy_build. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class CleanBuild extends Plugin { @@ -31,7 +35,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->removeFiles = isset($options['remove']) && is_array($options['remove']) ? $options['remove'] : []; + $this->removeFiles = isset($options['remove']) && \is_array($options['remove']) ? $options['remove'] : []; } /** diff --git a/src/Plugin/Codeception.php b/src/Plugin/Codeception.php index 625897b91..8af65569e 100644 --- a/src/Plugin/Codeception.php +++ b/src/Plugin/Codeception.php @@ -4,6 +4,7 @@ use Exception; use PHPCensor\Builder; +use PHPCensor\Common\Exception\InvalidArgumentException; use PHPCensor\Model\Build; use PHPCensor\Plugin; use PHPCensor\Plugin\Util\TestResultParsers\Codeception as Parser; @@ -13,9 +14,13 @@ /** * Codeception Plugin - Enables full acceptance, unit, and functional testing. * + * @package PHP Censor + * @subpackage Application + * * @author Don Gilbert * @author Igor Timoshenko * @author Adam Cooper + * @author Dmitry Khomutov */ class Codeception extends Plugin implements ZeroConfigPluginInterface { @@ -65,7 +70,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) } if (isset($options['output_path'])) { - array_unshift($this->outputPath, $options['output_path']); + \array_unshift($this->outputPath, $options['output_path']); } $this->executable = $this->findBinary(['codecept', 'codecept.phar']); @@ -76,7 +81,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) */ public static function canExecuteOnStage($stage, Build $build) { - return (Build::STAGE_TEST === $stage && !is_null(self::findConfigFile($build->getBuildPath()))); + return (Build::STAGE_TEST === $stage && !\is_null(self::findConfigFile($build->getBuildPath()))); } /** @@ -87,11 +92,11 @@ public static function canExecuteOnStage($stage, Build $build) */ public static function findConfigFile($buildPath) { - if (file_exists($buildPath . 'codeception.yml')) { + if (\file_exists($buildPath . 'codeception.yml')) { return $buildPath . 'codeception.yml'; } - if (file_exists($buildPath . 'codeception.dist.yml')) { + if (\file_exists($buildPath . 'codeception.dist.yml')) { return $buildPath . 'codeception.dist.yml'; } @@ -104,7 +109,7 @@ public static function findConfigFile($buildPath) public function execute() { if (empty($this->ymlConfigFile)) { - throw new Exception("No configuration file found"); + throw new InvalidArgumentException("No configuration file found"); } // Run any config files first. This can be either a single value or an array. @@ -122,7 +127,7 @@ protected function runConfigFile() $codeception = $this->executable; if (!$codeception) { - $this->builder->logFailure(sprintf('Could not find "%s" binary', 'codecept')); + $this->builder->logFailure(\sprintf('Could not find "%s" binary', 'codecept')); return false; } @@ -135,24 +140,24 @@ protected function runConfigFile() } $parser = new YamlParser(); - $yaml = file_get_contents($this->ymlConfigFile); + $yaml = \file_get_contents($this->ymlConfigFile); $config = (array)$parser->parse($yaml); $trueReportXmlPath = null; if ($config && isset($config['paths']['log'])) { - $trueReportXmlPath = rtrim($config['paths']['log'], '/\\') . '/'; + $trueReportXmlPath = \rtrim($config['paths']['log'], '/\\') . '/'; } - if (!file_exists($trueReportXmlPath . 'report.xml')) { + if (!\file_exists($trueReportXmlPath . 'report.xml')) { foreach ($this->outputPath as $outputPath) { - $trueReportXmlPath = rtrim($outputPath, '/\\') . '/'; - if (file_exists($trueReportXmlPath . 'report.xml')) { + $trueReportXmlPath = \rtrim($outputPath, '/\\') . '/'; + if (\file_exists($trueReportXmlPath . 'report.xml')) { break; } } } - if (!file_exists($trueReportXmlPath . 'report.xml')) { + if (!\file_exists($trueReportXmlPath . 'report.xml')) { $this->builder->logFailure('"report.xml" file can not be found in configured "output_path!"'); return false; @@ -169,7 +174,7 @@ protected function runConfigFile() // NOTE: Codeception does not use stderr, so failure can only be detected // through tests - $success = $success && (intval($meta['failures']) < 1); + $success = $success && (\intval($meta['failures']) < 1); $this->build->storeMeta((self::pluginName() . '-meta'), $meta); $this->build->storeMeta((self::pluginName() . '-data'), $output); diff --git a/src/Plugin/Composer.php b/src/Plugin/Composer.php index fb91ae454..97852eceb 100644 --- a/src/Plugin/Composer.php +++ b/src/Plugin/Composer.php @@ -11,15 +11,19 @@ /** * Composer Plugin - Provides access to Composer functionality. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class Composer extends Plugin implements ZeroConfigPluginInterface { - protected $action; - protected $preferDist; - protected $noDev; - protected $ignorePlatformReqs; - protected $preferSource; + protected $action = 'install'; + protected $preferDist = false; + protected $noDev = false; + protected $ignorePlatformReqs = false; + protected $preferSource = false; /** * @return string @@ -36,32 +40,26 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->action = 'install'; - $this->preferDist = false; - $this->preferSource = false; - $this->noDev = false; - $this->ignorePlatformReqs = false; - $this->executable = $this->findBinary(['composer', 'composer.phar']); - if (array_key_exists('action', $options)) { + if (\array_key_exists('action', $options)) { $this->action = $options['action']; } - if (array_key_exists('prefer_dist', $options)) { + if (\array_key_exists('prefer_dist', $options)) { $this->preferDist = (bool)$options['prefer_dist']; } - if (array_key_exists('prefer_source', $options)) { + if (\array_key_exists('prefer_source', $options)) { $this->preferDist = false; $this->preferSource = (bool)$options['prefer_source']; } - if (array_key_exists('no_dev', $options)) { + if (\array_key_exists('no_dev', $options)) { $this->noDev = (bool)$options['no_dev']; } - if (array_key_exists('ignore_platform_reqs', $options)) { + if (\array_key_exists('ignore_platform_reqs', $options)) { $this->ignorePlatformReqs = (bool)$options['ignore_platform_reqs']; } } @@ -73,7 +71,7 @@ public static function canExecuteOnStage($stage, Build $build) { $path = $build->getBuildPath() . '/composer.json'; - if (file_exists($path) && Build::STAGE_SETUP == $stage) { + if (\file_exists($path) && Build::STAGE_SETUP === $stage) { return true; } diff --git a/src/Plugin/CopyBuild.php b/src/Plugin/CopyBuild.php index a319bdb53..4cedb2bc0 100644 --- a/src/Plugin/CopyBuild.php +++ b/src/Plugin/CopyBuild.php @@ -3,14 +3,18 @@ namespace PHPCensor\Plugin; use PHPCensor\Builder; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Model\Build; use PHPCensor\Plugin; -use RuntimeException; /** * Copy Build Plugin - Copies the entire build to another directory. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class CopyBuild extends Plugin { @@ -52,9 +56,9 @@ public function execute() $this->wipeExistingDirectory(); - if (is_dir($this->directory)) { + if (\is_dir($this->directory)) { throw new RuntimeException( - sprintf( + \sprintf( 'Directory "%s" already exists! Use "wipe" option if you want to delete directory before copy.', $this->directory ) @@ -62,7 +66,7 @@ public function execute() } $cmd = 'cd "%s" && mkdir -p "%s" && cp -R %s/. "%s"'; - $success = $this->builder->executeCommand($cmd, $buildPath, $this->directory, rtrim($buildPath, '/'), $this->directory); + $success = $this->builder->executeCommand($cmd, $buildPath, $this->directory, \rtrim($buildPath, '/'), $this->directory); $this->deleteIgnoredFiles(); @@ -76,17 +80,17 @@ public function execute() */ protected function wipeExistingDirectory() { - if ($this->wipe === true && $this->directory !== '/' && is_dir($this->directory)) { + if ($this->wipe === true && $this->directory !== '/' && \is_dir($this->directory)) { $cmd = 'cd "%s" && rm -Rf "%s"'; $success = $this->builder->executeCommand($cmd, $this->builder->buildPath, $this->directory); if (!$success) { throw new RuntimeException( - sprintf('Failed to wipe existing directory "%s" before copy!', $this->directory) + \sprintf('Failed to wipe existing directory "%s" before copy!', $this->directory) ); } - clearstatcache(); + \clearstatcache(); } } diff --git a/src/Plugin/Deployer.php b/src/Plugin/Deployer.php index 0183bccbc..742ad89fa 100644 --- a/src/Plugin/Deployer.php +++ b/src/Plugin/Deployer.php @@ -10,12 +10,16 @@ /** * Integration with Deployer: https://github.com/rebelinblue/deployer * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class Deployer extends Plugin { protected $webhookUrl; - protected $reason; + protected $reason = 'PHP Censor Build #%BUILD_ID% - %COMMIT_MESSAGE%'; protected $updateOnly; /** @@ -33,7 +37,6 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->reason = 'PHP Censor Build #%BUILD_ID% - %COMMIT_MESSAGE%'; if (isset($options['webhook_url'])) { $this->webhookUrl = $options['webhook_url']; } @@ -61,11 +64,11 @@ public function execute() $this->webhookUrl, [ 'form_params' => [ - 'reason' => $this->builder->interpolate($this->reason), + 'reason' => $this->builder->interpolate($this->reason, true), 'source' => 'PHP Censor', - 'url' => $this->builder->interpolate('%BUILD_LINK%'), - 'branch' => $this->builder->interpolate('%BRANCH%'), - 'commit' => $this->builder->interpolate('%COMMIT_ID%'), + 'url' => $this->builder->interpolate('%BUILD_LINK%', true), + 'branch' => $this->builder->interpolate('%BRANCH%', true), + 'commit' => $this->builder->interpolate('%COMMIT_ID%', true), 'update_only' => $this->updateOnly, ] ] diff --git a/src/Plugin/DeployerOrg.php b/src/Plugin/DeployerOrg.php index 1c7fbdbc6..c8e3b4b94 100644 --- a/src/Plugin/DeployerOrg.php +++ b/src/Plugin/DeployerOrg.php @@ -9,7 +9,11 @@ /** * Deployer plugin for PHPCensor: http://deployer.org * + * @package PHP Censor + * @subpackage Application + * * @author Alexey Boyko + * @author Dmitry Khomutov */ class DeployerOrg extends Plugin { @@ -105,7 +109,7 @@ protected function getVerbosityOption($verbosity) 'quiet' => 'q' ]; - $verbosity = strtolower(trim($verbosity)); + $verbosity = \strtolower(\trim($verbosity)); if ($verbosity !== 'normal') { return '-' . $logLevelList[$verbosity]; } else { @@ -141,6 +145,6 @@ protected function getOptions($config) $options[] = '--file=' . $config['file']; } - return implode(' ', $options); + return \implode(' ', $options); } } diff --git a/src/Plugin/EmailNotify.php b/src/Plugin/EmailNotify.php index c735367e3..ee8549780 100644 --- a/src/Plugin/EmailNotify.php +++ b/src/Plugin/EmailNotify.php @@ -2,18 +2,21 @@ namespace PHPCensor\Plugin; -use PHPCensor\Config; use PHPCensor\Exception\HttpException; use PHPCensor\Helper\Email as EmailHelper; use PHPCensor\Plugin; use PHPCensor\View; use Psr\Log\LogLevel; -use RuntimeException; +use PHPCensor\Common\Exception\RuntimeException; /** * Email Plugin - Provides simple email capability. * + * @package PHP Censor + * @subpackage Application + * * @author Steve Brazier + * @author Dmitry Khomutov */ class EmailNotify extends Plugin { @@ -38,7 +41,7 @@ public function execute() // Without some email addresses in the yml file then we // can't do anything. - if (count($addresses) == 0) { + if (\count($addresses) === 0) { return false; } @@ -49,7 +52,7 @@ public function execute() $view = $this->getMailTemplate(); } catch (RuntimeException $e) { $this->builder->log( - sprintf('Unknown mail template "%s", falling back to default.', $this->options['template']), + \sprintf('Unknown mail template "%s", falling back to default.', $this->options['template']), LogLevel::WARNING ); $view = $this->getDefaultMailTemplate(); @@ -66,13 +69,13 @@ public function execute() $sendFailures = $this->sendSeparateEmails( $addresses, - sprintf("PHP Censor - %s - %s", $projectName, $buildStatus), + \sprintf("PHP Censor - %s - %s", $projectName, $buildStatus), $body ); // This is a success if we've not failed to send anything. - $this->builder->log(sprintf('%d emails sent.', (count($addresses) - $sendFailures))); - $this->builder->log(sprintf('%d emails failed to send.', $sendFailures)); + $this->builder->log(\sprintf('%d emails sent.', (\count($addresses) - $sendFailures))); + $this->builder->log(\sprintf('%d emails failed to send.', $sendFailures)); return ($sendFailures === 0); } @@ -87,16 +90,16 @@ public function execute() */ protected function sendEmail($toAddress, $ccList, $subject, $body) { - $email = new EmailHelper(Config::getInstance()); + $email = new EmailHelper($this->builder->getConfiguration()); - $email->setEmailTo($toAddress, $toAddress); - $email->setSubject($subject); - $email->setBody($body); + $email->setEmailTo($this->builder->interpolate($toAddress), $this->builder->interpolate($toAddress)); + $email->setSubject($this->builder->interpolate($subject)); + $email->setBody($this->builder->interpolate($body)); $email->setHtml(true); - if (is_array($ccList) && count($ccList)) { + if (\is_array($ccList) && \count($ccList)) { foreach ($ccList as $address) { - $email->addCc($address, $address); + $email->addCc($this->builder->interpolate($address), $this->builder->interpolate($address)); } } @@ -138,8 +141,8 @@ protected function getEmailAddresses() $addresses = []; $committer = $this->build->getCommitterEmail(); - $this->builder->logDebug(sprintf("Committer email: '%s'", $committer)); - $this->builder->logDebug(sprintf( + $this->builder->logDebug(\sprintf("Committer email: '%s'", $committer)); + $this->builder->logDebug(\sprintf( "Committer option: '%s'", (!empty($this->options['committer']) && $this->options['committer']) ? 'true' : 'false' )); @@ -150,18 +153,18 @@ protected function getEmailAddresses() } } - $this->builder->logDebug(sprintf( + $this->builder->logDebug(\sprintf( "Addresses option: '%s'", - (!empty($this->options['addresses']) && is_array($this->options['addresses'])) ? implode(', ', $this->options['addresses']) : 'false' + (!empty($this->options['addresses']) && \is_array($this->options['addresses'])) ? \implode(', ', $this->options['addresses']) : 'false' )); - if (!empty($this->options['addresses']) && is_array($this->options['addresses'])) { + if (!empty($this->options['addresses']) && \is_array($this->options['addresses'])) { foreach ($this->options['addresses'] as $address) { $addresses[] = $address; } } - $this->builder->logDebug(sprintf( + $this->builder->logDebug(\sprintf( "Default mailTo option: '%s'", !empty($this->options['default_mailto_address']) ? $this->options['default_mailto_address'] : 'false' )); @@ -170,7 +173,7 @@ protected function getEmailAddresses() $addresses[] = $this->options['default_mailto_address']; } - return array_unique($addresses); + return \array_unique($addresses); } /** diff --git a/src/Plugin/Env.php b/src/Plugin/Env.php index cbe547de3..934868044 100644 --- a/src/Plugin/Env.php +++ b/src/Plugin/Env.php @@ -7,7 +7,11 @@ /** * Environment variable plugin * + * @package PHP Censor + * @subpackage Application + * * @author Steve Kamerman + * @author Dmitry Khomutov */ class Env extends Plugin { @@ -26,15 +30,17 @@ public function execute() { $success = true; foreach ($this->options as $key => $value) { - if (is_numeric($key)) { + if (\is_numeric($key)) { // This allows the developer to specify env vars like " - FOO=bar" or " - FOO: bar" - $envVar = is_array($value) ? key($value).'='.current($value) : $value; + $envVar = \is_array($value) + ? (\key($value) . '=' . \current($value)) + : $value; } else { // This allows the standard syntax: "FOO: bar" $envVar = "$key=$value"; } - if (!putenv($this->builder->interpolate($envVar))) { + if (!\putenv($this->builder->interpolate($envVar, true))) { $success = false; $this->builder->logFailure('Unable to set environment variable'); } diff --git a/src/Plugin/FlowdockNotify.php b/src/Plugin/FlowdockNotify.php index 441dfb4e1..fb5e236a5 100644 --- a/src/Plugin/FlowdockNotify.php +++ b/src/Plugin/FlowdockNotify.php @@ -6,13 +6,19 @@ use FlowdockClient\Api\Push\Push; use FlowdockClient\Api\Push\TeamInboxMessage; use PHPCensor\Builder; +use PHPCensor\Common\Exception\InvalidArgumentException; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Model\Build; use PHPCensor\Plugin; /** * Flowdock Plugin * + * @package PHP Censor + * @subpackage Application + * * @author Petr Cervenka + * @author Dmitry Khomutov */ class FlowdockNotify extends Plugin { @@ -38,12 +44,12 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - if (!is_array($options) || (!isset($options['api_key']) && !isset($options['auth_token']))) { - throw new Exception('Please define the "auth_token" for Flowdock Notify plugin!'); + if (empty($options['auth_token'])) { + throw new InvalidArgumentException('Please define the "auth_token" for Flowdock Notify plugin!'); } if (\array_key_exists('auth_token', $options)) { - $this->authToken = $options['auth_token']; + $this->authToken = $this->builder->interpolate($options['auth_token'], true); } $this->message = isset($options['message']) ? $options['message'] : self::MESSAGE_DEFAULT; @@ -70,7 +76,7 @@ public function execute() ->setContent($message); if (!$push->sendTeamInboxMessage($flowMessage, ['connect_timeout' => 5000, 'timeout' => 5000])) { - throw new Exception(sprintf('Flowdock Failed: %s', $flowMessage->getResponseErrors())); + throw new RuntimeException(\sprintf('Flowdock Failed: %s', $flowMessage->getResponseErrors())); } return true; diff --git a/src/Plugin/Git.php b/src/Plugin/Git.php index 35ed183b9..e7d63f3c2 100644 --- a/src/Plugin/Git.php +++ b/src/Plugin/Git.php @@ -9,7 +9,11 @@ /** * Git plugin. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class Git extends Plugin { @@ -40,7 +44,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) public function execute() { // Check if there are any actions to be run for the branch we're running on: - if (!array_key_exists($this->build->getBranch(), $this->actions)) { + if (!\array_key_exists($this->build->getBranch(), $this->actions)) { return true; } @@ -48,6 +52,7 @@ public function execute() foreach ($this->actions[$this->build->getBranch()] as $action => $options) { if (!$this->runAction($action, $options)) { $success = false; + break; } } @@ -87,7 +92,7 @@ protected function runAction($action, array $options = []) */ protected function runMergeAction($options) { - if (array_key_exists('branch', $options)) { + if (\array_key_exists('branch', $options)) { $cmd = 'cd "%s" && git checkout %s && git merge "%s"'; $path = $this->builder->buildPath; @@ -101,15 +106,15 @@ protected function runMergeAction($options) */ protected function runTagAction($options) { - $tagName = date('Ymd-His'); - $message = sprintf('Tag created by PHP Censor: %s', date('Y-m-d H:i:s')); + $tagName = \date('Ymd-His'); + $message = \sprintf('Tag created by PHP Censor: %s', \date('Y-m-d H:i:s')); - if (array_key_exists('name', $options)) { - $tagName = $this->builder->interpolate($options['name']); + if (\array_key_exists('name', $options)) { + $tagName = $this->builder->interpolate($options['name'], true); } - if (array_key_exists('message', $options)) { - $message = $this->builder->interpolate($options['message']); + if (\array_key_exists('message', $options)) { + $message = $this->builder->interpolate($options['message'], true); } $cmd = 'git tag %s -m "%s"'; @@ -126,12 +131,12 @@ protected function runPullAction($options) $branch = $this->build->getBranch(); $remote = 'origin'; - if (array_key_exists('branch', $options)) { - $branch = $this->builder->interpolate($options['branch']); + if (\array_key_exists('branch', $options)) { + $branch = $this->builder->interpolate($options['branch'], true); } - if (array_key_exists('remote', $options)) { - $remote = $this->builder->interpolate($options['remote']); + if (\array_key_exists('remote', $options)) { + $remote = $this->builder->interpolate($options['remote'], true); } return $this->builder->executeCommand('git pull %s %s', $remote, $branch); @@ -146,12 +151,12 @@ protected function runPushAction($options) $branch = $this->build->getBranch(); $remote = 'origin'; - if (array_key_exists('branch', $options)) { - $branch = $this->builder->interpolate($options['branch']); + if (\array_key_exists('branch', $options)) { + $branch = $this->builder->interpolate($options['branch'], true); } - if (array_key_exists('remote', $options)) { - $remote = $this->builder->interpolate($options['remote']); + if (\array_key_exists('remote', $options)) { + $remote = $this->builder->interpolate($options['remote'], true); } return $this->builder->executeCommand('git push %s %s', $remote, $branch); diff --git a/src/Plugin/Grunt.php b/src/Plugin/Grunt.php index 71dbe00c2..0d49bac7d 100644 --- a/src/Plugin/Grunt.php +++ b/src/Plugin/Grunt.php @@ -9,12 +9,16 @@ /** * Grunt Plugin - Provides access to grunt functionality. * + * @package PHP Censor + * @subpackage Application + * * @author Tobias Tom + * @author Dmitry Khomutov */ class Grunt extends Plugin { - protected $task; - protected $gruntfile; + protected $task = null; + protected $gruntfile = 'Gruntfile.js'; /** * @return string @@ -31,10 +35,6 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->task = null; - - $this->gruntfile = 'Gruntfile.js'; - if (isset($options['task'])) { $this->task = $options['task']; } diff --git a/src/Plugin/Gulp.php b/src/Plugin/Gulp.php index cc3998f61..6aff9411a 100644 --- a/src/Plugin/Gulp.php +++ b/src/Plugin/Gulp.php @@ -9,12 +9,16 @@ /** * Gulp Plugin - Provides access to gulp functionality. * + * @package PHP Censor + * @subpackage Application + * * @author Dirk Heilig + * @author Dmitry Khomutov */ class Gulp extends Plugin { - protected $task; - protected $gulpfile; + protected $task = null; + protected $gulpfile = 'gulpfile.js'; /** * @return string @@ -31,12 +35,8 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->task = null; - $this->executable = $this->findBinary('gulp'); - $this->gulpfile = 'gulpfile.js'; - if (isset($options['task'])) { $this->task = $options['task']; } diff --git a/src/Plugin/HipchatNotify.php b/src/Plugin/HipchatNotify.php deleted file mode 100644 index f39b848d9..000000000 --- a/src/Plugin/HipchatNotify.php +++ /dev/null @@ -1,98 +0,0 @@ - - */ -class HipchatNotify extends Plugin -{ - protected $authToken; - protected $color; - protected $notify; - protected $userAgent; - protected $cookie; - protected $message; - protected $room; - - /** - * @return string - */ - public static function pluginName() - { - return 'hipchat_notify'; - } - - /** - * {@inheritDoc} - */ - public function __construct(Builder $builder, Build $build, array $options = []) - { - parent::__construct($builder, $build, $options); - - $version = $this->builder->interpolate('%SYSTEM_VERSION%'); - $this->userAgent = 'PHP Censor/' . $version; - $this->cookie = "php-censor-cookie"; - - if (!\is_array($options) || !isset($options['room']) || (!isset($options['authToken']) && !isset($options['auth_token']))) { - throw new Exception('Please define room and authToken for hipchat_notify plugin.'); - } - - if (\array_key_exists('auth_token', $options)) { - $this->authToken = $options['auth_token']; - } - - $this->room = $options['room']; - - if (isset($options['message'])) { - $this->message = $options['message']; - } else { - $this->message = '%PROJECT_TITLE% built at %BUILD_LINK%'; - } - - if (isset($options['color'])) { - $this->color = $options['color']; - } else { - $this->color = 'yellow'; - } - - if (isset($options['notify'])) { - $this->notify = $options['notify']; - } else { - $this->notify = false; - } - } - - /** - * Run the HipChat plugin. - * @return bool - */ - public function execute() - { - $hipChat = new HipChat($this->authToken); - $message = $this->builder->interpolate($this->message); - - $result = true; - if (is_array($this->room)) { - foreach ($this->room as $room) { - if (!$hipChat->message_room($room, 'PHP Censor', $message, $this->notify, $this->color)) { - $result = false; - } - } - } else { - if (!$hipChat->message_room($this->room, 'PHP Censor', $message, $this->notify, $this->color)) { - $result = false; - } - } - - return $result; - } -} diff --git a/src/Plugin/IrcNotify.php b/src/Plugin/IrcNotify.php index 177ad5377..84971e602 100644 --- a/src/Plugin/IrcNotify.php +++ b/src/Plugin/IrcNotify.php @@ -9,7 +9,11 @@ /** * IRC Plugin - Sends a notification to an IRC channel * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class IrcNotify extends Plugin { @@ -63,8 +67,8 @@ public function execute() $this->port = 6667; } - $sock = fsockopen($this->server, $this->port); - stream_set_timeout($sock, 1); + $sock = \fsockopen($this->server, $this->port); + \stream_set_timeout($sock, 1); $connectCommands = [ 'USER ' . $this->nick . ' 0 * :' . $this->nick, @@ -74,7 +78,7 @@ public function execute() $this->executeIrcCommand($sock, 'JOIN ' . $this->room); $this->executeIrcCommand($sock, 'PRIVMSG ' . $this->room . ' :' . $msg); - fclose($sock); + \fclose($sock); return true; } @@ -87,22 +91,22 @@ public function execute() private function executeIrcCommands($socket, array $commands) { foreach ($commands as $command) { - fputs($socket, $command . "\n"); + \fputs($socket, $command . "\n"); } $pingBack = false; // almost all servers expect pingback! - while ($response = fgets($socket)) { + while ($response = \fgets($socket)) { $matches = []; - if (preg_match('/^PING \\:([A-Z0-9]+)/', $response, $matches)) { + if (\preg_match('/^PING \\:([A-Z0-9]+)/', $response, $matches)) { $pingBack = $matches[1]; } } if ($pingBack) { $command = 'PONG :' . $pingBack . "\n"; - fputs($socket, $command); + \fputs($socket, $command); } } diff --git a/src/Plugin/Lint.php b/src/Plugin/Lint.php index 7c1d7ae8d..9a7eb92bf 100644 --- a/src/Plugin/Lint.php +++ b/src/Plugin/Lint.php @@ -11,7 +11,11 @@ /** * PHP Lint Plugin - Provides access to PHP lint functionality. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class Lint extends Plugin { @@ -37,20 +41,20 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->directory, ]; - if (!empty($options['directories']) && is_array($options['directories'])) { + if (!empty($options['directories']) && \is_array($options['directories'])) { foreach ($options['directories'] as $index => $directory) { - $relativePath = preg_replace( + $relativePath = \preg_replace( '#^(\./|/)?(.*)$#', '$2', $options['directories'][$index] ); - $relativePath = rtrim($relativePath, "\//"); + $relativePath = \rtrim($relativePath, "\//"); $this->directories[] = $this->builder->buildPath . $relativePath . '/'; } } - if (array_key_exists('recursive', $options)) { + if (\array_key_exists('recursive', $options)) { $this->recursive = $options['recursive']; } } @@ -81,7 +85,7 @@ protected function lintItem($php, $item, $itemPath) { $success = true; - if ($item->isFile() && $item->getExtension() == 'php' && !$this->lintFile($php, $itemPath)) { + if ($item->isFile() && $item->getExtension() === 'php' && !$this->lintFile($php, $itemPath)) { $success = false; } elseif ($item->isDir() && $this->recursive && @@ -108,7 +112,7 @@ protected function lintDirectory($php, $path) $itemPath = $path . $item->getFilename(); - if (in_array($itemPath, $this->ignore, true)) { + if (\in_array($itemPath, $this->ignore, true)) { continue; } diff --git a/src/Plugin/Mage.php b/src/Plugin/Mage.php index 79a180ce8..c276703b8 100644 --- a/src/Plugin/Mage.php +++ b/src/Plugin/Mage.php @@ -1,21 +1,20 @@ */ class Mage extends Plugin { @@ -39,7 +38,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->executable = $this->findBinary(['mage', 'mage.phar']); if (isset($options['env'])) { - $this->mageEnv = $builder->interpolate($options['env']); + $this->mageEnv = $builder->interpolate($options['env'], true); } } @@ -60,7 +59,7 @@ public function execute() $this->builder->log('########## MAGE LOG BEGIN ##########'); $this->builder->log($this->getMageLog()); $this->builder->log('########## MAGE LOG END ##########'); - } catch (Exception $e) { + } catch (\Throwable $e) { $this->builder->logFailure($e->getMessage()); } @@ -75,41 +74,38 @@ public function execute() protected function getMageLog() { $logsDir = $this->build->getBuildPath() . '/.mage/logs'; - if (!is_dir($logsDir)) { - throw new Exception('Log directory not found'); + if (!\is_dir($logsDir)) { + throw new RuntimeException('Log directory not found'); } - $list = scandir($logsDir); + $list = \scandir($logsDir); if ($list === false) { - throw new Exception('Log dir read fail'); + throw new RuntimeException('Log dir read fail'); } - $list = array_filter($list, function ($name) { - return preg_match('/^log-\d+-\d+\.log$/', $name); - }); + $list = \array_filter($list, fn ($name) => \preg_match('/^log-\d+-\d+\.log$/', $name)); if (empty($list)) { - throw new Exception('Log dir filter fail'); + throw new RuntimeException('Log dir filter fail'); } - $res = sort($list); + $res = \sort($list); if ($res === false) { - throw new Exception('Logs sort fail'); + throw new RuntimeException('Logs sort fail'); } - $lastLogFile = end($list); + $lastLogFile = \end($list); if ($lastLogFile === false) { - throw new Exception('Get last Log name fail'); + throw new RuntimeException('Get last Log name fail'); } - $logContent = file_get_contents($logsDir . '/' . $lastLogFile); + $logContent = \file_get_contents($logsDir . '/' . $lastLogFile); if ($logContent === false) { - throw new Exception('Get last Log content fail'); + throw new RuntimeException('Get last Log content fail'); } - $lines = explode("\n", $logContent); - $lines = array_map('trim', $lines); - $lines = array_filter($lines); + $lines = \explode("\n", $logContent); + $lines = \array_map('trim', $lines); - return $lines; + return \array_filter($lines); } } diff --git a/src/Plugin/Mage3.php b/src/Plugin/Mage3.php index b7175266a..c60ab9f47 100644 --- a/src/Plugin/Mage3.php +++ b/src/Plugin/Mage3.php @@ -1,21 +1,20 @@ */ class Mage3 extends Plugin { @@ -40,11 +39,11 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->executable = $this->findBinary(['mage', 'mage.phar']); if (isset($options['env'])) { - $this->mageEnv = $builder->interpolate($options['env']); + $this->mageEnv = $builder->interpolate($options['env'], true); } if (isset($options['log_dir'])) { - $this->mageLogDir = $builder->interpolate($options['log_dir']); + $this->mageLogDir = $builder->interpolate($options['log_dir'], true); } } @@ -65,7 +64,7 @@ public function execute() $this->builder->log('########## MAGE LOG BEGIN ##########'); $this->builder->log($this->getMageLog()); $this->builder->log('########## MAGE LOG END ##########'); - } catch (Exception $e) { + } catch (\Throwable $e) { $this->builder->logFailure($e->getMessage()); } @@ -80,41 +79,38 @@ public function execute() protected function getMageLog() { $logsDir = $this->build->getBuildPath() . (!empty($this->mageLogDir) ? '/' . $this->mageLogDir : ''); - if (!is_dir($logsDir)) { - throw new Exception('Log directory not found'); + if (!\is_dir($logsDir)) { + throw new RuntimeException('Log directory not found'); } - $list = scandir($logsDir); + $list = \scandir($logsDir); if ($list === false) { - throw new Exception('Log dir read fail'); + throw new RuntimeException('Log dir read fail'); } - $list = array_filter($list, function ($name) { - return preg_match('/^\d+_\d+\.log$/', $name); - }); + $list = \array_filter($list, fn ($name) => \preg_match('/^\d+_\d+\.log$/', $name)); if (empty($list)) { - throw new Exception('Log dir filter fail'); + throw new RuntimeException('Log dir filter fail'); } - $res = sort($list); + $res = \sort($list); if ($res === false) { - throw new Exception('Logs sort fail'); + throw new RuntimeException('Logs sort fail'); } - $lastLogFile = end($list); + $lastLogFile = \end($list); if ($lastLogFile === false) { - throw new Exception('Get last Log name fail'); + throw new RuntimeException('Get last Log name fail'); } - $logContent = file_get_contents($logsDir . '/' . $lastLogFile); + $logContent = \file_get_contents($logsDir . '/' . $lastLogFile); if ($logContent === false) { - throw new Exception('Get last Log content fail'); + throw new RuntimeException('Get last Log content fail'); } - $lines = explode("\n", $logContent); - $lines = array_map('trim', $lines); - $lines = array_filter($lines); + $lines = \explode("\n", $logContent); + $lines = \array_map('trim', $lines); - return $lines; + return \array_filter($lines); } } diff --git a/src/Plugin/Mysql.php b/src/Plugin/Mysql.php index 61f6b3e87..04e469bc5 100644 --- a/src/Plugin/Mysql.php +++ b/src/Plugin/Mysql.php @@ -5,15 +5,20 @@ use Exception; use PDO; use PHPCensor\Builder; -use PHPCensor\Database; +use PHPCensor\Common\Exception\InvalidArgumentException; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Model\Build; use PHPCensor\Plugin; /** * MySQL Plugin - Provides access to a MySQL database. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer * @author Steve Kamerman + * @author Dmitry Khomutov */ class Mysql extends Plugin { @@ -83,19 +88,19 @@ public function __construct(Builder $builder, Build $build, array $options = []) } if (!empty($buildSettings['mysql']['host'])) { - $this->host = $this->builder->interpolate($buildSettings['mysql']['host']); + $this->host = $this->builder->interpolate($buildSettings['mysql']['host'], true); } if (!empty($buildSettings['mysql']['port'])) { - $this->port = (int)$this->builder->interpolate($buildSettings['mysql']['port']); + $this->port = (int)$this->builder->interpolate($buildSettings['mysql']['port'], true); } if (!empty($buildSettings['mysql']['dbname'])) { - $this->dbName = $this->builder->interpolate($buildSettings['mysql']['dbname']); + $this->dbName = $this->builder->interpolate($buildSettings['mysql']['dbname'], true); } if (!empty($buildSettings['mysql']['charset'])) { - $this->charset = $this->builder->interpolate($buildSettings['mysql']['charset']); + $this->charset = $this->builder->interpolate($buildSettings['mysql']['charset'], true); } if (!empty($buildSettings['mysql']['options']) && \is_array($buildSettings['mysql']['options'])) { @@ -103,16 +108,16 @@ public function __construct(Builder $builder, Build $build, array $options = []) } if (!empty($buildSettings['mysql']['user'])) { - $this->user = $this->builder->interpolate($buildSettings['mysql']['user']); + $this->user = $this->builder->interpolate($buildSettings['mysql']['user'], true); } if (\array_key_exists('password', $buildSettings['mysql'])) { - $this->password = $this->builder->interpolate($buildSettings['mysql']['password']); + $this->password = $this->builder->interpolate($buildSettings['mysql']['password'], true); } if (!empty($this->options['queries']) && \is_array($this->options['queries'])) { foreach ($this->options['queries'] as $query) { - $this->queries[] = $this->builder->interpolate($query); + $this->queries[] = $this->builder->interpolate($query, true); } } @@ -150,7 +155,7 @@ public function execute() foreach ($this->imports as $import) { $this->executeFile($import); } - } catch (Exception $ex) { + } catch (\Throwable $ex) { $this->builder->logFailure($ex->getMessage()); return false; @@ -167,19 +172,19 @@ public function execute() protected function executeFile(array $query) { if (!isset($query['file'])) { - throw new Exception('Import statement must contain a \'file\' key'); + throw new InvalidArgumentException('Import statement must contain a \'file\' key'); } - $importFile = $this->builder->buildPath . $this->builder->interpolate($query['file']); - if (!is_readable($importFile)) { - throw new Exception(sprintf('Cannot open SQL import file: %s', $importFile)); + $importFile = $this->builder->buildPath . $this->builder->interpolate($query['file'], true); + if (!\is_readable($importFile)) { + throw new RuntimeException(\sprintf('Cannot open SQL import file: %s', $importFile)); } - $database = isset($query['database']) ? $this->builder->interpolate($query['database']) : null; + $database = isset($query['database']) ? $this->builder->interpolate($query['database'], true) : null; $importCommand = $this->getImportCommand($importFile, $database); if (!$this->builder->executeCommand($importCommand)) { - throw new Exception('Unable to execute SQL file'); + throw new RuntimeException('Unable to execute SQL file'); } return true; @@ -200,21 +205,21 @@ protected function getImportCommand($importFile, $database = null) 'gz' => '| gzip --decompress', ]; - $extension = strtolower(pathinfo($importFile, PATHINFO_EXTENSION)); + $extension = \strtolower(\pathinfo($importFile, PATHINFO_EXTENSION)); $decompressionCmd = ''; - if (array_key_exists($extension, $decompression)) { + if (\array_key_exists($extension, $decompression)) { $decompressionCmd = $decompression[$extension]; } $args = [ - ':import_file' => escapeshellarg($importFile), + ':import_file' => \escapeshellarg($importFile), ':decomp_cmd' => $decompressionCmd, - ':host' => escapeshellarg($this->host), - ':user' => escapeshellarg($this->user), - ':pass' => (!$this->password) ? '' : '-p' . escapeshellarg($this->password), - ':database' => ($database === null) ? '' : escapeshellarg($database), + ':host' => \escapeshellarg($this->host), + ':user' => \escapeshellarg($this->user), + ':pass' => (!$this->password) ? '' : '-p' . \escapeshellarg($this->password), + ':database' => ($database === null) ? '' : \escapeshellarg($database), ]; - return strtr('cat :import_file :decomp_cmd | mysql -h:host -u:user :pass :database', $args); + return \strtr('cat :import_file :decomp_cmd | mysql -h:host -u:user :pass :database', $args); } } diff --git a/src/Plugin/Option/PhpUnitOptions.php b/src/Plugin/Option/PhpUnitOptions.php index 5de0312a8..4bbe70e42 100644 --- a/src/Plugin/Option/PhpUnitOptions.php +++ b/src/Plugin/Option/PhpUnitOptions.php @@ -2,39 +2,32 @@ namespace PHPCensor\Plugin\Option; -use PHPCensor\Builder; -use PHPCensor\Config; +use PHPCensor\Common\Application\ConfigurationInterface; /** * Class PhpUnitOptions validates and parse the option for the PhpUnitV2 plugin * + * @package PHP Censor + * @subpackage Application + * * @author Pablo Tejada + * @author Dmitry Khomutov */ class PhpUnitOptions { - /** - * @var array - */ - protected $options; + protected array $options; - /** - * @var string - */ - protected $location; + protected string $location; - /** - * @var array - */ - protected $arguments = []; + protected array $arguments = []; - /** - * @param array $options - * @param string $location - */ - public function __construct($options, $location) + protected ConfigurationInterface $configuration; + + public function __construct(ConfigurationInterface $configuration, array $options, string $location) { - $this->options = $options; - $this->location = $location; + $this->configuration = $configuration; + $this->options = $options; + $this->location = $location; } /** @@ -59,9 +52,9 @@ public function buildArgumentString() { $argumentString = ''; foreach ($this->getCommandArguments() as $argumentName => $argumentValues) { - $prefix = $argumentName[0] == '-' ? '' : '--'; + $prefix = $argumentName[0] === '-' ? '' : '--'; - if (!is_array($argumentValues)) { + if (!\is_array($argumentValues)) { $argumentValues = [$argumentValues]; } @@ -103,15 +96,15 @@ private function parseArguments() */ if (isset($this->options['args'])) { $rawArgs = $this->options['args']; - if (is_array($rawArgs)) { + if (\is_array($rawArgs)) { $this->arguments = $rawArgs; } else { /* * Try to parse old arguments in a single string */ - preg_match_all('@--([a-z\-]+)([\s=]+)?[\'"]?((?!--)[-\w/.,\\\]+)?[\'"]?@', (string)$rawArgs, $argsMatch); + \preg_match_all('@--([a-z\-]+)([\s=]+)?[\'"]?((?!--)[-\w/.,\\\]+)?[\'"]?@', (string)$rawArgs, $argsMatch); - if (!empty($argsMatch) && sizeof($argsMatch) > 2) { + if (!empty($argsMatch) && \sizeof($argsMatch) > 2) { foreach ($argsMatch[1] as $index => $argName) { $this->addArgument($argName, $argsMatch[3][$index]); } @@ -123,7 +116,7 @@ private function parseArguments() * Handles command aliases outside of the args option */ if (isset($this->options['coverage']) && $this->options['coverage']) { - $allowPublicArtifacts = (bool)Config::getInstance()->get( + $allowPublicArtifacts = (bool)$this->configuration->get( 'php-censor.build.allow_public_artifacts', false ); @@ -155,7 +148,7 @@ private function parseArguments() public function addArgument($argumentName, $argumentValue = null) { if (isset($this->arguments[$argumentName])) { - if (!is_array($this->arguments[$argumentName])) { + if (!\is_array($this->arguments[$argumentName])) { // Convert existing argument values into an array $this->arguments[$argumentName] = [$this->arguments[$argumentName]]; } @@ -177,15 +170,15 @@ public function getDirectories() { $directories = $this->getOption('directories'); - if (is_string($directories)) { + if (\is_string($directories)) { $directories = [$directories]; } else { - if (is_null($directories)) { + if (\is_null($directories)) { $directories = []; } } - return is_array($directories) ? $directories : [$directories]; + return \is_array($directories) ? $directories : [$directories]; } /** @@ -254,7 +247,7 @@ public function getArgument($argumentName) $this->parseArguments(); if (isset($this->arguments[$argumentName])) { - return is_array( + return \is_array( $this->arguments[$argumentName] ) ? $this->arguments[$argumentName] : [$this->arguments[$argumentName]]; } @@ -281,7 +274,7 @@ public static function findConfigFile($buildPath) ]; foreach ($files as $file) { - if (file_exists($buildPath . $file)) { + if (\file_exists($buildPath . $file)) { return $file; } } diff --git a/src/Plugin/PackageBuild.php b/src/Plugin/PackageBuild.php index 1b0b1652c..e69fc7ad2 100644 --- a/src/Plugin/PackageBuild.php +++ b/src/Plugin/PackageBuild.php @@ -9,7 +9,11 @@ /** * Create a ZIP or TAR.GZ archive of the entire build. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class PackageBuild extends Plugin { @@ -46,9 +50,9 @@ public function execute() return false; } - $filename = preg_replace('/([^a-zA-Z0-9_-]+)/', '', $this->filename); + $filename = \preg_replace('/([^a-zA-Z0-9_-]+)/', '', $this->filename); - if (!is_array($this->format)) { + if (!\is_array($this->format)) { $this->format = [$this->format]; } @@ -57,17 +61,19 @@ public function execute() switch ($format) { case 'tar': $cmd = 'tar cfz "%s/%s.tar.gz" ./*'; + break; default: case 'zip': $cmd = 'zip -rq "%s/%s.zip" ./*'; + break; } $success = $this->builder->executeCommand( $cmd, $this->directory, - $this->builder->interpolate($filename) + $this->builder->interpolate($filename, true) ); } diff --git a/src/Plugin/Pahout.php b/src/Plugin/Pahout.php index 24f38cb71..a997ff785 100644 --- a/src/Plugin/Pahout.php +++ b/src/Plugin/Pahout.php @@ -10,8 +10,12 @@ use PHPCensor\Plugin; /** - * A pair programming partner for writing better PHP. - * https://github.com/wata727/pahout + * A pair programming partner for writing better PHP. https://github.com/wata727/pahout + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class Pahout extends Plugin { @@ -33,13 +37,13 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->executable = $this->findBinary(['pahout', 'pahout.phar']); - if (!empty($options['directory']) && is_string($options['directory'])) { + if (!empty($options['directory']) && \is_string($options['directory'])) { $this->directory = $options['directory']; } else { $this->directory = './src'; } - if (isset($options['allowed_warnings']) && is_int($options['allowed_warnings'])) { + if (isset($options['allowed_warnings']) && \is_int($options['allowed_warnings'])) { $this->allowedWarnings = $options['allowed_warnings']; } else { $this->allowedWarnings = -1; @@ -69,8 +73,8 @@ public function execute() list($files, $hints) = $this->processReport($this->builder->getLastOutput()); - if (0 < count($hints)) { - if (-1 !== $this->allowedWarnings && count($hints) > $this->allowedWarnings) { + if (0 < \count($hints)) { + if (-1 !== $this->allowedWarnings && \count($hints) > $this->allowedWarnings) { $success = false; } @@ -83,7 +87,7 @@ public function execute() $hint['message'], BuildError::SEVERITY_LOW, $hint['file'], - $hint['line_from'] + (int)$hint['line_from'] ); } } @@ -92,7 +96,7 @@ public function execute() $this->builder->logSuccess('Awesome! There is nothing from me to teach you!'); } - $this->builder->log(sprintf('%d files checked, %d hints detected.', count($files), count($hints))); + $this->builder->log(\sprintf('%d files checked, %d hints detected.', \count($files), \count($hints))); return $success; } @@ -121,17 +125,17 @@ public static function canExecuteOnStage($stage, Build $build) */ protected function processReport($output) { - $data = json_decode(trim($output), true); + $data = \json_decode(\trim($output), true); $hints = []; $files = []; - if (!empty($data) && is_array($data) && isset($data['hints'])) { + if (!empty($data) && \is_array($data) && isset($data['hints'])) { $files = $data['files']; foreach ($data['hints'] as $hint) { $hints[] = [ - 'full_message' => vsprintf('%s:%d' . PHP_EOL . self::TAB . '%s: %s [%s]', [ + 'full_message' => \vsprintf('%s:%d' . PHP_EOL . self::TAB . '%s: %s [%s]', [ $hint['filename'], $hint['lineno'], $hint['type'], diff --git a/src/Plugin/Pdepend.php b/src/Plugin/Pdepend.php index 03b921990..74964c0b9 100644 --- a/src/Plugin/Pdepend.php +++ b/src/Plugin/Pdepend.php @@ -2,9 +2,8 @@ namespace PHPCensor\Plugin; -use Exception; use PHPCensor\Builder; -use PHPCensor\Config; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Model\Build; use PHPCensor\Plugin; use Symfony\Component\Filesystem\Filesystem; @@ -12,7 +11,11 @@ /** * Pdepend Plugin - Allows Pdepend report * + * @package PHP Censor + * @subpackage Application + * * @author Johan van der Heide + * @author Dmitry Khomutov */ class Pdepend extends Plugin { @@ -31,17 +34,17 @@ class Pdepend extends Plugin /** * @var string File where the summary.xml is stored */ - protected $summary; + protected $summary = 'summary.xml'; /** * @var string File where the chart.svg is stored */ - protected $chart; + protected $chart = 'chart.svg'; /** * @var string File where the pyramid.svg is stored */ - protected $pyramid; + protected $pyramid = 'pyramid.svg'; /** * @var string @@ -68,10 +71,6 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->summary = 'summary.xml'; - $this->pyramid = 'pyramid.svg'; - $this->chart = 'chart.svg'; - $this->executable = $this->findBinary(['pdepend', 'pdepend.phar']); $this->buildDirectory = $build->getBuildDirectory(); @@ -86,7 +85,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) */ public function execute() { - $allowPublicArtifacts = (bool)Config::getInstance()->get( + $allowPublicArtifacts = (bool)$this->builder->getConfiguration()->get( 'php-censor.build.allow_public_artifacts', false ); @@ -94,11 +93,11 @@ public function execute() $fileSystem = new Filesystem(); if (!$fileSystem->exists($this->buildLocation)) { - $fileSystem->mkdir($this->buildLocation, (0777 & ~umask())); + $fileSystem->mkdir($this->buildLocation, (0777 & ~\umask())); } - if (!is_writable($this->buildLocation)) { - throw new Exception(sprintf( + if (!\is_writable($this->buildLocation)) { + throw new RuntimeException(\sprintf( 'The location %s is not writable or does not exist.', $this->buildLocation )); @@ -108,8 +107,8 @@ public function execute() $cmd = 'cd "%s" && ' . $pdepend . ' --summary-xml="%s" --jdepend-chart="%s" --overview-pyramid="%s" %s "%s"'; $ignore = ''; - if (count($this->ignore)) { - $ignore = sprintf(' --ignore="%s"', implode(',', $this->ignore)); + if (\count($this->ignore)) { + $ignore = \sprintf(' --ignore="%s"', \implode(',', $this->ignore)); } $success = $this->builder->executeCommand( @@ -125,14 +124,14 @@ public function execute() if (!$allowPublicArtifacts) { $fileSystem->remove($this->buildLocation); } - if ($allowPublicArtifacts && file_exists($this->buildLocation)) { + if ($allowPublicArtifacts && \file_exists($this->buildLocation)) { $fileSystem->remove($this->buildBranchLocation); $fileSystem->mirror($this->buildLocation, $this->buildBranchLocation); } if ($allowPublicArtifacts && $success) { $this->builder->logSuccess( - sprintf( + \sprintf( "\nPdepend successful build report.\nYou can use report for this build for inclusion in the readme.md file:\n%s,\n![Chart](%s \"Pdepend Chart\") and\n![Pyramid](%s \"Pdepend Pyramid\")\n\nOr report for last build in the branch:\n%s,\n![Chart](%s \"Pdepend Chart\") and\n![Pyramid](%s \"Pdepend Pyramid\")\n", APP_URL . 'artifacts/pdepend/' . $this->buildDirectory . '/' . $this->summary, APP_URL . 'artifacts/pdepend/' . $this->buildDirectory . '/' . $this->chart, diff --git a/src/Plugin/Pgsql.php b/src/Plugin/Pgsql.php index fb7e02478..88a6f6e1c 100644 --- a/src/Plugin/Pgsql.php +++ b/src/Plugin/Pgsql.php @@ -11,7 +11,11 @@ /** * PgSQL Plugin - Provides access to a PgSQL database. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class Pgsql extends Plugin { @@ -68,15 +72,15 @@ public function __construct(Builder $builder, Build $build, array $options = []) $buildSettings = $this->builder->getConfig('build_settings'); if (!empty($buildSettings['pgsql']['host'])) { - $this->host = $this->builder->interpolate($buildSettings['pgsql']['host']); + $this->host = $this->builder->interpolate($buildSettings['pgsql']['host'], true); } if (!empty($buildSettings['pgsql']['port'])) { - $this->port = (int)$this->builder->interpolate($buildSettings['pgsql']['port']); + $this->port = (int)$this->builder->interpolate($buildSettings['pgsql']['port'], true); } if (!empty($buildSettings['pgsql']['dbname'])) { - $this->dbName = $this->builder->interpolate($buildSettings['pgsql']['dbname']); + $this->dbName = $this->builder->interpolate($buildSettings['pgsql']['dbname'], true); } if (!empty($buildSettings['pgsql']['options']) && \is_array($buildSettings['pgsql']['options'])) { @@ -84,16 +88,16 @@ public function __construct(Builder $builder, Build $build, array $options = []) } if (!empty($buildSettings['pgsql']['user'])) { - $this->user = $this->builder->interpolate($buildSettings['pgsql']['user']); + $this->user = $this->builder->interpolate($buildSettings['pgsql']['user'], true); } - if (array_key_exists('password', $buildSettings['pgsql'])) { - $this->password = $this->builder->interpolate($buildSettings['pgsql']['password']); + if (\array_key_exists('password', $buildSettings['pgsql'])) { + $this->password = $this->builder->interpolate($buildSettings['pgsql']['password'], true); } if (!empty($this->options['queries']) && \is_array($this->options['queries'])) { foreach ($this->options['queries'] as $query) { - $this->queries[] = $this->builder->interpolate($query); + $this->queries[] = $this->builder->interpolate($query, true); } } } @@ -105,10 +109,10 @@ public function __construct(Builder $builder, Build $build, array $options = []) public function execute() { try { - $pdoOptions = array_merge([ + $pdoOptions = \array_merge([ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ], $this->pdoOptions); - $dsn = sprintf('pgsql:host=%s;port=%s', $this->host, $this->port); + $dsn = \sprintf('pgsql:host=%s;port=%s', $this->host, $this->port); if (null !== $this->dbName) { $dsn .= ';dbname=' . $this->dbName; @@ -119,7 +123,7 @@ public function execute() foreach ($this->queries as $query) { $pdo->query($query); } - } catch (Exception $ex) { + } catch (\Throwable $ex) { $this->builder->logFailure($ex->getMessage()); return false; diff --git a/src/Plugin/Phan.php b/src/Plugin/Phan.php index 2755f292a..f3eeefe9c 100644 --- a/src/Plugin/Phan.php +++ b/src/Plugin/Phan.php @@ -4,12 +4,18 @@ use Exception; use PHPCensor\Builder; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Model\Build; use PHPCensor\Model\BuildError; use PHPCensor\Plugin; /** * Launch Phan. + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class Phan extends Plugin { @@ -50,12 +56,12 @@ public function __construct(Builder $builder, Build $build, array $options = []) */ public function execute() { - if (!file_exists($this->location)) { - mkdir($this->location, (0777 & ~umask()), true); + if (!\file_exists($this->location)) { + \mkdir($this->location, (0777 & ~\umask()), true); } - if (!is_writable($this->location)) { - throw new Exception(sprintf('The location %s is not writable or does not exist.', $this->location)); + if (!\is_writable($this->location)) { + throw new RuntimeException(\sprintf('The location %s is not writable or does not exist.', $this->location)); } // Find PHP files in a file @@ -76,7 +82,7 @@ public function execute() $this->builder->executeCommand($cmd, $this->location . '/phan.in', $this->location . '/phan.out'); - $warningCount = $this->processReport(file_get_contents($this->location . '/phan.out')); + $warningCount = $this->processReport(\file_get_contents($this->location . '/phan.out')); $this->build->storeMeta((self::pluginName() . '-warnings'), $warningCount); @@ -100,12 +106,12 @@ public function execute() */ protected function processReport($jsonString) { - $json = json_decode($jsonString, true); + $json = \json_decode($jsonString, true); - if ($json === false || !is_array($json)) { + if ($json === false || !\is_array($json)) { $this->builder->log($jsonString); - throw new Exception('Could not process the report generated by Phan.'); + throw new RuntimeException('Could not process the report generated by Phan.'); } $warnings = 0; @@ -117,8 +123,8 @@ protected function processReport($jsonString) $data['check_name']."\n\n".$data['description'], $this->severity($data['severity']), isset($data['location']['path']) ? $data['location']['path'] : '??', - isset($data['location']['lines']['begin']) ? $data['location']['lines']['begin'] : '??', - isset($data['location']['lines']['end']) ? $data['location']['lines']['end'] : '??' + isset($data['location']['lines']['begin']) ? (int)$data['location']['lines']['begin'] : null, + isset($data['location']['lines']['end']) ? (int)$data['location']['lines']['end'] : null ); $warnings++; @@ -136,11 +142,11 @@ protected function processReport($jsonString) */ protected function severity($severity) { - if ($severity == 10) { + if ($severity === 10) { return BuildError::SEVERITY_CRITICAL; } - if ($severity == 5) { + if ($severity === 5) { return BuildError::SEVERITY_NORMAL; } diff --git a/src/Plugin/Phar.php b/src/Plugin/Phar.php index 8c8d4ffc9..de79d390b 100644 --- a/src/Plugin/Phar.php +++ b/src/Plugin/Phar.php @@ -10,6 +10,11 @@ /** * Phar Plugin + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class Phar extends Plugin { @@ -148,7 +153,7 @@ public function getStubContent() $content = ''; $filename = $this->getStub(); if ($filename) { - $content = file_get_contents($this->builder->buildPath . $this->getStub()); + $content = \file_get_contents($this->builder->buildPath . $this->getStub()); } return $content; @@ -172,7 +177,7 @@ public function execute() } $success = true; - } catch (Exception $e) { + } catch (\Throwable $e) { $this->builder->logFailure('Phar Plugin Internal Error. Exception: ' . $e->getMessage()); } diff --git a/src/Plugin/Phing.php b/src/Plugin/Phing.php index f8325b83d..231dadbe4 100644 --- a/src/Plugin/Phing.php +++ b/src/Plugin/Phing.php @@ -4,13 +4,18 @@ use Exception; use PHPCensor\Builder; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Model\Build; use PHPCensor\Plugin; /** * Phing Plugin - Provides access to Phing functionality. * + * @package PHP Censor + * @subpackage Application + * * @author Pavel Pavlov + * @author Dmitry Khomutov */ class Phing extends Plugin { @@ -75,7 +80,7 @@ public function execute() $cmd[] = $this->targetsToString(); $cmd[] = '2>&1'; - return $this->builder->executeCommand(implode(' ', $cmd), $this->directory, $this->targets); + return $this->builder->executeCommand(\implode(' ', $cmd), $this->directory, $this->targets); } /** @@ -92,7 +97,7 @@ public function getTargets() */ private function targetsToString() { - return implode(' ', $this->targets); + return \implode(' ', $this->targets); } /** @@ -102,7 +107,7 @@ private function targetsToString() */ public function setTargets($targets) { - if (is_string($targets)) { + if (\is_string($targets)) { $targets = [$targets]; } @@ -125,8 +130,8 @@ public function getBuildFile() */ public function setBuildFile($buildFile) { - if (!file_exists($this->directory . $buildFile)) { - throw new Exception('Specified build file does not exist.'); + if (!\file_exists($this->directory . $buildFile)) { + throw new RuntimeException('Specified build file does not exist.'); } $this->buildFile = $buildFile; @@ -168,7 +173,7 @@ public function propertiesToString() $propertiesString[] = '-D' . $name . '="' . $value . '"'; } - return implode(' ', $propertiesString); + return \implode(' ', $propertiesString); } /** @@ -178,7 +183,7 @@ public function propertiesToString() */ public function setProperties($properties) { - if (is_string($properties)) { + if (\is_string($properties)) { $properties = [$properties]; } @@ -201,8 +206,8 @@ public function getPropertyFile() */ public function setPropertyFile($propertyFile) { - if (!file_exists($this->directory . '/' . $propertyFile)) { - throw new Exception('Specified property file does not exist.'); + if (!\file_exists($this->directory . '/' . $propertyFile)) { + throw new RuntimeException('Specified property file does not exist.'); } $this->propertyFile = $propertyFile; diff --git a/src/Plugin/Phlint.php b/src/Plugin/Phlint.php index f2c105c75..91501e8a0 100644 --- a/src/Plugin/Phlint.php +++ b/src/Plugin/Phlint.php @@ -14,6 +14,11 @@ * issues. It focuses on how the code works rather than how the code looks. Phlint is designed from the start to do * deep semantic analysis rather than doing only shallow or stylistic analysis. * https://gitlab.com/phlint/phlint + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class Phlint extends Plugin { @@ -29,7 +34,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->executable = $this->findBinary(['phlint', 'phlint.phar']); - if (array_key_exists('allowed_errors', $options) && is_int($options['allowed_errors'])) { + if (\array_key_exists('allowed_errors', $options) && \is_int($options['allowed_errors'])) { $this->allowedErrors = $options['allowed_errors']; } } @@ -50,8 +55,8 @@ public function execute() $errors = $this->processReport($this->builder->getLastOutput()); - if (0 < count($errors)) { - if (-1 !== $this->allowedErrors && count($errors) > $this->allowedErrors) { + if (0 < \count($errors)) { + if (-1 !== $this->allowedErrors && \count($errors) > $this->allowedErrors) { $success = false; } @@ -62,7 +67,7 @@ public function execute() $error['message'], BuildError::SEVERITY_HIGH, $error['file'], - $error['line_from'] + (int)$error['line_from'] ); } } @@ -84,19 +89,19 @@ public static function pluginName() */ protected function processReport($output) { - $data = explode(chr(226), preg_replace('#\\x1b[[][^A-Za-z\n]*[A-Za-z]#', '', trim($output))); - array_pop($data); - array_shift($data); + $data = \explode(\chr(226), \preg_replace('#\\x1b[[][^A-Za-z\n]*[A-Za-z]#', '', \trim($output))); + \array_pop($data); + \array_shift($data); $errors = []; - if (0 < count($data)) { + if (0 < \count($data)) { foreach ($data as $error) { - $error = explode(PHP_EOL, $error); - $header = substr(trim(array_shift($error)), 3); - $file = strstr(substr(strstr($header, 'in '), 3), ':', true); - $line = substr(strrchr($header, ':'), 1); - $message = ltrim($error[0]) . PHP_EOL . ltrim($error[1]); + $error = \explode(PHP_EOL, $error); + $header = \substr(\trim(\array_shift($error)), 3); + $file = \strstr(\substr(\strstr($header, 'in '), 3), ':', true); + $line = \substr(\strrchr($header, ':'), 1); + $message = \ltrim($error[0]) . PHP_EOL . \ltrim($error[1]); $errors[] = [ 'message' => $message, diff --git a/src/Plugin/PhpCodeSniffer.php b/src/Plugin/PhpCodeSniffer.php index a18f4f588..082e221c3 100644 --- a/src/Plugin/PhpCodeSniffer.php +++ b/src/Plugin/PhpCodeSniffer.php @@ -9,43 +9,48 @@ use PHPCensor\Model\BuildError; use PHPCensor\Plugin; use PHPCensor\ZeroConfigPluginInterface; +use PHPCensor\Common\Exception\RuntimeException; /** * PHP Code Sniffer Plugin - Allows PHP Code Sniffer testing. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class PhpCodeSniffer extends Plugin implements ZeroConfigPluginInterface { /** * @var array */ - protected $suffixes; + protected $suffixes = ['php']; /** * @var string */ - protected $standard; + protected $standard = 'PSR2'; /** * @var string */ - protected $tabWidth; + protected $tabWidth = ''; /** * @var string */ - protected $encoding; + protected $encoding = ''; /** * @var int */ - protected $allowedErrors; + protected $allowedErrors = 0; /** * @var int */ - protected $allowedWarnings; + protected $allowedWarnings = 0; /** * @var int @@ -77,13 +82,6 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->suffixes = ['php']; - $this->standard = 'PSR2'; - $this->tabWidth = ''; - $this->encoding = ''; - $this->allowedWarnings = 0; - $this->allowedErrors = 0; - $this->executable = $this->findBinary(['phpcs', 'phpcs.phar']); if (isset($options['zero_config']) && $options['zero_config']) { @@ -91,11 +89,11 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->allowedErrors = -1; } - if (!empty($options['allowed_errors']) && is_int($options['allowed_errors'])) { + if (!empty($options['allowed_errors']) && \is_int($options['allowed_errors'])) { $this->allowedErrors = $options['allowed_errors']; } - if (!empty($options['allowed_warnings']) && is_int($options['allowed_warnings'])) { + if (!empty($options['allowed_warnings']) && \is_int($options['allowed_warnings'])) { $this->allowedWarnings = $options['allowed_warnings']; } @@ -115,15 +113,15 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->standard = $options['standard']; } - if (isset($options['severity']) && is_int($options['severity'])) { + if (isset($options['severity']) && \is_int($options['severity'])) { $this->severity = $options['severity']; } - if (isset($options['error_severity']) && is_int($options['error_severity'])) { + if (isset($options['error_severity']) && \is_int($options['error_severity'])) { $this->errorSeverity = $options['error_severity']; } - if (isset($options['warning_severity']) && is_int($options['warning_severity'])) { + if (isset($options['warning_severity']) && \is_int($options['warning_severity'])) { $this->warningSeverity = $options['warning_severity']; } } @@ -177,11 +175,11 @@ public function execute() $this->build->storeMeta((self::pluginName() . '-warnings'), $warnings); $this->build->storeMeta((self::pluginName() . '-errors'), $errors); - if (-1 != $this->allowedWarnings && $warnings > $this->allowedWarnings) { + if (-1 !== $this->allowedWarnings && $warnings > $this->allowedWarnings) { $success = false; } - if (-1 != $this->allowedErrors && $errors > $this->allowedErrors) { + if (-1 !== $this->allowedErrors && $errors > $this->allowedErrors) { $success = false; } @@ -195,20 +193,20 @@ public function execute() protected function getFlags() { $ignore = ''; - if (count($this->ignore)) { - $ignore = sprintf(' --ignore="%s"', implode(',', $this->ignore)); + if (\count($this->ignore)) { + $ignore = \sprintf(' --ignore="%s"', \implode(',', $this->ignore)); } $standardPath = $this->normalizePath($this->standard); - if (file_exists($standardPath)) { + if (\file_exists($standardPath)) { $standard = ' --standard=' . $standardPath; } else { $standard = ' --standard=' . $this->standard; } $suffixes = ''; - if (count($this->suffixes)) { - $suffixes = ' --extensions=' . implode(',', $this->suffixes); + if (\count($this->suffixes)) { + $suffixes = ' --extensions=' . \implode(',', $this->suffixes); } $severity = ''; @@ -236,28 +234,28 @@ protected function getFlags() */ protected function processReport($output) { - $data = json_decode(trim($output), true); + $data = \json_decode(\trim($output), true); - if (!is_array($data)) { + if (!\is_array($data)) { $this->builder->log($output); - throw new Exception('Could not process the report generated by PHP Code Sniffer.'); + throw new RuntimeException('Could not process the report generated by PHP Code Sniffer.'); } $errors = $data['totals']['errors']; $warnings = $data['totals']['warnings']; foreach ($data['files'] as $fileName => $file) { - $fileName = str_replace($this->builder->buildPath, '', $fileName); + $fileName = \str_replace($this->builder->buildPath, '', $fileName); foreach ($file['messages'] as $message) { $this->build->reportError( $this->builder, self::pluginName(), 'PHPCS: ' . $message['message'], - 'ERROR' == $message['type'] ? BuildError::SEVERITY_HIGH : BuildError::SEVERITY_LOW, + 'ERROR' === $message['type'] ? BuildError::SEVERITY_HIGH : BuildError::SEVERITY_LOW, $fileName, - $message['line'] + (int)$message['line'] ); } } diff --git a/src/Plugin/PhpCpd.php b/src/Plugin/PhpCpd.php index e03dc5db2..8ea652081 100644 --- a/src/Plugin/PhpCpd.php +++ b/src/Plugin/PhpCpd.php @@ -4,6 +4,7 @@ use Exception; use PHPCensor\Builder; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Model\Build; use PHPCensor\Model\BuildError; use PHPCensor\Plugin; @@ -12,7 +13,11 @@ /** * PHP Copy / Paste Detector - Allows PHP Copy / Paste Detector testing. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class PhpCpd extends Plugin implements ZeroConfigPluginInterface { @@ -104,19 +109,19 @@ public function execute() */ protected function processReport($xmlString) { - $xml = simplexml_load_string($xmlString); + $xml = \simplexml_load_string($xmlString); if (false === $xml) { $this->builder->log($xmlString); - throw new Exception('Could not process the report generated by PHPCpd.'); + throw new RuntimeException('Could not process the report generated by PHPCpd.'); } $warnings = 0; foreach ($xml->duplication as $duplication) { foreach ($duplication->file as $file) { $fileName = (string)$file['path']; - $fileName = str_replace($this->builder->buildPath, '', $fileName); + $fileName = \str_replace($this->builder->buildPath, '', $fileName); $message = << + * @author Dmitry Khomutov */ class PhpCsFixer extends Plugin { @@ -64,7 +69,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) if (isset($options['config']) && $options['config']) { $this->config = true; - $this->args .= ' --config=' . $builder->interpolate($options['config']); + $this->args .= ' --config=' . $builder->interpolate($options['config'], true); } if (isset($options['errors']) && $options['errors']) { @@ -139,7 +144,7 @@ public function execute() $this->build->storeMeta((self::pluginName() . '-warnings'), $warningCount); - if (-1 != $this->allowedWarnings && $warningCount > $this->allowedWarnings) { + if (-1 !== $this->allowedWarnings && $warningCount > $this->allowedWarnings) { $success = false; } } @@ -158,12 +163,12 @@ public function execute() */ protected function processReport($output) { - $data = json_decode(trim($output), true); + $data = \json_decode(\trim($output), true); - if (!is_array($data)) { + if (!\is_array($data)) { $this->builder->log($output); - throw new Exception('Could not process the report generated by PHP CS Fixer.'); + throw new RuntimeException('Could not process the report generated by PHP CS Fixer.'); } $warnings = 0; @@ -190,12 +195,15 @@ protected function processReport($output) switch ($line->getType()) { case Line::ADDED: $symbol = '+'; + break; case Line::REMOVED: $symbol = '-'; + break; default: $symbol = ' '; + break; } $chunkDiff[] = $symbol . $line->getContent(); @@ -204,6 +212,7 @@ protected function processReport($output) } if (Line::UNCHANGED === $line->getType()) { ++$firstModifiedLine; + continue; } @@ -216,7 +225,7 @@ protected function processReport($output) $this->build->reportError( $this->builder, self::pluginName(), - "PHP CS Fixer suggestion:\r\n```diff\r\n" . implode("\r\n", $chunkDiff) . "\r\n```", + "PHP CS Fixer suggestion:\r\n```diff\r\n" . \implode("\r\n", $chunkDiff) . "\r\n```", BuildError::SEVERITY_LOW, $filename, $firstModifiedLine @@ -228,7 +237,7 @@ protected function processReport($output) $this->build->reportError( $this->builder, self::pluginName(), - 'PHP CS Fixer failed fixers: ' . implode(', ', $appliedFixers), + 'PHP CS Fixer failed fixers: ' . \implode(', ', $appliedFixers), BuildError::SEVERITY_LOW, $filename ); diff --git a/src/Plugin/PhpDocblockChecker.php b/src/Plugin/PhpDocblockChecker.php index 4e828b422..516a75d2c 100644 --- a/src/Plugin/PhpDocblockChecker.php +++ b/src/Plugin/PhpDocblockChecker.php @@ -12,7 +12,11 @@ /** * PHP Docblock Checker Plugin - Checks your PHP files for appropriate uses of Docblocks * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class PhpDocblockChecker extends Plugin implements ZeroConfigPluginInterface { @@ -22,7 +26,7 @@ class PhpDocblockChecker extends Plugin implements ZeroConfigPluginInterface /** * @var int */ - protected $allowedWarnings; + protected $allowedWarnings = 0; /** * @return string @@ -39,25 +43,23 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->allowedWarnings = 0; - if (isset($options['zero_config']) && $options['zero_config']) { $this->allowedWarnings = -1; } - if (array_key_exists('skip_classes', $options)) { + if (\array_key_exists('skip_classes', $options)) { $this->skipClasses = true; } - if (array_key_exists('skip_methods', $options)) { + if (\array_key_exists('skip_methods', $options)) { $this->skipMethods = true; } - if (array_key_exists('skip_signatures', $options)) { + if (\array_key_exists('skip_signatures', $options)) { $this->skipSignatures = true; } - if (array_key_exists('allowed_warnings', $options)) { + if (\array_key_exists('allowed_warnings', $options)) { $this->allowedWarnings = (int)$options['allowed_warnings']; } @@ -89,8 +91,8 @@ public function execute() // Build ignore string: $ignore = ''; - if (is_array($this->ignore)) { - $ignore = sprintf(' --exclude="%s"', implode(',', $this->ignore)); + if (\is_array($this->ignore)) { + $ignore = \sprintf(' --exclude="%s"', \implode(',', $this->ignore)); } // Are we skipping any checks? @@ -122,11 +124,11 @@ public function execute() ); $this->builder->logExecOutput(true); - $output = json_decode($this->builder->getLastOutput(), true); + $output = \json_decode($this->builder->getLastOutput(), true); $errors = 0; - if ($output && is_array($output)) { - $errors = count($output); + if ($output && \is_array($output)) { + $errors = \count($output); $this->builder->logWarning("Number of error : " . $errors); $this->reportErrors($output); @@ -135,7 +137,7 @@ public function execute() $success = true; - if (-1 != $this->allowedWarnings && $errors > $this->allowedWarnings) { + if (-1 !== $this->allowedWarnings && $errors > $this->allowedWarnings) { $success = false; } @@ -152,38 +154,45 @@ protected function reportErrors(array $output) case 'class': $message = 'Class ' . $error['class'] . ' is missing a docblock.'; $severity = BuildError::SEVERITY_NORMAL; + break; case 'method': $message = 'Method ' . $error['class'] . '::' . $error['method'] . ' is missing a docblock.'; $severity = BuildError::SEVERITY_NORMAL; + break; case 'param-missing': $message = $error['class'] . '::' . $error['method'] . ' @param ' . $error['param'] . ' missing.'; $severity = BuildError::SEVERITY_LOW; + break; case 'param-mismatch': $message = $error['class'] . '::' . $error['method'] . ' @param ' . $error['param'] . '(' . $error['doc-type'] . ') does not match method signature (' . $error['param-type'] . ')'; $severity = BuildError::SEVERITY_LOW; + break; case 'return-missing': $message = $error['class'] . '::' . $error['method'] . ' @return missing.'; $severity = BuildError::SEVERITY_LOW; + break; case 'return-mismatch': $message = $error['class'] . '::' . $error['method'] . ' @return ' . $error['doc-type'] . ' does not match method signature (' . $error['return-type'] . ')'; $severity = BuildError::SEVERITY_LOW; + break; default: $message = 'Class ' . $error['class'] . ' invalid/missing a docblock.'; $severity = BuildError::SEVERITY_LOW; + break; } diff --git a/src/Plugin/PhpLoc.php b/src/Plugin/PhpLoc.php index 07af92691..0529cc3bf 100644 --- a/src/Plugin/PhpLoc.php +++ b/src/Plugin/PhpLoc.php @@ -5,13 +5,18 @@ use PHPCensor; use PHPCensor\Builder; use PHPCensor\Model\Build; +use PHPCensor\Model\User; use PHPCensor\Plugin; use PHPCensor\ZeroConfigPluginInterface; /** * PHP Loc - Allows PHP Copy / Lines of Code testing. * + * @package PHP Censor + * @subpackage Application + * * @author Johan van der Heide + * @author Dmitry Khomutov */ class PhpLoc extends Plugin implements ZeroConfigPluginInterface { @@ -51,21 +56,19 @@ public function __construct(Builder $builder, Build $build, array $options = []) public function execute() { $ignore = ''; - if (is_array($this->ignore)) { - $map = function ($item) { - return sprintf(' --exclude="%s"', $item); - }; + if (\is_array($this->ignore)) { + $map = fn ($item) => \sprintf(' --exclude="%s"', $item); - $ignore = array_map($map, $this->ignore); - $ignore = implode('', $ignore); + $ignore = \array_map($map, $this->ignore); + $ignore = \implode('', $ignore); } $phploc = $this->executable; - $success = $this->builder->executeCommand('cd "%s" && php -d xdebug.mode=0 -d error_reporting=0 ' . $phploc . ' %s %s', $this->builder->buildPath, $ignore, $this->directory); + $success = $this->builder->executeCommand('cd "%s" && php -d xdebug.mode=off -d error_reporting=0 ' . $phploc . ' %s %s', $this->builder->buildPath, $ignore, $this->directory); $output = $this->builder->getLastOutput(); - if (preg_match_all('/\((LOC|CLOC|NCLOC|LLOC)\)\s+([0-9]+)/', $output, $matches)) { + if (\preg_match_all('/\((LOC|CLOC|NCLOC|LLOC)\)\s+([0-9]+)/', $output, $matches)) { $data = []; foreach ($matches[1] as $k => $v) { $data[$v] = (int)$matches[2][$k]; diff --git a/src/Plugin/PhpMessDetector.php b/src/Plugin/PhpMessDetector.php index 5e7269499..afa99af93 100644 --- a/src/Plugin/PhpMessDetector.php +++ b/src/Plugin/PhpMessDetector.php @@ -8,26 +8,31 @@ use PHPCensor\Model\Build; use PHPCensor\Plugin; use PHPCensor\ZeroConfigPluginInterface; +use PHPCensor\Common\Exception\RuntimeException; /** * PHP Mess Detector Plugin - Allows PHP Mess Detector testing. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class PhpMessDetector extends Plugin implements ZeroConfigPluginInterface { /** * @var array */ - protected $suffixes; + protected $suffixes = ['php']; /** * Array of PHPMD rules. Can be one of the builtins (codesize, unusedcode, naming, design, controversial) * or a filename (detected by checking for a / in it), either absolute or relative to the project root. * @var array */ - protected $rules; - protected $allowedWarnings; + protected $rules = ['codesize', 'unusedcode', 'naming']; + protected $allowedWarnings = 0; /** * @return string @@ -44,15 +49,11 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->suffixes = ['php']; - $this->rules = ['codesize', 'unusedcode', 'naming']; - $this->allowedWarnings = 0; - if (isset($options['zero_config']) && $options['zero_config']) { $this->allowedWarnings = -1; } - if (array_key_exists('allowed_warnings', $options)) { + if (\array_key_exists('allowed_warnings', $options)) { $this->allowedWarnings = (int)$options['allowed_warnings']; } @@ -88,7 +89,7 @@ public function execute() $this->executePhpMd($phpmdBinaryPath); - $errorCount = $this->processReport(trim($this->builder->getLastOutput())); + $errorCount = $this->processReport(\trim($this->builder->getLastOutput())); $this->build->storeMeta((self::pluginName() . '-warnings'), $errorCount); return $this->wasLastExecSuccessful($errorCount); @@ -99,7 +100,7 @@ public function execute() */ protected function overrideSetting($options, $key) { - if (isset($options[$key]) && is_array($options[$key])) { + if (isset($options[$key]) && \is_array($options[$key])) { $this->{$key} = $options[$key]; } } @@ -114,19 +115,19 @@ protected function overrideSetting($options, $key) */ protected function processReport($xmlString) { - $xml = simplexml_load_string($xmlString); + $xml = \simplexml_load_string($xmlString); if (false === $xml) { $this->builder->log($xmlString); - throw new Exception('Could not process PHPMD report XML.'); + throw new RuntimeException('Could not process PHPMD report XML.'); } $warnings = 0; foreach ($xml->file as $file) { $fileName = (string)$file['name']; - $fileName = str_replace($this->builder->buildPath, '', $fileName); + $fileName = \str_replace($this->builder->buildPath, '', $fileName); foreach ($file->violation as $violation) { $warnings++; @@ -152,14 +153,14 @@ protected function processReport($xmlString) */ protected function tryAndProcessRules() { - if (!empty($this->rules) && !is_array($this->rules)) { + if (!empty($this->rules) && !\is_array($this->rules)) { $this->builder->logFailure('The "rules" option must be an array.'); return false; } foreach ($this->rules as &$rule) { - if (strpos($rule, '/') !== false) { + if (\strpos($rule, '/') !== false) { $rule = $this->builder->buildPath . $rule; } } @@ -175,18 +176,18 @@ protected function executePhpMd($binaryPath) $cmd = 'cd "%s" && ' . $binaryPath . ' "%s" xml %s %s %s'; $ignore = ''; - if (is_array($this->ignore) && count($this->ignore) > 0) { + if (\is_array($this->ignore) && \count($this->ignore) > 0) { $ignoreArray = []; foreach ($this->ignore as $ignoreItem) { $ignoreArray[] = /*$this->builder->buildPath .*/ $ignoreItem; } - $ignore = sprintf(' --exclude "%s"', implode(',', $ignoreArray)); + $ignore = \sprintf(' --exclude "%s"', \implode(',', $ignoreArray)); } $suffixes = ''; - if (is_array($this->suffixes) && count($this->suffixes) > 0) { - $suffixes = ' --suffixes ' . implode(',', $this->suffixes); + if (\is_array($this->suffixes) && \count($this->suffixes) > 0) { + $suffixes = ' --suffixes ' . \implode(',', $this->suffixes); } if (!$this->build->isDebug()) { @@ -198,7 +199,7 @@ protected function executePhpMd($binaryPath) $cmd, $this->builder->buildPath, $this->directory, - implode(',', $this->rules), + \implode(',', $this->rules), $ignore, $suffixes ); @@ -215,14 +216,10 @@ protected function executePhpMd($binaryPath) */ protected function wasLastExecSuccessful($errorCount) { - $success = true; - - if (-1 != $this->allowedWarnings && $errorCount > $this->allowedWarnings) { - $success = false; - - return $success; + if (-1 !== $this->allowedWarnings && $errorCount > $this->allowedWarnings) { + return false; } - return $success; + return true; } } diff --git a/src/Plugin/PhpParallelLint.php b/src/Plugin/PhpParallelLint.php index 3745e5df5..4ec480d77 100644 --- a/src/Plugin/PhpParallelLint.php +++ b/src/Plugin/PhpParallelLint.php @@ -10,19 +10,23 @@ /** * Php Parallel Lint Plugin - Provides access to PHP lint functionality. * + * @package PHP Censor + * @subpackage Application + * * @author Vaclav Makes + * @author Dmitry Khomutov */ class PhpParallelLint extends Plugin implements ZeroConfigPluginInterface { /** * @var string - comma separated list of file extensions */ - protected $extensions; + protected $extensions = 'php'; /** * @var bool - enable short tags */ - protected $shortTag; + protected $shortTag = false; /** * @return string @@ -43,9 +47,6 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->extensions = 'php'; - $this->shortTag = false; - $this->executable = $this->findBinary(['parallel-lint', 'parallel-lint.phar']); if (isset($options['shorttags'])) { @@ -56,8 +57,8 @@ public function __construct(Builder $builder, Build $build, array $options = []) // Only use if this is a comma delimited list $pattern = '/^([a-z]+)(,\ *[a-z]*)*$/'; - if (preg_match($pattern, $options['extensions'])) { - $this->extensions = str_replace(' ', '', $options['extensions']); + if (\preg_match($pattern, $options['extensions'])) { + $this->extensions = \str_replace(' ', '', $options['extensions']); } } } @@ -95,8 +96,8 @@ public function execute() $output = $this->builder->getLastOutput(); $matches = []; - if (preg_match_all('/Parse error\:/', $output, $matches)) { - $this->build->storeMeta((self::pluginName() . '-errors'), count($matches[0])); + if (\preg_match_all('/Parse error\:/', $output, $matches)) { + $this->build->storeMeta((self::pluginName() . '-errors'), \count($matches[0])); } return $success; @@ -110,10 +111,10 @@ protected function getFlags() { $ignoreFlags = []; foreach ($this->ignore as $ignoreDir) { - $ignoreFlags[] = sprintf(' --exclude "%s"', $this->builder->buildPath . $ignoreDir); + $ignoreFlags[] = \sprintf(' --exclude "%s"', $this->builder->buildPath . $ignoreDir); } - $ignore = implode(' ', $ignoreFlags); + $ignore = \implode(' ', $ignoreFlags); return [$ignore]; } diff --git a/src/Plugin/PhpSpec.php b/src/Plugin/PhpSpec.php index 84a245cad..c285dcdbe 100644 --- a/src/Plugin/PhpSpec.php +++ b/src/Plugin/PhpSpec.php @@ -10,7 +10,11 @@ /** * PHP Spec Plugin - Allows PHP Spec testing. * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer + * @author Dmitry Khomutov */ class PhpSpec extends Plugin { @@ -88,7 +92,7 @@ public function execute() 'status' => (string)$attr['status'], ]; - if ('failed' == $case['status']) { + if ('failed' === $case['status']) { $error = []; /* * ok, sad, we had an error diff --git a/src/Plugin/PhpStan.php b/src/Plugin/PhpStan.php index 79091bcb0..270efe20e 100644 --- a/src/Plugin/PhpStan.php +++ b/src/Plugin/PhpStan.php @@ -14,6 +14,11 @@ * before you write tests for the code. It moves PHP closer to compiled languages in the sense that the correctness of * each line of the code can be checked before you run the actual line. * https://github.com/phpstan/phpstan + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class PhpStan extends Plugin { @@ -62,7 +67,7 @@ public function execute() $this->builder->executeCommand( 'cd "%s" && ' . $phpStan . ' analyze --error-format=json %s', $this->builder->buildPath, - implode(' ', $this->directories) + \implode(' ', $this->directories) ); $this->builder->logExecOutput(true); @@ -78,18 +83,18 @@ public function execute() foreach ($files as $file => $payload) { if (0 < $payload['errors']) { - $file = str_replace($this->build->getBuildPath(), '', $file); - $len = strlen($file); + $file = \str_replace($this->build->getBuildPath(), '', $file); + $len = \strlen($file); $out = ''; - $filename = (false !== strpos($file, ' (')) ? strstr($file, ' (', true) : $file; + $filename = (false !== \strpos($file, ' (')) ? \strstr($file, ' (', true) : $file; foreach ($payload['messages'] as $message) { - if (strlen($message['message']) > $len) { - $len = strlen($message['message']); + if (\strlen($message['message']) > $len) { + $len = \strlen($message['message']); } - $out .= vsprintf(' %d%s %s' . PHP_EOL, [ + $out .= \vsprintf(' %d%s %s' . PHP_EOL, [ $message['line'], - str_repeat(' ', 6 - strlen($message['line'])), + \str_repeat(' ', 6 - \strlen($message['line'])), $message['message'] ]); @@ -99,12 +104,12 @@ public function execute() $message['message'], BuildError::SEVERITY_NORMAL, $filename, - $message['line'] + (int)$message['line'] ); } - $separator = str_repeat('-', 6) . ' ' . str_repeat('-', $len + 2) . PHP_EOL; + $separator = \str_repeat('-', 6) . ' ' . \str_repeat('-', $len + 2) . PHP_EOL; - $this->builder->logFailure(vsprintf('%s Line %s' . PHP_EOL . '%s', [ + $this->builder->logFailure(\vsprintf('%s Line %s' . PHP_EOL . '%s', [ $separator, $file, $separator . $out . $separator @@ -116,7 +121,7 @@ public function execute() if ($success) { $this->builder->logSuccess('[OK] No errors'); } else { - $this->builder->log(sprintf('[ERROR] Found %d errors', $total_errors)); + $this->builder->log(\sprintf('[ERROR] Found %d errors', $total_errors)); } return $success; @@ -146,12 +151,12 @@ public static function canExecuteOnStage($stage, Build $build) */ protected function processReport($output) { - $data = json_decode(trim($output), true); + $data = \json_decode(\trim($output), true); $totalErrors = 0; $files = []; - if (!empty($data) && is_array($data) && (0 < $data['totals']['file_errors'])) { + if (!empty($data) && \is_array($data) && (0 < $data['totals']['file_errors'])) { $totalErrors = $data['totals']['file_errors']; $files = $data['files']; } diff --git a/src/Plugin/PhpTalLint.php b/src/Plugin/PhpTalLint.php index 7d2d43a9e..61e549fdd 100644 --- a/src/Plugin/PhpTalLint.php +++ b/src/Plugin/PhpTalLint.php @@ -11,7 +11,11 @@ /** * PHPTAL Lint Plugin - Provides access to PHPTAL lint functionality. * + * @package PHP Censor + * @subpackage Application + * * @author Stephen Ball + * @author Dmitry Khomutov */ class PhpTalLint extends Plugin { @@ -49,11 +53,11 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - if (!empty($options['allowed_errors']) && is_int($options['allowed_errors'])) { + if (!empty($options['allowed_errors']) && \is_int($options['allowed_errors'])) { $this->allowedErrors = $options['allowed_errors']; } - if (!empty($options['allowed_warnings']) && is_int($options['allowed_warnings'])) { + if (!empty($options['allowed_warnings']) && \is_int($options['allowed_warnings'])) { $this->allowedWarnings = $options['allowed_warnings']; } @@ -73,7 +77,7 @@ public function execute() $warnings = 0; foreach ($this->failedPaths as $path) { - if ($path['type'] == 'error') { + if ($path['type'] === 'error') { $errors++; } else { $warnings++; @@ -105,7 +109,7 @@ protected function lintItem($item, $itemPath) { $success = true; - if ($item->isFile() && in_array(strtolower($item->getExtension()), $this->suffixes, true)) { + if ($item->isFile() && \in_array(\strtolower($item->getExtension()), $this->suffixes, true)) { if (!$this->lintFile($itemPath)) { $success = false; } @@ -132,7 +136,7 @@ protected function lintDirectory($path) $itemPath = $path . $item->getFilename(); - if (in_array($itemPath, $this->ignore, true)) { + if (\in_array($itemPath, $this->ignore, true)) { continue; } @@ -162,8 +166,8 @@ protected function lintFile($path) $output = $this->builder->getLastOutput(); - if (preg_match('/Found (.+?) (error|warning)/i', $output, $matches)) { - $rows = explode(PHP_EOL, $output); + if (\preg_match('/Found (.+?) (error|warning)/i', $output, $matches)) { + $rows = \explode(PHP_EOL, $output); unset($rows[0]); unset($rows[1]); @@ -171,15 +175,15 @@ protected function lintFile($path) unset($rows[3]); foreach ($rows as $row) { - $name = basename($path); + $name = \basename($path); - $row = str_replace('(use -i to include your custom modifier functions)', '', $row); - $message = str_replace($name . ': ', '', $row); + $row = \str_replace('(use -i to include your custom modifier functions)', '', $row); + $message = \str_replace($name . ': ', '', $row); - $parts = explode(' (line ', $message); + $parts = \explode(' (line ', $message); - $message = trim($parts[0]); - $line = str_replace(')', '', $parts[1]); + $message = \trim($parts[0]); + $line = \str_replace(')', '', $parts[1]); $this->failedPaths[] = [ 'file' => $path, diff --git a/src/Plugin/PhpUnit.php b/src/Plugin/PhpUnit.php index 8b741f949..aa8aad285 100644 --- a/src/Plugin/PhpUnit.php +++ b/src/Plugin/PhpUnit.php @@ -5,7 +5,6 @@ use Exception; use PHPCensor; use PHPCensor\Builder; -use PHPCensor\Config; use PHPCensor\Model\Build; use PHPCensor\Model\BuildError; use PHPCensor\Plugin; @@ -14,12 +13,17 @@ use PHPCensor\Plugin\Util\PhpUnitResultJunit; use PHPCensor\ZeroConfigPluginInterface; use Symfony\Component\Filesystem\Filesystem; +use PHPCensor\Common\Exception\RuntimeException; /** * PHP Unit Plugin - A rewrite of the original PHP Unit plugin * + * @package PHP Censor + * @subpackage Application + * * @author Dan Cryer * @author Pablo Tejada + * @author Dmitry Khomutov */ class PhpUnit extends Plugin implements ZeroConfigPluginInterface { @@ -79,7 +83,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->buildLocation = PUBLIC_DIR . 'artifacts/phpunit/' . $this->buildDirectory; $this->buildBranchLocation = PUBLIC_DIR . 'artifacts/phpunit/' . $this->buildBranchDirectory; - $this->options = new PhpUnitOptions($options, $this->buildLocation); + $this->options = new PhpUnitOptions($this->builder->getConfiguration(), $options, $this->buildLocation); $this->executable = $this->findBinary(['phpunit', 'phpunit.phar']); } @@ -89,7 +93,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) */ public static function canExecuteOnStage($stage, Build $build) { - if (Build::STAGE_TEST === $stage && !is_null(PhpUnitOptions::findConfigFile($build->getBuildPath()))) { + if (Build::STAGE_TEST === $stage && !\is_null(PhpUnitOptions::findConfigFile($build->getBuildPath()))) { return true; } @@ -112,8 +116,8 @@ public function execute() } $cmd = $this->executable; - $lastLine = exec($cmd . ' --log-json . --version'); - if (false !== strpos($lastLine, '--log-json')) { + $lastLine = \exec($cmd . ' --log-json . --version'); + if (false !== \strpos($lastLine, '--log-json')) { $logFormat = 'junit'; // --log-json is not supported } else { $logFormat = 'json'; @@ -135,7 +139,7 @@ public function execute() } } - return !in_array(false, $success, true); + return !\in_array(false, $success, true); } /** @@ -151,7 +155,7 @@ public function execute() */ protected function runConfig($directory, $configFile, $logFormat) { - $allowPublicArtifacts = (bool)Config::getInstance()->get( + $allowPublicArtifacts = (bool)$this->builder->getConfiguration()->get( 'php-censor.build.allow_public_artifacts', false ); @@ -162,7 +166,7 @@ protected function runConfig($directory, $configFile, $logFormat) $buildPath = $this->build->getBuildPath(); // Save the results into a log file - $logFile = tempnam(sys_get_temp_dir(), 'jlog_'); + $logFile = \tempnam(\sys_get_temp_dir(), 'jlog_'); $options->addArgument('log-' . $logFormat, $logFile); // Removes any current configurations files @@ -174,19 +178,24 @@ protected function runConfig($directory, $configFile, $logFormat) if ($options->getOption('coverage') && $allowPublicArtifacts) { if (!$fileSystem->exists($this->buildLocation)) { - $fileSystem->mkdir($this->buildLocation, (0777 & ~umask())); + $fileSystem->mkdir($this->buildLocation, (0777 & ~\umask())); } - if (!is_writable($this->buildLocation)) { - throw new Exception(sprintf( + if (!\is_writable($this->buildLocation)) { + throw new RuntimeException(\sprintf( 'The location %s is not writable or does not exist.', $this->buildLocation )); } } - $arguments = $this->builder->interpolate($options->buildArgumentString()); + $arguments = $this->builder->interpolate($options->buildArgumentString(), true); $cmd = $this->executable . ' %s %s'; + + if ($options->getOption('coverage')) { + $cmd = 'XDEBUG_MODE=coverage ' . $cmd; + } + $success = $this->executePhpUnitCommand($cmd, $arguments, $directory); $output = $this->builder->getLastOutput(); $covHtmlOk = false; @@ -208,7 +217,7 @@ protected function runConfig($directory, $configFile, $logFormat) if ($covHtmlOk) { $this->builder->logSuccess( - sprintf( + \sprintf( "\nPHPUnit successful build coverage report.\nYou can use coverage report for this build: %s\nOr coverage report for last build in the branch: %s", APP_URL . 'artifacts/phpunit/' . $this->buildDirectory . '/index.html', APP_URL . 'artifacts/phpunit/' . $this->buildBranchDirectory . '/index.html' @@ -216,7 +225,7 @@ protected function runConfig($directory, $configFile, $logFormat) ); } elseif ($allowPublicArtifacts) { $this->builder->logFailure( - sprintf( + \sprintf( "\nPHPUnit could not build coverage report.\nmissing: %s\nlast of this branch: %s", APP_URL . 'artifacts/phpunit/' . $this->buildDirectory . '/index.html', APP_URL . 'artifacts/phpunit/' . $this->buildBranchDirectory . '/index.html' @@ -239,7 +248,7 @@ protected function runConfig($directory, $configFile, $logFormat) */ protected function extractCoverage($output) { - preg_match( + \preg_match( '#Classes:[\s]*(.*?)%[^M]*?Methods:[\s]*(.*?)%[^L]*?Lines:[\s]*(.*?)\%#s', $output, $matches @@ -274,8 +283,8 @@ protected function executePhpUnitCommand($cmd, $arguments, $directory) protected function checkRequiredCoverage($coverage) { foreach ($coverage as $key => $currentValue) { - if ($requiredValue = $this->options->getOption(implode('_', ['required', $key, 'coverage']))) { - if (bccomp($requiredValue, $currentValue) === 1) { + if ($requiredValue = $this->options->getOption(\implode('_', ['required', $key, 'coverage']))) { + if (\bccomp($requiredValue, $currentValue) === 1) { return false; } } @@ -294,7 +303,7 @@ protected function checkRequiredCoverage($coverage) */ protected function processResults($logFile, $logFormat) { - if (file_exists($logFile)) { + if (\file_exists($logFile)) { if ('json' === $logFormat) { $parser = new PhpUnitResultJson($logFile, $this->build->getBuildPath()); } else { @@ -305,22 +314,22 @@ protected function processResults($logFile, $logFormat) $this->build->storeMeta((self::pluginName() . '-errors'), $parser->getFailures()); foreach ($parser->getErrors() as $error) { - $severity = $error['severity'] == - $parser::SEVERITY_ERROR ? - BuildError::SEVERITY_CRITICAL : - BuildError::SEVERITY_HIGH; + $severity = $error['severity'] === $parser::SEVERITY_ERROR + ? BuildError::SEVERITY_CRITICAL + : BuildError::SEVERITY_HIGH; + $this->build->reportError( $this->builder, self::pluginName(), $error['message'], $severity, $error['file'], - $error['line'] + (int)$error['line'] ); } - unlink($logFile); + \unlink($logFile); } else { - throw new Exception('log output file does not exist: ' . $logFile); + throw new RuntimeException('log output file does not exist: ' . $logFile); } } } diff --git a/src/Plugin/Psalm.php b/src/Plugin/Psalm.php index 0b5d8ef05..257add7d7 100644 --- a/src/Plugin/Psalm.php +++ b/src/Plugin/Psalm.php @@ -11,6 +11,11 @@ /** * A static analysis tool for finding errors in PHP applications https://getpsalm.org + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class Psalm extends Plugin { @@ -29,13 +34,13 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->executable = $this->findBinary(['psalm', 'psalm.phar']); - if (isset($options['allowed_errors']) && is_int($options['allowed_errors'])) { + if (isset($options['allowed_errors']) && \is_int($options['allowed_errors'])) { $this->allowedErrors = $options['allowed_errors']; } else { $this->allowedErrors = 0; } - if (isset($options['allowed_warnings']) && is_int($options['allowed_warnings'])) { + if (isset($options['allowed_warnings']) && \is_int($options['allowed_warnings'])) { $this->allowedWarnings = $options['allowed_warnings']; } else { $this->allowedWarnings = 0; @@ -61,8 +66,8 @@ public function execute() list($errors, $infos) = $this->processReport($this->builder->getLastOutput()); - if (0 < count($errors)) { - if (-1 !== $this->allowedErrors && count($errors) > $this->allowedErrors) { + if (0 < \count($errors)) { + if (-1 !== $this->allowedErrors && \count($errors) > $this->allowedErrors) { $success = false; } @@ -75,14 +80,14 @@ public function execute() $error['message'], BuildError::SEVERITY_HIGH, $error['file'], - $error['line_from'], - $error['line_to'] + (int)$error['line_from'], + (int)$error['line_to'] ); } } - if (0 < count($infos)) { - if (-1 !== $this->allowedWarnings && count($infos) > $this->allowedWarnings) { + if (0 < \count($infos)) { + if (-1 !== $this->allowedWarnings && \count($infos) > $this->allowedWarnings) { $success = false; } @@ -95,8 +100,8 @@ public function execute() $info['message'], BuildError::SEVERITY_LOW, $info['file'], - $info['line_from'], - $info['line_to'] + (int)$info['line_from'], + (int)$info['line_to'] ); } } @@ -122,19 +127,19 @@ public static function pluginName() */ protected function processReport($output) { - $data = json_decode(trim($output), true); + $data = \json_decode(\trim($output), true); $errors = []; $infos = []; - if (!empty($data) && is_array($data)) { + if (!empty($data) && \is_array($data)) { foreach ($data as $value) { - if (!in_array($value['severity'], ['error','info'], true)) { + if (!\in_array($value['severity'], ['error','info'], true)) { continue; } ${$value['severity'].'s'}[] = [ - 'full_message' => vsprintf('%s - %s:%d:%d - %s' . PHP_EOL . '%s', [ + 'full_message' => \vsprintf('%s - %s:%d:%d - %s' . PHP_EOL . '%s', [ $value['type'], $value['file_name'], $value['line_from'], diff --git a/src/Plugin/SecurityChecker.php b/src/Plugin/SecurityChecker.php index 53fc786b7..53bfeebe2 100644 --- a/src/Plugin/SecurityChecker.php +++ b/src/Plugin/SecurityChecker.php @@ -8,10 +8,14 @@ use PHPCensor\Model\BuildError; use PHPCensor\Plugin; use PHPCensor\ZeroConfigPluginInterface; +use PHPCensor\Common\Exception\RuntimeException; /** * SensioLabs Security Checker Plugin * + * @package PHP Censor + * @subpackage Application + * * @author Dmitry Khomutov */ class SecurityChecker extends Plugin implements ZeroConfigPluginInterface @@ -19,7 +23,7 @@ class SecurityChecker extends Plugin implements ZeroConfigPluginInterface /** * @var int */ - protected $allowedWarnings; + protected $allowedWarnings = 0; /** * @var string @@ -49,8 +53,6 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->allowedWarnings = 0; - if (isset($options['zero_config']) && $options['zero_config']) { $this->allowedWarnings = -1; } @@ -74,7 +76,7 @@ public static function canExecuteOnStage($stage, Build $build) { $path = $build->getBuildPath() . 'composer.lock'; - if (file_exists($path) && $stage === Build::STAGE_TEST) { + if (\file_exists($path) && $stage === Build::STAGE_TEST) { return true; } @@ -85,7 +87,7 @@ public function execute() { $composerLockFile = $this->builder->buildPath . 'composer.lock'; if (!\is_file($composerLockFile)) { - throw new \RuntimeException('Lock file does not exist.'); + throw new RuntimeException('Lock file does not exist.'); } if ('symfony' === $this->binaryType) { @@ -107,7 +109,7 @@ public function execute() $builder->logExecOutput(true); $success = true; - $result = (string)$builder->getLastOutput(); + $result = $builder->getLastOutput(); $warnings = \json_decode($result, true); if ($warnings) { @@ -117,18 +119,16 @@ public function execute() $this->builder, self::pluginName(), $library . ' (' . $warning['version'] . ")\n" . $data['cve'] . ': ' . $data['title'] . "\n" . $data['link'], - BuildError::SEVERITY_CRITICAL, - '-', - '-' + BuildError::SEVERITY_CRITICAL ); } } - if ($this->allowedWarnings != -1 && (\count($warnings) > $this->allowedWarnings)) { + if ($this->allowedWarnings !== -1 && (\count($warnings) > $this->allowedWarnings)) { $success = false; } } elseif (null === $warnings && $result) { - throw new \RuntimeException('invalid json: '.$result); + throw new RuntimeException('invalid json: '.$result); } return $success; diff --git a/src/Plugin/SensiolabsInsight.php b/src/Plugin/SensiolabsInsight.php index ab79cb690..4bd7790c0 100644 --- a/src/Plugin/SensiolabsInsight.php +++ b/src/Plugin/SensiolabsInsight.php @@ -7,12 +7,16 @@ use PHPCensor\Builder; use PHPCensor\Model\Build; use PHPCensor\Plugin; -use RuntimeException; +use PHPCensor\Common\Exception\RuntimeException; /** * Sensiolabs Insight Plugin - Allows Sensiolabs Insight testing. * + * @package PHP Censor + * @subpackage Application + * * @author Eugen Ganshorn + * @author Dmitry Khomutov */ class SensiolabsInsight extends Plugin { @@ -34,7 +38,7 @@ class SensiolabsInsight extends Plugin /** * @var int */ - protected $allowedWarnings; + protected $allowedWarnings = 0; /** * @return string @@ -50,21 +54,19 @@ public static function pluginName() public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - - $this->allowedWarnings = 0; - if (array_key_exists('allowed_warnings', $options)) { + if (\array_key_exists('allowed_warnings', $options)) { $this->allowedWarnings = (int)$options['allowed_warnings']; } - if (array_key_exists('user_uuid', $options)) { + if (\array_key_exists('user_uuid', $options)) { $this->userUuid = $options['user_uuid']; } if (\array_key_exists('auth_token', $options)) { - $this->authToken = $options['auth_token']; + $this->authToken = $this->builder->interpolate($options['auth_token'], true); } - if (array_key_exists('project_uuid', $options)) { + if (\array_key_exists('project_uuid', $options)) { $this->projectUuid = $options['project_uuid']; } @@ -82,7 +84,7 @@ public function execute() $this->executeSensiolabsInsight($insightBinaryPath); - $errorCount = $this->processReport(trim($this->builder->getLastOutput())); + $errorCount = $this->processReport(\trim($this->builder->getLastOutput())); $this->build->storeMeta((self::pluginName() . '-warnings'), $errorCount); return $this->wasLastExecSuccessful($errorCount); @@ -98,7 +100,7 @@ public function execute() */ protected function processReport($xmlString) { - $xml = simplexml_load_string($xmlString); + $xml = \simplexml_load_string($xmlString); if ($xml === false) { $this->builder->log($xmlString); @@ -110,7 +112,7 @@ protected function processReport($xmlString) foreach ($xml->file as $file) { $fileName = (string)$file['name']; - $fileName = str_replace($this->builder->buildPath, '', $fileName); + $fileName = \str_replace($this->builder->buildPath, '', $fileName); foreach ($file->violation as $violation) { $warnings++; @@ -166,14 +168,10 @@ protected function executeSensiolabsInsight($binaryPath) */ protected function wasLastExecSuccessful($errorCount) { - $success = true; - if ($this->allowedWarnings !== -1 && $errorCount > $this->allowedWarnings) { - $success = false; - - return $success; + return false; } - return $success; + return true; } } diff --git a/src/Plugin/Shell.php b/src/Plugin/Shell.php index ba6e260a0..debc07193 100644 --- a/src/Plugin/Shell.php +++ b/src/Plugin/Shell.php @@ -9,7 +9,11 @@ /** * Shell Plugin - Allows execute shell commands. * + * @package PHP Censor + * @subpackage Application + * * @author Kinn Coelho Julião + * @author Dmitry Khomutov */ class Shell extends Plugin { @@ -35,11 +39,11 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - if (array_key_exists('execute_all', $options) && $options['execute_all']) { + if (\array_key_exists('execute_all', $options) && $options['execute_all']) { $this->executeAll = true; } - if (isset($options['commands']) && is_array($options['commands'])) { + if (isset($options['commands']) && \is_array($options['commands'])) { $this->commands = $options['commands']; return; diff --git a/src/Plugin/SlackNotify.php b/src/Plugin/SlackNotify.php index 5b4abbb94..0b69bf9ae 100644 --- a/src/Plugin/SlackNotify.php +++ b/src/Plugin/SlackNotify.php @@ -2,18 +2,22 @@ namespace PHPCensor\Plugin; -use Exception; use Maknz\Slack\Attachment; use Maknz\Slack\AttachmentField; use Maknz\Slack\Client; use PHPCensor\Builder; +use PHPCensor\Common\Exception\InvalidArgumentException; use PHPCensor\Model\Build; use PHPCensor\Plugin; /** * Slack Plugin * + * @package PHP Censor + * @subpackage Application + * * @author Stephen Ball + * @author Dmitry Khomutov */ class SlackNotify extends Plugin { @@ -39,8 +43,8 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - if (is_array($options) && isset($options['webhook_url'])) { - $this->webHook = trim($options['webhook_url']); + if (\is_array($options) && isset($options['webhook_url'])) { + $this->webHook = \trim($options['webhook_url']); if (isset($options['message'])) { $this->message = $options['message']; @@ -72,7 +76,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->icon = $options['icon']; } } else { - throw new Exception('Please define the webhook_url for slack_notify plugin!'); + throw new InvalidArgumentException('Please define the webhook_url for slack_notify plugin!'); } } diff --git a/src/Plugin/Sqlite.php b/src/Plugin/Sqlite.php index 5b9d41ed4..9da80f889 100644 --- a/src/Plugin/Sqlite.php +++ b/src/Plugin/Sqlite.php @@ -11,6 +11,9 @@ /** * SQLite Plugin — Provides access to a SQLite database. * + * @package PHP Censor + * @subpackage Application + * * @author Dmitry Khomutov */ class Sqlite extends Plugin @@ -62,7 +65,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) if (!empty($this->options['queries']) && \is_array($this->options['queries'])) { foreach ($this->options['queries'] as $query) { - $this->queries[] = $this->builder->interpolate($query); + $this->queries[] = $this->builder->interpolate($query, true); } } } @@ -74,7 +77,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) public function execute() { try { - $pdoOptions = array_merge([ + $pdoOptions = \array_merge([ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ], $this->pdoOptions); @@ -83,7 +86,7 @@ public function execute() foreach ($this->queries as $query) { $pdo->query($query); } - } catch (Exception $ex) { + } catch (\Throwable $ex) { $this->builder->logFailure($ex->getMessage()); return false; diff --git a/src/Plugin/TechnicalDebt.php b/src/Plugin/TechnicalDebt.php index 41ed031a5..8c19f77ec 100644 --- a/src/Plugin/TechnicalDebt.php +++ b/src/Plugin/TechnicalDebt.php @@ -14,24 +14,28 @@ /** * Technical Debt Plugin - Checks for existence of "TODO", "FIXME", etc. * + * @package PHP Censor + * @subpackage Application + * * @author James Inman + * @author Dmitry Khomutov */ class TechnicalDebt extends Plugin implements ZeroConfigPluginInterface { /** * @var array */ - protected $suffixes; + protected $suffixes = ['php']; /** * @var int */ - protected $allowedErrors; + protected $allowedErrors = 0; /** * @var array - terms to search for */ - protected $searches; + protected $searches = ['TODO', 'FIXME', 'TO DO', 'FIX ME']; /** * @var array - lines of . and X to visualize errors @@ -96,10 +100,10 @@ protected function returnResult() $string = ''; $fileNumber = 0; foreach ($this->errorPerFile as $oneLine) { - $fileNumber += strlen($oneLine); - $string .= str_pad($oneLine, 60, ' ', STR_PAD_RIGHT); - $string .= str_pad($fileNumber, 4, ' ', STR_PAD_LEFT); - $string .= "/" . $this->numberOfAnalysedFile . " (" . floor($fileNumber * 100 / $this->numberOfAnalysedFile) . " %)\n"; + $fileNumber += \strlen($oneLine); + $string .= \str_pad($oneLine, 60, ' ', STR_PAD_RIGHT); + $string .= \str_pad($fileNumber, 4, ' ', STR_PAD_LEFT); + $string .= "/" . $this->numberOfAnalysedFile . " (" . \floor($fileNumber * 100 / $this->numberOfAnalysedFile) . " %)\n"; } $string .= "Checked {$fileNumber} files\n"; @@ -113,15 +117,11 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->suffixes = ['php']; - $this->allowedErrors = 0; - $this->searches = ['TODO', 'FIXME', 'TO DO', 'FIX ME']; - - if (!empty($options['suffixes']) && is_array($options['suffixes'])) { + if (!empty($options['suffixes']) && \is_array($options['suffixes'])) { $this->suffixes = $options['suffixes']; } - if (!empty($options['searches']) && is_array($options['searches'])) { + if (!empty($options['searches']) && \is_array($options['searches'])) { $this->searches = $options['searches']; } @@ -129,7 +129,7 @@ public function __construct(Builder $builder, Build $build, array $options = []) $this->allowedErrors = -1; } - if (array_key_exists('allowed_errors', $options) && $options['allowed_errors']) { + if (\array_key_exists('allowed_errors', $options) && $options['allowed_errors']) { $this->allowedErrors = (int)$options['allowed_errors']; } } @@ -154,7 +154,7 @@ public function execute() $success = true; $errorCount = $this->getErrorList(); - $this->builder->log($this->returnResult() . "Found $errorCount instances of " . implode(', ', $this->searches)); + $this->builder->log($this->returnResult() . "Found $errorCount instances of " . \implode(', ', $this->searches)); $this->build->storeMeta((self::pluginName() . '-warnings'), $errorCount); @@ -175,7 +175,7 @@ protected function getErrorList() $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->directory)); $this->builder->logDebug("Directory: " . $this->directory); - $this->builder->logDebug("Ignored path: ".json_encode($this->ignore, true)); + $this->builder->logDebug("Ignored path: " . \json_encode($this->ignore, true)); $errorCount = 0; /** @var SplFileInfo $file */ @@ -187,6 +187,7 @@ protected function getErrorList() foreach ($this->suffixes as $suffix) { if ($suffix !== $extension) { $ignored = true; + break; } } @@ -195,23 +196,24 @@ protected function getErrorList() $ignoreAbsolute = $this->builder->buildPath . $ignore; if ('/' === $ignoreAbsolute[0]) { - if (0 === strpos($filePath, $ignoreAbsolute)) { + if (0 === \strpos($filePath, $ignoreAbsolute)) { $ignored = true; + break; } } } if (!$ignored) { - $handle = fopen($filePath, "r"); + $handle = \fopen($filePath, "r"); $lineNumber = 1; $errorInFile = false; - while (false === feof($handle)) { - $line = fgets($handle); + while (false === \feof($handle)) { + $line = \fgets($handle); foreach ($this->searches as $search) { - if ($technicalDebtLine = trim(strstr($line, $search))) { - $fileName = str_replace($this->directory, '', $filePath); + if ($technicalDebtLine = \trim(\strstr($line, $search))) { + $fileName = \str_replace($this->directory, '', $filePath); $this->build->reportError( $this->builder, @@ -228,7 +230,7 @@ protected function getErrorList() } $lineNumber++; } - fclose($handle); + \fclose($handle); if ($errorInFile === true) { $this->buildLogString('X'); diff --git a/src/Plugin/TelegramNotify.php b/src/Plugin/TelegramNotify.php index d6ddc100f..dbbb0bb58 100644 --- a/src/Plugin/TelegramNotify.php +++ b/src/Plugin/TelegramNotify.php @@ -5,13 +5,18 @@ use Exception; use GuzzleHttp\Client; use PHPCensor\Builder; +use PHPCensor\Common\Exception\InvalidArgumentException; use PHPCensor\Model\Build; use PHPCensor\Plugin; /** * Telegram Plugin * + * @package PHP Censor + * @subpackage Application + * * @author LEXASOFT + * @author Dmitry Khomutov */ class TelegramNotify extends Plugin { @@ -38,16 +43,16 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - if (empty($options['auth_token']) && empty($options['api_key'])) { - throw new Exception("Not setting telegram 'auth_token'"); + if (empty($options['auth_token'])) { + throw new InvalidArgumentException("Not setting telegram 'auth_token'"); } if (empty($options['recipients'])) { - throw new Exception("Not setting recipients"); + throw new InvalidArgumentException("Not setting recipients"); } if (\array_key_exists('auth_token', $options)) { - $this->authToken = $options['auth_token']; + $this->authToken = $this->builder->interpolate($options['auth_token'], true); } $this->message = '[%ICON_BUILD%] [%PROJECT_TITLE%](%PROJECT_LINK%)' . @@ -60,9 +65,9 @@ public function __construct(Builder $builder, Build $build, array $options = []) } $this->recipients = []; - if (is_string($options['recipients'])) { + if (\is_string($options['recipients'])) { $this->recipients = [$options['recipients']]; - } elseif (is_array($options['recipients'])) { + } elseif (\is_array($options['recipients'])) { $this->recipients = $options['recipients']; } @@ -77,14 +82,22 @@ public function execute() { $message = $this->buildMessage(); $client = new Client(); - $url = '/bot'. $this->authToken . '/sendMessage'; + $url = '/bot' . $this->authToken . '/sendMessage'; foreach ($this->recipients as $chatId) { + $chatId = $this->builder->interpolate($chatId, true); + [$chatId, $topicId] = $this->splitChatIdAndTopicId($chatId); + $params = [ 'chat_id' => $chatId, 'text' => $message, 'parse_mode' => 'Markdown', ]; + + if ($topicId !== null) { + $params['message_thread_id'] = $topicId; + } + $client->post(('https://api.telegram.org' . $url), [ 'headers' => [ 'Content-Type' => 'application/json', @@ -98,6 +111,11 @@ public function execute() 'text' => $this->buildMsg, 'parse_mode' => 'Markdown', ]; + + if ($topicId !== null) { + $params['message_thread_id'] = $topicId; + } + $client->post(('https://api.telegram.org' . $url), [ 'headers' => [ 'Content-Type' => 'application/json', @@ -119,22 +137,36 @@ private function buildMessage() $this->buildMsg = ''; $buildIcon = $this->build->isSuccessful() ? '✅' : '❌'; $buildLog = $this->build->getLog(); - $buildLog = str_replace(['[0;32m', '[0;31m', '[0m', '/[0m'], '', $buildLog); - $buildMessages = explode('RUNNING PLUGIN: ', $buildLog); + $buildLog = \str_replace(['[0;32m', '[0;31m', '[0m', '/[0m'], '', $buildLog); + $buildMessages = \explode('RUNNING PLUGIN: ', $buildLog); foreach ($buildMessages as $bm) { - $pos = mb_strpos($bm, "\n"); - $firstRow = mb_substr($bm, 0, $pos); + $pos = \mb_strpos($bm, "\n"); + $firstRow = \mb_substr($bm, 0, $pos); //skip long outputs - if (in_array($firstRow, ['slack_notify', 'php_loc', 'telegram_notify'], true)) { + if (\in_array($firstRow, ['slack_notify', 'php_loc', 'telegram_notify'], true)) { continue; } $this->buildMsg .= '*RUNNING PLUGIN: ' . $firstRow . "*\n"; - $this->buildMsg .= $firstRow == 'composer' ? '' : ('```' . mb_substr($bm, $pos) . '```'); + $this->buildMsg .= $firstRow === 'composer' ? '' : ('```' . \mb_substr($bm, $pos) . '```'); } - return $this->builder->interpolate(str_replace(['%ICON_BUILD%'], [$buildIcon], $this->message)); + return $this->builder->interpolate(\str_replace(['%ICON_BUILD%'], [$buildIcon], $this->message)); + } + + /** + * Split chat group id to chat id and topic id + * + * @param int|string $chatId + * @return array{string, string|null} + */ + protected function splitChatIdAndTopicId($chatId) + { + $parts = \explode('/', \trim((string) $chatId) . '/'); + $topicId = $parts[1] !== '' ? $parts[1] : null; + + return [$parts[0], $topicId]; } } diff --git a/src/Plugin/Util/BitbucketNotifyPhpUnitResult.php b/src/Plugin/Util/BitbucketNotifyPhpUnitResult.php index e5352d5ac..b30f70738 100644 --- a/src/Plugin/Util/BitbucketNotifyPhpUnitResult.php +++ b/src/Plugin/Util/BitbucketNotifyPhpUnitResult.php @@ -2,6 +2,12 @@ namespace PHPCensor\Plugin\Util; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov + */ class BitbucketNotifyPhpUnitResult extends BitbucketNotifyPluginResult { public function __construct($plugin, $left, $right) diff --git a/src/Plugin/Util/BitbucketNotifyPluginResult.php b/src/Plugin/Util/BitbucketNotifyPluginResult.php index 2682b0104..8157dcdb6 100644 --- a/src/Plugin/Util/BitbucketNotifyPluginResult.php +++ b/src/Plugin/Util/BitbucketNotifyPluginResult.php @@ -2,6 +2,12 @@ namespace PHPCensor\Plugin\Util; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov + */ class BitbucketNotifyPluginResult { public const DEFAULT_PLUGIN_OUTPUT_FORMAT = "%s | %d\t=> %d\t%s"; @@ -16,14 +22,13 @@ class BitbucketNotifyPluginResult protected $right; /** @var string $outputFormat */ - protected $outputFormat; + protected $outputFormat = self::DEFAULT_PLUGIN_OUTPUT_FORMAT; public function __construct($plugin, $left, $right) { $this->plugin = $plugin; $this->left = $left; $this->right = $right; - $this->outputFormat = self::DEFAULT_PLUGIN_OUTPUT_FORMAT; } public function getPlugin() @@ -79,9 +84,9 @@ public function isUnchanged() public function generateFormattedOutput($maxPluginNameLength) { - return trim(sprintf( + return \trim(\sprintf( $this->outputFormat, - str_pad($this->plugin, $maxPluginNameLength), + \str_pad($this->plugin, $maxPluginNameLength), $this->left, $this->right, $this->generateComment() @@ -94,7 +99,7 @@ public function generateTaskDescription() return ''; } - return sprintf( + return \sprintf( $this->getTaskDescriptionMessage(), $this->plugin, $this->left, diff --git a/src/Plugin/Util/Executor.php b/src/Plugin/Util/Executor.php index 5f0d721c2..e4ff1b2ba 100644 --- a/src/Plugin/Util/Executor.php +++ b/src/Plugin/Util/Executor.php @@ -3,15 +3,21 @@ namespace PHPCensor\Plugin\Util; use Exception; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Helper\Lang; use PHPCensor\Logging\BuildLogger; use PHPCensor\Model\Build; use PHPCensor\Plugin; use PHPCensor\Store\BuildStore; -use PHPCensor\Store\Factory as StoreFactory; +use PHPCensor\StoreRegistry; /** * Plugin Executor - Runs the configured plugins for a given build stage. + * + * @package PHP Censor + * @subpackage Application + * + * @author Dmitry Khomutov */ class Executor { @@ -30,11 +36,18 @@ class Executor */ protected $store; - public function __construct(Factory $pluginFactory, BuildLogger $logger, BuildStore $store = null) - { + protected StoreRegistry $storeRegistry; + + public function __construct( + StoreRegistry $storeRegistry, + Factory $pluginFactory, + BuildLogger $logger, + BuildStore $store = null + ) { + $this->storeRegistry = $storeRegistry; $this->pluginFactory = $pluginFactory; - $this->logger = $logger; - $this->store = $store ?: StoreFactory::getStore('Build'); + $this->logger = $logger; + $this->store = $store; } /** @@ -51,7 +64,7 @@ public function executePlugins($config, $stage) $pluginsToExecute = []; // If we have global plugins to execute for this stage, add them to the list to be executed: - if (array_key_exists($stage, $config) && is_array($config[$stage])) { + if (\array_key_exists($stage, $config) && \is_array($config[$stage])) { $pluginsToExecute[] = $config[$stage]; } @@ -74,17 +87,17 @@ public function executePlugins($config, $stage) */ public function getBranchSpecificConfig($config, $branch) { - $configSections = array_keys($config); + $configSections = \array_keys($config); foreach ($configSections as $configSection) { - if (0 === strpos($configSection, 'branch-')) { + if (0 === \strpos($configSection, 'branch-')) { if ($configSection === ('branch-' . $branch)) { return $config[$configSection]; } - if (0 === strpos($configSection, 'branch-regex:')) { - $pattern = '#' . substr($configSection, 13) . '#u'; - preg_match($pattern, $branch, $matches); + if (0 === \strpos($configSection, 'branch-regex:')) { + $pattern = '#' . \substr($configSection, 13) . '#u'; + \preg_match($pattern, $branch, $matches); if (!empty($matches[0])) { return $config[$configSection]; } @@ -106,8 +119,7 @@ public function getBranchSpecificConfig($config, $branch) */ protected function getBranchSpecificPlugins($config, $stage, $pluginsToExecute) { - /** @var Build $build */ - $build = $this->pluginFactory->getResourceFor('PHPCensor\Model\Build'); + $build = $this->pluginFactory->getBuild(); $branch = $build->getBranch(); $branchConfig = $this->getBranchSpecificConfig($config, $branch); if (!$branchConfig) { @@ -126,20 +138,20 @@ protected function getBranchSpecificPlugins($config, $stage, $pluginsToExecute) case 'replace': $pluginsToExecute = []; $pluginsToExecute[] = $plugins; + break; // Run branch-specific plugins before standard plugins: case 'before': - array_unshift($pluginsToExecute, $plugins); + \array_unshift($pluginsToExecute, $plugins); + break; // Run branch-specific plugins after standard plugins: case 'after': - array_push($pluginsToExecute, $plugins); - break; - default: - array_push($pluginsToExecute, $plugins); + \array_push($pluginsToExecute, $plugins); + break; } @@ -154,21 +166,24 @@ protected function getBranchSpecificPlugins($config, $stage, $pluginsToExecute) protected function doExecutePlugins($plugins, $stage) { $success = true; + foreach ($plugins as $step => $options) { + $plugin = $step; + if (isset($options['plugin'])) { + $plugin = $options['plugin']; + } - foreach ($plugins as $plugin => $options) { $this->logger->log(''); $this->logger->logSuccess( - sprintf('RUNNING PLUGIN: %s', Lang::get($plugin)) . ' (' . - 'Stage' . ': ' . ucfirst($stage) . ')' + \sprintf('RUNNING PLUGIN: %s (Step: %s) (Stage: %s)', Lang::get($plugin), $step, \ucfirst($stage)) ); - $this->setPluginStatus($stage, $plugin, Plugin::STATUS_RUNNING); + $this->setPluginStatus($stage, $step, $plugin, Plugin::STATUS_RUNNING); // Try and execute it if ($this->executePlugin($plugin, $options)) { // Execution was successful $this->logger->logSuccess('PLUGIN: SUCCESS'); - $this->setPluginStatus($stage, $plugin, Plugin::STATUS_SUCCESS); + $this->setPluginStatus($stage, $step, $plugin, Plugin::STATUS_SUCCESS); } else { $status = Plugin::STATUS_FAILED; @@ -177,7 +192,8 @@ protected function doExecutePlugins($plugins, $stage) // If we're in the "setup" stage, execution should not continue after // a plugin has failed: - throw new Exception('Plugin failed: ' . $plugin); + + throw new RuntimeException('Plugin failed: ' . $plugin . ' (Step: ' . $step . ')'); } elseif ($stage === Build::STAGE_DEPLOY) { $this->logger->logFailure('PLUGIN: FAILED'); $success = false; @@ -194,7 +210,7 @@ protected function doExecutePlugins($plugins, $stage) } } - $this->setPluginStatus($stage, $plugin, $status); + $this->setPluginStatus($stage, $step, $plugin, $status); } } @@ -207,13 +223,13 @@ protected function doExecutePlugins($plugins, $stage) public function executePlugin($plugin, $options) { $class = $plugin; - if (!class_exists($class)) { - $class = str_replace('_', ' ', $plugin); - $class = ucwords($class); - $class = 'PHPCensor\\Plugin\\' . str_replace(' ', '', $class); + if (!\class_exists($class)) { + $class = \str_replace('_', ' ', $plugin); + $class = \ucwords($class); + $class = 'PHPCensor\Plugin\\' . \str_replace(' ', '', $class); - if (!class_exists($class)) { - $this->logger->logFailure(sprintf('Plugin does not exist: %s', $plugin)); + if (!\class_exists($class)) { + $this->logger->logFailure(\sprintf('Plugin does not exist: %s', $plugin)); return false; } @@ -221,10 +237,11 @@ public function executePlugin($plugin, $options) try { // Build and run it - $obj = $this->pluginFactory->buildPlugin($class, (is_null($options) ? [] : $options)); + $obj = $this->pluginFactory->buildPlugin($class, (\is_null($options) ? [] : $options)); + $obj->setStoreRegistry($this->storeRegistry); return $obj->execute(); - } catch (Exception $ex) { + } catch (\Throwable $ex) { $this->logger->logFailure('Exception: ' . $ex->getMessage(), $ex); return false; @@ -235,23 +252,26 @@ public function executePlugin($plugin, $options) * Change the status of a plugin for a given stage. * * @param string $stage The builder stage. + * @param string $step The name of the step * @param string $plugin The plugin name. * @param int $status The new status. */ - protected function setPluginStatus($stage, $plugin, $status) + protected function setPluginStatus($stage, $step, $plugin, $status) { $summary = $this->getBuildSummary(); - if (!isset($summary[$stage][$plugin])) { - $summary[$stage][$plugin] = []; + if (!isset($summary[$stage][$step])) { + $summary[$stage][$step] = [ + 'plugin' => $plugin + ]; } - $summary[$stage][$plugin]['status'] = $status; + $summary[$stage][$step]['status'] = $status; if ($status === Plugin::STATUS_RUNNING) { - $summary[$stage][$plugin]['started'] = time(); + $summary[$stage][$step]['started'] = \time(); } elseif ($status >= Plugin::STATUS_SUCCESS) { - $summary[$stage][$plugin]['ended'] = time(); + $summary[$stage][$step]['ended'] = \time(); } $this->setBuildSummary($summary); @@ -264,8 +284,7 @@ protected function setPluginStatus($stage, $plugin, $status) */ private function getBuildSummary() { - /** @var Build $build */ - $build = $this->pluginFactory->getResourceFor('PHPCensor\Model\Build'); + $build = $this->pluginFactory->getBuild(); $metas = $this->store->getMeta('plugin-summary', $build->getProjectId(), $build->getId()); return isset($metas[0]['meta_value']) ? $metas[0]['meta_value'] : []; @@ -278,8 +297,7 @@ private function getBuildSummary() */ private function setBuildSummary($summary) { - /** @var Build $build */ - $build = $this->pluginFactory->getResourceFor('PHPCensor\Model\Build'); - $this->store->setMeta($build->getId(), 'plugin-summary', json_encode($summary)); + $build = $this->pluginFactory->getBuild(); + $this->store->setMeta($build->getId(), 'plugin-summary', \json_encode($summary)); } } diff --git a/src/Plugin/Util/Factory.php b/src/Plugin/Util/Factory.php index 8d6dca903..e04210e0c 100644 --- a/src/Plugin/Util/Factory.php +++ b/src/Plugin/Util/Factory.php @@ -1,222 +1,40 @@ */ class Factory { - public const TYPE_ARRAY = "array"; - public const TYPE_CALLABLE = "callable"; - public const INTERFACE_PLUGIN = '\PHPCensor\Plugin'; - - private $currentPluginOptions; - - /** - * @var Container - */ - private $container; - - /** - */ - public function __construct(Container $container = null) - { - if ($container) { - $this->container = $container; - } else { - $this->container = new Container(); - } - } - - /** - * Trys to get a function from the file path specified. If the - * file returns a function then $this will be passed to it. - * This enables the config file to call any public methods. - * - * @return bool - true if the function exists else false. - */ - public function addConfigFromFile($configPath) - { - // The file is expected to return a function which can - // act on the pluginFactory to register any resources needed. - if (file_exists($configPath)) { - $configFunction = require($configPath); - if (is_callable($configFunction)) { - $configFunction($this); - - return true; - } - } - - return false; - } - - /** - * Get most recently used factory options. - * @return mixed - */ - public function getLastOptions() - { - return $this->currentPluginOptions; - } - - /** - * Builds an instance of plugin of class $className. $options will - * be passed along with any resources registered with the factory. - * - * @param string $className - * @param array|null $options - * - * @return Plugin - */ - public function buildPlugin($className, $options = []) - { - $this->currentPluginOptions = $options; - - $reflectedPlugin = new ReflectionClass($className); - - $constructor = $reflectedPlugin->getConstructor(); + private Builder $builder; - if ($constructor) { - $argsToUse = []; - foreach ($constructor->getParameters() as $param) { - if ('options' === $param->getName()) { - $argsToUse[] = $options; - } else { - $argsToUse = $this->addArgFromParam($argsToUse, $param); - } - } - /** @var Plugin $plugin */ - $plugin = $reflectedPlugin->newInstanceArgs($argsToUse); - } else { - /** @var Plugin $plugin */ - $plugin = $reflectedPlugin->newInstance(); - } + private Build $build; - return $plugin; - } - - /** - * @param callable $loader - * @param string|null $name - * @param string|null $type - * - * @throws InvalidArgumentException - * - * @internal param mixed $resource - */ - public function registerResource( - $loader, - $name = null, - $type = null + public function __construct( + Builder $builder, + Build $build ) { - if ($name === null && $type === null) { - throw new InvalidArgumentException( - "Type or Name must be specified" - ); - } - - if (!($loader instanceof Closure)) { - throw new InvalidArgumentException( - '$loader is expected to be a function' - ); - } - - $resourceID = $this->getInternalID($type, $name); - - $this->container[$resourceID] = $loader; - } - - /** - * Get an internal resource ID. - * @param null $type - * @param null $name - * @return string - */ - private function getInternalID($type = null, $name = null) - { - $type = $type ?: ""; - $name = $name ?: ""; - - return $type . "-" . $name; + $this->builder = $builder; + $this->build = $build; } - /** - * @param string $type - * @param string $name - * @return mixed - */ - public function getResourceFor($type = null, $name = null) + public function buildPlugin(string $className, array $options = []): Plugin { - $fullId = $this->getInternalID($type, $name); - if (isset($this->container[$fullId])) { - return $this->container[$fullId]; - } - - $typeOnlyID = $this->getInternalID($type, null); - if (isset($this->container[$typeOnlyID])) { - return $this->container[$typeOnlyID]; - } - - $nameOnlyID = $this->getInternalID(null, $name); - if (isset($this->container[$nameOnlyID])) { - return $this->container[$nameOnlyID]; - } - - return null; + return new $className($this->builder, $this->build, $options); } - /** - * @return string|null - */ - private function getParamType(ReflectionParameter $param) + public function getBuild(): Build { - $class = $param->getType() && !$param->getType()->isBuiltin() - ? new ReflectionClass($param->getType()->getName()) - : null; - - if ($class) { - return $class->getName(); - } elseif ($param->getType() && $param->getType()->getName() === 'array') { - return self::TYPE_ARRAY; - } elseif (is_callable($param)) { - return self::TYPE_CALLABLE; - } else { - return null; - } - } - - /** - * - * @return array - * - * @throws DomainException - */ - private function addArgFromParam($existingArgs, ReflectionParameter $param) - { - $name = $param->getName(); - $type = $this->getParamType($param); - $arg = $this->getResourceFor($type, $name); - - if ($arg !== null) { - $existingArgs[] = $arg; - } elseif ($arg === null && $param->isOptional()) { - $existingArgs[] = $param->getDefaultValue(); - } else { - throw new DomainException( - "Unsatisfied dependency: " . $param->getName() - ); - } - - return $existingArgs; + return $this->build; } } diff --git a/src/Plugin/Util/PhpUnitResult.php b/src/Plugin/Util/PhpUnitResult.php index a1832b4b7..e90788cb8 100644 --- a/src/Plugin/Util/PhpUnitResult.php +++ b/src/Plugin/Util/PhpUnitResult.php @@ -7,7 +7,11 @@ /** * Class PhpUnitResult parses the results for the PhpUnitV2 plugin * + * @package PHP Censor + * @subpackage Application + * * @author Pablo Tejada + * @author Dmitry Khomutov */ abstract class PhpUnitResult { @@ -38,26 +42,23 @@ public function __construct($outputFile, $buildPath = '') */ abstract public function parse(); - abstract protected function getSeverity($testcase); + abstract protected function getSeverity($testCase); - abstract protected function buildMessage($testcase); + abstract protected function buildMessage($testCase); - abstract protected function buildTrace($testcase); + abstract protected function buildTrace($testCase); - protected function getFileAndLine($testcase) - { - return $testcase; - } + abstract protected function getFileAndLine($testCase); - protected function getOutput($testcase) + protected function getOutput($testCase) { - return $testcase['output']; + return $testCase['output']; } protected function parseTestcase($testcase) { $severity = $this->getSeverity($testcase); - $pass = isset(array_fill_keys([self::SEVERITY_PASS, self::SEVERITY_SKIPPED], true)[$severity]); + $pass = isset(\array_fill_keys([self::SEVERITY_PASS, self::SEVERITY_SKIPPED], true)[$severity]); $data = [ 'pass' => $pass, 'severity' => $severity, diff --git a/src/Plugin/Util/PhpUnitResultJson.php b/src/Plugin/Util/PhpUnitResultJson.php index 8b6c8a31e..da47d4088 100644 --- a/src/Plugin/Util/PhpUnitResultJson.php +++ b/src/Plugin/Util/PhpUnitResultJson.php @@ -3,11 +3,16 @@ namespace PHPCensor\Plugin\Util; use Exception; +use PHPCensor\Common\Exception\RuntimeException; /** * Class PhpUnitResult parses the results for the PhpUnitV2 plugin * + * @package PHP Censor + * @subpackage Application + * * @author Pablo Tejada + * @author Dmitry Khomutov */ class PhpUnitResultJson extends PhpUnitResult { @@ -26,14 +31,14 @@ class PhpUnitResultJson extends PhpUnitResult */ public function parse() { - $rawResults = file_get_contents($this->outputFile); + $rawResults = \file_get_contents($this->outputFile); $events = []; - if ($rawResults && $rawResults[0] == '{') { - $fixedJson = '[' . str_replace('}{', '},{', $rawResults) . ']'; - $events = json_decode($fixedJson, true); + if ($rawResults && $rawResults[0] === '{') { + $fixedJson = '[' . \str_replace('}{', '},{', $rawResults) . ']'; + $events = \json_decode($fixedJson, true); } elseif ($rawResults) { - $events = json_decode($rawResults, true); + $events = \json_decode($rawResults, true); } // Reset the parsing variables @@ -44,10 +49,10 @@ public function parse() if ($events) { $started = null; foreach ($events as $event) { - if (isset($event['event']) && $event['event'] == self::EVENT_TEST) { + if (isset($event['event']) && $event['event'] === self::EVENT_TEST) { $this->parseTestcase($event); $started = null; - } elseif (isset($event['event']) && $event['event'] == self::EVENT_TEST_START) { + } elseif (isset($event['event']) && $event['event'] === self::EVENT_TEST_START) { $started = $event; } } @@ -67,33 +72,34 @@ public function parse() /** * Build the severity of the event * + * @param $testCase * * @return string The severity flags * @throws Exception */ - protected function getSeverity($event) + protected function getSeverity($testCase) { - $status = $event['status']; + $status = $testCase['status']; switch ($status) { case 'fail': $severity = self::SEVERITY_FAIL; + break; case 'error': - if (strpos($event['message'], 'Skipped') === 0 || strpos($event['message'], 'Incomplete') === 0) { + if (\strpos($testCase['message'], 'Skipped') === 0 || \strpos($testCase['message'], 'Incomplete') === 0) { $severity = self::SEVERITY_SKIPPED; } else { $severity = self::SEVERITY_ERROR; } + break; case 'pass': - $severity = self::SEVERITY_PASS; - break; case 'warning': $severity = self::SEVERITY_PASS; + break; default: - throw new Exception("Unexpected PHPUnit test status: {$status}"); - break; + throw new RuntimeException("Unexpected PHPUnit test status: {$status}"); } return $severity; @@ -102,16 +108,16 @@ protected function getSeverity($event) /** * Build the message string for an event * - * @param array $event + * @param array $testCase * * @return string */ - protected function buildMessage($event) + protected function buildMessage($testCase) { - $message = $event['test']; + $message = $testCase['test']; - if ($event['message']) { - $message .= PHP_EOL . $event ['message']; + if ($testCase['message']) { + $message .= PHP_EOL . $testCase ['message']; } return $message; @@ -120,17 +126,17 @@ protected function buildMessage($event) /** * Build a string base trace of the failure * - * @param array $event + * @param array $testCase * * @return string[] */ - protected function buildTrace($event) + protected function buildTrace($testCase) { $formattedTrace = []; - if (!empty($event['trace'])) { - foreach ($event['trace'] as $step) { - $line = str_replace($this->buildPath, '', $step['file']) . ':' . $step['line']; + if (!empty($testCase['trace'])) { + foreach ($testCase['trace'] as $step) { + $line = \str_replace($this->buildPath, '', $step['file']) . ':' . $step['line']; $formattedTrace[] = $line; } } @@ -141,23 +147,23 @@ protected function buildTrace($event) /** * Saves additional info for a failing test * - * @param array $event + * @param array $testCase * * @return array */ - protected function getFileAndLine($event) + protected function getFileAndLine($testCase) { - if (empty($event['trace'])) { + if (empty($testCase['trace'])) { return [ 'file' => '', 'line' => '', ]; } - $firstTrace = end($event['trace']); - reset($event['trace']); + $firstTrace = \end($testCase['trace']); + \reset($testCase['trace']); return [ - 'file' => str_replace($this->buildPath, '', $firstTrace['file']), + 'file' => \str_replace($this->buildPath, '', $firstTrace['file']), 'line' => $firstTrace['line'] ]; } diff --git a/src/Plugin/Util/PhpUnitResultJunit.php b/src/Plugin/Util/PhpUnitResultJunit.php index f29a327aa..003ad5874 100644 --- a/src/Plugin/Util/PhpUnitResultJunit.php +++ b/src/Plugin/Util/PhpUnitResultJunit.php @@ -3,14 +3,18 @@ namespace PHPCensor\Plugin\Util; use Exception; +use PHPCensor\Common\Exception\RuntimeException; use PHPCensor\Helper\Xml; -use RuntimeException; use SimpleXMLElement; /** * Class PhpUnitResultJunit parses the results for the PhpUnitV2 plugin * + * @package PHP Censor + * @subpackage Application + * * @author Simon Heimberg + * @author Dmitry Khomutov */ class PhpUnitResultJunit extends PhpUnitResult { @@ -45,20 +49,24 @@ protected function getSeverity($testCase) switch ($child->getName()) { case 'failure': $severity = self::SEVERITY_FAIL; + break 2; case 'error': - if ('PHPUnit\Framework\RiskyTestError' == $child['type']) { // == because convertion to string is desired + if ('PHPUnit\Framework\RiskyTestError' === (string)$child['type']) { // == because conversion to string is desired $severity = self::SEVERITY_RISKY; } else { $severity = self::SEVERITY_ERROR; } + break 2; case 'skipped': // skipped and ignored, can not distinguish $severity = self::SEVERITY_SKIPPED; + break 2; case 'warning': $severity = self::SEVERITY_WARN; + break 2; case 'system-out': case 'system-err': @@ -66,6 +74,7 @@ protected function getSeverity($testCase) continue 2; default: $severity = 'UNKNOWN RESULT TYPE: '.$child->getName(); + break 2; } } @@ -79,15 +88,15 @@ protected function buildMessage($testCase) $msg = $this->getMessageTrace($testCase); if ('' !== $msg) { //strip trace - $trPos = strrpos($msg, "\n\n"); + $trPos = \strrpos($msg, "\n\n"); if (false !== $trPos) { $tracePos = $trPos; - $msg = substr($msg, 0, $trPos); + $msg = \substr($msg, 0, $trPos); } } if ('' === $msg) { $msg = $testCase['class'].'::'.$testCase['name']; - }; + } $testCase['_tracePos'] = $tracePos; // will be converted to string return $msg; @@ -100,13 +109,13 @@ protected function getOutput($testCase) protected function buildTrace($testCase) { - if (!is_int($testCase['_tracePos'])) { + if (!\is_int($testCase['_tracePos'])) { $this->buildMessage($testCase); } if ($testCase['_tracePos'] >= 0) { - $stackStr = substr($this->getMessageTrace($testCase), (int)$testCase['_tracePos'] + 2, -1); - $trace = explode("\n", str_replace($this->buildPath, '.', $stackStr)); + $stackStr = \substr($this->getMessageTrace($testCase), (int)$testCase['_tracePos'] + 2, -1); + $trace = \explode("\n", \str_replace($this->buildPath, '.', $stackStr)); } else { $trace = []; } @@ -128,6 +137,7 @@ private function getMessageTrace($testCase) if ('' === $msg) { $msg = (string)$child; } + break 2; } } @@ -140,7 +150,7 @@ private function getMessageTrace($testCase) */ private function loadResultFile() { - if (!file_exists($this->outputFile) || 0 === filesize($this->outputFile)) { + if (!\file_exists($this->outputFile) || 0 === \filesize($this->outputFile)) { $this->internalProblem('empty output file'); return new SimpleXMLElement(''); // new empty element @@ -156,4 +166,14 @@ private function internalProblem($description) { throw new RuntimeException($description); } + + protected function getFileAndLine($testCase) + { + $attributes = $testCase->attributes(); + + return [ + 'file' => \str_replace($this->buildPath, '', $attributes['file']), + 'line' => (int) $attributes['line'] + ]; + } } diff --git a/src/Plugin/Util/TestResultParsers/Codeception.php b/src/Plugin/Util/TestResultParsers/Codeception.php index 2387d0f60..b64c57cb5 100644 --- a/src/Plugin/Util/TestResultParsers/Codeception.php +++ b/src/Plugin/Util/TestResultParsers/Codeception.php @@ -8,7 +8,11 @@ /** * Class Codeception * - * @author Adam Cooper + * @package PHP Censor + * @subpackage Application + * + * @author Adam Cooper + * @author Dmitry Khomutov */ class Codeception implements ParserInterface { @@ -74,7 +78,7 @@ public function parse() foreach ($testSuite->testcase as $testCase) { $testResult = [ 'suite' => (string)$testSuite['name'], - 'file' => str_replace($this->builder->buildPath, '/', (string)$testCase['file']), + 'file' => \str_replace($this->builder->buildPath, '/', (string)$testCase['file']), 'name' => (string)$testCase['name'], 'feature' => (string)$testCase['feature'], 'assertions' => (int)$testCase['assertions'], @@ -87,7 +91,7 @@ public function parse() // PHPUnit testcases does not have feature field. Use class::method instead if (!$testResult['feature']) { - $testResult['feature'] = sprintf('%s::%s', $testResult['class'], $testResult['name']); + $testResult['feature'] = \sprintf('%s::%s', $testResult['class'], $testResult['name']); } if (isset($testCase->failure) || isset($testCase->error)) { diff --git a/src/Plugin/Util/TestResultParsers/ParserInterface.php b/src/Plugin/Util/TestResultParsers/ParserInterface.php index ccd2d1eac..ecb08c977 100644 --- a/src/Plugin/Util/TestResultParsers/ParserInterface.php +++ b/src/Plugin/Util/TestResultParsers/ParserInterface.php @@ -2,6 +2,13 @@ namespace PHPCensor\Plugin\Util\TestResultParsers; +/** + * @package PHP Censor + * @subpackage Application + * + * @author Adam Cooper + * @author Dmitry Khomutov + */ interface ParserInterface { /** diff --git a/src/Plugin/WebhookNotify.php b/src/Plugin/WebhookNotify.php index c8331625f..eb0e7a0fe 100644 --- a/src/Plugin/WebhookNotify.php +++ b/src/Plugin/WebhookNotify.php @@ -7,20 +7,25 @@ use GuzzleHttp\Exception\GuzzleException; use PHPCensor\Builder; use PHPCensor\Exception\HttpException; +use PHPCensor\Common\Exception\InvalidArgumentException; use PHPCensor\Model\Build; use PHPCensor\Plugin; /** * Webhook notify Plugin * + * @package PHP Censor + * @subpackage Application + * * @author Lee Willis (Ademti Software) : https://www.ademti-software.co.uk + * @author Dmitry Khomutov */ class WebhookNotify extends Plugin { /** * @var string The URL to send the webhook to. */ - private $url; + private string $url; /** * @return string @@ -39,14 +44,14 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - if (!is_array($options)) { - throw new Exception('Please configure the options for the webhook_notify plugin!'); + if (!\is_array($options)) { + throw new InvalidArgumentException('Please configure the options for the webhook_notify plugin!'); } if (!isset($options['url'])) { - throw new Exception('Please define the url for webhook_notify plugin!'); + throw new InvalidArgumentException('Please define the url for webhook_notify plugin!'); } - $this->url = trim($options['url']); + $this->url = \trim($options['url']); } /** @@ -62,7 +67,7 @@ public function execute() 'project_title' => $this->build->getProjectTitle(), 'build_id' => $this->build->getId(), 'commit_id' => $this->build->getCommitId(), - 'short_commit_id' => substr($this->build->getCommitId(), 0, 7), + 'short_commit_id' => \substr($this->build->getCommitId(), 0, 7), 'branch' => $this->build->getBranch(), 'branch_link' => $this->build->getBranchLink(), 'committer_email' => $this->build->getCommitterEmail(), @@ -100,18 +105,22 @@ private function getReadableStatus($statusId) switch ($statusId) { case self::STATUS_PENDING: return 'Pending'; + break; case self::STATUS_RUNNING: return 'Running'; + break; case self::STATUS_SUCCESS: return 'Successful'; + break; case self::STATUS_FAILED: return 'Failed'; + break; } - return sprintf('Unknown (%d)', $statusId); + return \sprintf('Unknown (%d)', $statusId); } } diff --git a/src/Plugin/Wipe.php b/src/Plugin/Wipe.php index 764faa90e..590f2f025 100644 --- a/src/Plugin/Wipe.php +++ b/src/Plugin/Wipe.php @@ -9,7 +9,11 @@ /** * Wipe Plugin - Wipes a folder * + * @package PHP Censor + * @subpackage Application + * * @author Claus Due + * @author Dmitry Khomutov */ class Wipe extends Plugin { @@ -40,7 +44,7 @@ public function execute() return true; } - if (is_dir($this->directory)) { + if (\is_dir($this->directory)) { $cmd = 'rm -Rf "%s"'; return $this->builder->executeCommand($cmd, $this->directory); diff --git a/src/Plugin/XmppNotify.php b/src/Plugin/XmppNotify.php index 8d63156b3..1601728e0 100644 --- a/src/Plugin/XmppNotify.php +++ b/src/Plugin/XmppNotify.php @@ -9,44 +9,48 @@ /** * XMPP Notification - Send notification for successful or failure build * + * @package PHP Censor + * @subpackage Application + * * @author Alexandre Russo + * @author Dmitry Khomutov */ class XmppNotify extends Plugin { /** * @var string, username of sender account xmpp */ - protected $username; + protected $username = ''; /** * @var string, alias server of sender account xmpp */ - protected $server; + protected $server = ''; /** * @var string, password of sender account xmpp */ - protected $password; + protected $password = ''; /** * @var string, alias for sender */ - protected $alias; + protected $alias = ''; /** * @var string, use tls */ - protected $tls; + protected $tls = false; /** * @var array, list of recipients xmpp accounts */ - protected $recipients; + protected $recipients = []; /** * @var string, mask to format date */ - protected $dateFormat; + protected $dateFormat = '%c'; /** * @return string @@ -63,23 +67,15 @@ public function __construct(Builder $builder, Build $build, array $options = []) { parent::__construct($builder, $build, $options); - $this->username = ''; - $this->password = ''; - $this->server = ''; - $this->alias = ''; - $this->recipients = []; - $this->tls = false; - $this->dateFormat = '%c'; - $this->executable = $this->findBinary('sendxmpp'); /* * Set recipients list */ if (!empty($options['recipients'])) { - if (is_string($options['recipients'])) { + if (\is_string($options['recipients'])) { $this->recipients = [$options['recipients']]; - } elseif (is_array($options['recipients'])) { + } elseif (\is_array($options['recipients'])) { $this->recipients = $options['recipients']; } } @@ -111,9 +107,9 @@ protected function getConfigFormat() */ public function findConfigFile() { - if (file_exists($this->builder->buildPath . '.sendxmpprc')) { - if (md5(file_get_contents($this->builder->buildPath . '.sendxmpprc')) - !== md5($this->getConfigFormat())) { + if (\file_exists($this->builder->buildPath . '.sendxmpprc')) { + if (\md5(\file_get_contents($this->builder->buildPath . '.sendxmpprc')) + !== \md5($this->getConfigFormat())) { return null; } @@ -133,7 +129,7 @@ public function execute() /* * Without recipients we can't send notification */ - if (!is_array($this->recipients) || count($this->recipients) == 0) { + if (!\is_array($this->recipients) || \count($this->recipients) === 0) { return false; } @@ -141,9 +137,9 @@ public function execute() * Try to build conf file */ $configFile = $this->builder->buildPath . '.sendxmpprc'; - if (is_null($this->findConfigFile())) { - file_put_contents($configFile, $this->getConfigFormat()); - chmod($configFile, 0600); + if (\is_null($this->findConfigFile())) { + \file_put_contents($configFile, $this->getConfigFormat()); + \chmod($configFile, 0600); } /* @@ -154,7 +150,7 @@ public function execute() $tls = ' -t'; } - $messageFile = $this->builder->buildPath . uniqid('xmppmessage'); + $messageFile = $this->builder->buildPath . \uniqid('xmppmessage'); if ($this->buildMessage($messageFile) === false) { return false; } @@ -163,7 +159,7 @@ public function execute() * Send XMPP notification for all recipients */ $cmd = $sendxmpp . "%s -f %s -m %s %s"; - $recipients = implode(' ', $this->recipients); + $recipients = \implode(' ', $this->recipients); $success = $this->builder->executeCommand($cmd, $tls, $configFile, $messageFile, $recipients); @@ -188,8 +184,8 @@ protected function buildMessage($messageFile) $message = "✘ [" . $this->build->getProjectTitle() . "] Build #" . $this->build->getId() . " failure"; } - $message .= ' (' . strftime($this->dateFormat) . ')'; + $message .= ' (' . \strftime($this->dateFormat) . ')'; - return file_put_contents($messageFile, $message); + return \file_put_contents($messageFile, $message); } } diff --git a/src/ProcessControl/Factory.php b/src/ProcessControl/Factory.php deleted file mode 100644 index 19c89ac60..000000000 --- a/src/ProcessControl/Factory.php +++ /dev/null @@ -1,55 +0,0 @@ - - */ -class Factory -{ - /** - * ProcessControl singleton. - * - * @var ProcessControlInterface - */ - protected static $instance = null; - - /** - * Returns the ProcessControl singleton. - * - * @return ProcessControlInterface - */ - public static function getInstance() - { - if (static::$instance === null) { - static::$instance = static::createProcessControl(); - } - - return static::$instance; - } - - /** - * Create a ProcessControl depending on available extensions and the underlying OS. - * - * Check PosixProcessControl, WindowsProcessControl and UnixProcessControl, in that order. - * - * @return ProcessControlInterface - * - * @throws Exception - */ - public static function createProcessControl() - { - switch (true) { - case PosixProcessControl::isAvailable(): - return new PosixProcessControl(); - case UnixProcessControl::isAvailable(): - return new UnixProcessControl(); - } - - throw new Exception("No ProcessControl implementation available."); - } -} diff --git a/src/ProcessControl/PosixProcessControl.php b/src/ProcessControl/PosixProcessControl.php deleted file mode 100644 index 4677dada8..000000000 --- a/src/ProcessControl/PosixProcessControl.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -class PosixProcessControl implements ProcessControlInterface -{ - /** - * @param int $pid - * - * @return bool - */ - public function isRunning($pid) - { - // Signal "0" is not sent to the process, but posix_kill checks the process anyway; - return posix_kill($pid, 0); - } - - /** - * {@inheritDoc} - */ - public function kill($pid, $forcefully = false) - { - return posix_kill($pid, $forcefully ? 9 : 15); - } - - /** - * Check whether this posix_kill is available. - * - * @return bool - * - * @internal - */ - public static function isAvailable() - { - return function_exists('posix_kill'); - } -} diff --git a/src/ProcessControl/ProcessControlInterface.php b/src/ProcessControl/ProcessControlInterface.php deleted file mode 100644 index e3fce8c89..000000000 --- a/src/ProcessControl/ProcessControlInterface.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ -interface ProcessControlInterface -{ - /** - * Checks if a process exists. - * - * @param int $pid The process identifier. - * - * @return bool true is the process is running, else false. - */ - public function isRunning($pid); - - /** - * Terminate a running process. - * - * @param int $pid The process identifier. - * @param bool $forcefully Whether to gently (false) or forcefully (true) terminate the process. - * - * @return bool - */ - public function kill($pid, $forcefully = false); -} diff --git a/src/ProcessControl/UnixProcessControl.php b/src/ProcessControl/UnixProcessControl.php deleted file mode 100644 index 5f36bd117..000000000 --- a/src/ProcessControl/UnixProcessControl.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -class UnixProcessControl implements ProcessControlInterface -{ - /** - * Check process using the "ps" command. - * - * @param int $pid - * - * @return bool - */ - public function isRunning($pid) - { - $output = $exitCode = null; - exec(sprintf("ps %d", $pid), $output, $exitCode); - - return $exitCode === 0; - } - - /** - * {@inheritDoc} - */ - public function kill($pid, $forcefully = false) - { - $output = []; - $result = 1; - - exec(sprintf("kill -%d %d", $forcefully ? 9 : 15, $pid), $output, $result); - - return !$result; - } - - /** - * Check whether the commands "ps" and "kill" are available. - * - * @return bool - * - * @internal - */ - public static function isAvailable() - { - return DIRECTORY_SEPARATOR === '/' && exec("which ps") && exec("which kill"); - } -} diff --git a/src/Security/Authentication/LoginPasswordProviderInterface.php b/src/Security/Authentication/LoginPasswordProviderInterface.php index 9a46132ea..5e5d1c6f6 100644 --- a/src/Security/Authentication/LoginPasswordProviderInterface.php +++ b/src/Security/Authentication/LoginPasswordProviderInterface.php @@ -1,13 +1,19 @@ + * @author Dmitry Khomutov */ interface LoginPasswordProviderInterface extends UserProviderInterface { diff --git a/src/Security/Authentication/Service.php b/src/Security/Authentication/Service.php index 54369b10a..58b7ff901 100644 --- a/src/Security/Authentication/Service.php +++ b/src/Security/Authentication/Service.php @@ -1,30 +1,33 @@ + * @author Dmitry Khomutov */ class Service { /** - * @var Service + * The table of providers. */ - private static $instance; + private array $providers; - /** - * Return the service singleton. - * - * @return Service - */ - public static function getInstance() - { - if (self::$instance === null) { - $config = Config::getInstance()->get( + public function __construct( + ConfigurationInterface $configuration, + StoreRegistry $storeRegistry, + array $providers = [] + ) { + if (!$providers) { + $config = $configuration->get( 'php-censor.security.auth_providers', [ 'internal' => [ @@ -35,45 +38,21 @@ public static function getInstance() $providers = []; foreach ($config as $key => $providerConfig) { - $providers[$key] = self::buildProvider($key, $providerConfig); + $providers[$key] = self::buildProvider($storeRegistry, $key, $providerConfig); } - self::$instance = new self($providers); } - return self::$instance; + $this->providers = $providers; } - /** - * Create a provider from a given configuration. - * - * @param string $key - * @param array|string $config - * - * @return UserProviderInterface - */ - public static function buildProvider($key, $config) + public static function buildProvider(StoreRegistry $storeRegistry, string $key, array $config): UserProviderInterface { - $class = ucfirst($config['type']); - if (class_exists('\\PHPCensor\\Security\\Authentication\\UserProvider\\' . $class)) { + $class = \ucfirst($config['type']); + if (\class_exists('\\PHPCensor\\Security\\Authentication\\UserProvider\\' . $class)) { $class = '\\PHPCensor\\Security\\Authentication\\UserProvider\\' . $class; } - return new $class($key, $config); - } - - /** - * The table of providers. - * - * @var array - */ - private $providers; - - /** - * Initialize the service. - */ - public function __construct(array $providers) - { - $this->providers = $providers; + return new $class($storeRegistry, $key, $config); } /** @@ -81,7 +60,7 @@ public function __construct(array $providers) * * @return UserProviderInterface[] */ - public function getProviders() + public function getProviders(): array { return $this->providers; } @@ -91,7 +70,7 @@ public function getProviders() * * @return LoginPasswordProviderInterface[] */ - public function getLoginPasswordProviders() + public function getLoginPasswordProviders(): array { $providers = []; foreach ($this->providers as $key => $provider) { diff --git a/src/Security/Authentication/UserProvider/AbstractProvider.php b/src/Security/Authentication/UserProvider/AbstractProvider.php index a5b17e91b..683a49663 100644 --- a/src/Security/Authentication/UserProvider/AbstractProvider.php +++ b/src/Security/Authentication/UserProvider/AbstractProvider.php @@ -1,13 +1,18 @@ + * @author Dmitry Khomutov */ abstract class AbstractProvider implements UserProviderInterface { @@ -15,13 +20,16 @@ abstract class AbstractProvider implements UserProviderInterface protected array $config; - /** - * AbstractProvider constructor - */ - public function __construct(string $key, array $config) - { - $this->key = $key; - $this->config = $config; + protected StoreRegistry $storeRegistry; + + public function __construct( + StoreRegistry $storeRegistry, + string $key, + array $config + ) { + $this->key = $key; + $this->config = $config; + $this->storeRegistry = $storeRegistry; } public function getKey(): string diff --git a/src/Security/Authentication/UserProvider/Internal.php b/src/Security/Authentication/UserProvider/Internal.php index 169c49ba7..71f70b14b 100644 --- a/src/Security/Authentication/UserProvider/Internal.php +++ b/src/Security/Authentication/UserProvider/Internal.php @@ -1,31 +1,31 @@ + * @author Dmitry Khomutov */ class Internal extends AbstractProvider implements LoginPasswordProviderInterface { public function verifyPassword(User $user, string $password): bool { - return password_verify($password, $user->getHash()); + return \password_verify($password, $user->getHash()); } - public function checkRequirements() + public function checkRequirements(): void { // Always fine } - /** - * - * @return null - */ public function provisionUser(?string $identifier): ?User { return null; diff --git a/src/Security/Authentication/UserProvider/Ldap.php b/src/Security/Authentication/UserProvider/Ldap.php index 7461f804d..ed8bd81b0 100644 --- a/src/Security/Authentication/UserProvider/Ldap.php +++ b/src/Security/Authentication/UserProvider/Ldap.php @@ -1,17 +1,20 @@ */ class Ldap extends AbstractProvider implements LoginPasswordProviderInterface { @@ -54,21 +57,21 @@ public function verifyPassword(User $user, string $password): bool return false; } - public function checkRequirements() + public function checkRequirements(): void { // Always fine } /** - * + * @throws \PHPCensor\Common\Exception\RuntimeException */ public function provisionUser(?string $identifier): ?User { /** @var UserStore $user */ - $user = Factory::getStore('User'); - $userService = new UserService($user); + $user = $this->storeRegistry->get('User'); + $userService = new UserService($this->storeRegistry, $user); - $parts = explode("@", $identifier); + $parts = \explode("@", $identifier); $username = $parts[0]; return $userService->createUser($username, $identifier, $this->key, $this->config, '', false); diff --git a/src/Security/Authentication/UserProviderInterface.php b/src/Security/Authentication/UserProviderInterface.php index cfab243db..f89fca3a4 100644 --- a/src/Security/Authentication/UserProviderInterface.php +++ b/src/Security/Authentication/UserProviderInterface.php @@ -1,14 +1,18 @@ + * @author Dmitry Khomutov */ interface UserProviderInterface { @@ -17,12 +21,12 @@ interface UserProviderInterface * * @throws Exception */ - public function checkRequirements(); + public function checkRequirements(): void; /** * Provision an new user for the given identifier. * - * @param string $identifier The user identifier. + * @param string|null $identifier The user identifier. * * @return User|null The new user or null if the provider does not know the user. */ diff --git a/src/Service/BuildService.php b/src/Service/BuildService.php index 69936288d..ac12d10a1 100644 --- a/src/Service/BuildService.php +++ b/src/Service/BuildService.php @@ -1,5 +1,7 @@ + * @author Dmitry Khomutov */ class BuildService { - /** - * @var BuildStore - */ - protected $buildStore; + private BuildStore $buildStore; - /** - * @var ProjectStore - */ - protected $projectStore; + private ProjectStore $projectStore; - /** - * @var bool - */ - public $queueError = false; + private ConfigurationInterface $configuration; + + private StoreRegistry $storeRegistry; + + private BuildFactory $buildFactory; + + public bool $queueError = false; public function __construct( + ConfigurationInterface $configuration, + StoreRegistry $storeRegistry, + BuildFactory $buildFactory, BuildStore $buildStore, ProjectStore $projectStore ) { - $this->buildStore = $buildStore; - $this->projectStore = $projectStore; + $this->configuration = $configuration; + $this->storeRegistry = $storeRegistry; + $this->buildStore = $buildStore; + $this->projectStore = $projectStore; + $this->buildFactory = $buildFactory; } - /** - * @param int|null $environmentId - * @param string $commitId - * @param string|null $branch - * @param string|null $tag - * @param string|null $committerEmail - * @param string|null $commitMessage - * @param int $source - * @param int $userId - * @param array|null $extra - * - * @return Build - */ public function createBuild( Project $project, - $environmentId = null, - $commitId = '', - $branch = null, - $tag = null, - $committerEmail = null, - $commitMessage = null, - $source = Build::SOURCE_UNKNOWN, - $userId = null, - $extra = null - ) { - $build = new Build(); + ?int $environmentId = null, + string $commitId = '', + ?string $branch = null, + ?string $tag = null, + ?string $committerEmail = null, + ?string $commitMessage = null, + int $source = Build::SOURCE_UNKNOWN, + ?int $userId = null, + ?array $extra = null + ): Build { + $build = new Build($this->storeRegistry); $build->setCreateDate(new DateTime()); $build->setProjectId($project->getId()); $build->setStatusPending(); $build->setEnvironmentid($environmentId); - if (!is_null($extra)) { + if (!\is_null($extra)) { $build->setExtra($extra); } @@ -92,7 +92,7 @@ public function createBuild( $userId = null; } $build->setUserId($userId); - $build->setCommitId((string)$commitId); + $build->setCommitId($commitId); if (!empty($branch)) { $build->setBranch($branch); @@ -118,7 +118,7 @@ public function createBuild( if (!empty($buildId)) { $project = $build->getProject(); - $build = BuildFactory::getBuild($build); + $build = $this->buildFactory->getBuild($build); $build->sendStatusPostback(); $this->addBuildToQueue( $build, @@ -132,17 +132,17 @@ public function createBuild( /** * @throws HttpException */ - public function createPeriodicalBuilds(Logger $logger) + public function createPeriodicalBuilds(LoggerInterface $logger): void { $periodicalConfig = null; - if (file_exists(APP_DIR . 'periodical.yml')) { + if (\file_exists(APP_DIR . 'periodical.yml')) { try { $periodicalConfig = (new Yaml())->parse( - file_get_contents(APP_DIR . 'periodical.yml') + \file_get_contents(APP_DIR . 'periodical.yml') ); } catch (ParseException $e) { $logger->error( - sprintf( + \sprintf( 'Invalid periodical builds config ("app/periodical.yml")! Exception: %s', $e->getMessage() ), @@ -155,7 +155,7 @@ public function createPeriodicalBuilds(Logger $logger) if (empty($periodicalConfig) || empty($periodicalConfig['projects']) || - !is_array($periodicalConfig['projects'])) { + !\is_array($periodicalConfig['projects'])) { $logger->warning('Empty periodical builds config ("app/periodical.yml")!'); return; @@ -163,14 +163,15 @@ public function createPeriodicalBuilds(Logger $logger) $buildsCount = 0; foreach ($periodicalConfig['projects'] as $projectId => $projectConfig) { + /** @var Project $project */ $project = $this->projectStore->getById((int)$projectId); if (!$project || empty($projectConfig['interval']) || empty($projectConfig['branches']) || - !is_array($projectConfig['branches'])) { + !\is_array($projectConfig['branches'])) { $logger->warning( - sprintf( + \sprintf( 'Invalid/empty section for project #%s ("app/periodical.yml")!', $projectId ) @@ -183,14 +184,14 @@ public function createPeriodicalBuilds(Logger $logger) try { $interval = new DateInterval($projectConfig['interval']); - } catch (Exception $e) { + } catch (\Throwable $e) { $logger->error( - sprintf( + \sprintf( 'Invalid datetime interval for project #%s! Exception: %s', $projectId, $e->getMessage() ), - $e + [$e] ); return; @@ -228,24 +229,20 @@ public function createPeriodicalBuilds(Logger $logger) } $logger->notice( - sprintf( + \sprintf( 'Created %d periodical builds for %d projects.', $buildsCount, - count($periodicalConfig['projects']) + \count($periodicalConfig['projects']) ) ); } /** - * @param int $source - * - * @return Build - * * @throws Exception */ - public function createDuplicateBuild(Build $originalBuild, $source) + public function createDuplicateBuild(Build $originalBuild, int $source): Build { - $build = new Build(); + $build = new Build($this->storeRegistry); $build->setParentId($originalBuild->getId()); $build->setProjectId($originalBuild->getProjectId()); $build->setCommitId($originalBuild->getCommitId()); @@ -265,7 +262,7 @@ public function createDuplicateBuild(Build $originalBuild, $source) $buildId = $build->getId(); if (!empty($buildId)) { - $build = BuildFactory::getBuild($build); + $build = $this->buildFactory->getBuild($build); $project = $build->getProject(); $build->sendStatusPostback(); $this->addBuildToQueue( @@ -278,13 +275,11 @@ public function createDuplicateBuild(Build $originalBuild, $source) } /** - * @param int $projectId - * * @throws HttpException */ - public function deleteOldByProject($projectId) + public function deleteOldByProject(int $projectId): void { - $keepBuilds = (int)Config::getInstance()->get('php-censor.build.keep_builds', 100); + $keepBuilds = (int)$this->configuration->get('php-censor.build.keep_builds', 100); $builds = $this->buildStore->getOldByProject((int)$projectId, $keepBuilds); /** @var Build $build */ @@ -294,10 +289,7 @@ public function deleteOldByProject($projectId) } } - /** - * @param int $projectId - */ - public function deleteAllByProject($projectId) + public function deleteAllByProject(int $projectId): void { $this->buildStore->deleteAllByProject((int)$projectId); @@ -311,24 +303,26 @@ public function deleteAllByProject($projectId) $fileSystem = new Filesystem(); foreach ($projectPaths as $projectPath) { - if (is_link($projectPath)) { + if (\is_link($projectPath)) { // Remove the symlink without using recursive. - exec(sprintf('rm "%s"', $projectPath)); + \exec(\sprintf('rm "%s"', $projectPath)); } else { $fileSystem->remove($projectPath); } } - } catch (Exception $e) { + } catch (\Throwable $e) { } } /** * Delete a given build. - * - * @return bool */ - public function deleteBuild(Build $build) + public function deleteBuild(Build $build): bool { + if (!$build->getId()) { + return false; + } + $build->removeBuildDirectory(true); return $this->buildStore->delete($build); @@ -337,9 +331,9 @@ public function deleteBuild(Build $build) /** * Takes a build and puts it into the queue to be run (if using a queue) * - * @param int $buildPriority priority in queue relative to default + * @param int $buildPriority priority in queue relative to default */ - public function addBuildToQueue(Build $build, $buildPriority = Project::DEFAULT_BUILD_PRIORITY) + public function addBuildToQueue(Build $build, int $buildPriority = Project::DEFAULT_BUILD_PRIORITY): void { $buildId = $build->getId(); @@ -354,31 +348,25 @@ public function addBuildToQueue(Build $build, $buildPriority = Project::DEFAULT_ $this->addJobToQueue(BuildWorker::JOB_TYPE_BUILD, $jobData, ($buildPriority + Project::OFFSET_BETWEEN_BUILD_AND_QUEUE)); } - /** - * @param string $jobType - * @param int $queuePriority - */ - public function addJobToQueue($jobType, array $jobData, $queuePriority = PheanstalkInterface::DEFAULT_PRIORITY) + public function addJobToQueue(string $jobType, array $jobData, int $queuePriority = PheanstalkInterface::DEFAULT_PRIORITY): void { - $config = Config::getInstance(); - $settings = $config->get('php-censor.queue', []); - + $settings = $this->configuration->get('php-censor.queue', []); if (!empty($settings['host']) && !empty($settings['name'])) { $jobData['type'] = $jobType; try { $pheanstalk = Pheanstalk::create( $settings['host'], - $config->get('php-censor.queue.port', PheanstalkInterface::DEFAULT_PORT) + (int)$this->configuration->get('php-censor.queue.port', PheanstalkInterface::DEFAULT_PORT) ); $pheanstalk->useTube($settings['name']); $pheanstalk->put( - json_encode($jobData), + \json_encode($jobData), $queuePriority, PheanstalkInterface::DEFAULT_DELAY, - $config->get('php-censor.queue.lifetime', 600) + $this->configuration->get('php-censor.queue.lifetime', 600) ); - } catch (Exception $ex) { + } catch (\Throwable $ex) { $this->queueError = true; } } diff --git a/src/Service/BuildStatusService.php b/src/Service/BuildStatusService.php index df40e403e..12230e076 100644 --- a/src/Service/BuildStatusService.php +++ b/src/Service/BuildStatusService.php @@ -1,58 +1,43 @@ + * @author Dmitry Khomutov */ class BuildStatusService { - /** - * @var BuildStatusService - */ - protected $prevService = null; + private ?BuildStatusService $prevService = null; - /** - * @var Project - */ - protected $project; + private Project $project; - /** - * @var string - */ - protected $branch; + private string $branch; - /** - * @var Build - */ - protected $build; + private ?Build $build; - /** - * @var string - */ - protected $url; + private string $url; - /** - * @var array - */ - protected $finishedStatusIds = [ + private array $finishedStatusIds = [ Build::STATUS_SUCCESS, Build::STATUS_FAILED, ]; - /** - * @param string $branch - * @param bool $isParent - */ public function __construct( - $branch, + string $branch, Project $project, - Build $build = null, - $isParent = false + ?Build $build = null, + bool $isParent = false ) { $this->project = $project; $this->branch = $branch; @@ -60,33 +45,25 @@ public function __construct( if ($this->build) { $this->loadParentBuild($isParent); } - if (defined('APP_URL')) { + if (\defined('APP_URL')) { $this->setUrl(APP_URL); } } - /** - * @param string $url - */ - public function setUrl($url) + public function setUrl(string $url): void { $this->url = $url; } - /** - * @return Build - */ - public function getBuild() + public function getBuild(): Build { return $this->build; } /** - * @param bool $isParent - * * @throws Exception */ - protected function loadParentBuild($isParent = true) + protected function loadParentBuild(bool $isParent = true): void { if ($isParent === false && !$this->isFinished()) { $lastFinishedBuild = $this->project->getLatestBuild($this->branch, $this->finishedStatusIds); @@ -102,46 +79,32 @@ protected function loadParentBuild($isParent = true) } } - /** - * @return string - */ - public function getActivity() + public function getActivity(): string { - if (in_array($this->build->getStatus(), $this->finishedStatusIds, true)) { + if (\in_array($this->build->getStatus(), $this->finishedStatusIds, true)) { return 'Sleeping'; - } elseif ($this->build->getStatus() == Build::STATUS_PENDING) { + } elseif ($this->build->getStatus() === Build::STATUS_PENDING) { return 'Pending'; - } elseif ($this->build->getStatus() == Build::STATUS_RUNNING) { - return 'Building'; } - return 'Unknown'; + return 'Building'; } - /** - * @return string - */ - public function getName() + public function getName(): string { return $this->project->getTitle() . ' / ' . $this->branch; } - /** - * @return bool - */ - public function isFinished() + public function isFinished(): bool { - if (in_array($this->build->getStatus(), $this->finishedStatusIds, true)) { + if (\in_array($this->build->getStatus(), $this->finishedStatusIds, true)) { return true; } return false; } - /** - * @return Build|null - */ - public function getFinishedBuildInfo() + public function getFinishedBuildInfo(): ?Build { if ($this->isFinished()) { return $this->build; @@ -152,22 +115,16 @@ public function getFinishedBuildInfo() return null; } - /** - * @return int|string - */ - public function getLastBuildLabel() + public function getLastBuildLabel(): string { if ($buildInfo = $this->getFinishedBuildInfo()) { - return $buildInfo->getId(); + return (string)$buildInfo->getId(); } return ''; } - /** - * @return string - */ - public function getLastBuildTime() + public function getLastBuildTime(): string { $dateFormat = 'Y-m-d\\TH:i:sO'; if ($buildInfo = $this->getFinishedBuildInfo()) { @@ -177,45 +134,25 @@ public function getLastBuildTime() return ''; } - /** - * @return string - */ - public function getBuildStatus(Build $build) + public function getLastBuildStatus(): string { - switch ($build->getStatus()) { - case Build::STATUS_SUCCESS: + if ($build = $this->getFinishedBuildInfo()) { + if (Build::STATUS_SUCCESS === $build->getStatus()) { return 'Success'; - case Build::STATUS_FAILED: - return 'Failure'; - } - - return 'Unknown'; - } + } - /** - * @return string - */ - public function getLastBuildStatus() - { - if ($build = $this->getFinishedBuildInfo()) { - return $this->getBuildStatus($build); + return 'Failure'; } return ''; } - /** - * @return string - */ - public function getBuildUrl() + public function getBuildUrl(): string { return $this->url . 'build/view/' . $this->build->getId(); } - /** - * @return array - */ - public function toArray() + public function toArray(): array { if (!$this->build) { return []; diff --git a/src/Service/ProjectService.php b/src/Service/ProjectService.php index 9f3bf44b4..d90f5de20 100644 --- a/src/Service/ProjectService.php +++ b/src/Service/ProjectService.php @@ -1,43 +1,46 @@ + * @author Dmitry Khomutov */ class ProjectService { - /** - * @var ProjectStore - */ - protected $projectStore; + private ProjectStore $projectStore; - public function __construct(ProjectStore $projectStore) - { - $this->projectStore = $projectStore; + private StoreRegistry $storeRegistry; + + public function __construct( + StoreRegistry $storeRegistry, + ProjectStore $projectStore + ) { + $this->storeRegistry = $storeRegistry; + $this->projectStore = $projectStore; } /** * Create a new project model and use the project store to save it. - * - * @param string $title - * @param string $type - * @param string $reference - * @param int $userId - * @param array $options - * - * @return Project */ - public function createProject($title, $type, $reference, $userId, $options = []) + public function createProject(string $title, string $type, string $reference, int $userId, array $options = []): Project { // Create base project and use updateProject() to set its properties: - $project = new Project(); + $project = new Project($this->storeRegistry); $project->setCreateDate(new DateTime()); $project->setUserId((int)$userId); @@ -46,15 +49,8 @@ public function createProject($title, $type, $reference, $userId, $options = []) /** * Update the properties of a given project. - * - * @param string $title - * @param string $type - * @param string $reference - * @param array $options - * - * @return Project */ - public function updateProject(Project $project, $title, $type, $reference, $options = []) + public function updateProject(Project $project, string $title, string $type, string $reference, array $options = []): Project { // Set basic properties: $project->setTitle($title); @@ -65,39 +61,39 @@ public function updateProject(Project $project, $title, $type, $reference, $opti $project->setOverwriteBuildConfig(true); // Handle extra project options: - if (array_key_exists('ssh_private_key', $options)) { + if (\array_key_exists('ssh_private_key', $options)) { $project->setSshPrivateKey($options['ssh_private_key']); } - if (array_key_exists('ssh_public_key', $options)) { + if (\array_key_exists('ssh_public_key', $options)) { $project->setSshPublicKey($options['ssh_public_key']); } - if (array_key_exists('overwrite_build_config', $options)) { + if (\array_key_exists('overwrite_build_config', $options)) { $project->setOverwriteBuildConfig($options['overwrite_build_config']); } - if (array_key_exists('build_config', $options)) { + if (\array_key_exists('build_config', $options)) { $project->setBuildConfig($options['build_config']); } - if (array_key_exists('allow_public_status', $options)) { + if (\array_key_exists('allow_public_status', $options)) { $project->setAllowPublicStatus($options['allow_public_status']); } - if (array_key_exists('archived', $options)) { + if (\array_key_exists('archived', $options)) { $project->setArchived($options['archived']); } - if (array_key_exists('default_branch', $options)) { + if (\array_key_exists('default_branch', $options)) { $project->setDefaultBranch($options['default_branch']); } - if (array_key_exists('default_branch_only', $options)) { + if (\array_key_exists('default_branch_only', $options)) { $project->setDefaultBranchOnly($options['default_branch_only']); } - if (array_key_exists('group', $options)) { + if (\array_key_exists('group', $options)) { $project->setGroupId((int)$options['group']); } else { $project->setGroupId(1); @@ -109,7 +105,7 @@ public function updateProject(Project $project, $title, $type, $reference, $opti /** @var Project $project */ $project = $this->projectStore->save($project); - if (array_key_exists('environments', $options)) { + if (\array_key_exists('environments', $options)) { $project->setEnvironments($options['environments']); } @@ -118,39 +114,36 @@ public function updateProject(Project $project, $title, $type, $reference, $opti /** * Delete a given project. - * - * @return bool */ - public function deleteProject(Project $project) + public function deleteProject(Project $project): bool { - try { - $fileSystem = new Filesystem(); - - $fileSystem->remove(RUNTIME_DIR . 'builds/' . $project->getId()); - $fileSystem->remove(PUBLIC_DIR . 'artifacts/pdepend/' . $project->getId()); - $fileSystem->remove(PUBLIC_DIR . 'artifacts/phpunit/' . $project->getId()); - } catch (Exception $e) { + if (!$project->getId()) { + return false; } + $fileSystem = new Filesystem(); + + $fileSystem->remove(RUNTIME_DIR . 'builds/' . $project->getId()); + $fileSystem->remove(PUBLIC_DIR . 'artifacts/pdepend/' . $project->getId()); + $fileSystem->remove(PUBLIC_DIR . 'artifacts/phpunit/' . $project->getId()); + return $this->projectStore->delete($project); } /** * In circumstances where it is necessary, populate access information based on other project properties. - * - * @return Project */ - protected function processAccessInformation(Project $project) + public function processAccessInformation(Project $project): Project { $reference = $project->getReference(); - if (in_array($project->getType(), [ + if (\in_array($project->getType(), [ Project::TYPE_GITHUB, Project::TYPE_GITLAB ], true)) { $info = []; - if (preg_match( + if (\preg_match( '#^((https|http|ssh)://)?((.+)@)?(([^/:]+):?)(:?([0-9]*)/?)(.+)\.git#', $reference, $matches diff --git a/src/Service/UserService.php b/src/Service/UserService.php index 981a7914a..49b1a19f0 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -1,43 +1,42 @@ + * @author Dmitry Khomutov */ class UserService { - /** - * @var UserStore - */ - protected $store; + private UserStore $store; - public function __construct(UserStore $store) - { - $this->store = $store; + private StoreRegistry $storeRegistry; + + public function __construct( + StoreRegistry $storeRegistry, + UserStore $store + ) { + $this->storeRegistry = $storeRegistry; + $this->store = $store; } - /** - * Create a new user. - * - * @param string $name - * @param string $email - * @param string $providerKey - * @param array $providerData - * @param string $password - * @param bool $isAdmin - * - * @return User - */ - public function createUser($name, $email, $providerKey, $providerData, $password, $isAdmin = false) + public function createUser(string $name, string $email, string $providerKey, array $providerData, string $password, bool $isAdmin = false): ?User { - $user = new User(); + $user = new User($this->storeRegistry); $user->setName($name); $user->setEmail($email); - $user->setHash(password_hash($password, PASSWORD_DEFAULT)); + $user->setHash(\password_hash($password, PASSWORD_DEFAULT)); $user->setProviderKey($providerKey); $user->setProviderData($providerData); $user->setIsAdmin($isAdmin); @@ -48,8 +47,6 @@ public function createUser($name, $email, $providerKey, $providerData, $password /** * Update a user. * - * @param string $name - * @param string $emailAddress * @param string $password * @param bool $isAdmin * @param string $language @@ -57,16 +54,16 @@ public function createUser($name, $email, $providerKey, $providerData, $password * * @return User */ - public function updateUser(User $user, $name, $emailAddress, $password = null, $isAdmin = null, $language = null, $perPage = null) + public function updateUser(User $user, string $name, string $emailAddress, ?string $password = null, ?bool $isAdmin = null, ?string $language = null, ?int $perPage = null): ?User { $user->setName($name); $user->setEmail($emailAddress); if (!empty($password)) { - $user->setHash(password_hash($password, PASSWORD_DEFAULT)); + $user->setHash(\password_hash($password, PASSWORD_DEFAULT)); } - if (!is_null($isAdmin)) { + if (!\is_null($isAdmin)) { $user->setIsAdmin($isAdmin); } @@ -78,11 +75,13 @@ public function updateUser(User $user, $name, $emailAddress, $password = null, $ /** * Delete a user. - * - * @return bool */ - public function deleteUser(User $user) + public function deleteUser(User $user): bool { + if (!$user->getId()) { + return false; + } + return $this->store->delete($user); } } diff --git a/src/Store.php b/src/Store.php index 9794e3226..02949dabf 100644 --- a/src/Store.php +++ b/src/Store.php @@ -1,66 +1,92 @@ + * @author Dmitry Khomutov + */ abstract class Store { - /** - * @var string - */ - protected $modelName = null; + protected string $modelName = ''; - /** - * @var string - */ - protected $tableName = ''; + protected string $tableName = ''; - /** - * @var string - */ - protected $primaryKey = null; + protected string $primaryKey = 'id'; - /** - * @param string $key - * @param string $useConnection - * - * @return Model|null - */ - abstract public function getByPrimaryKey($key, $useConnection = 'read'); + protected DatabaseManager $databaseManager; - /** - * @throws RuntimeException - */ - public function __construct() + protected StoreRegistry $storeRegistry; + + public function __construct( + DatabaseManager $databaseManager, + StoreRegistry $storeRegistry + ) { + $this->databaseManager = $databaseManager; + $this->storeRegistry = $storeRegistry; + } + + public function getById(int $id, string $useConnection = 'read'): ?Model { - if (empty($this->primaryKey)) { - throw new RuntimeException('Save not implemented for this store.'); + $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{id}} = :id LIMIT 1'; + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); + + if ($stmt) { + $stmt->bindValue(':id', $id); + + if ($stmt->execute()) { + if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { + return new $this->modelName($this->storeRegistry, $data); + } + } + } + + return null; + } + + public function getAll(string $useConnection = 'read'): array + { + $query = 'SELECT * FROM {{' . $this->tableName . '}}'; + $countQuery = 'SELECT COUNT(*) AS {{count}} FROM {{' . $this->tableName . '}}'; + + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($countQuery); + $stmt->execute(); + $res = $stmt->fetch(PDO::FETCH_ASSOC); + $count = (int)$res['count']; + + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); + $stmt->execute(); + $res = $stmt->fetchAll(PDO::FETCH_ASSOC); + $rtn = []; + + foreach ($res as $data) { + $rtn[] = new $this->modelName($this->storeRegistry, $data); } + + return ['items' => $rtn, 'count' => $count]; } /** - * @param array $where - * @param int $limit - * @param int $offset - * @param array $order - * @param string $whereType - * - * @return array - * + * @throws Common\Exception\Exception * @throws InvalidArgumentException */ public function getWhere( - $where = [], - $limit = 25, - $offset = 0, - $order = [], - $whereType = 'AND' - ) { - $query = 'SELECT * FROM {{' . $this->tableName . '}}'; + array $where = [], + int $limit = 25, + int $offset = 0, + array $order = [], + string $whereType = 'AND' + ): array { + $query = 'SELECT * FROM {{' . $this->tableName . '}}'; $countQuery = 'SELECT COUNT(*) AS {{count}} FROM {{' . $this->tableName . '}}'; $wheres = []; @@ -68,24 +94,24 @@ public function getWhere( foreach ($where as $key => $value) { $key = $this->fieldCheck($key); - if (!is_array($value)) { + if (!\is_array($value)) { $params[] = $value; $wheres[] = $key . ' = ?'; } } - if (count($wheres)) { - $query .= ' WHERE (' . implode(' ' . $whereType . ' ', $wheres) . ')'; - $countQuery .= ' WHERE (' . implode(' ' . $whereType . ' ', $wheres) . ')'; + if (\count($wheres)) { + $query .= ' WHERE (' . \implode(' ' . $whereType . ' ', $wheres) . ')'; + $countQuery .= ' WHERE (' . \implode(' ' . $whereType . ' ', $wheres) . ')'; } - if (count($order)) { + if (\count($order)) { $orders = []; foreach ($order as $key => $value) { $orders[] = $this->fieldCheck($key) . ' ' . $value; } - $query .= ' ORDER BY ' . implode(', ', $orders); + $query .= ' ORDER BY ' . \implode(', ', $orders); } if ($limit) { @@ -96,175 +122,195 @@ public function getWhere( $query .= ' OFFSET ' . $offset; } - $stmt = Database::getConnection('read')->prepareCommon($countQuery); + $stmt = $this->databaseManager->getConnection('read')->prepare($countQuery); $stmt->execute($params); $res = $stmt->fetch(PDO::FETCH_ASSOC); $count = (int)$res['count']; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->execute($params); $res = $stmt->fetchAll(PDO::FETCH_ASSOC); $rtn = []; foreach ($res as $data) { - $rtn[] = new $this->modelName($data); + $rtn[] = new $this->modelName($this->storeRegistry, $data); } return ['items' => $rtn, 'count' => $count]; } /** - * @param bool $saveAllColumns - * * @throws InvalidArgumentException - * - * @return Model|null + * @throws Exception */ - public function save(Model $obj, $saveAllColumns = false) + public function save(Model $model): ?Model { - if (!($obj instanceof $this->modelName)) { - throw new InvalidArgumentException(get_class($obj) . ' is an invalid model type for this store.'); + if (!($model instanceof $this->modelName)) { + throw new InvalidArgumentException(\get_class($model) . ' is an invalid model type for this store.'); } - $data = $obj->getDataArray(); + $data = $this->getData($model); - if (isset($data[$this->primaryKey])) { - $rtn = $this->saveByUpdate($obj, $saveAllColumns); - } else { - $rtn = $this->saveByInsert($obj, $saveAllColumns); + if ($model->getId() !== null) { + return $this->saveByUpdate($model, $data); } - return $rtn; + return $this->saveByInsert($model, $data); } /** - * @param bool $saveAllColumns - * - * @return Model|null - * * @throws Exception */ - public function saveByUpdate(Model $obj, $saveAllColumns = false) + protected function saveByUpdate(Model $model, array $data): ?Model { - $rtn = null; - $data = $obj->getDataArray(); - $modified = ($saveAllColumns) ? array_keys($data) : $obj->getModified(); + if (empty($data)) { + return $model; + } - $updates = []; + $updates = []; $updateParams = []; - foreach ($modified as $key) { - $updates[] = $key . ' = :' . $key; - $updateParams[] = [$key, $data[$key]]; + foreach ($data as $column => $value) { + $updates[] = $column . ' = :' . $column; + $updateParams[] = [$column, $value]; } - if (count($updates)) { - $qs = sprintf( - 'UPDATE {{%s}} SET %s WHERE {{%s}} = :primaryKey', - $this->tableName, - implode(', ', $updates), - $this->primaryKey - ); - $q = Database::getConnection('write')->prepareCommon($qs); - - foreach ($updateParams as $updateParam) { - $q->bindValue(':' . $updateParam[0], $updateParam[1]); - } - - $q->bindValue(':primaryKey', $data[$this->primaryKey]); - $q->execute(); - - $rtn = $this->getByPrimaryKey($data[$this->primaryKey], 'write'); - } else { - $rtn = $obj; + $queryString = \sprintf( + 'UPDATE {{%s}} SET %s WHERE {{%s}} = :primaryKey', + $this->tableName, + \implode(', ', $updates), + $this->primaryKey + ); + $query = $this->databaseManager + ->getConnection('write') + ->prepare($queryString); + + foreach ($updateParams as $updateParam) { + $query->bindValue(':' . $updateParam[0], $updateParam[1]); } - return $rtn; + $query->bindValue(':primaryKey', $data[$this->primaryKey]); + $query->execute(); + + return $this->getById($model->getId()); } /** - * @param bool $saveAllColumns - * - * @return Model|null - * * @throws Exception */ - public function saveByInsert(Model $obj, $saveAllColumns = false) + protected function saveByInsert(Model $model, array $data): ?Model { - $rtn = null; - $data = $obj->getDataArray(); - $modified = ($saveAllColumns) ? array_keys($data) : $obj->getModified(); - - $cols = []; - $values = []; - $qParams = []; - foreach ($modified as $key) { - $cols[] = $key; - $values[] = ':' . $key; - $qParams[':' . $key] = $data[$key]; + if (empty($data)) { + return $model; } - if (count($cols)) { - $qs = sprintf( - 'INSERT INTO {{%s}} (%s) VALUES (%s)', - $this->tableName, - implode(', ', $cols), - implode(', ', $values) - ); - $q = Database::getConnection('write')->prepareCommon($qs); - - if ($q->execute($qParams)) { - $id = Database::getConnection('write')->lastInsertIdExtended($this->tableName); - $rtn = $this->getByPrimaryKey($id, 'write'); + $cols = []; + $values = []; + $queryParams = []; + foreach ($data as $column => $value) { + if ('id' !== $column) { + $cols[] = $column; + $values[] = ':' . $column; + $queryParams[':' . $column] = $value; } } - return $rtn; + $queryString = \sprintf( + 'INSERT INTO {{%s}} (%s) VALUES (%s)', + $this->tableName, + \implode(', ', $cols), + \implode(', ', $values) + ); + $query = $this->databaseManager + ->getConnection('write') + ->prepare($queryString); + + if (!$query->execute($queryParams)) { + return $model; + } + + $id = $this->databaseManager + ->getConnection('write') + ->lastInsertId($this->tableName); + + return $this->getById($id); } /** - * @return bool - * + * @throws Common\Exception\Exception * @throws InvalidArgumentException */ - public function delete(Model $obj) + public function delete(Model $model): bool { - if (!($obj instanceof $this->modelName)) { - throw new InvalidArgumentException(get_class($obj) . ' is an invalid model type for this store.'); + if (!($model instanceof $this->modelName)) { + throw new InvalidArgumentException(\get_class($model) . ' is an invalid model type for this store.'); } - $data = $obj->getDataArray(); - - $q = Database::getConnection('write') - ->prepareCommon( - sprintf( + $query = $this->databaseManager->getConnection('write') + ->prepare( + \sprintf( 'DELETE FROM {{%s}} WHERE {{%s}} = :primaryKey', $this->tableName, $this->primaryKey ) ); - $q->bindValue(':primaryKey', $data[$this->primaryKey]); - $q->execute(); + $query->bindValue(':primaryKey', $model->getId()); + $query->execute(); return true; } /** - * @param string $field - * - * @return string - * * @throws InvalidArgumentException */ - protected function fieldCheck($field) + protected function fieldCheck(string $field): string { if (empty($field)) { throw new InvalidArgumentException('You cannot have an empty field name.'); } - if (strpos($field, '.') === false) { + if (\strpos($field, '.') === false) { return '{{' . $this->tableName . '}}.{{' . $field . '}}'; } return $field; } + + protected function getData(Model $model): array + { + $rawData = $model->getDataArray(); + $modified = \array_keys($model->getId() === null ? $rawData : $model->getModified()); + $data = []; + foreach ($rawData as $column => $value) { + if (!\in_array($column, $modified, true)) { + continue; + } + $data[$column] = $this->castToDatabase($model->getDataType($column), $value); + } + + return $data; + } + + /** + * @return mixed + */ + private function castToDatabase(string $type, $value) + { + if ($value === null || \gettype($value) === 'string') { + return $value; + } + + switch ($type) { + case 'datetime': + return $value->format('Y-m-d H:i:s'); + case 'array': + return \json_encode($value); + case 'newline': + return \implode("\n", $value); + case 'bool': + case 'boolean': + return $value ? 1 : 0; + default: + return $value; + } + } } diff --git a/src/Store/BuildErrorStore.php b/src/Store/BuildErrorStore.php index e9a6ed2d3..b40894faf 100644 --- a/src/Store/BuildErrorStore.php +++ b/src/Store/BuildErrorStore.php @@ -1,90 +1,37 @@ + * @author Dmitry Khomutov + */ class BuildErrorStore extends Store { - /** - * @var string - */ - protected $tableName = 'build_errors'; - - /** - * @var string - */ - protected $modelName = '\PHPCensor\Model\BuildError'; + protected string $tableName = 'build_errors'; - /** - * @var string - */ - protected $primaryKey = 'id'; - - /** - * Get a BuildError by primary key (Id) - * - * @param int $key - * @param string $useConnection - * - * @return BuildError|null - */ - public function getByPrimaryKey($key, $useConnection = 'read') - { - return $this->getById($key, $useConnection); - } - - /** - * Get a single BuildError by Id. - * - * @param int $id - * @param string $useConnection - * - * @return BuildError|null - * - * @throws HttpException - */ - public function getById($id, $useConnection = 'read') - { - if (is_null($id)) { - throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); - } - - $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{id}} = :id LIMIT 1'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); - $stmt->bindValue(':id', $id); - - if ($stmt->execute()) { - if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new BuildError($data); - } - } - - return null; - } + protected string $modelName = BuildError::class; /** * Get multiple BuildError by BuildId. * - * @param int $buildId - * @param int|null $limit - * @param int $offset - * @param string|null $plugin - * @param int|null $severity - * @param string|null $isNew - * - * @return array - * * @throws HttpException */ - public function getByBuildId($buildId, $limit = null, $offset = 0, $plugin = null, $severity = null, $isNew = null) + public function getByBuildId(int $buildId, ?int $limit = null, int $offset = 0, ?string $plugin = null, ?int $severity = null, ?string $isNew = null): array { - if (is_null($buildId)) { + if (\is_null($buildId)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } @@ -109,7 +56,7 @@ public function getByBuildId($buildId, $limit = null, $offset = 0, $plugin = nul if ($offset) { $query .= ' OFFSET :offset'; } - $stmt = Database::getConnection()->prepareCommon($query); + $stmt = $this->databaseManager->getConnection()->prepare($query); $stmt->bindValue(':build_id', $buildId); if ($plugin) { $stmt->bindValue(':plugin', $plugin, PDO::PARAM_STR); @@ -127,12 +74,10 @@ public function getByBuildId($buildId, $limit = null, $offset = 0, $plugin = nul if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); - $map = function ($item) { - return new BuildError($item); - }; - $rtn = array_map($map, $res); + $map = fn ($item) => new BuildError($this->storeRegistry, $item); + $rtn = \array_map($map, $res); - $count = count($rtn); + $count = \count($rtn); return ['items' => $rtn, 'count' => $count]; } else { @@ -143,16 +88,9 @@ public function getByBuildId($buildId, $limit = null, $offset = 0, $plugin = nul /** * Gets the total number of errors for a given build. * - * @param int $buildId - * @param string|null $plugin - * @param int|null $severity - * @param string|null $isNew - * - * @return int - * * @throws Exception */ - public function getErrorTotalForBuild($buildId, $plugin = null, $severity = null, $isNew = null) + public function getErrorTotalForBuild(int $buildId, ?string $plugin = null, ?int $severity = null, ?string $isNew = null): int { $query = 'SELECT COUNT(*) AS {{total}} FROM {{' . $this->tableName . '}} WHERE {{build_id}} = :build'; @@ -170,7 +108,7 @@ public function getErrorTotalForBuild($buildId, $plugin = null, $severity = null $query .= ' AND {{is_new}} = false'; } - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':build', $buildId, PDO::PARAM_INT); @@ -191,14 +129,7 @@ public function getErrorTotalForBuild($buildId, $plugin = null, $severity = null } } - /** - * @param int $buildId - * @param int $severity - * @param string $isNew - * - * @return array - */ - public function getKnownPlugins($buildId, $severity = null, $isNew = '') + public function getKnownPlugins(int $buildId, ?int $severity = null, ?string $isNew = null): array { $query = 'SELECT DISTINCT {{plugin}} from {{' . $this->tableName . '}} WHERE {{build_id}} = :build'; @@ -212,7 +143,7 @@ public function getKnownPlugins($buildId, $severity = null, $isNew = '') $query .= ' AND {{is_new}} = false'; } - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':build', $buildId); if (null !== $severity) { $stmt->bindValue(':severity', (int)$severity, PDO::PARAM_INT); @@ -220,26 +151,15 @@ public function getKnownPlugins($buildId, $severity = null, $isNew = '') if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); + $map = fn ($item) => $item['plugin']; - $map = function ($item) { - return $item['plugin']; - }; - $rtn = array_map($map, $res); - - return $rtn; + return \array_map($map, $res); } else { return []; } } - /** - * @param int $buildId - * @param string $plugin - * @param string $isNew - * - * @return array - */ - public function getKnownSeverities($buildId, $plugin = '', $isNew = '') + public function getKnownSeverities(int $buildId, ?string $plugin = null, ?string $isNew = null): array { $query = 'SELECT DISTINCT {{severity}} FROM {{' . $this->tableName . '}} WHERE {{build_id}} = :build'; @@ -253,7 +173,7 @@ public function getKnownSeverities($buildId, $plugin = '', $isNew = '') $query .= ' AND {{is_new}} = false'; } - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':build', $buildId); if ($plugin) { $stmt->bindValue(':plugin', $plugin); @@ -261,13 +181,9 @@ public function getKnownSeverities($buildId, $plugin = '', $isNew = '') if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); + $map = fn ($item) => (int)$item['severity']; - $map = function ($item) { - return (int)$item['severity']; - }; - $rtn = array_map($map, $res); - - return $rtn; + return \array_map($map, $res); } else { return []; } @@ -276,20 +192,17 @@ public function getKnownSeverities($buildId, $plugin = '', $isNew = '') /** * Check if a build error is new. * - * @param int $projectId - * @param string $hash - * - * @return bool * @throws Exception */ - public function getIsNewError($projectId, $hash) + public function getIsNewError(int $projectId, string $hash): bool { $query = ' SELECT COUNT(*) AS {{total}} FROM {{' . $this->tableName . '}} AS be LEFT JOIN {{builds}} AS b ON be.build_id = b.id WHERE be.hash = :hash AND b.project_id = :project'; - $stmt = Database::getConnection('read')->prepareCommon($query); + + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':project', $projectId); $stmt->bindValue(':hash', $hash); @@ -304,10 +217,9 @@ public function getIsNewError($projectId, $hash) } /** - * @return array * @throws Exception */ - public function getErrorAmountPerPluginForBuild($buildId) + public function getErrorAmountPerPluginForBuild(int $buildId): array { $query = ' SELECT {{plugin}}, COUNT(*) AS {{amount}} @@ -316,7 +228,7 @@ public function getErrorAmountPerPluginForBuild($buildId) GROUP BY {{plugin}} '; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':build', $buildId); $stmt->execute(); diff --git a/src/Store/BuildErrorWriter.php b/src/Store/BuildErrorWriter.php index 6013ff0d7..fdee2ed59 100644 --- a/src/Store/BuildErrorWriter.php +++ b/src/Store/BuildErrorWriter.php @@ -1,108 +1,95 @@ + * @author Dmitry Khomutov */ class BuildErrorWriter { - /** - * @var int - */ - protected $buildId; + private int $buildId; - /** - * @var int - */ - protected $projectId; + private int $projectId; - /** - * @var array - */ - protected $errors = []; + private array $errors = []; - /** - * @var int - * - * @see https://stackoverflow.com/questions/40361164/pdoexception-sqlstatehy000-general-error-7-number-of-parameters-must-be-bet - */ - protected $bufferSize; + private DatabaseManager $databaseManager; + + private StoreRegistry $storeRegistry; /** - * @param int $projectId - * @param int $buildId + * @see https://stackoverflow.com/questions/40361164/pdoexception-sqlstatehy000-general-error-7-number-of-parameters-must-be-bet */ - public function __construct($projectId, $buildId) - { - $this->bufferSize = (int)Config::getInstance()->get('php-censor.build.writer_buffer_size', 500); + private int $bufferSize; + + public function __construct( + ConfigurationInterface $configuration, + DatabaseManager $databaseManager, + StoreRegistry $storeRegistry, + int $projectId, + int $buildId + ) { + $this->bufferSize = (int)$configuration->get('php-censor.build.writer_buffer_size', 500); $this->projectId = $projectId; $this->buildId = $buildId; + + $this->databaseManager = $databaseManager; + $this->storeRegistry = $storeRegistry; } - /** - * Destructor - */ public function __destruct() { $this->flush(); } - /** - * Write error - * - * @param string $plugin - * @param string $message - * @param int $severity - * @param string $file - * @param int $lineStart - * @param int $lineEnd - * @param DateTime $createdDate - */ public function write( - $plugin, - $message, - $severity, - $file = null, - $lineStart = null, - $lineEnd = null, - $createdDate = null - ) { - if (is_null($createdDate)) { + string $plugin, + string $message, + int $severity, + ?string $file = null, + ?int $lineStart = null, + ?int $lineEnd = null, + ?DateTime $createdDate = null + ): void { + if (\is_null($createdDate)) { $createdDate = new DateTime(); } /** @var BuildErrorStore $errorStore */ - $errorStore = Factory::getStore('BuildError'); + $errorStore = $this->storeRegistry->get('BuildError'); $hash = BuildError::generateHash($plugin, $file, $lineStart, $lineEnd, $severity, $message); $this->errors[] = [ - 'plugin' => (string)$plugin, - 'message' => (string)$message, - 'severity' => (int)$severity, - 'file' => !is_null($file) ? (string)$file : null, - 'line_start' => !is_null($lineStart) ? (int)$lineStart : null, - 'line_end' => !is_null($lineEnd) ? (int)$lineEnd : null, + 'plugin' => $plugin, + 'message' => $message, + 'severity' => $severity, + 'file' => $file, + 'line_start' => $lineStart, + 'line_end' => $lineEnd, 'create_date' => $createdDate->format('Y-m-d H:i:s'), 'hash' => $hash, 'is_new' => $errorStore->getIsNewError($this->projectId, $hash) ? 1 : 0, ]; - if (count($this->errors) >= $this->bufferSize) { + if (\count($this->errors) >= $this->bufferSize) { $this->flush(); } } - /** - * Flush buffer - */ - public function flush() + public function flush(): void { if (empty($this->errors)) { return; @@ -147,9 +134,9 @@ public function flush() {{hash}}, {{is_new}} ) - VALUES ' . join(', ', $insertValuesPlaceholders) . ' + VALUES ' . \join(', ', $insertValuesPlaceholders) . ' '; - $stmt = Database::getConnection('write')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('write')->prepare($query); $stmt->execute($insertValuesData); $this->errors = []; diff --git a/src/Store/BuildMetaStore.php b/src/Store/BuildMetaStore.php index ce3315353..ff358c705 100644 --- a/src/Store/BuildMetaStore.php +++ b/src/Store/BuildMetaStore.php @@ -1,83 +1,35 @@ + * @author Dmitry Khomutov + */ class BuildMetaStore extends Store { - /** - * @var string - */ - protected $tableName = 'build_metas'; - - /** - * @var string - */ - protected $modelName = '\PHPCensor\Model\BuildMeta'; - - /** - * @var string - */ - protected $primaryKey = 'id'; - - /** - * Get a BuildMeta by primary key (Id) - * - * @param int $key - * @param string $useConnection - * - * @return BuildMeta|null - */ - public function getByPrimaryKey($key, $useConnection = 'read') - { - return $this->getById($key, $useConnection); - } - - /** - * Get a single BuildMeta by Id. - * - * @param int $id - * @param string $useConnection - * - * @return BuildMeta|null - * - * @throws HttpException - */ - public function getById($id, $useConnection = 'read') - { - if (is_null($id)) { - throw new HttpException('id passed to ' . __FUNCTION__ . ' cannot be null.'); - } - - $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{id}} = :id LIMIT 1'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); - $stmt->bindValue(':id', $id); - - if ($stmt->execute()) { - if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new BuildMeta($data); - } - } + protected string $tableName = 'build_metas'; - return null; - } + protected string $modelName = BuildMeta::class; /** - * @param int $buildId - * @param string $key - * - * @return BuildMeta|null - * * @throws HttpException */ - public function getByKey($buildId, $key) + public function getByKey(int $buildId, string $key): ?BuildMeta { - if (is_null($buildId)) { + if (\is_null($buildId)) { throw new HttpException('buildId passed to ' . __FUNCTION__ . ' cannot be null.'); } @@ -86,13 +38,13 @@ public function getByKey($buildId, $key) } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{build_id}} = :build_id AND {{meta_key}} = :meta_key LIMIT 1'; - $stmt = Database::getConnection()->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':build_id', $buildId); $stmt->bindValue(':meta_key', $key); if ($stmt->execute()) { if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new BuildMeta($data); + return new BuildMeta($this->storeRegistry, $data); } } @@ -102,34 +54,26 @@ public function getByKey($buildId, $key) /** * Get multiple BuildMeta by BuildId. * - * @param int $buildId - * @param int $limit - * @param string $useConnection - * - * @return array - * * @throws HttpException */ - public function getByBuildId($buildId, $limit = 1000, $useConnection = 'read') + public function getByBuildId(int $buildId, int $limit = 1000, string $useConnection = 'read'): array { - if (is_null($buildId)) { + if (\is_null($buildId)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{build_id}} = :build_id LIMIT :limit'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); $stmt->bindValue(':build_id', $buildId); $stmt->bindValue(':limit', (int)$limit, PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); - $map = function ($item) { - return new BuildMeta($item); - }; - $rtn = array_map($map, $res); + $map = fn ($item) => new BuildMeta($this->storeRegistry, $item); + $rtn = \array_map($map, $res); - $count = count($rtn); + $count = \count($rtn); return ['items' => $rtn, 'count' => $count]; } else { @@ -139,30 +83,22 @@ public function getByBuildId($buildId, $limit = 1000, $useConnection = 'read') /** * Only used by an upgrade migration to move errors from build_meta to build_error - * - * @param int $limit - * - * @return array */ - public function getErrorsForUpgrade($limit) + public function getErrorsForUpgrade(int $limit): array { $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{meta_key}} IN (\'phpmd-data\', \'phpcs-data\', \'phpdoccheck-data\', \'technical_debt-data\') ORDER BY {{id}} ASC LIMIT :limit'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); + $map = fn ($item) => new BuildMeta($this->storeRegistry, $item); - $map = function ($item) { - return new BuildMeta($item); - }; - $rtn = array_map($map, $res); - - return $rtn; + return \array_map($map, $res); } else { return []; } diff --git a/src/Store/BuildStore.php b/src/Store/BuildStore.php index 6663b98a3..98fd7b814 100644 --- a/src/Store/BuildStore.php +++ b/src/Store/BuildStore.php @@ -1,108 +1,53 @@ + * @author Dmitry Khomutov */ class BuildStore extends Store { - /** - * @var string - */ - protected $tableName = 'builds'; - - /** - * @var string - */ - protected $modelName = '\PHPCensor\Model\Build'; - - /** - * @var string - */ - protected $primaryKey = 'id'; - - /** - * Get a Build by primary key (Id) - * - * @param int $key - * @param string $useConnection - * - * @return Build|null - */ - public function getByPrimaryKey($key, $useConnection = 'read') - { - return $this->getById($key, $useConnection); - } - - /** - * Get a single Build by Id. - * - * @param int $id - * @param string $useConnection - * - * @return Build|null - * - * @throws HttpException - */ - public function getById($id, $useConnection = 'read') - { - if (is_null($id)) { - throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); - } + protected string $tableName = 'builds'; - $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{id}} = :id LIMIT 1'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); - $stmt->bindValue(':id', $id); - - if ($stmt->execute()) { - if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new Build($data); - } - } - - return null; - } + protected string $modelName = Build::class; /** * Get multiple Build by ProjectId. * - * @param int $projectId - * @param int $limit - * @param string $useConnection - * - * @return array - * * @throws HttpException */ - public function getByProjectId($projectId, $limit = 1000, $useConnection = 'read') + public function getByProjectId(int $projectId, int $limit = 1000, string $useConnection = 'read'): array { - if (is_null($projectId)) { + if (\is_null($projectId)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{project_id}} = :project_id LIMIT :limit'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); $stmt->bindValue(':project_id', $projectId); $stmt->bindValue(':limit', (int)$limit, PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); - $map = function ($item) { - return new Build($item); - }; - $rtn = array_map($map, $res); + $map = fn ($item) => new Build($this->storeRegistry, $item); + $rtn = \array_map($map, $res); - $count = count($rtn); + $count = \count($rtn); return ['items' => $rtn, 'count' => $count]; } else { @@ -113,34 +58,26 @@ public function getByProjectId($projectId, $limit = 1000, $useConnection = 'read /** * Get multiple Build by Status. * - * @param int $status - * @param int $limit - * @param string $useConnection - * - * @return array - * * @throws HttpException */ - public function getByStatus($status, $limit = 1000, $useConnection = 'read') + public function getByStatus(int $status, int $limit = 1000, string $useConnection = 'read'): array { - if (is_null($status)) { + if (\is_null($status)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{status}} = :status ORDER BY {{create_date}} ASC LIMIT :limit'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); $stmt->bindValue(':status', $status); $stmt->bindValue(':limit', (int)$limit, PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); - $map = function ($item) { - return new Build($item); - }; - $rtn = array_map($map, $res); + $map = fn ($item) => new Build($this->storeRegistry, $item); + $rtn = \array_map($map, $res); - $count = count($rtn); + $count = \count($rtn); return ['items' => $rtn, 'count' => $count]; } else { @@ -148,52 +85,37 @@ public function getByStatus($status, $limit = 1000, $useConnection = 'read') } } - /** - * @param int $limit - * @param int $offset - * - * @return array - */ - public function getBuilds($limit = 5, $offset = 0) + public function getBuilds(int $limit = 5, int $offset = 0): array { $query = 'SELECT * FROM {{' . $this->tableName . '}} ORDER BY {{id}} DESC LIMIT :limit OFFSET :offset'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); + $map = fn ($item) => new Build($this->storeRegistry, $item); - $map = function ($item) { - return new Build($item); - }; - $rtn = array_map($map, $res); - - return $rtn; + return \array_map($map, $res); } else { return []; } } /** - * @param int $projectId - * @param string $branch - * - * @return Build|null - * * @throws Exception */ - public function getLatestBuildByProjectAndBranch($projectId, $branch) + public function getLatestBuildByProjectAndBranch(int $projectId, string $branch): ?Build { $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{project_id}} = :project_id AND {{branch}} = :branch ORDER BY {{id}} DESC'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':project_id', $projectId); $stmt->bindValue(':branch', $branch); if ($stmt->execute() && $data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new Build($data); + return new Build($this->storeRegistry, $data); } return null; @@ -202,24 +124,19 @@ public function getLatestBuildByProjectAndBranch($projectId, $branch) /** * Return an array of the latest builds for a given project. * - * @param int $projectId - * @param int $limit - * - * @return array - * * @throws Exception */ - public function getLatestBuilds($projectId = null, $limit = 5) + public function getLatestBuilds(?int $projectId = null, int $limit = 5): array { - if (!is_null($projectId)) { + if (!\is_null($projectId)) { $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{project_id}} = :pid ORDER BY {{id}} DESC LIMIT :limit'; } else { $query = 'SELECT * FROM {{' . $this->tableName . '}} ORDER BY {{id}} DESC LIMIT :limit'; } - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); - if (!is_null($projectId)) { + if (!\is_null($projectId)) { $stmt->bindValue(':pid', $projectId); } @@ -227,13 +144,9 @@ public function getLatestBuilds($projectId = null, $limit = 5) if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); + $map = fn ($item) => new Build($this->storeRegistry, $item); - $map = function ($item) { - return new Build($item); - }; - $rtn = array_map($map, $res); - - return $rtn; + return \array_map($map, $res); } else { return []; } @@ -241,37 +154,27 @@ public function getLatestBuilds($projectId = null, $limit = 5) /** * Return the latest build for a specific project, of a specific build status. - * - * @param int|null $projectId - * @param int $status - * - * @return array|Build */ - public function getLastBuildByStatus($projectId = null, $status = Build::STATUS_SUCCESS) + public function getLastBuildByStatus(?int $projectId = null, int $status = Build::STATUS_SUCCESS): ?Build { $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{project_id}} = :pid AND {{status}} = :status ORDER BY {{id}} DESC LIMIT 1'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':pid', $projectId); $stmt->bindValue(':status', $status); if ($stmt->execute()) { if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new Build($data); + return new Build($this->storeRegistry, $data); } - } else { - return []; } + + return null; } /** * Return an array of the latest builds for all projects. - * - * @param int $limitByProject - * @param int $limitAll - * - * @return array */ - public function getAllProjectsLatestBuilds($limitByProject = 5, $limitAll = 10) + public function getAllProjectsLatestBuilds(int $limitByProject = 5, int $limitAll = 10): array { // don't fetch log field - contain many data $query = ' @@ -294,7 +197,7 @@ public function getAllProjectsLatestBuilds($limitByProject = 5, $limitAll = 10) LIMIT 10000 '; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -315,37 +218,35 @@ public function getAllProjectsLatestBuilds($limitByProject = 5, $limitAll = 10) ]; } $build = null; - if (count($projects[$projectId][$environment]['latest']) < $limitByProject) { - $build = new Build($item); + if (\count($projects[$projectId][$environment]['latest']) < $limitByProject) { + $build = new Build($this->storeRegistry, $item); $projects[$projectId][$environment]['latest'][] = $build; } - if (count($latest) < $limitAll) { - if (is_null($build)) { - $build = new Build($item); + if (\count($latest) < $limitAll) { + if (\is_null($build)) { + $build = new Build($this->storeRegistry, $item); } $latest[] = $build; } if (empty($projects[$projectId][$environment]['success']) && Build::STATUS_SUCCESS === $item['status']) { - if (is_null($build)) { - $build = new Build($item); + if (\is_null($build)) { + $build = new Build($this->storeRegistry, $item); } $projects[$projectId][$environment]['success'] = $build; } if (empty($projects[$projectId][$environment]['failed']) && Build::STATUS_FAILED === $item['status']) { - if (is_null($build)) { - $build = new Build($item); + if (\is_null($build)) { + $build = new Build($this->storeRegistry, $item); } $projects[$projectId][$environment]['failed'] = $build; } } foreach ($projects as $idx => $project) { - $projects[$idx] = array_filter($project, function ($val) { - return ($val['latest'][0]->getStatus() != Build::STATUS_SUCCESS); - }); + $projects[$idx] = \array_filter($project, fn ($val) => $val['latest'][0]->getStatus() !== Build::STATUS_SUCCESS); } - $projects = array_filter($projects); + $projects = \array_filter($projects); return ['projects' => $projects, 'latest' => $latest]; } else { @@ -355,30 +256,22 @@ public function getAllProjectsLatestBuilds($limitByProject = 5, $limitAll = 10) /** * Return an array of builds for a given project and commit ID. - * - * @param int $projectId - * @param string $commitId - * - * @return array */ - public function getByProjectAndCommit($projectId, $commitId) + public function getByProjectAndCommit(int $projectId, string $commitId): array { $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{project_id}} = :project_id AND {{commit_id}} = :commit_id'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':project_id', $projectId); $stmt->bindValue(':commit_id', $commitId); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); + $map = fn ($item) => new Build($this->storeRegistry, $item); - $map = function ($item) { - return new Build($item); - }; - - $rtn = array_map($map, $res); + $rtn = \array_map($map, $res); - return ['items' => $rtn, 'count' => count($rtn)]; + return ['items' => $rtn, 'count' => \count($rtn)]; } else { return ['items' => [], 'count' => 0]; } @@ -387,22 +280,16 @@ public function getByProjectAndCommit($projectId, $commitId) /** * Returns all registered branches for project * - * @param int $projectId - * - * @return array - * * @throws Exception */ - public function getBuildBranches($projectId) + public function getBuildBranches(int $projectId): array { $query = 'SELECT DISTINCT {{branch}} FROM {{' . $this->tableName . '}} WHERE {{project_id}} = :project_id'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':project_id', $projectId); if ($stmt->execute()) { - $res = $stmt->fetchAll(PDO::FETCH_COLUMN); - - return $res; + return $stmt->fetchAll(PDO::FETCH_COLUMN); } else { return []; } @@ -410,16 +297,8 @@ public function getBuildBranches($projectId) /** * Return build metadata by key, project and optionally build id. - * - * @param string $key - * @param int $projectId - * @param int|null $buildId - * @param string|null $branch - * @param int $numResults - * - * @return array|null */ - public function getMeta($key, $projectId, $buildId = null, $branch = null, $numResults = 1) + public function getMeta(string $key, ?int $projectId, ?int $buildId = null, ?string $branch = null, int $numResults = 1): ?array { $query = 'SELECT bm.build_id, bm.meta_key, bm.meta_value FROM {{build_metas}} AS {{bm}} @@ -435,19 +314,19 @@ public function getMeta($key, $projectId, $buildId = null, $branch = null, $numR } // Include specific branch information if required: - if (!is_null($branch)) { + if (!\is_null($branch)) { $query .= ' AND b.branch = :branch '; } $query .= ' ORDER BY bm.id DESC LIMIT :numResults'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':key', $key, PDO::PARAM_STR); - $stmt->bindValue(':projectId', (int)$projectId, PDO::PARAM_INT); - $stmt->bindValue(':buildId', (int)$buildId, PDO::PARAM_INT); - $stmt->bindValue(':numResults', (int)$numResults, PDO::PARAM_INT); + $stmt->bindValue(':projectId', $projectId, PDO::PARAM_INT); + $stmt->bindValue(':buildId', $buildId, PDO::PARAM_INT); + $stmt->bindValue(':numResults', $numResults, PDO::PARAM_INT); - if (!is_null($branch)) { + if (!\is_null($branch)) { $stmt->bindValue(':branch', $branch, PDO::PARAM_STR); } @@ -455,11 +334,11 @@ public function getMeta($key, $projectId, $buildId = null, $branch = null, $numR $rtn = $stmt->fetchAll(PDO::FETCH_ASSOC); /** @var BuildErrorStore $errorStore */ - $errorStore = Factory::getStore('BuildError'); + $errorStore = $this->storeRegistry->get('BuildError'); - $rtn = array_reverse($rtn); - $rtn = array_map(function ($item) use ($key, $errorStore, $buildId) { - $item['meta_value'] = json_decode($item['meta_value'], true); + $rtn = \array_reverse($rtn); + $rtn = \array_map(function ($item) use ($key, $errorStore, $buildId) { + $item['meta_value'] = \json_decode($item['meta_value'], true); if ('plugin-summary' === $key) { foreach ($item['meta_value'] as $stage => $stageData) { foreach ($stageData as $plugin => $pluginData) { @@ -474,7 +353,7 @@ public function getMeta($key, $projectId, $buildId = null, $branch = null, $numR return $item; }, $rtn); - if (!count($rtn)) { + if (!\count($rtn)) { return null; } else { return $rtn; @@ -486,18 +365,14 @@ public function getMeta($key, $projectId, $buildId = null, $branch = null, $numR /** * Set a metadata value for a given project and build ID. - * - * @param int $buildId - * @param string $key - * @param string $value */ - public function setMeta($buildId, $key, $value) + public function setMeta(int $buildId, string $key, string $value): void { /** @var BuildMetaStore $store */ - $store = Factory::getStore('BuildMeta'); + $store = $this->storeRegistry->get('BuildMeta'); $meta = $store->getByKey($buildId, $key); - if (is_null($meta)) { - $meta = new BuildMeta(); + if (\is_null($meta)) { + $meta = new BuildMeta($this->storeRegistry); $meta->setBuildId($buildId); $meta->setMetaKey($key); } @@ -506,10 +381,10 @@ public function setMeta($buildId, $key, $value) $store->save($meta); } - public function deleteAllByProject($projectId) + public function deleteAllByProject(int $projectId): int { - $q = Database::getConnection('write') - ->prepareCommon( + $q = $this->databaseManager->getConnection('write') + ->prepare( 'DELETE FROM {{' . $this->tableName . '}} WHERE {{project_id}} = :project_id' ); $q->bindValue(':project_id', $projectId); @@ -519,33 +394,25 @@ public function deleteAllByProject($projectId) } /** - * @param int $projectId - * @param int $keep - * - * @return array - * * @throws HttpException */ - public function getOldByProject($projectId, $keep = 100) + public function getOldByProject(int $projectId, int $keep = 100): array { - if (is_null($projectId)) { + if (\is_null($projectId)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{project_id}} = :project_id ORDER BY {{create_date}} DESC LIMIT 1000000 OFFSET :keep'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':project_id', $projectId); $stmt->bindValue(':keep', (int)$keep, PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); + $map = fn ($item) => new Build($this->storeRegistry, $item); + $rtn = \array_map($map, $res); - $map = function ($item) { - return new Build($item); - }; - $rtn = array_map($map, $res); - - $count = count($rtn); + $count = \count($rtn); return ['items' => $rtn, 'count' => $count]; } @@ -554,17 +421,13 @@ public function getOldByProject($projectId, $keep = 100) } /** - * @param int $buildId - * - * @return int - * * @throws Exception */ - public function getNewErrorsCount($buildId) + public function getNewErrorsCount(int $buildId): int { $query = 'SELECT COUNT(*) AS {{total}} FROM {{build_errors}} WHERE {{build_id}} = :build_id AND {{is_new}} = true'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':build_id', $buildId); @@ -578,57 +441,114 @@ public function getNewErrorsCount($buildId) } /** - * @param int $buildId - * - * @return int - * * @throws Exception */ - public function getErrorsCount($buildId) + public function getErrorsCount(int $buildId): int { $query = 'SELECT COUNT(*) AS {{total}} FROM {{build_errors}} WHERE {{build_id}} = :build_id'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); - $stmt->bindValue(':build_id', $buildId); + if ($stmt) { + $stmt->bindValue(':build_id', $buildId); - if ($stmt->execute()) { - $res = $stmt->fetch(PDO::FETCH_ASSOC); + if ($stmt->execute()) { + $res = $stmt->fetch(PDO::FETCH_ASSOC); - return (int)$res['total']; + return (int)$res['total']; + } } return 0; } /** - * @param int $buildId - * @param int $projectId - * @param string $branch - * - * @return array - * * @throws Exception */ - public function getBuildErrorsTrend($buildId, $projectId, $branch) + public function getTestCoverage(int $buildId): string + { + $coverage = '0.00'; + $type = 'lines'; + + try { + $build = $this->getById($buildId); + + if (isset($build) && $build instanceof Build) { + $coverageMeta = $this->getMeta( + 'php_unit-coverage', + $build->getProjectId(), + $build->getId(), + $build->getBranch() + ); + + if ($coverageMeta && isset($coverageMeta[0]['meta_value'][$type])) { + $coverage = $coverageMeta[0]['meta_value'][$type]; + } + } + } catch (\Throwable $e) { + } + + return $coverage; + } + + /** + * @throws Exception + */ + public function getBuildErrorsTrend(int $buildId, int $projectId, string $branch): array { $query = ' SELECT b.id AS {{build_id}}, count(be.id) AS {{count}} FROM {{' . $this->tableName . '}} AS b LEFT JOIN {{build_errors}} AS be ON b.id = be.build_id -WHERE b.project_id = :project_id AND b.branch = :branch AND b.id <= :build_id +WHERE b.project_id = :project_id + AND b.branch = :branch + AND b.id < :build_id + AND b.status NOT IN (' . Build::STATUS_PENDING . ', ' . Build::STATUS_RUNNING . ') GROUP BY b.id -order BY b.id DESC -LIMIT 2'; +ORDER BY b.id DESC +LIMIT 1'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); - $stmt->bindValue(':build_id', $buildId, PDO::PARAM_INT); - $stmt->bindValue(':project_id', $projectId, PDO::PARAM_INT); - $stmt->bindValue(':branch', $branch, PDO::PARAM_STR); + if ($stmt) { + $stmt->bindValue(':build_id', $buildId, PDO::PARAM_INT); + $stmt->bindValue(':project_id', $projectId, PDO::PARAM_INT); + $stmt->bindValue(':branch', $branch, PDO::PARAM_STR); - if ($stmt->execute()) { - return $stmt->fetchAll(PDO::FETCH_ASSOC); + if ($stmt->execute()) { + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + } + + return []; + } + + /** + * @throws Exception + */ + public function getBuildTestCoverageTrend(int $buildId, int $projectId, string $branch): array + { + $query = ' +SELECT b.id AS {{build_id}}, bm.coverage AS {{coverage}} FROM {{' . $this->tableName . '}} AS b +LEFT JOIN (SELECT {{build_id}}, {{meta_value}} AS {{coverage}} FROM {{build_metas}} WHERE {{meta_key}} = \'php_unit-coverage\') AS bm +ON b.id = bm.build_id +WHERE b.project_id = :project_id + AND b.branch = :branch + AND b.id < :build_id + AND b.status NOT IN (' . Build::STATUS_PENDING . ', ' . Build::STATUS_RUNNING . ') +ORDER BY b.id DESC +LIMIT 1'; + + $stmt = $this->databaseManager->getConnection('read')->prepare($query); + + if ($stmt) { + $stmt->bindValue(':build_id', $buildId, PDO::PARAM_INT); + $stmt->bindValue(':project_id', $projectId, PDO::PARAM_INT); + $stmt->bindValue(':branch', $branch, PDO::PARAM_STR); + + if ($stmt->execute()) { + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } } return []; diff --git a/src/Store/EnvironmentStore.php b/src/Store/EnvironmentStore.php index 60f56bdaf..01c6192b0 100644 --- a/src/Store/EnvironmentStore.php +++ b/src/Store/EnvironmentStore.php @@ -1,98 +1,47 @@ + */ class EnvironmentStore extends Store { - /** - * @var string - */ - protected $tableName = 'environments'; - - /** - * @var string - */ - protected $modelName = '\PHPCensor\Model\Environment'; + protected string $tableName = 'environments'; - /** - * @var string - */ - protected $primaryKey = 'id'; - - /** - * Get a Environment by primary key (Id) - * - * @param int $key - * @param string $useConnection - * - * @return Environment|null - */ - public function getByPrimaryKey($key, $useConnection = 'read') - { - return $this->getById($key, $useConnection); - } - - /** - * Get a single Environment by Id. - * - * @param int $id - * @param string $useConnection - * - * @return Environment|null - * - * @throws HttpException - */ - public function getById($id, $useConnection = 'read') - { - if (is_null($id)) { - throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); - } - - $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{id}} = :id LIMIT 1'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); - $stmt->bindValue(':id', $id); - - if ($stmt->execute()) { - if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new Environment($data); - } - } - - return null; - } + protected string $modelName = Environment::class; /** * Get a single Environment by Name. * - * @param string $name - * @param int $projectId - * @param string $useConnection - * - * @return Environment|null - * * @throws HttpException */ - public function getByNameAndProjectId($name, $projectId, $useConnection = 'read') + public function getByNameAndProjectId(string $name, int $projectId, string $useConnection = 'read'): ?Environment { - if (is_null($name)) { + if (\is_null($name)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{name}} = :name AND {{project_id}} = :project_id LIMIT 1'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); $stmt->bindValue(':name', $name); $stmt->bindValue(':project_id', $projectId); if ($stmt->execute()) { if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new Environment($data); + return new Environment($this->storeRegistry, $data); } } @@ -102,33 +51,26 @@ public function getByNameAndProjectId($name, $projectId, $useConnection = 'read' /** * Get multiple Environment by Project id. * - * @param int $projectId - * @param string $useConnection - * - * @return array - * * @throws Exception */ - public function getByProjectId($projectId, $useConnection = 'read') + public function getByProjectId(int $projectId, string $useConnection = 'read'): array { - if (is_null($projectId)) { - throw new Exception('Value passed to ' . __FUNCTION__ . ' cannot be null.'); + if (\is_null($projectId)) { + throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{project_id}} = :project_id'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); $stmt->bindValue(':project_id', $projectId); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); - $map = function ($item) { - return new Environment($item); - }; - $rtn = array_map($map, $res); + $map = fn ($item) => new Environment($this->storeRegistry, $item); + $rtn = \array_map($map, $res); - $count = count($rtn); + $count = \count($rtn); return ['items' => $rtn, 'count' => $count]; } else { diff --git a/src/Store/Factory.php b/src/Store/Factory.php deleted file mode 100644 index d076476f3..000000000 --- a/src/Store/Factory.php +++ /dev/null @@ -1,65 +0,0 @@ -loadStore($storeName); - } - - protected function __construct() - { - } - - /** - * @param string $store - * - * @return Store; - */ - public function loadStore($store) - { - if (!isset($this->loadedStores[$store])) { - $class = 'PHPCensor\\Store\\' . $store . 'Store'; - $obj = new $class(); - - $this->loadedStores[$store] = $obj; - } - - return $this->loadedStores[$store]; - } -} diff --git a/src/Store/ProjectGroupStore.php b/src/Store/ProjectGroupStore.php index 59c9b186b..b1421e9c2 100644 --- a/src/Store/ProjectGroupStore.php +++ b/src/Store/ProjectGroupStore.php @@ -1,97 +1,45 @@ + */ class ProjectGroupStore extends Store { - /** - * @var string - */ - protected $tableName = 'project_groups'; - - /** - * @var string - */ - protected $modelName = '\PHPCensor\Model\ProjectGroup'; + protected string $tableName = 'project_groups'; - /** - * @var string - */ - protected $primaryKey = 'id'; - - /** - * Get a ProjectGroup by primary key (Id) - * - * @param int $key - * @param string $useConnection - * - * @return ProjectGroup|null - */ - public function getByPrimaryKey($key, $useConnection = 'read') - { - return $this->getById($key, $useConnection); - } - - /** - * Get a single ProjectGroup by Id. - * - * @param int $id - * @param string $useConnection - * - * @return ProjectGroup|null - * - * @throws HttpException - */ - public function getById($id, $useConnection = 'read') - { - if (is_null($id)) { - throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); - } - - $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{id}} = :id LIMIT 1'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); - - $stmt->bindValue(':id', $id); - - if ($stmt->execute()) { - if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new ProjectGroup($data); - } - } - - return null; - } + protected string $modelName = ProjectGroup::class; /** * Get a single ProjectGroup by title. * - * @param int $title - * @param string $useConnection - * - * @return ProjectGroup|null - * * @throws HttpException */ - public function getByTitle($title, $useConnection = 'read') + public function getByTitle(string $title, string $useConnection = 'read'): ?ProjectGroup { - if (is_null($title)) { + if (\is_null($title)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{title}} = :title LIMIT 1'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); $stmt->bindValue(':title', $title); if ($stmt->execute()) { if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new ProjectGroup($data); + return new ProjectGroup($this->storeRegistry, $data); } } diff --git a/src/Store/ProjectStore.php b/src/Store/ProjectStore.php index cdcdc461c..f9151207d 100644 --- a/src/Store/ProjectStore.php +++ b/src/Store/ProjectStore.php @@ -1,99 +1,51 @@ + * @author Dmitry Khomutov */ class ProjectStore extends Store { - /** - * @var string - */ - protected $tableName = 'projects'; + protected string $tableName = 'projects'; - /** - * @var string - */ - protected $modelName = '\PHPCensor\Model\Project'; - - /** - * @var string - */ - protected $primaryKey = 'id'; - - /** - * Get a Project by primary key (Id) - * - * @param int $key - * @param string $useConnection - * - * @return Project|null - */ - public function getByPrimaryKey($key, $useConnection = 'read') - { - return $this->getById($key, $useConnection); - } - - /** - * Get a single Project by Id. - * - * @param int $id - * @param string $useConnection - * - * @return Project|null - * - * @throws HttpException - */ - public function getById($id, $useConnection = 'read') - { - if (is_null($id)) { - throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); - } - - $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{id}} = :id LIMIT 1'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); - $stmt->bindValue(':id', $id); - - if ($stmt->execute()) { - if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new Project($data); - } - } - - return null; - } + protected string $modelName = Project::class; /** * Get a single Project by Ids. * * @param int[] $values - * @param string $useConnection * * @throws HttpException * * @return Project[] */ - public function getByIds($values, $useConnection = 'read') + public function getByIds(array $values, string $useConnection = 'read'): array { if (empty($values)) { throw new HttpException('Values passed to ' . __FUNCTION__ . ' cannot be empty.'); } - $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{id}} IN ('.implode(', ', array_map('intval', $values)).')'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); + $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{id}} IN (' . \implode(', ', \array_map('intval', $values)).')'; + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); $rtn = []; if ($stmt->execute()) { while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - $rtn[$data['id']] = new Project($data); + $rtn[$data['id']] = new Project($this->storeRegistry, $data); } } @@ -103,35 +55,26 @@ public function getByIds($values, $useConnection = 'read') /** * Get multiple Project by Title. * - * @param string $title - * @param int $limit - * @param string $useConnection - * - * @return array - * * @throws HttpException */ - public function getByTitle($title, $limit = 1000, $useConnection = 'read') + public function getByTitle(string $title, int $limit = 1000, string $useConnection = 'read'): array { - if (is_null($title)) { + if (\is_null($title)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } - $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{title}} = :title LIMIT :limit'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); $stmt->bindValue(':title', $title); $stmt->bindValue(':limit', (int)$limit, PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); - $map = function ($item) { - return new Project($item); - }; - $rtn = array_map($map, $res); + $map = fn ($item) => new Project($this->storeRegistry, $item); + $rtn = \array_map($map, $res); - $count = count($rtn); + $count = \count($rtn); return ['items' => $rtn, 'count' => $count]; } else { @@ -142,24 +85,20 @@ public function getByTitle($title, $limit = 1000, $useConnection = 'read') /** * Returns a list of all branch names. * - * - * @return array + * @param $projectId */ - public function getKnownBranches($projectId) + public function getKnownBranches(int $projectId): array { $query = 'SELECT {{branch}}, COUNT(1) AS {{count}} from {{builds}} WHERE {{project_id}} = :pid GROUP BY {{branch}} ORDER BY {{count}} DESC'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':pid', $projectId); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); - $map = function ($item) { - return $item['branch']; - }; - $rtn = array_map($map, $res); + $map = fn ($item) => $item['branch']; - return $rtn; + return \array_map($map, $res); } else { return []; } @@ -167,29 +106,23 @@ public function getKnownBranches($projectId) /** * Get a list of all projects, ordered by their title. - * - * @param bool $archived - * - * @return array */ - public function getAll($archived = false) + public function getAll(string $useConnection = 'read', bool $archived = false): array { $archived = (int)$archived; $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{archived}} = :archived ORDER BY {{title}} ASC'; - $stmt = Database::getConnection('read')->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':archived', $archived); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); - $map = function ($item) { - return new Project($item); - }; - $rtn = array_map($map, $res); + $map = fn ($item) => new Project($this->storeRegistry, $item); + $rtn = \array_map($map, $res); - $count = count($rtn); + $count = \count($rtn); return ['items' => $rtn, 'count' => $count]; @@ -201,24 +134,17 @@ public function getAll($archived = false) /** * Get multiple Project by GroupId. * - * @param int $groupId - * @param bool $archived - * @param int $limit - * @param string $useConnection - * - * @return array - * * @throws Exception */ - public function getByGroupId($groupId, $archived = false, $limit = 1000, $useConnection = 'read') + public function getByGroupId(int $groupId, bool $archived = false, int $limit = 1000, string $useConnection = 'read'): array { - if (is_null($groupId)) { - throw new Exception('Value passed to ' . __FUNCTION__ . ' cannot be null.'); + if (\is_null($groupId)) { + throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $archived = (int)$archived; $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{group_id}} = :group_id AND {{archived}} = :archived ORDER BY {{title}} LIMIT :limit'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); $stmt->bindValue(':group_id', $groupId); $stmt->bindValue(':archived', $archived); @@ -227,12 +153,10 @@ public function getByGroupId($groupId, $archived = false, $limit = 1000, $useCon if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); - $map = function ($item) { - return new Project($item); - }; - $rtn = array_map($map, $res); + $map = fn ($item) => new Project($this->storeRegistry, $item); + $rtn = \array_map($map, $res); - $count = count($rtn); + $count = \count($rtn); return ['items' => $rtn, 'count' => $count]; } else { diff --git a/src/Store/SecretStore.php b/src/Store/SecretStore.php new file mode 100644 index 000000000..e2761a3a0 --- /dev/null +++ b/src/Store/SecretStore.php @@ -0,0 +1,54 @@ + + */ +class SecretStore extends Store +{ + protected string $tableName = 'secrets'; + + protected string $modelName = Secret::class; + + /** + * Get a single Project by Ids. + * + * @param string[] $values + * + * @throws HttpException + * + * @return Secret[] + */ + public function getByNames(array $values, string $useConnection = 'read'): array + { + if (empty($values)) { + throw new HttpException('Values passed to ' . __FUNCTION__ . ' cannot be empty.'); + } + + $query = \sprintf( + 'SELECT * FROM {{%s}} WHERE {{name}} IN (%s)', + $this->tableName, + ("'" . \implode('\', \'', $values) . "'") + ); + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); + + $rtn = []; + if ($stmt->execute()) { + while ($data = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $rtn[$data['name']] = new Secret($this->storeRegistry, $data); + } + } + + return $rtn; + } +} diff --git a/src/Store/UserStore.php b/src/Store/UserStore.php index 350fd4c12..fb0ad4a33 100644 --- a/src/Store/UserStore.php +++ b/src/Store/UserStore.php @@ -1,98 +1,46 @@ + * @author Dmitry Khomutov */ class UserStore extends Store { - /** - * @var string - */ - protected $tableName = 'users'; - - /** - * @var string - */ - protected $modelName = '\PHPCensor\Model\User'; - - /** - * @var string - */ - protected $primaryKey = 'id'; - - /** - * Get a User by primary key (Id) - * - * @param int $key - * @param string $useConnection - * - * @return User|null - */ - public function getByPrimaryKey($key, $useConnection = 'read') - { - return $this->getById($key, $useConnection); - } - - /** - * Get a single User by Id. - * - * @param int $id - * @param string $useConnection - * - * @return User|null - * - * @throws HttpException - */ - public function getById($id, $useConnection = 'read') - { - if (is_null($id)) { - throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); - } + protected string $tableName = 'users'; - $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{id}} = :id LIMIT 1'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); - $stmt->bindValue(':id', $id); - - if ($stmt->execute()) { - if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new User($data); - } - } - - return null; - } + protected string $modelName = User::class; /** * Get a single User by Email. * - * @param string $email - * * @throws HttpException - * - * @return User */ - public function getByEmail($email) + public function getByEmail(string $email): ?User { - if (is_null($email)) { + if (\is_null($email)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{email}} = :email LIMIT 1'; - $stmt = Database::getConnection()->prepareCommon($query); + $stmt = $this->databaseManager->getConnection()->prepare($query); $stmt->bindValue(':email', $email); if ($stmt->execute()) { if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new User($data); + return new User($this->storeRegistry, $data); } } @@ -102,25 +50,21 @@ public function getByEmail($email) /** * Get a single User by Email or Name. * - * @param string $emailOrName - * * @throws HttpException - * - * @return User */ - public function getByEmailOrName($emailOrName) + public function getByEmailOrName(string $emailOrName): ?User { - if (is_null($emailOrName)) { + if (\is_null($emailOrName)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{email}} = :value OR {{name}} = :value LIMIT 1'; - $stmt = Database::getConnection()->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':value', $emailOrName); if ($stmt->execute()) { if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new User($data); + return new User($this->storeRegistry, $data); } } @@ -130,25 +74,21 @@ public function getByEmailOrName($emailOrName) /** * Get a single User by RememberKey. * - * @param string $rememberKey - * * @throws HttpException - * - * @return User */ - public function getByRememberKey($rememberKey) + public function getByRememberKey(string $rememberKey): ?User { - if (is_null($rememberKey)) { + if (\is_null($rememberKey)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{remember_key}} = :remember_key LIMIT 1'; - $stmt = Database::getConnection()->prepareCommon($query); + $stmt = $this->databaseManager->getConnection('read')->prepare($query); $stmt->bindValue(':remember_key', $rememberKey); if ($stmt->execute()) { if ($data = $stmt->fetch(PDO::FETCH_ASSOC)) { - return new User($data); + return new User($this->storeRegistry, $data); } } @@ -158,34 +98,26 @@ public function getByRememberKey($rememberKey) /** * Get multiple User by Name. * - * @param string $name - * @param int $limit - * @param string $useConnection - * - * @return array - * * @throws HttpException */ - public function getByName($name, $limit = 1000, $useConnection = 'read') + public function getByName(string $name, int $limit = 1000, string $useConnection = 'read'): array { - if (is_null($name)) { + if (\is_null($name)) { throw new HttpException('Value passed to ' . __FUNCTION__ . ' cannot be null.'); } $query = 'SELECT * FROM {{' . $this->tableName . '}} WHERE {{name}} = :name LIMIT :limit'; - $stmt = Database::getConnection($useConnection)->prepareCommon($query); + $stmt = $this->databaseManager->getConnection($useConnection)->prepare($query); $stmt->bindValue(':name', $name); $stmt->bindValue(':limit', (int)$limit, PDO::PARAM_INT); if ($stmt->execute()) { $res = $stmt->fetchAll(PDO::FETCH_ASSOC); - $map = function ($item) { - return new User($item); - }; - $rtn = array_map($map, $res); + $map = fn ($item) => new User($this->storeRegistry, $item); + $rtn = \array_map($map, $res); - $count = count($rtn); + $count = \count($rtn); return ['items' => $rtn, 'count' => $count]; } else { diff --git a/src/Store/WebhookRequestStore.php b/src/Store/WebhookRequestStore.php new file mode 100644 index 000000000..cb10c635a --- /dev/null +++ b/src/Store/WebhookRequestStore.php @@ -0,0 +1,21 @@ + + */ +class WebhookRequestStore extends Store +{ + protected string $tableName = 'webhook_requests'; + + protected string $modelName = WebhookRequest::class; +} diff --git a/src/StoreRegistry.php b/src/StoreRegistry.php new file mode 100644 index 000000000..4b8eb968a --- /dev/null +++ b/src/StoreRegistry.php @@ -0,0 +1,49 @@ + + */ +class StoreRegistry +{ + private DatabaseManager $databaseManager; + + /** + * A collection of the stores currently loaded by the factory. + * + * @var Store[] + */ + private array $loadedStores = []; + + public function __construct(DatabaseManager $databaseManager) + { + $this->databaseManager = $databaseManager; + } + + /** + * @throws RuntimeException + */ + public function get(string $storeName): Store + { + if (!isset($this->loadedStores[$storeName])) { + try { + $class = 'PHPCensor\Store\\' . $storeName . 'Store'; + $store = new $class($this->databaseManager, $this); + + $this->loadedStores[$storeName] = $store; + } catch (\Throwable $exception) { + throw new RuntimeException($exception->getMessage()); + } + } + + return $this->loadedStores[$storeName]; + } +} diff --git a/src/Traits/Model/Build/GitGetDiffLineNumberTrait.php b/src/Traits/Model/Build/GitGetDiffLineNumberTrait.php new file mode 100644 index 000000000..8dfde96fb --- /dev/null +++ b/src/Traits/Model/Build/GitGetDiffLineNumberTrait.php @@ -0,0 +1,89 @@ + + * + * @mixin GitBuild + */ +trait GitGetDiffLineNumberTrait +{ + /** + * Uses git diff to figure out what the diff line position is, based on the error line number. + */ + protected function getDiffLineNumber(Builder $builder, string $file, ?int $line): ?int + { + $builder->logExecOutput(false); + + $prNumber = $this->getExtra('pull_request_number'); + $path = $builder->buildPath; + + if (!empty($prNumber)) { + $builder->executeCommand('cd "%s" && git diff "origin/%s" "%s"', $path, $this->getBranch(), $file); + } else { + $commitId = $this->getCommitId(); + $compare = empty($commitId) ? 'HEAD' : $commitId; + + $builder->executeCommand('cd "%s" && git diff "%s^^" "%s"', $path, $compare, $file); + } + + $builder->logExecOutput(true); + + $diff = $builder->getLastOutput(); + + $lines = $this->getLinePositions($diff); + + return $lines[(int)$line] ?? null; + } + + /** + * Take a diff + */ + private function getLinePositions(string $diff): ?array + { + if (empty($diff)) { + return null; + } + + $rtn = []; + + $diffLines = \explode(PHP_EOL, $diff); + + while (\count($diffLines)) { + $line = \array_shift($diffLines); + + if (\substr($line, 0, 2) === '@@') { + \array_unshift($diffLines, $line); + + break; + } + } + + $lineNumber = 0; + $position = 0; + + foreach ($diffLines as $diffLine) { + if (\preg_match('/@@\s+\-[0-9]+\,[0-9]+\s+\+([0-9]+)\,([0-9]+)/', $diffLine, $matches)) { + $lineNumber = (int)$matches[1] - 1; + } + + $rtn[$lineNumber] = $position; + + $lineNumber++; + $position++; + } + + return $rtn; + } +} diff --git a/src/Traits/Model/HasCreateDateTrait.php b/src/Traits/Model/HasCreateDateTrait.php new file mode 100644 index 000000000..e902853d6 --- /dev/null +++ b/src/Traits/Model/HasCreateDateTrait.php @@ -0,0 +1,24 @@ +getDataItem('create_date'); + } + + public function setCreateDate(DateTime $value): bool + { + return $this->setDataItem('create_date', $value); + } +} diff --git a/src/Traits/Model/HasUserIdTrait.php b/src/Traits/Model/HasUserIdTrait.php new file mode 100644 index 000000000..4af5c8cf3 --- /dev/null +++ b/src/Traits/Model/HasUserIdTrait.php @@ -0,0 +1,23 @@ +getDataItem('user_id'); + } + + public function setUserId(?int $value): bool + { + return $this->setDataItem('user_id', $value); + } +} diff --git a/src/View.php b/src/View.php index 1e0bbe067..8e76aa75c 100644 --- a/src/View.php +++ b/src/View.php @@ -1,34 +1,30 @@ + * @author Dmitry Khomutov + */ class View { - /** - * @var array - */ - protected $data = []; + protected array $data = []; - /** - * @var string - */ - protected $viewFile; + protected string $viewFile; - /** - * @var string - */ - protected static $extension = 'phtml'; + protected static string $extension = 'phtml'; /** - * @param string $file - * @param string|null $path + * @throws RuntimeException */ - public function __construct($file, $path = null) + public function __construct(string $file, ?string $path = null) { if (!self::exists($file, $path)) { throw new RuntimeException('View file does not exist: ' . $file); @@ -37,106 +33,54 @@ public function __construct($file, $path = null) $this->viewFile = self::getViewFile($file, $path); } - /** - * @param string $file - * @param string|null $path - * - * @return string - */ - protected static function getViewFile($file, $path = null) + protected static function getViewFile(string $file, ?string $path = null): string { - $viewPath = is_null($path) ? (SRC_DIR . 'View/') : $path; - $fullPath = $viewPath . $file . '.' . static::$extension; + $viewPath = \is_null($path) ? (SRC_DIR . 'View/') : $path; - return $fullPath; + return $viewPath . $file . '.' . static::$extension; } - /** - * @param string $file - * @param string|null $path - * - * @return bool - */ - public static function exists($file, $path = null) + public static function exists(string $file, ?string $path = null): bool { - if (!file_exists(self::getViewFile($file, $path))) { + if (!\file_exists(self::getViewFile($file, $path))) { return false; } return true; } - /** - * @param string $key - * - * @return bool - */ - public function __isset($key) + public function __isset(string $key): bool { return isset($this->data[$key]); } /** - * @param string $key - * * @return mixed */ - public function __get($key) + public function __get(string $key) { return $this->data[$key]; } /** - * @param string $key * @param mixed $value */ - public function __set($key, $value) + public function __set(string $key, $value): void { $this->data[$key] = $value; } - /** - * @return string - */ - public function render() + public function render(): string { - extract($this->data); + \extract($this->data); - ob_start(); + \ob_start(); require($this->viewFile); - $html = ob_get_contents(); - ob_end_clean(); + $html = \ob_get_contents(); + \ob_end_clean(); return $html; } - - /** - * @return bool - */ - protected function loginIsDisabled() - { - $config = Config::getInstance(); - $disableAuth = (bool)$config->get('php-censor.security.disable_auth', false); - - return $disableAuth; - } - - /** - * @return User|null - * - * @throws Exception\HttpException - */ - protected function getUser() - { - if (empty($_SESSION['php-censor-user-id'])) { - return null; - } - - /** @var UserStore $userStore */ - $userStore = Factory::getStore('User'); - - return $userStore->getById($_SESSION['php-censor-user-id']); - } } diff --git a/src/View/Build/errors.phtml b/src/View/Build/errors.phtml index c28a53865..bff36390d 100644 --- a/src/View/Build/errors.phtml +++ b/src/View/Build/errors.phtml @@ -14,15 +14,15 @@ $linkTemplate = $build->getFileLinkTemplate(); foreach ($errors as $error): - $link = str_replace('{BASEFILE}', basename($error->getFile()), $linkTemplate); - $link = str_replace('{FILE}', $error->getFile(), $link); - $link = str_replace('{LINE}', $error->getLineStart(), $link); + $link = \str_replace('{BASEFILE}', \basename($error->getFile()), $linkTemplate); + $link = \str_replace('{FILE}', $error->getFile(), $link); + $link = \str_replace('{LINE}', $error->getLineStart(), $link); if ($error->getLineStart() == $error->getLineEnd() || !$error->getLineEnd()) { # For Github - $link = str_replace('-L{LINE_END}', '', $link); + $link = \str_replace('-L{LINE_END}', '', $link); } else { - $link = str_replace('{LINE_END}', $error->getLineEnd(), $link); + $link = \str_replace('{LINE_END}', $error->getLineEnd(), $link); } ?>
- getSource(), Build::$pullRequestSources, true)): ?> + getSource(), Build::$pullRequestSources, true)): ?> getRemoteBranch(); ?> : @@ -59,18 +59,11 @@ use PHPCensor\Store\Factory; getEnvironmentId(); - $environment = null; - if ($environmentId) { - /** @var EnvironmentStore $environmentStore */ - $environmentStore = Factory::getStore('Environment'); - $environmentObject = $environmentStore->getById($environmentId); - if ($environmentObject) { - $environment = $environmentObject->getName(); - } + if ($environment) { + $environmentName = $environment->getName(); } - echo !empty($environment) ? (' ' . $environment) : '—' ; + echo !empty($environmentName) ? (' ' . $environmentName) : '—' ; ?>
- getSource(), Build::$pullRequestSources, true)): ?> + getSource(), Build::$pullRequestSources, true)): ?> getRemoteBranch(); ?> : @@ -79,7 +81,7 @@ $branches = $build->getExtra('branches'); getBranch(); ?> - + getTag()): ?> / @@ -92,38 +94,58 @@ $branches = $build->getExtra('branches'); $environmentId = $build->getEnvironmentId(); $environment = null; if ($environmentId) { - /** @var EnvironmentStore $environmentStore */ - $environmentStore = Factory::getStore('Environment'); $environmentObject = $environmentStore->getById($environmentId); if ($environmentObject) { $environment = $environmentObject->getName(); } } - echo !empty($environment) ? $environment : '—' ; + echo !empty($environment) ? ' ' . $environment : '—' ; ?> - getDuration(); ?> + getStatus(), [Build::STATUS_PENDING, Build::STATUS_RUNNING], true)): ?> + getDuration(); ?> + - getErrorsTotal(); ?> - getStatus(), [Build::STATUS_PENDING, Build::STATUS_RUNNING], true)): ?> + getStatus(), [Build::STATUS_PENDING, Build::STATUS_RUNNING], true)): ?> getErrorsTrend(); ?> - - - - + + getErrorsTotal(); ?> () + + getErrorsTotal(); ?> (+) + + getErrorsTotal(); ?> - getErrorsNew(); ?> + getStatus(), [Build::STATUS_PENDING, Build::STATUS_RUNNING], true)): ?> + getErrorsNew(); ?> + + + + + + + + getStatus(), [Build::STATUS_PENDING, Build::STATUS_RUNNING], true)): ?> + getTestCoverageTrend(); ?> + + getTestCoverage(); ?>% (+%) + + getTestCoverage(); ?>% (%) + + getTestCoverage(); ?>% + +
- getUser()->getIsAdmin()): ?> + getIsAdmin()): ?> diff --git a/src/View/Project/edit.phtml b/src/View/Project/edit.phtml index d5b4f1e3c..a3eea3517 100644 --- a/src/View/Project/edit.phtml +++ b/src/View/Project/edit.phtml @@ -34,7 +34,7 @@ use PHPCensor\Helper\Lang;
- +
diff --git a/src/View/Project/view.phtml b/src/View/Project/view.phtml index f588eb3be..7e6c6679f 100644 --- a/src/View/Project/view.phtml +++ b/src/View/Project/view.phtml @@ -3,9 +3,11 @@ use JasonGrimes\Paginator; use PHPCensor\Helper\Lang; use PHPCensor\Model\Project; +use PHPCensor\Model\User; /** * @var Paginator $paginator + * @var User $user */ ?> @@ -21,12 +23,15 @@ use PHPCensor\Model\Project; + + + - getUser()->getIsAdmin()): ?> + getIsAdmin()): ?> - + @@ -39,28 +44,28 @@ use PHPCensor\Model\Project; getId(); ?>
getArchived()): ?> - getUser()->getIsAdmin()): ?> + getIsAdmin()): ?> - + - + - + - + - + @@ -128,6 +133,7 @@ use PHPCensor\Model\Project;
+ + + + + + + + + + + + + + +
+
+ + getIsAdmin()): ?> + + + +
+
+ + + diff --git a/src/View/User/index.phtml b/src/View/User/index.phtml index b702fd325..554368c38 100644 --- a/src/View/User/index.phtml +++ b/src/View/User/index.phtml @@ -2,8 +2,11 @@ use PHPCensor\Helper\Lang; use PHPCensor\Helper\Template; +use PHPCensor\Model\User; -$currentUser = $this->getUser(); +/** + * @var User $currentUser + */ ?>
diff --git a/src/View/WidgetAllProjects/index-projects.phtml b/src/View/WidgetAllProjects/index-projects.phtml index 1a56242fd..55ea5dc39 100644 --- a/src/View/WidgetAllProjects/index-projects.phtml +++ b/src/View/WidgetAllProjects/index-projects.phtml @@ -17,7 +17,7 @@ foreach ($projects as $project): $success = null; $failure = null; - if (count($builds[$project->getId()])) { + if (\count($builds[$project->getId()])) { // Get the most recent build status to determine the main block colour. $lastBuild = $builds[$project->getId()][0]; $status = $lastBuild->getStatus(); @@ -50,18 +50,18 @@ foreach ($projects as $project): break; case Build::STATUS_SUCCESS: $statuses[] = 'ok'; - $success = is_null($success) && !is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $success; + $success = \is_null($success) && !\is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $success; break; case Build::STATUS_FAILED: $failures++; $statuses[] = 'failed'; - $failure = is_null($failure) && !is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $failure; + $failure = \is_null($failure) && !\is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $failure; break; } } } - $buildCount = count($builds[$project->getId()]); + $buildCount = \count($builds[$project->getId()]); $lastSuccess = $successful[$project->getId()]; $lastFailure = $failed[$project->getId()]; $message = Lang::get('no_builds_yet'); @@ -70,7 +70,7 @@ foreach ($projects as $project): if ($failures > 0) { $message = Lang::get('x_of_x_failed', $failures, $buildCount); - if (!is_null($lastSuccess) && !is_null($lastSuccess->getFinishDate())) { + if (!\is_null($lastSuccess) && !\is_null($lastSuccess->getFinishDate())) { $message .= Lang::get('last_successful_build', $lastSuccess->getFinishDate()->format('Y-m-d H:i:s')); } else { $message .= Lang::get('never_built_successfully'); @@ -78,7 +78,7 @@ foreach ($projects as $project): } else { $message = Lang::get('all_builds_passed', $buildCount); - if (!is_null($lastFailure) && !is_null($lastFailure->getFinishDate())) { + if (!\is_null($lastFailure) && !\is_null($lastFailure->getFinishDate())) { $message .= Lang::get('last_failed_build', $lastFailure->getFinishDate()->format('Y-m-d H:i:s')); } else { $message .= Lang::get('never_failed_build'); @@ -87,11 +87,11 @@ foreach ($projects as $project): } ?> -
+
-

+

getTitle(); ?> @@ -127,11 +127,11 @@ foreach ($projects as $project): switch ($build->getStatus()) { case Build::STATUS_PENDING: $class = 'bg-blue'; - $icon = 'fa-clock-o'; + $icon = 'fa-clock-o blink-item'; break; case BUild::STATUS_RUNNING: $class = 'bg-yellow'; - $icon = 'fa-cogs'; + $icon = 'fa-cogs blink-item'; break; case Build::STATUS_SUCCESS: $class = 'bg-green'; @@ -142,6 +142,7 @@ foreach ($projects as $project): $icon = 'fa-times'; break; } + echo ''; } } diff --git a/src/View/WidgetAllProjects/update.phtml b/src/View/WidgetAllProjects/update.phtml index ec3b4c6c9..2b79f8534 100644 --- a/src/View/WidgetAllProjects/update.phtml +++ b/src/View/WidgetAllProjects/update.phtml @@ -13,7 +13,7 @@ $backgroundClass = 'gray'; $success = null; $failure = null; -if (count($builds)) { +if (\count($builds)) { // Get the most recent build status to determine the main block colour. $lastBuild = $builds[0]; $status = $lastBuild->getStatus(); @@ -44,18 +44,18 @@ if (count($builds)) { break; case Build::STATUS_SUCCESS: $statuses[] = 'ok'; - $success = is_null($success) && !is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $success; + $success = \is_null($success) && !\is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $success; break; case Build::STATUS_FAILED: $failures++; $statuses[] = 'failed'; - $failure = is_null($failure) && !is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $failure; + $failure = \is_null($failure) && !\is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $failure; break; } } } -$buildCount = count($builds); +$buildCount = \count($builds); $lastSuccess = $successful; $lastFailure = $failed; $message = Lang::get('no_builds_yet'); @@ -64,7 +64,7 @@ if ($buildCount > 0) { if ($failures > 0) { $message = Lang::get('x_of_x_failed', $failures, $buildCount); - if (!is_null($lastSuccess) && !is_null($lastSuccess->getFinishDate())) { + if (!\is_null($lastSuccess) && !\is_null($lastSuccess->getFinishDate())) { $message .= Lang::get('last_successful_build', $lastSuccess->getFinishDate()->format('Y-m-d H:i:s')); } else { $message .= Lang::get('never_built_successfully'); @@ -72,7 +72,7 @@ if ($buildCount > 0) { } else { $message = Lang::get('all_builds_passed', $buildCount); - if (!is_null($lastFailure) && !is_null($lastFailure->getFinishDate())) { + if (!\is_null($lastFailure) && !\is_null($lastFailure->getFinishDate())) { $message .= Lang::get('last_failed_build', $lastFailure->getFinishDate()->format('Y-m-d H:i:s')); } else { $message .= Lang::get('never_failed_build'); @@ -85,7 +85,7 @@ if ($buildCount > 0) {
-

+

getTitle(); ?> @@ -119,11 +119,11 @@ if ($buildCount > 0) { switch ($build->getStatus()) { case Build::STATUS_PENDING: $class = 'bg-blue'; - $icon = 'fa-clock-o'; + $icon = 'fa-clock-o blink-item'; break; case Build::STATUS_RUNNING: $class = 'bg-yellow'; - $icon = 'fa-cogs'; + $icon = 'fa-cogs blink-item'; break; case Build::STATUS_SUCCESS: $class = 'bg-green'; @@ -134,6 +134,7 @@ if ($buildCount > 0) { $icon = 'fa-times'; break; } + echo ''; } } ?> diff --git a/src/View/WidgetBuildErrors/update.phtml b/src/View/WidgetBuildErrors/update.phtml index df912fa82..9af655833 100644 --- a/src/View/WidgetBuildErrors/update.phtml +++ b/src/View/WidgetBuildErrors/update.phtml @@ -51,17 +51,17 @@ foreach ($builds as $projectId => $projectEnvs): break; case Build::STATUS_SUCCESS: $statuses[] = 'ok'; - $success = is_null($success) && !is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $success; + $success = \is_null($success) && !\is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $success; break; case Build::STATUS_FAILED: $failures++; $statuses[] = 'failed'; - $failure = is_null($failure) && !is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $failure; + $failure = \is_null($failure) && !\is_null($build->getFinishDate()) ? $build->getFinishDate()->format('Y-m-d H:i:s') : $failure; break; } } - $buildCount = count($projectEnv['latest']); + $buildCount = \count($projectEnv['latest']); $lastSuccess = $projectEnv['success']; $lastFailure = $projectEnv['failed']; $message = Lang::get('no_builds_yet'); @@ -70,7 +70,7 @@ foreach ($builds as $projectId => $projectEnvs): if ($failures > 0) { $message = Lang::get('x_of_x_failed', $failures, $buildCount); - if (!is_null($lastSuccess) && !is_null($lastSuccess->getFinishDate())) { + if (!\is_null($lastSuccess) && !\is_null($lastSuccess->getFinishDate())) { $message .= Lang::get('last_successful_build', $lastSuccess->getFinishDate()->format('Y-m-d H:i:s')); } else { $message .= Lang::get('never_built_successfully'); @@ -78,7 +78,7 @@ foreach ($builds as $projectId => $projectEnvs): } else { $message = Lang::get('all_builds_passed', $buildCount); - if (!is_null($lastFailure) && !is_null($lastFailure->getFinishDate())) { + if (!\is_null($lastFailure) && !\is_null($lastFailure->getFinishDate())) { $message .= Lang::get('last_failed_build', $lastFailure->getFinishDate()->format('Y-m-d H:i:s')); } else { $message .= Lang::get('never_failed_build'); @@ -87,11 +87,11 @@ foreach ($builds as $projectId => $projectEnvs): } ?> -
+
-

+

getTitle(); ?> @@ -128,11 +128,11 @@ foreach ($builds as $projectId => $projectEnvs): switch ($build->getStatus()) { case Build::STATUS_PENDING: $class = 'bg-blue'; - $icon = 'fa-clock-o'; + $icon = 'fa-clock-o blink-item'; break; case Build::STATUS_RUNNING: $class = 'bg-yellow'; - $icon = 'fa-cogs'; + $icon = 'fa-cogs blink-item'; break; case Build::STATUS_SUCCESS: $class = 'bg-green'; diff --git a/src/View/WidgetLastBuilds/index.phtml b/src/View/WidgetLastBuilds/index.phtml index 4da541e83..1a371ffa3 100644 --- a/src/View/WidgetLastBuilds/index.phtml +++ b/src/View/WidgetLastBuilds/index.phtml @@ -6,6 +6,11 @@ use PHPCensor\Helper\Lang;

+
+ +
diff --git a/src/View/WidgetLastBuilds/update.phtml b/src/View/WidgetLastBuilds/update.phtml index c1662e52e..bf6f34792 100644 --- a/src/View/WidgetLastBuilds/update.phtml +++ b/src/View/WidgetLastBuilds/update.phtml @@ -4,10 +4,10 @@ use PHPCensor\Helper\Lang; use PHPCensor\Helper\Template; use PHPCensor\Model\Build; use PHPCensor\Store\EnvironmentStore; -use PHPCensor\Store\Factory; /** - * @var Build[] $builds + * @var Build[] $builds + * @var EnvironmentStore $environmentStore */ ?> @@ -19,8 +19,6 @@ use PHPCensor\Store\Factory; $environmentId = $build->getEnvironmentId(); $environment = null; if ($environmentId) { - /** @var EnvironmentStore $environmentStore */ - $environmentStore = Factory::getStore('Environment'); $environmentObject = $environmentStore->getById($environmentId); if ($environmentObject) { $environment = $environmentObject->getName(); @@ -33,13 +31,13 @@ use PHPCensor\Store\Factory; case Build::STATUS_PENDING: $updated = $build->getCreateDate(); $label = Lang::get('pending'); - $color = 'blue'; + $color = 'blue blink-item'; break; case Build::STATUS_RUNNING: $updated = $build->getStartDate(); $label = Lang::get('running'); - $color = 'yellow'; + $color = 'yellow blink-item'; break; case Build::STATUS_SUCCESS: @@ -82,39 +80,24 @@ use PHPCensor\Store\Factory; ?>

- - getProject()->getTitle(); ?> - - - — - #getId(); ?> + #getId(); ?> — - getSourceHumanize(); ?> - - — - : - getErrorsTotal(); ?> - getStatus(), [Build::STATUS_PENDING, Build::STATUS_RUNNING], true)): ?> - getErrorsTrend(); ?> - - - - - - - - getErrorsNew()): ?> - / - : - + + getProject()->getTitle(); ?> + + + — + +

- getSource(), Build::$pullRequestSources, true)): ?> + getSourceHumanize(); ?> — + getSource(), Build::$pullRequestSources, true)): ?> getRemoteBranch(); ?> : @@ -125,7 +108,7 @@ use PHPCensor\Store\Factory; getBranch(); ?> - + getTag()): ?> / @@ -135,7 +118,7 @@ use PHPCensor\Store\Factory; getCommitId())) { echo ' — '; - echo sprintf( + echo \sprintf( '%s %s', $build->getCommitLink(), substr($build->getCommitId(), 0, 7), @@ -143,12 +126,50 @@ use PHPCensor\Store\Factory; ); $buildCommitMessage = Template::clean($build->getCommitMessage()); if ($buildCommitMessage) { - echo '

'; + echo ' — '; echo $buildCommitMessage; } } ?>

+ getStatus(), [Build::STATUS_PENDING, Build::STATUS_RUNNING], true)): ?> +

+ : + getStatus(), [Build::STATUS_PENDING, Build::STATUS_RUNNING], true)): ?> + getErrorsTrend(); ?> + + getErrorsTotal(); ?> () + + getErrorsTotal(); ?> (+) + + getErrorsTotal(); ?> + + + + getErrorsNew()): ?> + / + : + + + + + + + + | + : + getStatus(), [Build::STATUS_PENDING, Build::STATUS_RUNNING], true)): ?> + getTestCoverageTrend(); ?> + + getTestCoverage(); ?>% (+%) + + getTestCoverage(); ?>% (%) + + getTestCoverage(); ?>% + + +

+
diff --git a/src/View/exception.phtml b/src/View/exception.phtml index fff7cd7bf..aafe0f774 100644 --- a/src/View/exception.phtml +++ b/src/View/exception.phtml @@ -3,19 +3,17 @@ use PHPCensor\Model\User; /** - * @var $exception Exception + * @var Exception $exception + * @var User $user */ -/** @var User $user */ -$user = $this->getUser(); - ?>

Sorry, there was a problem

- getIsAdmin()): ?> +
Message: getMessage(); ?>
File: getFile(); ?>
@@ -23,5 +21,5 @@ $user = $this->getUser(); Trace:
getTraceAsString(); ?>
- +
diff --git a/src/View/layout.phtml b/src/View/layout.phtml index 5a89b5726..a178d7103 100644 --- a/src/View/layout.phtml +++ b/src/View/layout.phtml @@ -3,17 +3,13 @@ use PHPCensor\Helper\Lang; use PHPCensor\Model\User; -/** - * @var User $user - */ -$user = $this->getUser(); - /** * @var string $title * @var string $subtitle * @var string $version * @var string $content * @var string $skin + * @var User $user */ ?> @@ -47,12 +43,13 @@ $user = $this->getUser(); @@ -105,7 +102,7 @@ $user = $this->getUser(); - loginIsDisabled()): ?> + isLoginDisabled): ?>