diff --git a/.all-contributorsrc b/.all-contributorsrc index 41f8ba6f..174060f4 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -31,6 +31,52 @@ "contributions": [ "code" ] + }, + { + "login": "LadySolveig", + "name": "Martina Scholz", + "avatar_url": "https://avatars.githubusercontent.com/u/64533137?v=4", + "profile": "https://github.com/LadySolveig", + "contributions": [ + "code" + ] + }, + { + "login": "escopecz", + "name": "John Linhart", + "avatar_url": "https://avatars.githubusercontent.com/u/1235442?v=4", + "profile": "http://johnlinhart.com", + "contributions": [ + "review" + ] + }, + { + "login": "Rocksheep", + "name": "Marinus van Velzen", + "avatar_url": "https://avatars.githubusercontent.com/u/1311371?v=4", + "profile": "https://github.com/Rocksheep", + "contributions": [ + "code" + ] + }, + { + "login": "PierreAmmeloot", + "name": "Pierre Ammeloot", + "avatar_url": "https://avatars.githubusercontent.com/u/4603318?v=4", + "profile": "https://pierre.ammeloot.fr", + "contributions": [ + "userTesting" + ] + }, + { + "login": "matbcvo", + "name": "Martin Vooremäe", + "avatar_url": "https://avatars.githubusercontent.com/u/1006437?v=4", + "profile": "https://matbcvo.github.io", + "contributions": [ + "code", + "test" + ] } ], "contributorsPerLine": 7, @@ -38,5 +84,7 @@ "projectOwner": "mautic", "repoType": "github", "repoHost": "https://github.com", - "skipCi": true + "skipCi": true, + "commitConvention": "angular", + "commitType": "docs" } diff --git a/.ddev/local.config.php.dist b/.ddev/local.config.php.dist index c145ba34..fbf689e4 100644 --- a/.ddev/local.config.php.dist +++ b/.ddev/local.config.php.dist @@ -52,10 +52,10 @@ return [ 'version' => 'OAuth1a', // Required for OAuth1a and OAuth2 - 'baseUrl' => 'http://localhost/mautic/index_dev.php', + 'baseUrl' => 'http://localhost/mautic/index.php', // Required for All tests - 'apiUrl' => 'http://localhost/mautic/index_dev.php/api/', + 'apiUrl' => 'http://localhost/mautic/index.php/api/', // Required for EmailsTest 'testEmail' => 'notexisting@email.com', diff --git a/.ddev/mautic-local.php.dist b/.ddev/mautic-local.php.dist index 72f0206f..4afef178 100644 --- a/.ddev/mautic-local.php.dist +++ b/.ddev/mautic-local.php.dist @@ -14,4 +14,9 @@ $parameters = [ 'db_password' => 'db', 'admin_email' => 'mautic@ddev.local', 'admin_password' => 'mautic', + 'mailer_from_name' => 'DDEV', + 'mailer_from_email' => 'mautic@ddev.local', + 'mailer_transport' => 'smtp', + 'mailer_host' => 'localhost', + 'mailer_port' => '1025', ]; diff --git a/.ddev/mautic-setup.sh b/.ddev/mautic-setup.sh index 265697e3..ae7b8f2c 100755 --- a/.ddev/mautic-setup.sh +++ b/.ddev/mautic-setup.sh @@ -11,9 +11,7 @@ setup_mautic() { cp ../.ddev/mautic-local.php.dist ./app/config/local.php printf "Installing Mautic...\n" - php bin/console mautic:install --force http://localhost/mautic \ - --mailer_from_name="DDEV" --mailer_from_email="mautic@ddev.local" \ - --mailer_transport="smtp" --mailer_host="localhost" --mailer_port="1025" + php bin/console mautic:install --force http://localhost/mautic php bin/console cache:warmup --no-interaction --env=dev printf "Enabling Twilio plugin for tests...\n" diff --git a/.github/ci-files/.env b/.github/ci-files/.env new file mode 100644 index 00000000..ad4a26e6 --- /dev/null +++ b/.github/ci-files/.env @@ -0,0 +1,28 @@ +# In all environments, the following files are loaded if they exist, +# the latter taking precedence over the former: +# +# * .env contains default values for the environment variables needed by the app +# * .env.local uncommitted file with local overrides +# * .env.$APP_ENV committed environment-specific defaults +# * .env.$APP_ENV.local uncommitted environment-specific overrides +# +# Real environment variables win over .env files. +# +# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES. +# +# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2). +# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration +APP_ENV=test +APP_DEBUG=0 + +DB_HOST=127.0.0.1 +DB_NAME=mautictest +DB_USER=root +DB_PORT=3306 +DB_PASSWD= +MAUTIC_DB_PREFIX= +MAUTIC_TABLE_PREFIX= +MAUTIC_ENV=test +MAUTIC_ADMIN_USERNAME=admin +MAUTIC_ADMIN_PASSWORD=mautic +MAUTIC_DB_DRIVER=pdo_mysql diff --git a/.github/ci-files/local.config.php b/.github/ci-files/local.config.php index d9213085..a61f0b97 100644 --- a/.github/ci-files/local.config.php +++ b/.github/ci-files/local.config.php @@ -34,7 +34,7 @@ | */ 'userName' => 'admin', - 'password' => 'mautic', + 'password' => 'Maut1cR0cks!', /* |-------------------------------------------------------------------------- @@ -52,10 +52,10 @@ 'version' => 'OAuth1a', // Required for OAuth1a and OAuth2 - 'baseUrl' => 'http://localhost/index_dev.php', + 'baseUrl' => 'http://localhost/index.php', // Required for All tests - 'apiUrl' => 'http://localhost/index_dev.php/api/', + 'apiUrl' => 'http://localhost/index.php/api/', // Required for EmailsTest 'testEmail' => 'notexisting@email.com', diff --git a/.github/ci-files/local.php b/.github/ci-files/local_4.php similarity index 81% rename from .github/ci-files/local.php rename to .github/ci-files/local_4.php index 0fc13904..a207e534 100644 --- a/.github/ci-files/local.php +++ b/.github/ci-files/local_4.php @@ -14,4 +14,7 @@ 'db_password' => '', 'admin_email' => 'github-actions@mautic.org', 'admin_password' => 'mautic', + 'mailer_transport' => 'smtp', + 'mailer_host' => 'localhost', + 'mailer_port' => '1025', ]; diff --git a/.github/ci-files/local_5.php b/.github/ci-files/local_5.php new file mode 100644 index 00000000..5c5371e5 --- /dev/null +++ b/.github/ci-files/local_5.php @@ -0,0 +1,21 @@ + true, + 'api_enable_basic_auth' => true, + 'db_driver' => 'pdo_mysql', + 'db_host' => '127.0.0.1', + 'db_table_prefix' => null, + 'db_port' => getenv('DB_PORT'), + 'db_name' => 'mautictest', + 'db_user' => 'root', + 'db_password' => '', + 'admin_email' => 'github-actions@mautic.org', + 'admin_password' => 'Maut1cR0cks!', + 'mailer_from_name' => 'GitHub Actions', + 'mailer_from_email' => 'github-actions@mautic.org', + 'mailer_transport' => 'smtp', + 'mailer_dsn' => 'smtp://localhost:1025', +]; diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4f458d5e..f33e88b1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,12 +13,12 @@ on: required: false jobs: phpunit: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 name: PHPUnit tests services: mysql: - image: mysql:5.7 + image: mysql:8.4 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: mautictest @@ -30,7 +30,7 @@ jobs: image: mailhog/mailhog:latest ports: - 1025:1025 - + redis: image: redis:6 ports: @@ -45,7 +45,7 @@ jobs: - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.3 ini-values: session.save_handler=redis, session.save_path="tcp://127.0.0.1:6379" extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, pdo_mysql coverage: pcov @@ -57,14 +57,15 @@ jobs: # We need the sed command at the bottom to set the PHP session save path to a directory that's writable for PHP # NOTE: update the PHP version below as well if you're updating PHP! run: | - sudo add-apt-repository ppa:ondrej/php -y - sudo add-apt-repository ppa:ondrej/apache2 -y - sudo apt-get install apache2 libapache2-mod-php8.0 + sudo apt-get update -o Acquire::Retries=3 + sudo apt-get install -y --no-install-recommends apache2 libapache2-mod-php8.3 + sudo a2enmod rewrite - sudo sed -i 's,^session.save_handler =.*$,session.save_handler = redis,' /etc/php/8.0/apache2/php.ini - sudo sed -i 's,^;session.save_path =.*$,session.save_path = "tcp://127.0.0.1:6379",' /etc/php/8.0/apache2/php.ini + sudo sed -i 's,^session.save_handler =.*$,session.save_handler = redis,' /etc/php/8.3/apache2/php.ini + sudo sed -i 's,^;session.save_path =.*$,session.save_path = "tcp://127.0.0.1:6379",' /etc/php/8.3/apache2/php.ini + sudo sed -i 's,^memory_limit =.*$,memory_limit = 256M,' /etc/php/8.3/apache2/php.ini sudo service apache2 restart - cat /etc/php/8.0/apache2/php.ini | grep session + cat /etc/php/8.3/apache2/php.ini | grep session - name: Install dependencies run: | @@ -89,37 +90,49 @@ jobs: sudo chown -R www-data:www-data /var/www/html rm -rf /var/www/html/* mv $GITHUB_WORKSPACE/mautic/* /var/www/html/ + cp ./.github/ci-files/.env /var/www/html/ + sed -i 's/DB_PORT=3306/DB_PORT=${{ job.services.mysql.ports[3306] }}/g' /var/www/html/.env + rm -f /var/www/html/.env.test - name: Install Mautic env: DB_PORT: ${{ job.services.mysql.ports[3306] }} run: | composer install --prefer-dist --no-progress - cp $GITHUB_WORKSPACE/.github/ci-files/local.php ./app/config/local.php - php bin/console mautic:install http://localhost/ \ - --force --mailer_from_name="GitHub Actions" --mailer_from_email="github-actions@mautic.org" \ - --mailer_transport="smtp" --mailer_host="localhost" --mailer_port="1025" --env=dev - php bin/console cache:warmup --no-interaction --env=dev + if [[ "$(jq -r '.version | .[0:1]' app/release_metadata.json)" == "4" ]]; then + cp $GITHUB_WORKSPACE/.github/ci-files/local_4.php ./app/config/local.php + php bin/console mautic:install http://localhost/ --force --env=test --mailer_from_name="GitHub Actions" --mailer_from_email="github-actions@mautic.org" + sed -i "s/('prod',/('test',/g" ./index.php + echo "MAUTIC_VERSION=4" >> $GITHUB_ENV + else + cp $GITHUB_WORKSPACE/.github/ci-files/local_5.php ./config/local.php + php bin/console mautic:install http://localhost/ --force --env=test + echo "MAUTIC_VERSION=5" >> $GITHUB_ENV + fi + + php bin/console cache:warmup --no-interaction --env=test working-directory: /var/www/html/ # Enable Twilio plugin with random credentials (needed for MessagesTest to function, doesn't actually contact Twilio API). - name: Enable Twilio plugin run: | mysql -uroot -P${{ job.services.mysql.ports[3306] }} -h127.0.0.1 -e "USE mautictest; INSERT INTO plugin_integration_settings (plugin_id, name, is_published, supported_features, api_keys, feature_settings) VALUES (NULL, 'Twilio', 1, 'a:0:{}', 'a:2:{s:8:\"username\";s:169:\"bzFmNlIydWRSZXlIN2lQVkdpanJ4aTQ2NUh6RVdDbHlLRVhsWGZ4b0kyZVNxLzYrQ1J6V1RvMnlhVEp0c245TEp6eStQekx5ZVhLWjB1YVdoR3RnR2dHQ3k1emVVdGt5NzZKUmtjUnJ3c1E9|L8tbZRIYhwatT7Mq+HAdYA==\";s:8:\"password\";s:169:\"T2d2cFpXQWE5YVZnNFFianJSYURRYUtGRHBNZGZjM1VETXg2Wm5Va3NheW43MjVWUlJhTVlCL2pYMDBpbElONStiVVBNbEM3M3BaeGJMNkFKNUFEN1pTNldSRjc4bUM4SDh1SE9OY1k5MTg9|TeuSvfx4XSUOvp0O7T49Cg==\";}', 'a:4:{s:20:\"sending_phone_number\";N;s:22:\"disable_trackable_urls\";i:0;s:16:\"frequency_number\";N;s:14:\"frequency_time\";N;}');" - php bin/console mautic:plugins:reload --env=dev + php bin/console mautic:plugins:reload --env=test working-directory: /var/www/html - name: Set correct ownership so Apache can access the files run: sudo chown -R www-data:www-data /var/www/html - + - name: Run tests - run: vendor/bin/paratest -p 3 --coverage-clover coverage.xml + run: | + sed -i 's/env name="MAUTIC_VERSION" value="5"/env name="MAUTIC_VERSION" value="${{ env.MAUTIC_VERSION }}"/g' ./phpunit.xml.dist + vendor/bin/phpunit --coverage-clover coverage.xml - name: Upload code coverage run: bash <(curl -s https://codecov.io/bash) - + - name: Upload logs as artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: name: mautic-logs @@ -132,9 +145,9 @@ jobs: SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_MESSAGE: 'The daily API library tests against mautic/mautic have failed. Most likely a PR was merged recently which introduced a regression of some sort.' - cs: - runs-on: ubuntu-20.04 - name: CS tests + tests: + runs-on: ubuntu-24.04 + name: CS & PHPSTAN tests steps: - uses: actions/checkout@v3 @@ -142,7 +155,7 @@ jobs: - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 with: - php-version: 8.0 + php-version: 8.3 extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, pdo_mysql - name: Install dependencies @@ -151,4 +164,7 @@ jobs: composer install --prefer-dist --no-progress - name: Run CS tests - run: vendor/bin/php-cs-fixer fix --config=.php_cs -v --dry-run --using-cache=no --show-progress=dots --diff $(git diff -- '*.php' --name-only --diff-filter=ACMRTUXB "HEAD~..HEAD") + run: vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php -v --dry-run --using-cache=no --show-progress=dots --diff $(git diff -- '*.php' --name-only --diff-filter=ACMRTUXB "HEAD~..HEAD") + + - name: Run PHPSTAN tests + run: composer phpstan \ No newline at end of file diff --git a/.gitignore b/.gitignore index c847cd4a..574b2f92 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,11 @@ /tests/local.config.php_* /tests/local.tokens.php /tests/phpunit.phar +/composer.lock /vendor .DS_Store +/.php-cs-fixer.cache .phpunit.result.cache /.ddev/mautic-preference /mautic +CLAUDE.md diff --git a/.php_cs b/.php_cs deleted file mode 100644 index d3b6865b..00000000 --- a/.php_cs +++ /dev/null @@ -1,20 +0,0 @@ -in(__DIR__.'/lib') - ->in(__DIR__.'/tests'); - -return PhpCsFixer\Config::create() - ->setRules([ - '@Symfony' => true, - 'binary_operator_spaces' => [ - 'align_double_arrow' => true, - 'align_equals' => true - ], - 'ordered_imports' => true, - 'array_syntax' => [ - 'syntax' => 'short' - ], - 'no_unused_imports' => false, - ]) - ->setFinder($finder); \ No newline at end of file diff --git a/.well-known/funding-manifest-urls b/.well-known/funding-manifest-urls new file mode 100644 index 00000000..1f60e471 --- /dev/null +++ b/.well-known/funding-manifest-urls @@ -0,0 +1 @@ +https://github.com/mautic/mautic/blob/5.x/funding.json diff --git a/README.md b/README.md index 6fc99f22..6fc2eebc 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ [![codecov](https://codecov.io/gh/mautic/api-library/branch/master/graph/badge.svg)](https://codecov.io/gh/mautic/api-library) [![Latest Stable Version](https://poser.pugx.org/mautic/api-library/v)](//packagist.org/packages/mautic/api-library) [![Total Downloads](https://poser.pugx.org/mautic/api-library/downloads)](//packagist.org/packages/mautic/api-library) [![Latest Unstable Version](https://poser.pugx.org/mautic/api-library/v/unstable)](//packagist.org/packages/mautic/api-library) [![License](https://poser.pugx.org/mautic/api-library/license)](//packagist.org/packages/mautic/api-library) -[![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-) # Using the Mautic API Library ## Requirements -* PHP 7.2 or newer -* cURL support +* PHP 8.0 or newer ## Installing the API Library You can install the API Library with the following command: @@ -16,6 +15,38 @@ You can install the API Library with the following command: composer require mautic/api-library ``` +N.B. Make sure you have installed a PSR-18 HTTP Client before you install this package or install one at the same time e.g. `composer require mautic/api-library guzzlehttp/guzzle:^7.3`. + +### HTTP Client + +We are decoupled from any HTTP messaging client with the help of [PSR-18 HTTP Client](https://www.php-fig.org/psr/psr-18/). This requires an extra package providing [psr/http-client-implementation](https://packagist.org/providers/psr/http-client-implementation). To use Guzzle 7, for example, simply require `guzzlehttp/guzzle`: + +``` bash +composer require guzzlehttp/guzzle:^7.3 +``` + +The installed HTTP Client is auto-discovered using [php-http/discovery](https://packagist.org/providers/php-http/discovery), but you can also provide your own HTTP Client if you like. + +```php + 10, +]); + +// Initiate the auth object +$initAuth = new ApiAuth($httpClient); +$auth = $initAuth->newAuth($settings); +// etc. +``` + ## Mautic Setup The API must be enabled in Mautic. Within Mautic, go to the Configuration page (located in the Settings menu) and under API Settings enable Mautic's API. If you intend to use Basic Authentication, ensure you enable it. You can also choose which OAuth protocol to use here. After saving the configuration, go to the API Credentials page @@ -167,13 +198,47 @@ $auth = $initAuth->newAuth($settings, 'BasicAuth'); ]; ``` -**Note:** You can also specify a CURLOPT_TIMEOUT in the request (default is set to wait indefinitely): + +### Using 2-Legged OAuth2 (Client Credentials) + +The Client Credentials grant is used when applications request an access token to access their own resources, not on behalf of a user. This is ideal for server-to-server communication (cron jobs, background processes, etc.). + +**Note:** This requires Mautic 4.0+ with the client_credentials grant type enabled. + ```php + 'TwoLeggedOAuth2', + 'baseUrl' => 'https://your-mautic.com', + 'clientKey' => '', // Client ID from Mautic API credentials + 'clientSecret' => '', // Client Secret from Mautic API credentials +]; + +// If you have a stored access token, you can pass it to avoid requesting a new one +// $settings['accessToken'] = 'your_stored_access_token'; +// $settings['accessTokenExpires'] = 1234567890; // Unix timestamp + $initAuth = new ApiAuth(); -$auth = $initAuth->newAuth($settings, 'BasicAuth'); -$timeout = 10; +$auth = $initAuth->newAuth($settings, $settings['AuthMethod']); + +// Request a new access token if needed +if (!$auth->isAuthorized()) { + $auth->requestAccessToken(); +} + +// Check if token was updated (for caching purposes) +if ($auth->accessTokenUpdated()) { + $tokenData = $auth->getAccessTokenData(); + // Store $tokenData['access_token'] and $tokenData['expires'] for future use +} -$auth->setCurlTimeout($timeout); +// The auth object is now ready to use with API contexts ``` ## API Requests @@ -325,9 +390,16 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - + + + + + + + + + +

Zdeno Kuzmany

💻

dlopez-akalam

💻

mollux

💻
Zdeno Kuzmany
Zdeno Kuzmany

💻
dlopez-akalam
dlopez-akalam

💻
mollux
mollux

💻
Martina  Scholz
Martina Scholz

💻
John Linhart
John Linhart

👀
Marinus van Velzen
Marinus van Velzen

💻
Pierre Ammeloot
Pierre Ammeloot

📓
Martin Vooremäe
Martin Vooremäe

💻 ⚠️
diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..93962af2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,86 @@ +# Security Policy + +Goals of the Mautic Security Team +--------------------------------- + +* Resolve reported security issues in a Security Advisory +* Provide documentation on how to write secure code +* Provide documentation on securing your Mautic instance +* Help the infrastructure team to keep the \*.mautic.org infrastructure secure + +Scope of the Mautic Security Team +--------------------------------- + +The Mautic Security Team operates with a limited scope and only directly responds to issues with Mautic core, officially supported plugins and resources, and the \*.mautic.org network of websites. The team does not directly handle potential vulnerabilities with third party plugins or individual Mautic instances. + +Which Mautic releases get Security Advisories? +---------------------------------------------- + +Check the [Releases page](https://www.mautic.org/mautic-releases) to find which are the currently supported releases. + +Security advisories are only made for issues affecting stable releases in the supported major version branches. That means there will be no security advisories for development releases, alphas, betas or release candidates. + +Which API Library releases get Security Advisories? +--------------------------------------------------- +At this time, Mautic's security team supports the current stable version and the previous major version of the API library. For example, when 4.0 is released, security advisories will be made for 4.x and 3.x. + +How to report a potential security issue +---------------------------------------- + +If you discover or learn about a potential error, weakness, or threat that can compromise the security of Mautic and is covered by the [Security Advisory Policy](https://www.mautic.org/mautic-security-team/mautic-security-advisory-policy), we ask you to keep it confidential and submit your concern to the Mautic security team. + +To make your report please submit it as a private disclosure at [https://github.com/mautic/api-library/security](https://github.com/mautic/api-library/security). You can also create a private fork to provide a fix, if you're able to do so. See the documentation from GitHub on [privately reporting a security issue](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability). + +Do not post it in GitHub as an issue or a Pull Request, on the forums, or discuss it in Slack. + +[Read more: How to report a security issue with Mautic](https://www.mautic.org/mautic-security-team/how-to-report-a-security-issue) + +How are security issues resolved? +--------------------------------- + +The Mautic Security Team are responsible for triaging incoming security issues relating to Mautic core and officially supported plugins and resources, and for releasing fixes in a timely manner. + +[Read more: How are security issues triaged and resolved by the Mautic Security Team?](https://www.mautic.org/mautic-security-team/triaging-and-resolving-security-issues) + +How are security fixes announced and released? +---------------------------------------------- + +The Security Team coordinates security announcements in release cycles and evaluates whether security issues are ready for release several days in advance. + +The team may deem it necessary to make an out-of-sequence release, in which case at least two weeks’ notice will be provided to ensure that Mautic users are made aware of a security release being made on an unscheduled basis. + +[Read more: Security fix announcements and releases](https://www.mautic.org/mautic-security-team/triaging-and-resolving-security-issues) + +What is a Security Advisory? +---------------------------- + +A security advisory is a public announcement managed by the Mautic Security Team which informs Mautic users about a reported security problem in Mautic core or officially supported plugins and resources, and the steps Mautic users should take to address it. (Usually this involves updating to a new release of the code that fixes the security problem.) + +[Read more: Mautic Security Advisory Policy](https://www.mautic.org/mautic-security-team/mautic-security-advisory-policy) + +What is the disclosure policy of the Mautic Security Team? +---------------------------------------------------------- + +The security team follows a Coordinated Disclosure policy: we keep issues private until there is a fix. Public announcements are made when the threat has been addressed and a secure version is available. + +When reporting a security issue, observe the same policy. **Do not** share your knowledge of security issues with others. + +How do I join the Mautic Security Team? +--------------------------------------- + +As membership in the team gives the individual access to potentially destructive information, membership is limited to people who have a proven track record in the Mautic community. + +Team members are expected to work at least a few hours every month. Exceptions to that can be made for short periods to accommodate other priorities, but people who can't maintain some level of involvement will be asked to reconsider their membership on the team. + +[Read more: How do I join the Mautic Security Team?](https://www.mautic.org/mautic-security-team/join-the-team) + +Who are the Mautic Security Team members? +----------------------------------------- + +You can meet the Mautic Security Team on the page below. + +[Read more: Meet the Mautic Security Team](https://www.mautic.org/meet-the-mautic-security-team) + +Resources and guidance from the [Drupal](https://www.drupal.org/security), [Joomla](https://developer.joomla.org/security.html) and [Mozilla](https://www.mozilla.org/en-US/security/) projects have been drawn from to create these documents and develop our processes/workflows. + +Always [report the issue to the team](https://www.mautic.org/mautic-security-team/how-to-report-a-security-issue) and let them make the decision on whether to handle it in public or private. diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md deleted file mode 100644 index c266ce61..00000000 --- a/UPGRADE-3.0.md +++ /dev/null @@ -1,12 +0,0 @@ -# Dependencies -* PHP 7.2.0 is the now the minimum - -# Contacts -* \Mautic\Api\Contacts::getEvents has been removed. Use \Mautic\Api\Contacts::getActivityForContact instead. - -# Leads -* \Mautic\Api\Leads has been removed. Use \Mautic\Api\Contacts instead. - -# Lists -* The \Mautic\Api\Lists has been removed. Use \Mautic\Api\Segments instead. - diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md new file mode 100644 index 00000000..e05dd13b --- /dev/null +++ b/UPGRADE-4.0.md @@ -0,0 +1,89 @@ +# Dependencies +* PHP 8.0 is now the minimum (was PHP 7.2 in 3.x) +* We are decoupled from any HTTP messaging client with the help of [PSR-18 HTTP Client](https://www.php-fig.org/psr/psr-18/). This requires an extra package providing [psr/http-client-implementation](https://packagist.org/providers/psr/http-client-implementation). To use Guzzle 7, for example, simply require `guzzlehttp/guzzle` in your project. If you do not have an HTTP client installed, `php-http/discovery` will install one if you allow the Composer plugin. + +## Installation Change +```bash +# Old (3.x) - cURL was built-in +composer require webmecanik/api-library + +# New (4.x) - HTTP client required +composer require webmecanik/api-library guzzlehttp/guzzle:^7.3 +``` + +# Api +* \Mautic\Api\Api::getLogger now returns void as the LoggerAwareInterface dictates +* \Mautic\Api\Api::getResponseInfo and \Mautic\Response::getInfo have been removed, all Auth classes now have a getResponse method to get the PSR-7 response message + +# HTTP +## Timeout +The setCurlTimeout-method (`$auth->setCurlTimeout(10);`) has been removed. If you like to set the timeout, you should configure your HTTP client to do so. For example, with Guzzle 7, you can do this: + +```php +$httpClient = new \GuzzleHttp\Client([ + 'timeout' => 10, +]); +$settings = [ + // ... +]; + +$initAuth = new \Mautic\Auth\ApiAuth($httpClient); +$auth = $initAuth->newAuth($settings); +``` + +# TwoLeggedOAuth2 (Client Credentials) + +The TwoLeggedOAuth2 authentication has been updated for PSR-18 compatibility. + +## Method Changes +* `getAccessToken()` now automatically calls `requestAccessToken()` if token is missing/expired +* New method `requestAccessToken()` explicitly requests a new token +* New method `validateAccessToken()` checks if current token is valid +* New method `getAccessTokenData()` returns token data for caching +* New method `accessTokenUpdated()` indicates if token was refreshed + +## Usage Change +```php +// Old (3.x) +$auth = $initAuth->newAuth($settings, 'TwoLeggedOAuth2'); +$token = $auth->getAccessToken(); // Always made HTTP request + +// New (4.x) +$auth = $initAuth->newAuth($settings, 'TwoLeggedOAuth2'); +if (!$auth->isAuthorized()) { + $auth->requestAccessToken(); +} +// Or simply: +$token = $auth->getAccessToken(); // Only requests if needed +``` + +# New Features in 4.x + +## Point Groups API (Mautic 5.x) +New `PointGroups` context and contact methods for point group scoring: +```php +$contactApi->getPointGroupScores($contactId); +$contactApi->addPointGroupScore($contactId, $groupId, $points); +$contactApi->subtractPointGroupScore($contactId, $groupId, $points); +``` + +## Send Custom Email to Contact +New method to send custom emails without pre-defined templates: +```php +$emailApi->sendCustomToContact($contactId, [ + 'fromEmail' => 'noreply@example.com', + 'fromName' => 'My App', // optional + 'subject' => 'Hello', + 'content' => '

Custom HTML content with {{contactfield=firstname}}

', +]); +``` + +**Note:** This feature requires Mautic with [PR #12854](https://github.com/mautic/mautic/pull/12854) merged. +It is not available in standard Mautic 5.x releases. + +# Version Compatibility + +| Library Version | PHP | Mautic | +|-----------------|-----|--------| +| 3.x | 7.2+ | 3.x, 4.x | +| 4.x | 8.0+ | 4.x, 5.x | diff --git a/composer.json b/composer.json index 6fbb18f4..783515b5 100644 --- a/composer.json +++ b/composer.json @@ -14,18 +14,31 @@ } }, "require": { - "php": ">=7.2.0", - "ext-curl": "*", + "php": ">=8.0", "ext-json": "*", - "psr/log": "~1.0 || ~2.0 || ~3.0" + "psr/log": "~1.0 || ~2.0 || ~3.0", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "^1.0", + "guzzlehttp/psr7": "^2.4", + "php-http/discovery": "^1.15" }, "require-dev": { "kint-php/kint": "^4.1", - "friendsofphp/php-cs-fixer": "^2.19.3", + "friendsofphp/php-cs-fixer": "^3.73", "phpunit/phpunit": "~9.5.0", - "brianium/paratest": "^6.2" + "guzzlehttp/guzzle": "^7.5", + "phpstan/phpstan": "^1.11" + }, + "suggest": { + "guzzlehttp/guzzle": "A popular HTTP client that implements psr/http-client-implementation." }, "scripts": { - "test": "vendor/bin/paratest" + "test": "vendor/bin/phpunit", + "phpstan": "vendor/bin/phpstan analyse" + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } } } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 6347dae6..00000000 --- a/composer.lock +++ /dev/null @@ -1,4263 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", - "This file is @generated automatically" - ], - "content-hash": "786e116622ff75904afc35a26f231705", - "packages": [ - { - "name": "psr/log", - "version": "1.1.4", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" - }, - "time": "2021-05-03T11:20:27+00:00" - } - ], - "packages-dev": [ - { - "name": "brianium/paratest", - "version": "v6.8.0", - "source": { - "type": "git", - "url": "https://github.com/paratestphp/paratest.git", - "reference": "4b70abf4c2ffa08c64e2e89c7b5b7e43cdf26d52" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/4b70abf4c2ffa08c64e2e89c7b5b7e43cdf26d52", - "reference": "4b70abf4c2ffa08c64e2e89c7b5b7e43cdf26d52", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-simplexml": "*", - "fidry/cpu-core-counter": "^0.4.1", - "jean85/pretty-package-versions": "^2.0.5", - "php": "^7.3 || ^8.0", - "phpunit/php-code-coverage": "^9.2.23", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-timer": "^5.0.3", - "phpunit/phpunit": "^9.5.27", - "sebastian/environment": "^5.1.4", - "symfony/console": "^5.4.16 || ^6.2.3", - "symfony/process": "^5.4.11 || ^6.2" - }, - "require-dev": { - "doctrine/coding-standard": "^10.0.0", - "ext-pcov": "*", - "ext-posix": "*", - "infection/infection": "^0.26.16", - "squizlabs/php_codesniffer": "^3.7.1", - "symfony/filesystem": "^5.4.13 || ^6.2", - "vimeo/psalm": "^5.4" - }, - "bin": [ - "bin/paratest", - "bin/paratest.bat", - "bin/paratest_for_phpstorm" - ], - "type": "library", - "autoload": { - "psr-4": { - "ParaTest\\": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Brian Scaturro", - "email": "scaturrob@gmail.com", - "role": "Developer" - }, - { - "name": "Filippo Tessarotto", - "email": "zoeslam@gmail.com", - "role": "Developer" - } - ], - "description": "Parallel testing for PHP", - "homepage": "https://github.com/paratestphp/paratest", - "keywords": [ - "concurrent", - "parallel", - "phpunit", - "testing" - ], - "support": { - "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v6.8.0" - }, - "funding": [ - { - "url": "https://github.com/sponsors/Slamdunk", - "type": "github" - }, - { - "url": "https://paypal.me/filippotessarotto", - "type": "paypal" - } - ], - "time": "2022-12-29T09:42:35+00:00" - }, - { - "name": "composer/pcre", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/67a32d7d6f9f560b726ab25a061b38ff3a80c560", - "reference": "67a32d7d6f9f560b726ab25a061b38ff3a80c560", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.3", - "phpstan/phpstan-strict-rules": "^1.1", - "symfony/phpunit-bridge": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/1.0.1" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-01-21T20:24:37+00:00" - }, - { - "name": "composer/semver", - "version": "3.3.2", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9", - "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0 || ^8.0" - }, - "require-dev": { - "phpstan/phpstan": "^1.4", - "symfony/phpunit-bridge": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.3.2" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-04-01T19:23:25+00:00" - }, - { - "name": "composer/xdebug-handler", - "version": "2.0.5", - "source": { - "type": "git", - "url": "https://github.com/composer/xdebug-handler.git", - "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/9e36aeed4616366d2b690bdce11f71e9178c579a", - "reference": "9e36aeed4616366d2b690bdce11f71e9178c579a", - "shasum": "" - }, - "require": { - "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", - "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Composer\\XdebugHandler\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "John Stevenson", - "email": "john-stevenson@blueyonder.co.uk" - } - ], - "description": "Restarts a process without Xdebug.", - "keywords": [ - "Xdebug", - "performance" - ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/2.0.5" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "time": "2022-02-24T20:20:32+00:00" - }, - { - "name": "doctrine/annotations", - "version": "1.14.2", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "ad785217c1e9555a7d6c6c8c9f406395a5e2882b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/ad785217c1e9555a7d6c6c8c9f406395a5e2882b", - "reference": "ad785217c1e9555a7d6c6c8c9f406395a5e2882b", - "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 || ^10", - "phpstan/phpstan": "~1.4.10 || ^1.8.0", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "symfony/cache": "^4.4 || ^5.4 || ^6", - "vimeo/psalm": "^4.10" - }, - "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.2" - }, - "time": "2022-12-15T06:48:22+00:00" - }, - { - "name": "doctrine/deprecations", - "version": "v1.0.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/deprecations.git", - "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/deprecations/zipball/0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", - "reference": "0e2a4f1f8cdfc7a92ec3b01c9334898c806b30de", - "shasum": "" - }, - "require": { - "php": "^7.1|^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9", - "phpunit/phpunit": "^7.5|^8.5|^9.5", - "psr/log": "^1|^2|^3" - }, - "suggest": { - "psr/log": "Allows logging deprecations via PSR-3 logger implementation" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", - "homepage": "https://www.doctrine-project.org/", - "support": { - "issues": "https://github.com/doctrine/deprecations/issues", - "source": "https://github.com/doctrine/deprecations/tree/v1.0.0" - }, - "time": "2022-05-02T15:47:09+00:00" - }, - { - "name": "doctrine/instantiator", - "version": "1.5.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", - "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^0.16 || ^1", - "phpstan/phpstan": "^1.4", - "phpstan/phpstan-phpunit": "^1", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.30 || ^5.4" - }, - "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": "doctrine/lexer", - "version": "2.1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", - "reference": "39ab8fcf5a51ce4b85ca97c7a7d033eb12831124", - "shasum": "" - }, - "require": { - "doctrine/deprecations": "^1.0", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "doctrine/coding-standard": "^9 || ^10", - "phpstan/phpstan": "^1.3", - "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^4.11 || ^5.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Doctrine\\Common\\Lexer\\": "src" - } - }, - "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": "Johannes Schmitt", - "email": "schmittjoh@gmail.com" - } - ], - "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "https://www.doctrine-project.org/projects/lexer.html", - "keywords": [ - "annotations", - "docblock", - "lexer", - "parser", - "php" - ], - "support": { - "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/2.1.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%2Flexer", - "type": "tidelift" - } - ], - "time": "2022-12-14T08:49:07+00:00" - }, - { - "name": "fidry/cpu-core-counter", - "version": "0.4.1", - "source": { - "type": "git", - "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "79261cc280aded96d098e1b0e0ba0c4881b432c2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/79261cc280aded96d098e1b0e0ba0c4881b432c2", - "reference": "79261cc280aded96d098e1b0e0ba0c4881b432c2", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "require-dev": { - "fidry/makefile": "^0.2.0", - "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": "^9.5.26 || ^8.5.31", - "theofidry/php-cs-fixer-config": "^1.0", - "webmozarts/strict-phpunit": "^7.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "Fidry\\CpuCoreCounter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Théo FIDRY", - "email": "theo.fidry@gmail.com" - } - ], - "description": "Tiny utility to get the number of CPU cores.", - "keywords": [ - "CPU", - "core" - ], - "support": { - "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/0.4.1" - }, - "funding": [ - { - "url": "https://github.com/theofidry", - "type": "github" - } - ], - "time": "2022-12-16T22:01:02+00:00" - }, - { - "name": "friendsofphp/php-cs-fixer", - "version": "v2.19.3", - "source": { - "type": "git", - "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "75ac86f33fab4714ea5a39a396784d83ae3b5ed8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/75ac86f33fab4714ea5a39a396784d83ae3b5ed8", - "reference": "75ac86f33fab4714ea5a39a396784d83ae3b5ed8", - "shasum": "" - }, - "require": { - "composer/semver": "^1.4 || ^2.0 || ^3.0", - "composer/xdebug-handler": "^1.2 || ^2.0", - "doctrine/annotations": "^1.2", - "ext-json": "*", - "ext-tokenizer": "*", - "php": "^5.6 || ^7.0 || ^8.0", - "php-cs-fixer/diff": "^1.3", - "symfony/console": "^3.4.43 || ^4.1.6 || ^5.0", - "symfony/event-dispatcher": "^3.0 || ^4.0 || ^5.0", - "symfony/filesystem": "^3.0 || ^4.0 || ^5.0", - "symfony/finder": "^3.0 || ^4.0 || ^5.0", - "symfony/options-resolver": "^3.0 || ^4.0 || ^5.0", - "symfony/polyfill-php70": "^1.0", - "symfony/polyfill-php72": "^1.4", - "symfony/process": "^3.0 || ^4.0 || ^5.0", - "symfony/stopwatch": "^3.0 || ^4.0 || ^5.0" - }, - "require-dev": { - "justinrainbow/json-schema": "^5.0", - "keradus/cli-executor": "^1.4", - "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.4.2", - "php-cs-fixer/accessible-object": "^1.0", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1", - "phpspec/prophecy-phpunit": "^1.1 || ^2.0", - "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.13 || ^9.5", - "phpunitgoodpractices/polyfill": "^1.5", - "phpunitgoodpractices/traits": "^1.9.1", - "sanmai/phpunit-legacy-adapter": "^6.4 || ^8.2.1", - "symfony/phpunit-bridge": "^5.2.1", - "symfony/yaml": "^3.0 || ^4.0 || ^5.0" - }, - "suggest": { - "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters.", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", - "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." - }, - "bin": [ - "php-cs-fixer" - ], - "type": "application", - "extra": { - "branch-alias": { - "dev-master": "2.19-dev" - } - }, - "autoload": { - "psr-4": { - "PhpCsFixer\\": "src/" - }, - "classmap": [ - "tests/Test/AbstractFixerTestCase.php", - "tests/Test/AbstractIntegrationCaseFactory.php", - "tests/Test/AbstractIntegrationTestCase.php", - "tests/Test/Assert/AssertTokensTrait.php", - "tests/Test/IntegrationCase.php", - "tests/Test/IntegrationCaseFactory.php", - "tests/Test/IntegrationCaseFactoryInterface.php", - "tests/Test/InternalIntegrationCaseFactory.php", - "tests/Test/IsIdenticalConstraint.php", - "tests/Test/TokensWithObservedTransformers.php", - "tests/TestCase.php" - ] - }, - "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/v2.19.3" - }, - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], - "time": "2021-11-15T17:17:55+00:00" - }, - { - "name": "jean85/pretty-package-versions", - "version": "2.0.5", - "source": { - "type": "git", - "url": "https://github.com/Jean85/pretty-package-versions.git", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "reference": "ae547e455a3d8babd07b96966b17d7fd21d9c6af", - "shasum": "" - }, - "require": { - "composer-runtime-api": "^2.0.0", - "php": "^7.1|^8.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^2.17", - "jean85/composer-provided-replaced-stub-package": "^1.0", - "phpstan/phpstan": "^0.12.66", - "phpunit/phpunit": "^7.5|^8.5|^9.4", - "vimeo/psalm": "^4.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Jean85\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Alessandro Lai", - "email": "alessandro.lai85@gmail.com" - } - ], - "description": "A library to get pretty versions strings of installed dependencies", - "keywords": [ - "composer", - "package", - "release", - "versions" - ], - "support": { - "issues": "https://github.com/Jean85/pretty-package-versions/issues", - "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.5" - }, - "time": "2021-10-08T21:21:46+00:00" - }, - { - "name": "kint-php/kint", - "version": "4.2.3", - "source": { - "type": "git", - "url": "https://github.com/kint-php/kint.git", - "reference": "7601bfd95ccc50a1b903c2764b31d00919e8edd9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/kint-php/kint/zipball/7601bfd95ccc50a1b903c2764b31d00919e8edd9", - "reference": "7601bfd95ccc50a1b903c2764b31d00919e8edd9", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.0", - "phpspec/prophecy-phpunit": "^2", - "phpunit/phpunit": "^9.0", - "seld/phar-utils": "^1.0", - "symfony/finder": "^3.0 || ^4.0 || ^5.0", - "vimeo/psalm": "^4.0" - }, - "suggest": { - "kint-php/kint-helpers": "Provides extra helper functions", - "kint-php/kint-twig": "Provides d() and s() functions in twig templates" - }, - "type": "library", - "autoload": { - "files": [ - "init.php" - ], - "psr-4": { - "Kint\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Vollebregt", - "homepage": "https://github.com/jnvsor" - }, - { - "name": "Contributors", - "homepage": "https://github.com/kint-php/kint/graphs/contributors" - } - ], - "description": "Kint - debugging tool for PHP developers", - "homepage": "https://kint-php.github.io/kint/", - "keywords": [ - "debug", - "kint", - "php" - ], - "support": { - "issues": "https://github.com/kint-php/kint/issues", - "source": "https://github.com/kint-php/kint/tree/4.2.3" - }, - "time": "2022-10-01T20:16:33+00:00" - }, - { - "name": "myclabs/deep-copy", - "version": "1.11.0", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614", - "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3,<3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2022-03-03T13:19:32+00:00" - }, - { - "name": "nikic/php-parser", - "version": "v4.15.2", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "reference": "f59bbe44bf7d96f24f3e2b4ddc21cd52c1d2adbc", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.2" - }, - "time": "2022-11-12T15:38:23+00:00" - }, - { - "name": "phar-io/manifest", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/phar-io/manifest.git", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/97803eca37d319dfa7826cc2437fc020857acb53", - "reference": "97803eca37d319dfa7826cc2437fc020857acb53", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-phar": "*", - "ext-xmlwriter": "*", - "phar-io/version": "^3.0.1", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "support": { - "issues": "https://github.com/phar-io/manifest/issues", - "source": "https://github.com/phar-io/manifest/tree/2.0.3" - }, - "time": "2021-07-20T11:28:43+00:00" - }, - { - "name": "phar-io/version", - "version": "3.2.1", - "source": { - "type": "git", - "url": "https://github.com/phar-io/version.git", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", - "shasum": "" - }, - "require": { - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - }, - { - "name": "Sebastian Heuer", - "email": "sebastian@phpeople.de", - "role": "Developer" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "Developer" - } - ], - "description": "Library for handling version information and constraints", - "support": { - "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.2.1" - }, - "time": "2022-02-21T01:04:05+00:00" - }, - { - "name": "php-cs-fixer/diff", - "version": "v1.3.1", - "source": { - "type": "git", - "url": "https://github.com/PHP-CS-Fixer/diff.git", - "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/dbd31aeb251639ac0b9e7e29405c1441907f5759", - "reference": "dbd31aeb251639ac0b9e7e29405c1441907f5759", - "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": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "SpacePossum" - } - ], - "description": "sebastian/diff v2 backport support for PHP5.6", - "homepage": "https://github.com/PHP-CS-Fixer", - "keywords": [ - "diff" - ], - "support": { - "issues": "https://github.com/PHP-CS-Fixer/diff/issues", - "source": "https://github.com/PHP-CS-Fixer/diff/tree/v1.3.1" - }, - "abandoned": true, - "time": "2020-10-14T08:39:05+00:00" - }, - { - "name": "phpunit/php-code-coverage", - "version": "9.2.23", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", - "reference": "9f1f0f9a2fbb680b26d1cf9b61b6eac43a6e4e9c", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-libxml": "*", - "ext-xmlwriter": "*", - "nikic/php-parser": "^4.14", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.23" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-12-28T12:41:10+00:00" - }, - { - "name": "phpunit/php-file-iterator", - "version": "3.0.6", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2021-12-02T12:48:52+00:00" - }, - { - "name": "phpunit/php-invoker", - "version": "3.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcntl": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", - "keywords": [ - "process" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:58:55+00:00" - }, - { - "name": "phpunit/php-text-template", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T05:33:50+00:00" - }, - { - "name": "phpunit/php-timer", - "version": "5.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:16:10+00:00" - }, - { - "name": "phpunit/phpunit", - "version": "9.5.28", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/954ca3113a03bf780d22f07bf055d883ee04b65e", - "reference": "954ca3113a03bf780d22f07bf055d883ee04b65e", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.3.1 || ^2", - "ext-dom": "*", - "ext-json": "*", - "ext-libxml": "*", - "ext-mbstring": "*", - "ext-xml": "*", - "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", - "phar-io/manifest": "^2.0.3", - "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.13", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.5", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^3.2", - "sebastian/version": "^3.0.2" - }, - "suggest": { - "ext-soap": "*", - "ext-xdebug": "*" - }, - "bin": [ - "phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.5-dev" - } - }, - "autoload": { - "files": [ - "src/Framework/Assert/Functions.php" - ], - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "https://phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.28" - }, - "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": "2023-01-14T12:32:24+00:00" - }, - { - "name": "psr/cache", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/php-fig/cache.git", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", - "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Cache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Common interface for caching libraries", - "keywords": [ - "cache", - "psr", - "psr-6" - ], - "support": { - "source": "https://github.com/php-fig/cache/tree/master" - }, - "time": "2016-08-06T20:24:11+00:00" - }, - { - "name": "psr/container", - "version": "1.1.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" - }, - "time": "2021-11-05T16:50:12+00:00" - }, - { - "name": "psr/event-dispatcher", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", - "shasum": "" - }, - "require": { - "php": ">=7.2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\EventDispatcher\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Standard interfaces for event handling.", - "keywords": [ - "events", - "psr", - "psr-14" - ], - "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" - }, - "time": "2019-01-08T18:20:26+00:00" - }, - { - "name": "sebastian/cli-parser", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", - "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:08:49+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" - }, - { - "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Looks up which function or method a line of code belongs to", - "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:30:19+00:00" - }, - { - "name": "sebastian/comparator", - "version": "4.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - } - ], - "description": "Provides the functionality to compare PHP values for equality", - "homepage": "https://github.com/sebastianbergmann/comparator", - "keywords": [ - "comparator", - "compare", - "equality" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-09-14T12:41:17+00:00" - }, - { - "name": "sebastian/complexity", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T15:52:27+00:00" - }, - { - "name": "sebastian/diff", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff", - "udiff", - "unidiff", - "unified diff" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:10:38+00:00" - }, - { - "name": "sebastian/environment", - "version": "5.1.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-posix": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", - "keywords": [ - "Xdebug", - "environment", - "hhvm" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-04-03T09:37:03+00:00" - }, - { - "name": "sebastian/exporter", - "version": "4.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "reference": "ac230ed27f0f98f597c8a2b6eb7ac563af5e5b9d", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Volker Dusch", - "email": "github@wallbash.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - }, - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Provides the functionality to export PHP variables for visualization", - "homepage": "https://www.github.com/sebastianbergmann/exporter", - "keywords": [ - "export", - "exporter" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-09-14T06:03:37+00:00" - }, - { - "name": "sebastian/global-state", - "version": "5.0.5", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "reference": "0ca8db5a5fc9c8646244e629625ac486fa286bf2", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", - "keywords": [ - "global state" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.5" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-02-14T08:28:10+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-28T06:42:11+00:00" - }, - { - "name": "sebastian/object-enumerator", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", - "shasum": "" - }, - "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Traverses array structures and object graphs to enumerate all referenced objects", - "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:12:34+00:00" - }, - { - "name": "sebastian/object-reflector", - "version": "2.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Allows reflection of object attributes, including inherited and non-public ones", - "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "support": { - "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:14:26+00:00" - }, - { - "name": "sebastian/recursion-context", - "version": "4.0.4", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, - { - "name": "Jeff Welch", - "email": "whatthejeff@gmail.com" - }, - { - "name": "Adam Harvey", - "email": "aharvey@php.net" - } - ], - "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "support": { - "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:17:30+00:00" - }, - { - "name": "sebastian/resource-operations", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "support": { - "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:45:17+00:00" - }, - { - "name": "sebastian/type", - "version": "3.2.0", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.2-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", - "support": { - "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2022-09-12T14:47:03+00:00" - }, - { - "name": "sebastian/version", - "version": "3.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library that helps with managing the version number of Git-hosted PHP projects", - "homepage": "https://github.com/sebastianbergmann/version", - "support": { - "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:39:44+00:00" - }, - { - "name": "symfony/console", - "version": "v5.4.17", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f", - "reference": "58422fdcb0e715ed05b385f70d3e8b5ed4bbd45f", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.1|^6.0" - }, - "conflict": { - "psr/log": ">=3", - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0" - }, - "require-dev": { - "psr/log": "^1|^2", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/lock": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/v5.4.17" - }, - "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": "2022-12-28T14:15:31+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "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": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" - }, - "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": "2022-01-02T09:53:40+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v5.4.17", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "8e18a9d559eb8ebc2220588f1faa726a2fcd31c9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8e18a9d559eb8ebc2220588f1faa726a2fcd31c9", - "reference": "8e18a9d559eb8ebc2220588f1faa726a2fcd31c9", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^2|^3", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/dependency-injection": "<4.4" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.17" - }, - "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": "2022-12-12T15:54:21+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/f98b54df6ad059855739db6fcbc2d36995283fe1", - "reference": "f98b54df6ad059855739db6fcbc2d36995283fe1", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/event-dispatcher": "^1" - }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "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": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.2" - }, - "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": "2022-01-02T09:53:40+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v5.4.13", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "ac09569844a9109a5966b9438fc29113ce77cf51" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/ac09569844a9109a5966b9438fc29113ce77cf51", - "reference": "ac09569844a9109a5966b9438fc29113ce77cf51", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.13" - }, - "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": "2022-09-21T19:53:16+00:00" - }, - { - "name": "symfony/finder", - "version": "v5.4.17", - "source": { - "type": "git", - "url": "https://github.com/symfony/finder.git", - "reference": "40c08632019838dfb3350f18cf5563b8080055fc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/40c08632019838dfb3350f18cf5563b8080055fc", - "reference": "40c08632019838dfb3350f18cf5563b8080055fc", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Finder\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v5.4.17" - }, - "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": "2022-12-22T10:31:03+00:00" - }, - { - "name": "symfony/options-resolver", - "version": "v5.4.11", - "source": { - "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "54f14e36aa73cb8f7261d7686691fd4d75ea2690" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/54f14e36aa73cb8f7261d7686691fd4d75ea2690", - "reference": "54f14e36aa73cb8f7261d7686691fd4d75ea2690", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php73": "~1.0", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an improved replacement for the array_replace PHP function", - "homepage": "https://symfony.com", - "keywords": [ - "config", - "configuration", - "options" - ], - "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.4.11" - }, - "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": "2022-07-20T13:00:38+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/5bbc823adecdae860bb64756d639ecfec17b050a", - "reference": "5bbc823adecdae860bb64756d639ecfec17b050a", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.27.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": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/511a08c03c1960e08a883f4cffcacd219b758354", - "reference": "511a08c03c1960e08a883f4cffcacd219b758354", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/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.27.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": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "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 Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.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": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "reference": "8ad114f6b39e2c98a8b0e3bd907732c207c2b534", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "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 Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.27.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": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/polyfill-php70", - "version": "v1.20.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/5f03a781d984aae42cebd18e7912fa80f02ee644", - "reference": "5f03a781d984aae42cebd18e7912fa80f02ee644", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "metapackage", - "extra": { - "branch-alias": { - "dev-main": "1.20-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "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 backporting some PHP 7.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php70/tree/v1.20.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": "2020-10-23T14:02:19+00:00" - }, - { - "name": "symfony/polyfill-php72", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97", - "reference": "869329b1e9894268a8a61dabb69153029b7a8c97", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - } - }, - "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 backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.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": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/9e8ecb5f92152187c4799efd3c96b78ccab18ff9", - "reference": "9e8ecb5f92152187c4799efd3c96b78ccab18ff9", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "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 backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.27.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": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.27.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "reference": "7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.27-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.27.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": "2022-11-03T14:55:06+00:00" - }, - { - "name": "symfony/process", - "version": "v5.4.11", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/6e75fe6874cbc7e4773d049616ab450eff537bf1", - "reference": "6e75fe6874cbc7e4773d049616ab450eff537bf1", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-php80": "^1.16" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v5.4.11" - }, - "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": "2022-06-27T16:58:25+00:00" - }, - { - "name": "symfony/service-contracts", - "version": "v2.5.2", - "source": { - "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/4b426aac47d6427cc1a1d0f7e2ac724627f5966c", - "reference": "4b426aac47d6427cc1a1d0f7e2ac724627f5966c", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" - }, - "conflict": { - "ext-psr": "<1.1|>=2" - }, - "suggest": { - "symfony/service-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } - }, - "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": "Generic abstractions related to writing services", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.2" - }, - "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": "2022-05-30T19:17:29+00:00" - }, - { - "name": "symfony/stopwatch", - "version": "v5.4.13", - "source": { - "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "6df7a3effde34d81717bbef4591e5ffe32226d69" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/6df7a3effde34d81717bbef4591e5ffe32226d69", - "reference": "6df7a3effde34d81717bbef4591e5ffe32226d69", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/service-contracts": "^1|^2|^3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides a way to profile code", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.4.13" - }, - "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": "2022-09-28T13:19:49+00:00" - }, - { - "name": "symfony/string", - "version": "v5.4.17", - "source": { - "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "55733a8664b8853b003e70251c58bc8cb2d82a6b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/55733a8664b8853b003e70251c58bc8cb2d82a6b", - "reference": "55733a8664b8853b003e70251c58bc8cb2d82a6b", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" - }, - "conflict": { - "symfony/translation-contracts": ">=3.0" - }, - "require-dev": { - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0|^6.0" - }, - "type": "library", - "autoload": { - "files": [ - "Resources/functions.php" - ], - "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "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": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", - "homepage": "https://symfony.com", - "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" - ], - "support": { - "source": "https://github.com/symfony/string/tree/v5.4.17" - }, - "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": "2022-12-12T15:54:21+00:00" - }, - { - "name": "theseer/tokenizer", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/theseer/tokenizer.git", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e", - "reference": "34a41e998c2183e22995f158c581e7b5e755ab9e", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": "^7.2 || ^8.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Arne Blankerts", - "email": "arne@blankerts.de", - "role": "Developer" - } - ], - "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "support": { - "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.1" - }, - "funding": [ - { - "url": "https://github.com/theseer", - "type": "github" - } - ], - "time": "2021-07-28T10:34:58+00:00" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=7.2.0", - "ext-curl": "*", - "ext-json": "*" - }, - "platform-dev": [], - "plugin-api-version": "2.2.0" -} diff --git a/lib/Api/Api.php b/lib/Api/Api.php index 01b3825f..ed351c51 100644 --- a/lib/Api/Api.php +++ b/lib/Api/Api.php @@ -1,4 +1,5 @@ logger; } - /** - * Sets a logger. - * - * @return $this - */ - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - - return $this; - } - /** * Get the array of available search commands. * @@ -198,7 +185,7 @@ public function setBaseUrl($url) /** * Make the API request. * - * @param $endpoint + * @param string $endpoint * @param string $method * * @return array @@ -244,12 +231,12 @@ public function makeRequest($endpoint, array $parameters = [], $method = 'GET') if (false === strpos($url, 'http')) { $error = [ - 'code' => 500, - 'message' => sprintf( - 'URL is incomplete. Please use %s, set the base URL as the third argument to $MauticApi->newApi(), or make $endpoint a complete URL.', - __CLASS__.'setBaseUrl()' - ), - ]; + 'code' => 500, + 'message' => sprintf( + 'URL is incomplete. Please use %s, set the base URL as the third argument to $MauticApi->newApi(), or make $endpoint a complete URL.', + __CLASS__.'setBaseUrl()' + ), + ]; } else { try { $settings = []; @@ -263,36 +250,35 @@ public function makeRequest($endpoint, array $parameters = [], $method = 'GET') if (!is_array($response)) { $this->getLogger()->warning($response); - //assume an error + // assume an error $error = [ - 'code' => 500, - 'message' => $response, - ]; + 'code' => 500, + 'message' => $response, + ]; } } catch (\Exception $e) { $this->getLogger()->error('Failed connecting to Mautic API: '.$e->getMessage(), ['trace' => $e->getTraceAsString()]); $error = [ - 'code' => $e->getCode(), - 'message' => $e->getMessage(), - ]; + 'code' => $e->getCode(), + 'message' => $e->getMessage(), + ]; } } if (!empty($error)) { return [ - 'errors' => [$error], - ]; + 'errors' => [$error], + ]; } elseif (!empty($response['errors'])) { $this->getLogger()->error('Mautic API returned errors: '.var_export($response['errors'], true)); } // Ensure a code is present in the error array if (!empty($response['errors'])) { - $info = $this->auth->getResponseInfo(); foreach ($response['errors'] as $key => $error) { if (!isset($response['errors'][$key]['code'])) { - $response['errors'][$key]['code'] = $info['http_code']; + $response['errors'][$key]['code'] = $this->auth->getResponse()->getStatusCode(); } } } @@ -308,16 +294,6 @@ public function makeRequest($endpoint, array $parameters = [], $method = 'GET') return $response; } - /** - * Returns HTTP response info. - * - * @return array - */ - public function getResponseInfo() - { - return $this->auth->getResponseInfo(); - } - /** * Returns HTTP response headers. * @@ -336,10 +312,10 @@ public function getResponseHeaders() */ public function getMauticVersion() { - $headers = $this->auth->getResponseHeaders(); + $headers = array_change_key_case($this->auth->getResponseHeaders(), CASE_LOWER); - if (isset($headers['Mautic-Version'])) { - return $headers['Mautic-Version']; + if (isset($headers['mautic-version'])) { + return $headers['mautic-version']; } return null; @@ -358,7 +334,7 @@ public function get($id) } /** - * @param $id + * @param int $id * * @return array|bool */ @@ -496,7 +472,7 @@ public function editBatch(array $parameters, $createIfNotExists = false) /** * Delete an item. * - * @param $id + * @param int $id * * @return array|mixed */ @@ -510,8 +486,6 @@ public function delete($id) /** * Delete a batch of items. * - * @param $ids - * * @return array|mixed */ public function deleteBatch(array $ids) @@ -543,7 +517,7 @@ protected function actionNotSupported($action) /** * Verify that a default endpoint is supported by the API. * - * @param $action + * @param string $action * * @return bool */ diff --git a/lib/Api/Assets.php b/lib/Api/Assets.php index 3a7e5285..baffe5d9 100644 --- a/lib/Api/Assets.php +++ b/lib/Api/Assets.php @@ -1,4 +1,5 @@ 'campaigns/$1/contact/remove/$2', // 2.6.0 ]; - /** - * {@inheritdoc} - */ protected $searchCommands = [ 'ids', 'is:published', diff --git a/lib/Api/Categories.php b/lib/Api/Categories.php index bcf9cebc..5660a698 100644 --- a/lib/Api/Categories.php +++ b/lib/Api/Categories.php @@ -1,4 +1,5 @@ 'companies/$1/contact/remove/$2', // 2.6.0 ]; - /** - * {@inheritdoc} - */ protected $searchCommands = [ 'ids', 'is:mine', diff --git a/lib/Api/CompanyFields.php b/lib/Api/CompanyFields.php index 9fcd39ce..2c479905 100644 --- a/lib/Api/CompanyFields.php +++ b/lib/Api/CompanyFields.php @@ -1,4 +1,5 @@ 'contacts/$1/dnc/remove/$2', // 2.6.0 ]; - /** - * {@inheritdoc} - */ protected $searchCommands = [ 'ids', 'is:anonymous', @@ -121,13 +110,11 @@ public function getSegments() /** * Get a list of contact activity events for all contacts. * - * @param int $id Contact ID - * @param string $search - * @param string $orderBy - * @param string $orderByDir - * @param int $page - * @param \DateTime $dateFrom - * @param \DateTime $dateTo + * @param int $id Contact ID + * @param string $search + * @param string $orderBy + * @param string $orderByDir + * @param int $page * * @return array|mixed */ @@ -139,8 +126,8 @@ public function getActivityForContact( $orderBy = '', $orderByDir = 'ASC', $page = 1, - \DateTime $dateFrom = null, - \DateTime $dateTo = null + ?\DateTime $dateFrom = null, + ?\DateTime $dateTo = null, ) { return $this->fetchActivity('/'.$id.'/activity', $search, $includeEvents, $excludeEvents, $orderBy, $orderByDir, $page, $dateFrom, $dateTo); } @@ -149,12 +136,10 @@ public function getActivityForContact( * Get a list of contact engagement events. * Not related to a specific contact ID. * - * @param string $search - * @param string $orderBy - * @param string $orderByDir - * @param int $page - * @param \DateTime $dateFrom - * @param \DateTime $dateTo + * @param string $search + * @param string $orderBy + * @param string $orderByDir + * @param int $page * * @return array|mixed */ @@ -165,8 +150,8 @@ public function getActivity( $orderBy = '', $orderByDir = 'ASC', $page = 1, - \DateTime $dateFrom = null, - \DateTime $dateTo = null + ?\DateTime $dateFrom = null, + ?\DateTime $dateTo = null, ) { return $this->fetchActivity('/activity', $search, $includeEvents, $excludeEvents, $orderBy, $orderByDir, $page, $dateFrom, $dateTo); } @@ -174,13 +159,11 @@ public function getActivity( /** * Get a list of contact activity events for all contacts. * - * @param string $path of the URL after the endpoint - * @param string $search - * @param string $orderBy - * @param string $orderByDir - * @param int $page - * @param \DateTime $dateFrom - * @param \DateTime $dateTo + * @param string $path of the URL after the endpoint + * @param string $search + * @param string $orderBy + * @param string $orderByDir + * @param int $page * * @return array|mixed */ @@ -192,8 +175,8 @@ protected function fetchActivity( $orderBy = '', $orderByDir = 'ASC', $page = 1, - \DateTime $dateFrom = null, - \DateTime $dateTo = null + ?\DateTime $dateFrom = null, + ?\DateTime $dateTo = null, ) { $parameters = [ 'filters' => [ @@ -276,7 +259,7 @@ public function getContactDevices($id, $search = '', $start = 0, $limit = 0, $or /** * Get a list of smart segments the contact is in. * - * @param $id + * @param int $id * * @return array|mixed */ @@ -288,7 +271,7 @@ public function getContactSegments($id) /** * Get a list of companies the contact is in. * - * @param $id + * @param int $id * * @return array|mixed */ @@ -300,7 +283,7 @@ public function getContactCompanies($id) /** * Get a list of campaigns the contact is in. * - * @param $id + * @param int $id * * @return array|mixed */ @@ -315,8 +298,6 @@ public function getContactCampaigns($id) * @param int $id * @param int $points * @param array $parameters 'eventName' and 'actionName' - * - * @return mixed */ public function addPoints($id, $points, array $parameters = []) { @@ -329,14 +310,78 @@ public function addPoints($id, $points, array $parameters = []) * @param int $id * @param int $points * @param array $parameters 'eventName' and 'actionName' - * - * @return mixed */ public function subtractPoints($id, $points, array $parameters = []) { return $this->makeRequest('contacts/'.$id.'/points/minus/'.$points, $parameters, 'POST'); } + /** + * Get all point group scores associated with contact. + */ + public function getPointGroupScores(int $contactId): array + { + return $this->makeRequest('contacts/'.$contactId.'/points/groups'); + } + + /** + * Get the contact score for a specified point group. + */ + public function getPointGroupScore(int $contactId, int $groupId): array + { + return $this->makeRequest('contacts/'.$contactId.'/points/groups/'.$groupId); + } + + /** + * Increase the score of the contact point group. + * + * @param array $parameters 'eventName' and 'actionName' + */ + public function addPointGroupScore(int $contactId, int $groupId, int $points, array $parameters = []): array + { + return $this->makeRequest('contacts/'.$contactId.'/points/groups/'.$groupId.'/plus/'.$points, $parameters, 'POST'); + } + + /** + * Decrease the score of the contact point group. + * + * @param array $parameters 'eventName' and 'actionName' + */ + public function subtractPointGroupScore(int $contactId, int $groupId, int $points, array $parameters = []): array + { + return $this->makeRequest('contacts/'.$contactId.'/points/groups/'.$groupId.'/minus/'.$points, $parameters, 'POST'); + } + + /** + * Multiply the score of the contact point group. + * + * @param array $parameters 'eventName' and 'actionName' + */ + public function multiplyPointGroupScore(int $contactId, int $groupId, int $value, array $parameters = []): array + { + return $this->makeRequest('contacts/'.$contactId.'/points/groups/'.$groupId.'/times/'.$value, $parameters, 'POST'); + } + + /** + * Divide the score of the contact point group. + * + * @param array $parameters 'eventName' and 'actionName' + */ + public function dividePointGroupScore(int $contactId, int $groupId, int $value, array $parameters = []): array + { + return $this->makeRequest('contacts/'.$contactId.'/points/groups/'.$groupId.'/divide/'.$value, $parameters, 'POST'); + } + + /** + * Set the score of the contact point group. + * + * @param array $parameters 'eventName' and 'actionName' + */ + public function setPointGroupScore(int $contactId, int $groupId, int $points, array $parameters = []): array + { + return $this->makeRequest('contacts/'.$contactId.'/points/groups/'.$groupId.'/set/'.$points, $parameters, 'POST'); + } + /** * Adds Do Not Contact. * @@ -366,8 +411,6 @@ public function addDnc($id, $channel = 'email', $reason = Contacts::MANUAL, $cha * * @param int $id * @param string $channel - * - * @return mixed */ public function removeDnc($id, $channel = 'email') { @@ -383,8 +426,6 @@ public function removeDnc($id, $channel = 'email') * * @param int $id * @param array $utmTags - * - * @return mixed */ public function addUtm($id, $utmTags) { @@ -396,7 +437,28 @@ public function addUtm($id, $utmTags) } /** - * Create a new item (if supported). + * Remove UTM Tags from a Contact. + * + * @param int $id + * @param int $utmId + */ + public function removeUtm($id, $utmId) + { + return $this->makeRequest( + 'contacts/'.$id.'/utm/'.$utmId.'/remove', + [], + 'POST' + ); + } + + /** + * Create a new contact. + * + * Overrides parent to support additional query arguments for plugins + * like Custom Objects (e.g., ['includeCustomObjects' => true]). + * + * @param array $parameters Contact data + * @param array $queryArguments Optional query parameters (e.g., for Custom Objects) * * @return array|mixed */ @@ -411,7 +473,13 @@ public function create(array $parameters, array $queryArguments = []) } /** - * Create a batch of new items. + * Create a batch of new contacts. + * + * Overrides parent to support additional query arguments for plugins + * like Custom Objects (e.g., ['includeCustomObjects' => true]). + * + * @param array $parameters Array of contact data + * @param array $queryArguments Optional query parameters (e.g., for Custom Objects) * * @return array|mixed */ @@ -424,21 +492,4 @@ public function createBatch(array $parameters, array $queryArguments = []) ? $this->makeRequest($this->endpoint.'/batch/new'.$queryAppend, $parameters, 'POST') : $supported; } - - /** - * Remove UTM Tags from a Contact. - * - * @param int $id - * @param int $utmId - * - * @return mixed - */ - public function removeUtm($id, $utmId) - { - return $this->makeRequest( - 'contacts/'.$id.'/utm/'.$utmId.'/remove', - [], - 'POST' - ); - } } diff --git a/lib/Api/Data.php b/lib/Api/Data.php index b1959b77..5d21504f 100644 --- a/lib/Api/Data.php +++ b/lib/Api/Data.php @@ -1,4 +1,5 @@ makeRequest("{$this->endpoint}/$id", $options); } - /** - * {@inheritdoc} - */ public function getPublishedList($search = '', $start = 0, $limit = 0, $orderBy = '', $orderByDir = 'ASC') { return $this->actionNotSupported(__FUNCTION__); } - /** - * {@inheritdoc} - */ public function create(array $parameters) { return $this->actionNotSupported(__FUNCTION__); } - /** - * {@inheritdoc} - */ public function edit($id, array $parameters, $createIfNotExists = false) { return $this->actionNotSupported(__FUNCTION__); } - /** - * {@inheritdoc} - */ public function delete($id) { return $this->actionNotSupported(__FUNCTION__); diff --git a/lib/Api/Devices.php b/lib/Api/Devices.php index a7af1266..b14a990a 100644 --- a/lib/Api/Devices.php +++ b/lib/Api/Devices.php @@ -1,4 +1,5 @@ 'emails/$1/send/contact/$2', // 2.6.0 ]; - /** - * {@inheritdoc} - */ protected $searchCommands = [ 'ids', 'is:published', @@ -89,4 +78,32 @@ public function sendToLead($id, $leadId) { return $this->sendToContact($id, $leadId); } + + /** + * Send a custom email to a specific contact. + * + * This allows sending an email with custom content directly to a contact + * without using a pre-defined email template. + * + * Required data parameters: + * - fromEmail: Sender email address + * - subject: Email subject + * - content: Email body (HTML) + * + * Optional data parameters: + * - fromName: Sender name + * - replyToEmail: Reply-to email address + * - replyToName: Reply-to name + * + * Note: This endpoint requires Mautic with PR #12854 merged (not in standard Mautic 5.x). + * + * @param int $contactId The ID of the contact to send the email to + * @param array $data email data array with keys: fromEmail, subject, content, etc + * + * @return array Response with 'success' and 'trackingHash' on success + */ + public function sendCustomToContact(int $contactId, array $data = []): array + { + return $this->makeRequest($this->endpoint.'/contact/'.$contactId.'/send/custom', $data, 'POST'); + } } diff --git a/lib/Api/Files.php b/lib/Api/Files.php index d22ad01e..f97e25c8 100644 --- a/lib/Api/Files.php +++ b/lib/Api/Files.php @@ -1,4 +1,5 @@ endpoint = 'files/'.$folder; } - /** - * {@inheritdoc} - */ public function edit($id, array $parameters, $createIfNotExists = false) { return $this->actionNotSupported('edit'); @@ -61,25 +50,16 @@ public function create(array $parameters) return parent::create($parameters); } - /** - * {@inheritdoc} - */ public function createBatch(array $parameters) { return $this->actionNotSupported('createBatch'); } - /** - * {@inheritdoc} - */ public function editBatch(array $parameters, $createIfNotExists = false) { return $this->actionNotSupported('editBatch'); } - /** - * {@inheritdoc} - */ public function deleteBatch(array $ids) { return $this->actionNotSupported('deleteBatch'); diff --git a/lib/Api/Focus.php b/lib/Api/Focus.php index e09ac8a7..7258c74e 100644 --- a/lib/Api/Focus.php +++ b/lib/Api/Focus.php @@ -1,4 +1,5 @@ 'stages/$1/contact/remove/$2', // 2.6.0 ]; - /** - * {@inheritdoc} - */ protected $searchCommands = [ 'ids', ]; diff --git a/lib/Api/Stats.php b/lib/Api/Stats.php index d1576c24..ee48218c 100644 --- a/lib/Api/Stats.php +++ b/lib/Api/Stats.php @@ -1,4 +1,5 @@ makeRequest($endpoint, $parameters); } - /** - * {@inheritdoc} - */ public function delete($id) { return $this->actionNotSupported('delete'); } - /** - * {@inheritdoc} - */ public function getList($search = '', $start = 0, $limit = 0, $orderBy = '', $orderByDir = 'ASC', $publishedOnly = false, $minimal = false) { return $this->actionNotSupported('getList'); } - /** - * {@inheritdoc} - */ public function create(array $parameters) { return $this->actionNotSupported('create'); } - /** - * {@inheritdoc} - */ public function getPublishedList($search = '', $start = 0, $limit = 0, $orderBy = '', $orderByDir = 'ASC') { return $this->actionNotSupported('getPublishedList'); } - /** - * {@inheritdoc} - */ public function edit($id, array $parameters, $createIfNotExists = false) { return $this->actionNotSupported('edit'); diff --git a/lib/Api/Tags.php b/lib/Api/Tags.php index 9267ec36..fa99bdaa 100644 --- a/lib/Api/Tags.php +++ b/lib/Api/Tags.php @@ -1,4 +1,5 @@ actionNotSupported('edit'); @@ -52,25 +41,16 @@ public function create(array $parameters) return parent::create($parameters); } - /** - * {@inheritdoc} - */ public function createBatch(array $parameters) { return $this->actionNotSupported('createBatch'); } - /** - * {@inheritdoc} - */ public function editBatch(array $parameters, $createIfNotExists = false) { return $this->actionNotSupported('editBatch'); } - /** - * {@inheritdoc} - */ public function deleteBatch(array $ids) { return $this->actionNotSupported('deleteBatch'); diff --git a/lib/Api/Tweets.php b/lib/Api/Tweets.php index 23bcb26f..c80660cd 100644 --- a/lib/Api/Tweets.php +++ b/lib/Api/Tweets.php @@ -1,4 +1,5 @@ client = $client; + } /** - * @param $url - * @param $method - * - * @return mixed + * @param string $url + * @param string $method */ abstract protected function prepareRequest($url, array $headers, array $parameters, $method, array $settings); @@ -84,161 +76,108 @@ public function getDebugInfo() */ public function getResponseHeaders() { - return $this->parseHeaders($this->_httpResponseHeaders); + return array_map( + static function ($values) { + return $values[0]; + }, + $this->_httpResponse->getHeaders() + ); } /** - * Returns array of HTTP response headers. + * Returns the HTTP response. * - * @return array + * @return ResponseInterface */ - public function getResponseInfo() + public function getResponse() { - return $this->_httpResponseInfo; + return $this->_httpResponse; } /** - * {@inheritdoc} - * * @throws UnexpectedResponseFormatException|Exception */ public function makeRequest($url, array $parameters = [], $method = 'GET', array $settings = []) { $this->log('makeRequest('.$url.', '.http_build_query($parameters).', '.$method.',...)'); - if ($method === 'GET') { // We want to keep get arguments for non GET - list($url, $parameters) = $this->separateUrlParams($url, $parameters); + if ('GET' === $method) { // We want to keep get arguments for non-GET requests + [$url, $parameters] = $this->separateUrlParams($url, $parameters); } - //make sure $method is capitalized for congruency + // Make sure $method is capitalized for congruency $method = strtoupper($method); $headers = (isset($settings['headers']) && is_array($settings['headers'])) ? $settings['headers'] : []; - list($headers, $parameters) = $this->prepareRequest($url, $headers, $parameters, $method, $settings); + [$headers, $parameters] = $this->prepareRequest($url, $headers, $parameters, $method, $settings); - //Set default CURL options - $options = [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_HEADER => true, - ]; - - if (null !== $this->_curlTimeout) { - $options[CURLOPT_TIMEOUT] = $this->_curlTimeout; - } - - // CURLOPT_FOLLOWLOCATION cannot be activated when an open_basedir is set - $options[CURLOPT_FOLLOWLOCATION] = (ini_get('open_basedir')) ? false : true; - - //Set custom REST method if not GET or POST - if (!in_array($method, ['GET', 'POST'])) { - $options[CURLOPT_CUSTOMREQUEST] = $method; - } - - //Set post fields for POST/PUT/PATCH requests - $isPost = false; + // Prepare parameters/body + $body = null; if (in_array($method, ['POST', 'PUT', 'PATCH'])) { - $isPost = true; - // Set file to upload - // Sending file data requires an array to set - // the Content-Type header to multipart/form-data if (!empty($parameters['file']) && file_exists($parameters['file'])) { - $options[CURLOPT_INFILESIZE] = filesize($parameters['file']); - $parameters['file'] = $this->createCurlFile($parameters['file']); - $headers[] = 'Content-Type: multipart/form-data'; + $elements = []; + foreach ($parameters as $key => $value) { + $elements[] = [ + 'name' => $key, + 'contents' => 'file' === $key ? Utils::tryFopen($value, 'r+') : $value, + ]; + } + + $body = new MultipartStream($elements); + $headers[] = 'Content-Type: multipart/form-data; boundary='.$body->getBoundary(); } else { - $parameters = json_encode($parameters); - $headers[] = 'Content-Type: application/json'; + $body = Utils::streamFor(json_encode($parameters)); + $headers[] = 'Content-Type: application/json'; } - $options[CURLOPT_POST] = true; - $options[CURLOPT_POSTFIELDS] = $parameters; - $this->log('Posted parameters = '.print_r($parameters, true)); } - $query = $this->getQueryParameters($isPost, $parameters); + $query = $this->getQueryParameters(null !== $body, $parameters); $this->log('Query parameters = '.print_r($query, true)); - //Create a query string for GET/DELETE requests + // Create a query string for GET/DELETE requests if (count($query) > 0) { $queryGlue = false === strpos($url, '?') ? '?' : '&'; - $url = $url.$queryGlue.http_build_query($query, '', '&'); + $url .= $queryGlue.http_build_query($query, '', '&'); $this->log('URL updated to '.$url); } - // Set the URL - $options[CURLOPT_URL] = $url; - - $headers[] = 'Accept: application/json'; - $options[CURLOPT_HTTPHEADER] = $headers; + $headers[] = 'Accept: application/json'; - //Make CURL request - $curl = curl_init(); - curl_setopt_array($curl, $options); - - $response = new Response(curl_exec($curl), curl_getinfo($curl)); + // Build request + $request = new Request($method, $url); + foreach ($headers as $header) { + [$name, $value] = explode(':', $header, 2); + $request = $request->withHeader(trim($name), trim($value)); + } + if ($body) { + $request = $request->withBody($body); + } - $this->_httpResponseHeaders = $response->getHeaders(); - $this->_httpResponseInfo = $response->getInfo(); + // Send request + $this->_httpResponse = $this->client->sendRequest($request); - curl_close($curl); + // Parse response + $response = new Response($this->_httpResponse); if ($this->_debug) { - $_SESSION['oauth']['debug']['info'] = $response->getInfo(); - $_SESSION['oauth']['debug']['returnedHeaders'] = $response->getHeaders(); - $_SESSION['oauth']['debug']['returnedBody'] = $response->getBody(); + $_SESSION['oauth']['debug']['returnedStatusCode'] = $response->getStatusCode(); + $_SESSION['oauth']['debug']['returnedHeaders'] = $response->getHeaders(); + $_SESSION['oauth']['debug']['returnedBody'] = $response->getBody(); } // Handle zip file response if ($response->isZip()) { - $temporaryFilePath = isset($settings['temporaryFilePath']) ? $settings['temporaryFilePath'] : sys_get_temp_dir(); - - return $response->saveToFile($temporaryFilePath); - } else { - return $response->getDecodedBody(); - } - } - - /** - * @deprecated 2.6.0 to be removed in 3.0; use createCurlFile instead - * - * @param $filename - * @param string $mimetype - * @param string $postname - * - * @return \CURLFile|string - */ - protected function crateCurlFile($filename, $mimetype = '', $postname = '') - { - return $this->createCurlFile($filename, $mimetype, $postname); - } - - /** - * Build the CURL file based on PHP version. - * - * @param string $filename - * @param string $mimetype - * @param string $postname - * - * @return string|\CURLFile - */ - protected function createCurlFile($filename, $mimetype = '', $postname = '') - { - if (!function_exists('curl_file_create')) { - // For PHP < 5.5 - return "@$filename;filename=" - .($postname ?: basename($filename)) - .($mimetype ? ";type=$mimetype" : ''); + return $response->saveToFile($settings['temporaryFilePath'] ?? sys_get_temp_dir()); } - // For PHP >= 5.5 - return curl_file_create($filename, $mimetype, $postname); + return $response->getDecodedBody(); } /** - * @param $isPost - * @param $parameters + * @param bool $isPost + * @param array $parameters * * @return array */ @@ -257,35 +196,11 @@ protected function log($message) } } - /** - * Build the HTTP response array out of the headers string. - * - * @param string $headersStr - * - * @return array - */ - protected function parseHeaders($headersStr) - { - $headersArr = []; - $headersHlpr = explode("\r\n", $headersStr); - - foreach ($headersHlpr as $header) { - $pos = strpos($header, ':'); - if (false === $pos) { - $headersArr[] = trim($header); - } else { - $headersArr[trim(substr($header, 0, $pos))] = trim(substr($header, ($pos + 1))); - } - } - - return $headersArr; - } - /** * Separates parameters from base URL. * - * @param $url - * @param $params + * @param string $url + * @param array $params * * @return array */ @@ -306,14 +221,4 @@ protected function separateUrlParams($url, $params) return [$url, $params]; } - - /** - * Set the timeout for a cURL request. - * - * @param int $timeout - */ - public function setCurlTimeout($timeout) - { - $this->_curlTimeout = $timeout; - } } diff --git a/lib/Auth/ApiAuth.php b/lib/Auth/ApiAuth.php index 3c55dd43..044bf58b 100644 --- a/lib/Auth/ApiAuth.php +++ b/lib/Auth/ApiAuth.php @@ -11,11 +11,21 @@ namespace Mautic\Auth; +use Http\Discovery\Psr18ClientDiscovery; +use Psr\Http\Client\ClientInterface; + /** * OAuth Client modified from https://code.google.com/p/simple-php-oauth/. */ class ApiAuth { + protected ClientInterface $client; + + public function __construct(?ClientInterface $client = null) + { + $this->client = $client ?: Psr18ClientDiscovery::find(); + } + /** * Get an API Auth object. * @@ -44,7 +54,7 @@ public static function initiate($parameters = [], $authMethod = 'OAuth') public function newAuth($parameters = [], $authMethod = 'OAuth') { $class = 'Mautic\\Auth\\'.$authMethod; - $authObject = new $class(); + $authObject = new $class($this->client); $reflection = new \ReflectionMethod($class, 'setup'); $pass = []; diff --git a/lib/Auth/AuthInterface.php b/lib/Auth/AuthInterface.php index 92f72af4..4924f8f4 100644 --- a/lib/Auth/AuthInterface.php +++ b/lib/Auth/AuthInterface.php @@ -11,6 +11,8 @@ namespace Mautic\Auth; +use Psr\Http\Client\ClientInterface; + interface AuthInterface { /** diff --git a/lib/Auth/BasicAuth.php b/lib/Auth/BasicAuth.php index 781aff05..b7d6f6f1 100644 --- a/lib/Auth/BasicAuth.php +++ b/lib/Auth/BasicAuth.php @@ -74,9 +74,6 @@ class BasicAuth extends AbstractAuth */ private $userName; - /** - * {@inheritdoc} - */ public function isAuthorized() { return !empty($this->userName) && !empty($this->password); @@ -97,7 +94,7 @@ public function setup($userName, $password) $password = trim($password); if (empty($userName) || empty($password)) { - //Throw exception if the required parameters were not found + // Throw exception if the required parameters were not found $this->log('parameters did not include username and/or password'); throw new RequiredParameterMissingException('One or more required parameters was not supplied. Both userName and password required!'); } @@ -107,14 +104,14 @@ public function setup($userName, $password) } /** - * @param $url - * @param $method + * @param string $url + * @param string $method * * @return array */ protected function prepareRequest($url, array $headers, array $parameters, $method, array $settings) { - //Set Basic Auth parameters/headers + // Set Basic Auth parameters/headers $headers = array_merge($headers, [$this->buildAuthorizationHeader(), 'Expect:']); return [$headers, $parameters]; diff --git a/lib/Auth/OAuth.php b/lib/Auth/OAuth.php index 1ea8ca9a..36d30899 100755 --- a/lib/Auth/OAuth.php +++ b/lib/Auth/OAuth.php @@ -155,39 +155,16 @@ public function getAccessTokenData() ]; } - /** - * Returns array of HTTP response headers. - * - * @return array - */ - public function getResponseHeaders() - { - return $this->parseHeaders($this->_httpResponseHeaders); - } - - /** - * Returns array of HTTP response headers. - * - * @return array - */ - public function getResponseInfo() - { - return $this->_httpResponseInfo; - } - - /** - * {@inheritdoc} - */ public function isAuthorized() { - //Check for existing access token + // Check for existing access token if (!empty($this->_request_token_url)) { if (strlen($this->_access_token) > 0 && strlen($this->_access_token_secret) > 0) { return true; } } - //Check to see if token in session has expired + // Check to see if token in session has expired if (!empty($this->_expires) && $this->_expires < time()) { return false; } @@ -217,7 +194,7 @@ public function setAccessTokenDetails(array $accessTokenDetails) /** * Set access token URL. * - * @param $url + * @param string $url * * @return $this */ @@ -231,7 +208,7 @@ public function setAccessTokenUrl($url) /** * Set authorization URL. * - * @param $url + * @param string $url * * @return $this */ @@ -245,7 +222,7 @@ public function setAuthorizeUrl($url) /** * Set redirect type for OAuth2. * - * @param $type + * @param string $type * * @return $this */ @@ -259,7 +236,7 @@ public function setRedirectType($type) /** * Set request token URL. * - * @param $url + * @param string $url * * @return $this */ @@ -310,7 +287,7 @@ public function setup( $accessTokenExpires = null, $callback = null, $scope = null, - $refreshToken = null + $refreshToken = null, ) { $this->_client_id = $clientKey; $this->_client_secret = $clientSecret; @@ -369,50 +346,50 @@ public function validateAccessToken($redirect = true) $this->_do_not_redirect = !$redirect; $this->log('validateAccessToken()'); - //Check to see if token in session has expired + // Check to see if token in session has expired if (!empty($this->_expires) && $this->_expires < time()) { $this->log('access token expired so reauthorize'); if (strlen($this->_refresh_token) > 0) { - //use a refresh token to get a new token + // use a refresh token to get a new token return $this->requestAccessToken(); } - //Reauthorize + // Reauthorize $this->authorize($this->_scope); return false; } - //Check for existing access token - if (strlen($this->_access_token) > 0) { + // Check for existing access token + if ($this->_access_token) { $this->log('has access token'); return true; } - //Reauthorize if no token was found - if (0 == strlen($this->_access_token)) { + // Reauthorize if no token was found + if (!$this->_access_token) { $this->log('access token empty so authorize'); - //OAuth flows + // OAuth flows if ($this->isOauth1()) { - //OAuth 1.0 + // OAuth 1.0 $this->log('authorizing with OAuth1.0a spec'); - //Request token and authorize app + // Request token and authorize app if (!isset($_GET['oauth_token']) && !isset($_GET['oauth_verifier'])) { $this->log('initializing authorization'); - //Request token + // Request token $this->requestToken(); - //Authorize token + // Authorize token $this->authorize(); return false; } - //Request access token + // Request access token if ($_GET['oauth_token'] != $_SESSION['oauth']['token']) { unset($_SESSION['oauth']['token'], $_SESSION['oauth']['token_secret']); @@ -425,10 +402,10 @@ public function validateAccessToken($redirect = true) return true; } - //OAuth 2.0 + // OAuth 2.0 $this->log('authorizing with OAuth2 spec'); - //Authorize app + // Authorize app if (!isset($_GET['state']) && !isset($_GET['code'])) { $this->authorize($this->_scope); @@ -439,7 +416,7 @@ public function validateAccessToken($redirect = true) $_SESSION['oauth']['debug']['received_state'] = $_GET['state']; } - //Request an access token + // Request an access token if ($_GET['state'] != $_SESSION['oauth']['state']) { unset($_SESSION['oauth']['state']); @@ -467,16 +444,16 @@ protected function authorize(array $scope = [], $scope_separator = ',', $attach { $authUrl = $this->_authorize_url; - //Build authorization URL + // Build authorization URL if ($this->isOauth1()) { - //OAuth 1.0 + // OAuth 1.0 $authUrl .= '?oauth_token='.$_SESSION['oauth']['token']; if (!empty($this->_callback)) { $authUrl .= '&oauth_callback='.urlencode($this->_callback); } } else { - //OAuth 2.0 + // OAuth 2.0 $authUrl .= '?client_id='.$this->_client_id.'&redirect_uri='.urlencode($this->_callback); $state = md5(time().mt_rand()); $_SESSION['oauth']['state'] = $state; @@ -490,7 +467,7 @@ protected function authorize(array $scope = [], $scope_separator = ',', $attach $this->log('redirecting to auth url '.$authUrl); - //Redirect to authorization URL + // Redirect to authorization URL if (!$this->_do_not_redirect) { header('Location: '.$authUrl); exit; @@ -500,8 +477,8 @@ protected function authorize(array $scope = [], $scope_separator = ',', $attach } /** - * @param $isPost - * @param $parameters + * @param bool $isPost + * @param array $parameters * * @return array */ @@ -522,36 +499,12 @@ protected function getQueryParameters($isPost, $parameters) */ protected function isOauth1() { - return !empty($this->_request_token_url) && strlen($this->_request_token_url) > 0; - } - - /** - * Build the HTTP response array out of the headers string. - * - * @param string $headersStr - * - * @return array - */ - protected function parseHeaders($headersStr) - { - $headersArr = []; - $headersHlpr = explode("\r\n", $headersStr); - - foreach ($headersHlpr as $header) { - $pos = strpos($header, ':'); - if (false === $pos) { - $headersArr[] = trim($header); - } else { - $headersArr[trim(substr($header, 0, $pos))] = trim(substr($header, ($pos + 1))); - } - } - - return $headersArr; + return !empty($this->_request_token_url); } /** - * @param $url - * @param array $method + * @param string $url + * @param string $method * * @return array */ @@ -560,12 +513,12 @@ protected function prepareRequest($url, array $headers, array $parameters, $meth $includeCallback = (isset($settings['includeCallback'])) ? $settings['includeCallback'] : false; $includeVerifier = (isset($settings['includeVerifier'])) ? $settings['includeVerifier'] : false; - //Set OAuth parameters/headers + // Set OAuth parameters/headers if ($this->isOauth1()) { - //OAuth 1.0 + // OAuth 1.0 $this->log('making request using OAuth1.0a spec'); - //Get standard OAuth headers + // Get standard OAuth headers $oAuthHeaders = $this->getOauthHeaders($includeCallback); if ($includeVerifier && isset($_GET['oauth_verifier'])) { @@ -576,7 +529,7 @@ protected function prepareRequest($url, array $headers, array $parameters, $meth } } - //Add the parameters + // Add the parameters $oAuthHeaders = array_merge($oAuthHeaders, $parameters); $base_info = $this->buildBaseString($url, $method, $oAuthHeaders); $composite_key = $this->getCompositeKey(); @@ -589,7 +542,7 @@ protected function prepareRequest($url, array $headers, array $parameters, $meth $_SESSION['oauth']['debug']['headers'] = $headers; } } else { - //OAuth 2.0 + // OAuth 2.0 $this->log('making request using OAuth2 spec'); $headers[] = 'Authorization: Bearer '.$this->_access_token; @@ -612,15 +565,15 @@ protected function requestAccessToken($method = 'POST', array $params = [], $res { $this->log('requestAccessToken()'); - //Set OAuth flow parameters + // Set OAuth flow parameters if ($this->isOauth1()) { - //OAuth 1.0 + // OAuth 1.0 $this->log('using OAuth1.0a spec'); $parameters = ['oauth_verifier' => $_GET['oauth_verifier']]; $parameters = array_merge($parameters, $params); } else { - //OAuth 2.0 + // OAuth 2.0 $this->log('using OAuth2 spec'); $parameters = [ @@ -634,7 +587,7 @@ protected function requestAccessToken($method = 'POST', array $params = [], $res $parameters['code'] = $_GET['code']; } - if (strlen($this->_refresh_token) > 0) { + if ($this->_refresh_token) { $this->log('Using refresh token'); $parameters['grant_type'] = 'refresh_token'; $parameters['refresh_token'] = $this->_refresh_token; @@ -643,7 +596,7 @@ protected function requestAccessToken($method = 'POST', array $params = [], $res $parameters = array_merge($parameters, $params); } - //Make the request + // Make the request $settings = [ 'responseType' => $responseType, 'includeCallback' => true, @@ -652,10 +605,10 @@ protected function requestAccessToken($method = 'POST', array $params = [], $res $params = $this->makeRequest($this->_access_token_url, $parameters, $method, $settings); - //Add the token and secret to session + // Add the token and secret to session if (is_array($params)) { if ($this->isOauth1()) { - //OAuth 1.0a + // OAuth 1.0a if (isset($params['oauth_token']) && isset($params['oauth_token_secret'])) { $this->log('access token set as '.$params['oauth_token']); @@ -671,7 +624,7 @@ protected function requestAccessToken($method = 'POST', array $params = [], $res return true; } } else { - //OAuth 2.0 + // OAuth 2.0 if (isset($params['access_token']) && isset($params['expires_in'])) { $this->log('access token set as '.$params['access_token']); @@ -727,7 +680,7 @@ protected function requestToken($responseType = 'flat') { $this->log('requestToken()'); - //Make the request + // Make the request $settings = [ 'responseType' => $responseType, 'includeCallback' => true, @@ -735,7 +688,7 @@ protected function requestToken($responseType = 'flat') ]; $params = $this->makeRequest($this->_request_token_url, [], 'POST', $settings); - //Add token and secret to the session + // Add token and secret to the session if (is_array($params) && isset($params['oauth_token']) && isset($params['oauth_token_secret'])) { $this->log('token set as '.$params['oauth_token']); @@ -747,7 +700,7 @@ protected function requestToken($responseType = 'flat') $_SESSION['oauth']['debug']['token_secret'] = $params['oauth_token_secret']; } } else { - //Throw exception if the required parameters were not found + // Throw exception if the required parameters were not found $this->log('request did not return oauth tokens'); if ($this->_debug) { @@ -768,36 +721,10 @@ protected function requestToken($responseType = 'flat') } } - /** - * Separates parameters from base URL. - * - * @param $url - * @param $params - * - * @return array - */ - protected function separateUrlParams($url, $params) - { - $a = parse_url($url); - - if (!empty($a['query'])) { - parse_str($a['query'], $qparts); - $cleanParams = []; - foreach ($qparts as $k => $v) { - $cleanParams[$k] = $v ? $v : ''; - } - $params = array_merge($params, $cleanParams); - $urlParts = explode('?', $url, 2); - $url = $urlParts[0]; - } - - return [$url, $params]; - } - /** * Build header for OAuth 1 authorization. * - * @param $oauth + * @param array $oauth * * @return string */ diff --git a/lib/Auth/TwoLeggedOAuth2.php b/lib/Auth/TwoLeggedOAuth2.php index ad7c0533..5c70fae8 100644 --- a/lib/Auth/TwoLeggedOAuth2.php +++ b/lib/Auth/TwoLeggedOAuth2.php @@ -11,106 +11,261 @@ namespace Mautic\Auth; +use Mautic\Exception\IncorrectParametersReturnedException; use Mautic\Exception\RequiredParameterMissingException; +/** + * OAuth2 Client Credentials (2-legged OAuth2) Authentication. + * + * This authentication method is used for server-to-server communication + * where no user interaction is required. The application authenticates + * using its own credentials (client_id and client_secret). + * + * @see https://developer.mautic.org/#client-credentials + */ class TwoLeggedOAuth2 extends AbstractAuth { /** - * Password associated with Username. - * - * @var string + * Consumer or client key. */ - private $clientSecret; + protected string $_client_id; /** - * Username or email, basically the Login Identifier. - * - * @var string + * Consumer or client secret. */ - private $clientKey; + protected string $_client_secret; /** * Access token returned by OAuth server. - * - * @var string */ - protected $_access_token; + protected ?string $_access_token = null; + + /** + * Unix timestamp for when token expires. + */ + protected ?int $_expires = null; + + /** + * OAuth2 token type. + */ + protected string $_token_type = 'bearer'; /** - * @var string + * Set to true if access token was updated. */ - private $baseurl; + protected bool $_access_token_updated = false; /** - * @var string + * Access token URL. */ - private $_access_token_url; + protected string $_access_token_url; /** - * {@inheritdoc} + * Check if the current access token is still valid. */ - public function isAuthorized() + public function isAuthorized(): bool { - return !empty($this->clientKey) && !empty($this->clientSecret); + $this->log('isAuthorized()'); + + return $this->validateAccessToken(); } /** - * @param string $baseUrl - * @param string $clientKey The username to use for Authentication *Required* - * @param string $clientSecret The Password to use *Required* + * Setup the authentication credentials. + * + * @param string|null $baseUrl URL of the Mautic instance + * @param string|null $clientKey Client ID from Mautic API credentials + * @param string|null $clientSecret Client Secret from Mautic API credentials + * @param string|null $accessToken Previously stored access token (optional) + * @param int|null $accessTokenExpires Unix timestamp when token expires (optional) * * @throws RequiredParameterMissingException */ - public function setup($baseUrl, $clientKey, $clientSecret, $accessToken = null) - { - // we MUST have the username and password. No Blanks allowed! - // - // remove blanks else Empty doesn't work - $clientKey = trim($clientKey); - $clientSecret = trim($clientSecret); - + public function setup( + ?string $baseUrl = null, + ?string $clientKey = null, + ?string $clientSecret = null, + ?string $accessToken = null, + ?int $accessTokenExpires = null, + ): void { if (empty($clientKey) || empty($clientSecret)) { - //Throw exception if the required parameters were not found - $this->log('parameters did not include clientkey and/or clientSecret'); + $this->log('parameters did not include clientKey and/or clientSecret'); throw new RequiredParameterMissingException('One or more required parameters was not supplied. Both clientKey and clientSecret required!'); } - $this->baseurl = $baseUrl; - $this->clientKey = $clientKey; - $this->clientSecret = $clientSecret; - $this->_access_token = $accessToken; + if (empty($baseUrl)) { + $this->log('parameters did not include baseUrl'); + throw new RequiredParameterMissingException('One or more required parameters was not supplied. baseUrl required!'); + } - if (!$this->_access_token_url) { - $this->_access_token_url = $baseUrl.'/oauth/v2/token'; + $this->_client_id = trim($clientKey); + $this->_client_secret = trim($clientSecret); + $this->_access_token_url = rtrim($baseUrl, '/').'/oauth/v2/token'; + + if (!empty($accessToken)) { + $this->setAccessTokenDetails([ + 'access_token' => $accessToken, + 'expires' => $accessTokenExpires, + ]); } } /** - * @param $url - * @param $method + * Check if the access token was updated during the last request. + */ + public function accessTokenUpdated(): bool + { + return $this->_access_token_updated; + } + + /** + * Get the current access token data. * - * @return array + * @return array{access_token: string|null, expires: int|null, token_type: string} */ - protected function prepareRequest($url, array $headers, array $parameters, $method, array $settings) + public function getAccessTokenData(): array { - if (null !== $this->_access_token) { - $headers = array_merge($headers, ['Authorization: Bearer '.$this->_access_token]); + return [ + 'access_token' => $this->_access_token, + 'expires' => $this->_expires, + 'token_type' => $this->_token_type, + ]; + } + + /** + * Set access token details from stored data. + * + * @param array{access_token?: string|null, expires?: int|null, token_type?: string} $accessTokenDetails + * + * @return $this + */ + public function setAccessTokenDetails(array $accessTokenDetails): static + { + $this->_access_token = $accessTokenDetails['access_token'] ?? null; + $this->_expires = isset($accessTokenDetails['expires']) + ? (int) $accessTokenDetails['expires'] + : null; + + if (isset($accessTokenDetails['token_type'])) { + $this->_token_type = $accessTokenDetails['token_type']; } - return [$headers, $parameters]; + return $this; } - public function getAccessToken(): string + /** + * Validate if the current access token is still valid. + */ + public function validateAccessToken(): bool { - $parameters = [ - 'client_id' => $this->clientKey, - 'client_secret' => $this->clientSecret, + $this->log('validateAccessToken()'); + + // Check if token is expired (with 10 second buffer) + if (!empty($this->_access_token) && !empty($this->_expires) + && $this->_expires < (time() + 10)) { + $this->log('access token expired'); + + return false; + } + + if (!empty($this->_access_token)) { + $this->log('has valid access token'); + + return true; + } + + $this->log('no access token'); + + return false; + } + + /** + * Request a new access token using client credentials. + * + * @throws IncorrectParametersReturnedException + */ + public function requestAccessToken(): bool + { + $this->log('requestAccessToken()'); + + $parameters = [ + 'client_id' => $this->_client_id, + 'client_secret' => $this->_client_secret, 'grant_type' => 'client_credentials', ]; - $accessTokenData = $this->makeRequest($this->_access_token_url, $parameters, 'POST'); - //store access token data however you want - $this->_access_token = $accessTokenData['access_token'] ?? null; + + $params = $this->makeRequest($this->_access_token_url, $parameters, 'POST'); + + if (is_array($params)) { + if (isset($params['access_token']) && isset($params['expires_in'])) { + $this->log('access token set as '.$params['access_token']); + + $this->_access_token = $params['access_token']; + $this->_expires = time() + (int) $params['expires_in']; + $this->_token_type = $params['token_type'] ?? 'bearer'; + $this->_access_token_updated = true; + + if ($this->_debug) { + $_SESSION['oauth']['debug']['tokens']['access_token'] = $params['access_token']; + $_SESSION['oauth']['debug']['tokens']['expires_in'] = $params['expires_in']; + $_SESSION['oauth']['debug']['tokens']['token_type'] = $params['token_type'] ?? null; + } + + return true; + } + } + + $this->log('response did not have an access token'); + + if ($this->_debug) { + $_SESSION['oauth']['debug']['response'] = $params; + } + + if (isset($params['errors'])) { + $errors = []; + foreach ($params['errors'] as $error) { + $errors[] = $error['message']; + } + $response = implode('; ', $errors); + } else { + $response = print_r($params, true); + } + + throw new IncorrectParametersReturnedException('Incorrect access token parameters returned: '.$response); + } + + /** + * Get the access token, requesting a new one if necessary. + * + * @throws IncorrectParametersReturnedException + */ + public function getAccessToken(): string + { + if (!$this->validateAccessToken()) { + $this->requestAccessToken(); + } return $this->_access_token; } + + protected function prepareRequest($url, array $headers, array $parameters, $method, array $settings): array + { + if (!empty($this->_access_token)) { + $headers = array_merge($headers, ['Authorization: Bearer '.$this->_access_token]); + } + + return [$headers, $parameters]; + } + + protected function getQueryParameters($isPost, $parameters): array + { + $query = parent::getQueryParameters($isPost, $parameters); + + // Support for file uploads - pass access token as query parameter + if (isset($parameters['file'])) { + $query['access_token'] = $this->_access_token; + } + + return $query; + } } diff --git a/lib/Exception/AbstractApiException.php b/lib/Exception/AbstractApiException.php index 324b11c3..ee4b99e5 100644 --- a/lib/Exception/AbstractApiException.php +++ b/lib/Exception/AbstractApiException.php @@ -1,4 +1,5 @@ authUrl = $authUrl; diff --git a/lib/Exception/ContextNotFoundException.php b/lib/Exception/ContextNotFoundException.php index d05999cb..13dc0658 100644 --- a/lib/Exception/ContextNotFoundException.php +++ b/lib/Exception/ContextNotFoundException.php @@ -1,4 +1,5 @@ response = $response; diff --git a/lib/MauticApi.php b/lib/MauticApi.php index 15c44ba8..cf464c3b 100644 --- a/lib/MauticApi.php +++ b/lib/MauticApi.php @@ -1,4 +1,5 @@ info = $info; - $this->parseResponse($response); + $this->response = $response; + $this->body = (string) $this->response->getBody(); $this->validate(); } + /** + * @return int + */ + public function getStatusCode() + { + return $this->response->getStatusCode(); + } + /** * @return array */ public function getHeaders() { - return $this->headers; + return $this->response->getHeaders(); } /** @@ -95,20 +108,12 @@ public function decodeFromUrlParams() return $parsed; } - /** - * @return array - */ - public function getInfo() - { - return $this->info; - } - /** * @return bool */ public function isZip() { - return !empty($this->info['content_type']) && 'application/zip' === $this->info['content_type']; + return $this->response->hasHeader('Content-Type') && 'application/zip' === $this->response->getHeader('Content-Type')[0]; } /** @@ -152,24 +157,14 @@ public function saveToFile($path) ]; } - /** - * @param string $response - */ - private function parseResponse($response) - { - $exploded = explode("\r\n\r\n", $response); - $this->body = array_pop($exploded); - $this->headers = implode("\r\n\r\n", $exploded); - } - /** * @throws UnexpectedResponseFormatException */ private function validate() { - if (!in_array($this->info['http_code'], [200, 201])) { - $message = 'The response has unexpected status code ('.$this->info['http_code'].').'; - throw new UnexpectedResponseFormatException($this, $message, $this->info['http_code']); + if (!in_array($this->response->getStatusCode(), [200, 201])) { + $message = 'The response has unexpected status code ('.$this->response->getStatusCode().').'; + throw new UnexpectedResponseFormatException($this, $message, $this->response->getStatusCode()); } if ($this->isHtml()) { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..6c4b2eb1 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 1 + paths: + - lib + - tests \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 64811a6b..dead1160 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,4 +10,7 @@ tests + + + diff --git a/tests/Api/AssetsTest.php b/tests/Api/AssetsTest.php index 05757bfe..87af883d 100644 --- a/tests/Api/AssetsTest.php +++ b/tests/Api/AssetsTest.php @@ -1,4 +1,5 @@ api = $this->getContext('assets'); @@ -22,6 +25,10 @@ public function setUp(): void 'storageLocation' => 'remote', 'file' => 'https://www.mautic.org/media/logos/logo/Mautic_Logo_DB.pdf', ]; + + if ('4' == $this->mauticVersion) { + $this->mediaFolder = 'assets'; + } } public function testGetList() @@ -37,12 +44,12 @@ public function testGetListOfSpecificIds() public function testCreateWithLocalFileGetAndDelete() { // Upload a testing file - $this->apiFiles = $this->getContext('files'); - $this->apiFiles->setFolder('assets'); + $apiFiles = $this->getContext('files'); + $apiFiles->setFolder($this->mediaFolder); $fileRequest = [ - 'file' => dirname(__DIR__).'/'.'mauticlogo.png', + 'file' => dirname(__DIR__).'/mauticlogo.png', ]; - $response = $this->apiFiles->create($fileRequest); + $response = $apiFiles->create($fileRequest); $this->assertErrors($response); $file = $response['file']; diff --git a/tests/Api/Auth/AbstractAuthTest.php b/tests/Api/Auth/AbstractAuthTest.php index 7e2ef118..652a26ba 100644 --- a/tests/Api/Auth/AbstractAuthTest.php +++ b/tests/Api/Auth/AbstractAuthTest.php @@ -1,4 +1,5 @@ getMockForAbstractClass(AbstractAuth::class); + $auth = $this->getMockForAbstractClass(AbstractAuth::class, [new Client()]); $this->expectException(UnexpectedResponseFormatException::class); $auth->makeRequest('https://github.com/mautic/api-library/this-page-does-not-exist'); } public function testHtmlResponse() { - $auth = $this->getMockForAbstractClass(AbstractAuth::class); + $auth = $this->getMockForAbstractClass(AbstractAuth::class, [new Client()]); $this->expectException(UnexpectedResponseFormatException::class); $auth->makeRequest($this->config['baseUrl']); } public function testJsonResponse() { - $auth = $this->getMockForAbstractClass(AbstractAuth::class); + $auth = $this->getMockForAbstractClass(AbstractAuth::class, [new Client()]); try { - $response = $auth->makeRequest($this->config['apiUrl'].'contacts'); - $this->assertTrue(is_array($response)); - $this->assertFalse(empty($response)); + $auth->makeRequest($this->config['apiUrl'].'contacts'); + self::fail('This should not happen, as the API does not have the authentication.'); } catch (UnexpectedResponseFormatException $exception) { - $response = json_decode($exception->getResponse()->getBody(), true); - $this->assertTrue(is_array($response)); - $this->assertFalse(empty($response)); + $body = $exception->getResponse()->getBody(); + try { + $response = json_decode($body, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + if ('' === $body) { + $body = '(empty string)'; + } + + self::fail('Mautic returned wrong json response: '.$body.'. JSON exception: '.$e->getMessage()); + } + $this->assertIsArray($response, $body); + $this->assertGreaterThan(0, count($response)); } } } diff --git a/tests/Api/Auth/BasicAuthTest.php b/tests/Api/Auth/BasicAuthTest.php index 9861281c..2fcf4f3a 100644 --- a/tests/Api/Auth/BasicAuthTest.php +++ b/tests/Api/Auth/BasicAuthTest.php @@ -1,4 +1,5 @@ 'new_43', /// Event ID will be replaced on /new + 'sourceId' => 'new_43', // / Event ID will be replaced on /new 'targetId' => 'new_44', // Event ID will be replaced on /new 'anchors' => [ 'source' => 'yes', @@ -224,7 +225,7 @@ public function testEditPatch() { $response = $this->api->edit(10000, $this->testPayload); - //there should be an error as the campaign shouldn't exist + // there should be an error as the campaign shouldn't exist $this->assertTrue(isset($response['errors']), $response['errors'][0]['message']); $this->setUpPayloadClass(); @@ -246,7 +247,7 @@ public function testEditPatch() $this->assertEquals($event['name'], 'Event Name Modified'); } - //now delete the campaign + // now delete the campaign $response = $this->api->delete($campaign['id']); $this->assertErrors($response); $this->clearPayloadItems(); @@ -351,7 +352,7 @@ public function testEventAndSourceDeleteViaPut() $this->assertEquals($response[$this->api->itemName()]['lists'][0]['id'], $newSegmentsArray[0]['id']); $this->assertEquals($response[$this->api->itemName()]['lists'][0]['name'], $newSegmentsArray[0]['name']); - //now delete the form + // now delete the form $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); $this->clearPayloadItems(); @@ -471,7 +472,7 @@ public function testCampaignContactEditEvent() $date = new \DateTime($log['triggerDate'], new \DateTimeZone('UTC')); $this->assertEquals($log['triggerDate'], $date->format('c')); } else { - $this->assertFalse(false, 'Event ID not recognized in the log.', var_export($event, true)); + $this->fail('Event ID not recognized in the log.'.var_export($event, true)); } } diff --git a/tests/Api/CategoriesTest.php b/tests/Api/CategoriesTest.php index f3d334c7..392016f0 100644 --- a/tests/Api/CategoriesTest.php +++ b/tests/Api/CategoriesTest.php @@ -1,4 +1,5 @@ config['baseUrl'].'/mtracking.gif?url='.urlencode('http://mautic.org')); - curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); - - curl_exec($curl); + $client = new Client(['verify' => false]); + $request = new Request('GET', $this->config['baseUrl'].'/mtracking.gif?url='.urlencode('http://mautic.org')); + $client->send($request); $response = $this->api->getActivity('', ['page.hit']); $this->assertEventResponse($response, ['page.hit']); @@ -325,7 +325,7 @@ public function testEditPatch() $pointsSet = 5; $response = $this->api->edit(10000, $this->testPayload); - //there should be an error as the contact shouldn't exist + // there should be an error as the contact shouldn't exist $this->assertTrue(isset($response['errors'][0]), $response['errors'][0]['message']); $response = $this->api->create($this->testPayload); @@ -343,7 +343,7 @@ public function testEditPatch() $this->assertErrors($response); $this->assertSame($response[$this->api->itemName()]['points'], $pointsSet, 'Points were not set correctly'); - //now delete the contact + // now delete the contact $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } @@ -360,7 +360,7 @@ public function testEditPatchFormError() ] ); - //there should be an error as the country does not exist + // there should be an error as the country does not exist $this->assertTrue(isset($response['errors'][0]), $response['errors'][0]['message']); } @@ -394,7 +394,7 @@ public function testAddPoints() $response = $this->api->get($contact['id']); $this->assertErrors($response); - $this->assertSame($response[$this->api->itemName()]['points'], ($contact['points'] + $pointToAdd), 'Points were not added correctly'); + $this->assertSame($response[$this->api->itemName()]['points'], $contact['points'] + $pointToAdd, 'Points were not added correctly'); $response = $this->api->delete($contact['id']); $this->assertErrors($response); @@ -414,7 +414,144 @@ public function testSubtractPoints() $response = $this->api->get($contact['id']); $this->assertErrors($response); - $this->assertSame($response[$this->api->itemName()]['points'], ($contact['points'] - $pointToSub), 'Points were not subtracted correctly'); + $this->assertSame($response[$this->api->itemName()]['points'], $contact['points'] - $pointToSub, 'Points were not subtracted correctly'); + + $response = $this->api->delete($contact['id']); + $this->assertErrors($response); + } + + public function testGetPointGroupScores(): void + { + $response = $this->api->create($this->testPayload); + $this->assertErrors($response); + $contact = $response[$this->api->itemName()]; + + // test empty group points list + $response = $this->api->getPointGroupScores($contact['id']); + $this->assertErrors($response); + $this->assertSame(0, $response['total']); + $this->assertIsArray($response['groupScores']); + $this->assertEmpty($response['groupScores']); + + // add score + $pointsToAdd = 5; + $pointGroupApi = $this->getContext('pointGroups'); + $response = $pointGroupApi->create(['name' => 'Group A']); + $pointGroup = $response[$pointGroupApi->itemName()]; + $response = $this->api->addPointGroupScore($contact['id'], $pointGroup['id'], $pointsToAdd); + $this->assertErrors($response); + $this->assertNotEmpty($response['groupScore'], 'Adding point group score to a contact with ID ='.$contact['id'].' was not successful'); + + // test get point group scores list + $response = $this->api->getPointGroupScores($contact['id']); + $this->assertErrors($response); + $this->assertSame(1, $response['total']); + $this->assertIsArray($response['groupScores']); + $this->assertCount(1, $response['groupScores']); + $this->assertSame(5, $response['groupScores'][0]['score']); + $this->assertSame($pointGroup['id'], $response['groupScores'][0]['group']['id']); + $this->assertSame($pointGroup['name'], $response['groupScores'][0]['group']['name']); + + $response = $this->api->delete($contact['id']); + $this->assertErrors($response); + } + + public function testAddPointGroupScore(): void + { + $pointsToAdd = 5; + $pointGroupApi = $this->getContext('pointGroups'); + $response = $pointGroupApi->create(['name' => 'Group A']); + $pointGroup = $response[$pointGroupApi->itemName()]; + + $response = $this->api->create($this->testPayload); + $this->assertErrors($response); + $contact = $response[$this->api->itemName()]; + + $response = $this->api->addPointGroupScore($contact['id'], $pointGroup['id'], $pointsToAdd); + $this->assertErrors($response); + $this->assertTrue(!empty($response['groupScore']), 'Adding point group score to a contact with ID ='.$contact['id'].' was not successful'); + + $response = $this->api->getPointGroupScore($contact['id'], $pointGroup['id']); + $this->assertErrors($response); + $this->assertSame($response['groupScore']['score'], $pointsToAdd, 'Point group score was not added accurately'); + + $response = $this->api->delete($contact['id']); + $this->assertErrors($response); + } + + public function testSubtractPointGroupScore(): void + { + $pointsToSubtract = 3; + $pointGroupApi = $this->getContext('pointGroups'); + $response = $pointGroupApi->create(['name' => 'Group B']); + $pointGroup = $response[$pointGroupApi->itemName()]; + + $response = $this->api->create($this->testPayload); + $this->assertErrors($response); + $contact = $response[$this->api->itemName()]; + + $response = $this->api->setPointGroupScore($contact['id'], $pointGroup['id'], 10); + $this->assertErrors($response); + + $response = $this->api->subtractPointGroupScore($contact['id'], $pointGroup['id'], $pointsToSubtract); + $this->assertErrors($response); + $this->assertTrue(!empty($response['groupScore']), 'Subtracting point group score from a contact with ID ='.$contact['id'].' was not successful'); + + $response = $this->api->getPointGroupScore($contact['id'], $pointGroup['id']); + $this->assertErrors($response); + $this->assertSame($response['groupScore']['score'], 10 - $pointsToSubtract, 'Point group score was not subtracted accurately'); + + $response = $this->api->delete($contact['id']); + $this->assertErrors($response); + } + + public function testMultiplyPointGroupScore(): void + { + $multiplier = 2; + $pointGroupApi = $this->getContext('pointGroups'); + $response = $pointGroupApi->create(['name' => 'Group C']); + $pointGroup = $response[$pointGroupApi->itemName()]; + + $response = $this->api->create($this->testPayload); + $this->assertErrors($response); + $contact = $response[$this->api->itemName()]; + + $response = $this->api->setPointGroupScore($contact['id'], $pointGroup['id'], 5); + $this->assertErrors($response); + + $response = $this->api->multiplyPointGroupScore($contact['id'], $pointGroup['id'], $multiplier); + $this->assertErrors($response); + $this->assertTrue(!empty($response['groupScore']), 'Multiplying point group score for a contact with ID ='.$contact['id'].' was not successful'); + + $response = $this->api->getPointGroupScore($contact['id'], $pointGroup['id']); + $this->assertErrors($response); + $this->assertSame($response['groupScore']['score'], 5 * $multiplier, 'Point group score was not multiplied accurately'); + + $response = $this->api->delete($contact['id']); + $this->assertErrors($response); + } + + public function testDividePointGroupScore(): void + { + $divisor = 4; + $pointGroupApi = $this->getContext('pointGroups'); + $response = $pointGroupApi->create(['name' => 'Group D']); + $pointGroup = $response[$pointGroupApi->itemName()]; + + $response = $this->api->create($this->testPayload); + $this->assertErrors($response); + $contact = $response[$this->api->itemName()]; + + $response = $this->api->setPointGroupScore($contact['id'], $pointGroup['id'], 20); + $this->assertErrors($response); + + $response = $this->api->dividePointGroupScore($contact['id'], $pointGroup['id'], $divisor); + $this->assertErrors($response); + $this->assertTrue(!empty($response['groupScore']), 'Dividing point group score for a contact with ID ='.$contact['id'].' was not successful'); + + $response = $this->api->getPointGroupScore($contact['id'], $pointGroup['id']); + $this->assertErrors($response); + $this->assertSame($response['groupScore']['score'], 20 / $divisor, 'Point group score was not divided accurately'); $response = $this->api->delete($contact['id']); $this->assertErrors($response); diff --git a/tests/Api/DataTest.php b/tests/Api/DataTest.php index e2a63472..c3626b4f 100644 --- a/tests/Api/DataTest.php +++ b/tests/Api/DataTest.php @@ -1,4 +1,5 @@ [ 'recent.activity' => 'Recent Activity', - ], + ], 'Asset Widgets' => [ 'asset.downloads.in.time' => 'Downloads in time', 'unique.vs.repetitive.downloads' => 'Unique vs repetitive downloads', 'popular.assets' => 'Popular assets', 'created.assets' => 'Created assets', - ], + ], 'Campaign Widgets' => [ 'events.in.time' => 'Events triggered in time', 'leads.added.in.time' => 'Leads added in time', - ], + ], 'Email Widgets' => [ 'emails.in.time' => 'Emails in time', 'ignored.vs.read.emails' => 'Ignored vs read', @@ -37,13 +38,13 @@ class DataTest extends MauticApiTestCase 'most.read.emails' => 'Most read emails', 'created.emails' => 'Created emails', 'device.granularity.email' => 'Devices for emails read', - ], + ], 'Form Widgets' => [ 'submissions.in.time' => 'Submissions in time', 'top.submission.referrers' => 'Top submission referrers', 'top.submitters' => 'Top submitters', 'created.forms' => 'Created forms', - ], + ], 'Contact Widgets' => [ 'created.leads.in.time' => 'Created contacts in time', 'anonymous.vs.identified.leads' => 'Anonymous vs identified contacts', @@ -122,7 +123,7 @@ public function testUnsupportedFeatures() 'create' => function () { return $this->api->create([]); }, 'edit' => function () { return $this->api->edit('x', []); }, 'delete' => function () { return $this->api->delete('x'); }, - ]; + ]; foreach ($notSupported as $key => $closure) { $response = $closure(); $this->assertTrue(!empty($response['errors']), 'Should contain Error element'); @@ -137,7 +138,7 @@ public function testGetListData() $this->assertTrue(!empty($response['types']), 'Check "types" exists in Array'); $this->assertPayload($response); // Types contains 10 elements when test written - $this->assertTrue((count($response[$this->api->listName()])) >= 10, 'Should contain 10 or more elements'); + $this->assertTrue(count($response[$this->api->listName()]) >= 10, 'Should contain 10 or more elements'); // test all currently known types exist foreach ($this->dataToTest as $key => $data) { diff --git a/tests/Api/DevicesTest.php b/tests/Api/DevicesTest.php index 78dd8ecb..af3defc7 100644 --- a/tests/Api/DevicesTest.php +++ b/tests/Api/DevicesTest.php @@ -1,4 +1,5 @@ 'email', 'object' => 'lead', 'type' => 'email', - 'filter' => 'Prague', + 'filter' => null, 'display' => null, 'operator' => '!empty', ], @@ -138,7 +139,7 @@ public function testEditPatch() { $response = $this->api->edit(10000, $this->testPayload); - //there should be an error as the email shouldn't exist + // there should be an error as the email shouldn't exist $this->assertTrue(isset($response['errors'][0]), $response['errors'][0]['message']); // Unset the emailType, 'template' must be the default value @@ -158,7 +159,7 @@ public function testEditPatch() $this->assertErrors($response); - //now delete the email + // now delete the email $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } @@ -187,7 +188,7 @@ public function testEditPut() $this->assertSame($response[$this->api->itemName()]['lists'][0]['id'], $segment2['id']); $this->assertequals(count($response[$this->api->itemName()]['lists']), 1); - //now delete the email + // now delete the email $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); $response = $segmentApi->delete($segment1['id']); diff --git a/tests/Api/ExceptionsTest.php b/tests/Api/ExceptionsTest.php index 63970c33..b488e1eb 100644 --- a/tests/Api/ExceptionsTest.php +++ b/tests/Api/ExceptionsTest.php @@ -1,4 +1,5 @@ 200])); + $exception = new UnexpectedResponseFormatException(new Response(new HttpResponse())); $this->assertEquals($expected, $exception->getMessage(), 'This should return "'.$expected.'"'); $this->assertEquals(500, $exception->getCode()); } @@ -77,14 +79,14 @@ public function testUnexpectedResponseFormatException() public function testUnexpectedResponseFormatExceptionCustomMessage() { $expected = self::CUSTOM_ERROR_MESSAGE."\n\nResponse: "; - $exception = new UnexpectedResponseFormatException(new Response('', ['http_code' => 200]), self::CUSTOM_ERROR_MESSAGE); + $exception = new UnexpectedResponseFormatException(new Response(new HttpResponse()), self::CUSTOM_ERROR_MESSAGE); $this->assertEquals($expected, $exception->getMessage(), 'This should return "'.$expected.'"'); $this->assertEquals(500, $exception->getCode()); } public function testUnexpectedResponseFormatExceptionCustomCode() { - $exception = new UnexpectedResponseFormatException(new Response('', ['http_code' => 200]), null, 404); + $exception = new UnexpectedResponseFormatException(new Response(new HttpResponse()), null, 404); $this->assertEquals(404, $exception->getCode()); } diff --git a/tests/Api/FilesTest.php b/tests/Api/FilesTest.php index 6d3e5077..565206e6 100644 --- a/tests/Api/FilesTest.php +++ b/tests/Api/FilesTest.php @@ -1,4 +1,5 @@ api = $this->getContext('files'); - $this->testPayload['file'] = dirname(__DIR__).'/'.'mauticlogo.png'; + $this->testPayload['file'] = dirname(__DIR__).'/mauticlogo.png'; + + if ('4' == $this->mauticVersion) { + $this->mediaFolder = 'assets'; + } + $this->assertTrue(file_exists($this->testPayload['file']), 'A file for test at '.$this->testPayload['file'].' does not exist.'); } @@ -34,15 +42,19 @@ public function testGetList() public function testGetListSubdir() { - $this->api->setFolder('images/flags'); + $this->api->setFolder('images/test_api_dir'); + $createResponse = $this->api->create($this->testPayload); + $response = $this->api->getList(); $this->assertTrue(isset($response['files'])); $this->assertErrors($response); + + $this->api->delete($createResponse['file']['name']); } - public function testGetListAssetFiles() + public function testGetListMediaFiles() { - $this->api->setFolder('assets'); + $this->api->setFolder($this->mediaFolder); $response = $this->api->getList(); $this->assertErrors($response); } @@ -80,9 +92,9 @@ public function testCreateAndDeleteImageInSubdir() $this->assertSuccess($response); } - public function testCreateAndDeleteAsset() + public function testCreateAndDeleteMedia() { - $this->api->setFolder('assets'); + $this->api->setFolder($this->mediaFolder); $response = $this->api->create($this->testPayload); $this->assertPayload($response); diff --git a/tests/Api/FocusTest.php b/tests/Api/FocusTest.php index 79fa5962..793cea81 100644 --- a/tests/Api/FocusTest.php +++ b/tests/Api/FocusTest.php @@ -1,4 +1,5 @@ '
html mode enabled
', 'properties' => [ 'bar' => [ - 'allow_hide' => 1, - 'sticky' => 1, - 'size' => 'large', - 'placement' => 'top', - ], + 'allow_hide' => 1, + 'sticky' => 1, + 'size' => 'large', + 'placement' => 'top', + ], 'modal' => [ - 'placement' => 'top', - ], + 'placement' => 'top', + ], 'notification' => [ - 'placement' => 'top_left', - ], + 'placement' => 'top_left', + ], 'animate' => 1, 'link_activation' => 1, 'colors' => [ diff --git a/tests/Api/FormsTest.php b/tests/Api/FormsTest.php index a56005d2..536e0f17 100644 --- a/tests/Api/FormsTest.php +++ b/tests/Api/FormsTest.php @@ -1,4 +1,5 @@ assertErrors($response); $this->assertTrue(empty($response[$this->api->itemName()]['fields']), 'Fields were not deleted'); - //now delete the form + // now delete the form $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } @@ -115,7 +116,7 @@ public function testDeleteActions() $this->assertErrors($response); $this->assertTrue(empty($response[$this->api->itemName()]['actions']), 'Actions were not deleted'); - //now delete the form + // now delete the form $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } @@ -124,7 +125,7 @@ public function testEditPatch() { $response = $this->api->edit(10000, $this->testPayload); - //there should be an error as the form shouldn't exist + // there should be an error as the form shouldn't exist $this->assertTrue(isset($response['errors']), $response['errors'][0]['message']); $response = $this->api->create($this->testPayload); @@ -157,7 +158,7 @@ public function testEditPatch() $this->assertSame($lastField['label'], 'edited field'); $this->assertSame($lastAction['name'], 'edited action'); - //now delete the form + // now delete the form $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } @@ -188,7 +189,7 @@ public function testFieldAndActionDeleteViaPut() $this->assertTrue(empty($response[$this->api->itemName()]['fields']), 'Fields were not deleted via PUT request'); $this->assertTrue(empty($response[$this->api->itemName()]['actions']), 'Actions were not deleted via PUT request'); - //now delete the form + // now delete the form $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } @@ -218,10 +219,15 @@ public function testFormSubmissions() $response = $this->api->getSubmissions($formId); $this->assertErrors($response); + $submissions = $response['submissions']; + $this->assertTrue(count($submissions) > 0, 'Expected at least one form submission'); + foreach ($response['submissions'] as $submission) { $this->assertSubmission($submission, $formId); } + $submission = end($submissions); + // Try to fetch the last submission $response = $this->api->getSubmission($formId, $submission['id']); $this->assertErrors($response); diff --git a/tests/Api/MauticApiTestCase.php b/tests/Api/MauticApiTestCase.php index 4b330fb3..c1a4fb1b 100644 --- a/tests/Api/MauticApiTestCase.php +++ b/tests/Api/MauticApiTestCase.php @@ -1,4 +1,5 @@ newAuth($this->config, $authMethod); if ('BasicAuth' != $authMethod) { if (empty($this->config['refreshToken']) && !$auth->isAuthorized()) { - $this->assertTrue($authorized, 'Authorization failed. Check credentials in local.config.php.'); + $this->fail('Authorization failed. Check credentials in local.config.php.'); } else { try { $auth->validateAccessToken(); } catch (\Exception $e) { - $this->assertTrue(false, $e->getMessage()); + $this->fail($e->getMessage()); } if ($auth->accessTokenUpdated()) { @@ -67,6 +70,7 @@ protected function getAuth() protected function getContext($context) { + $this->mauticVersion = $_ENV['MAUTIC_VERSION'] ?? '5'; list($auth, $apiUrl) = $this->getAuth(); $api = new MauticApi(); @@ -99,7 +103,7 @@ protected function assertPayload( array $payload = [], $isBatch = false, $idColumn = 'id', - $callback = null + $callback = null, ) { $this->assertErrors($response); @@ -226,7 +230,7 @@ protected function standardTestGetList() } } - protected function standardTestCreateGetAndDelete(array $payload = null) + protected function standardTestCreateGetAndDelete(?array $payload = null) { if (empty($payload)) { $payload = $this->testPayload; @@ -247,7 +251,7 @@ protected function standardTestCreateGetAndDelete(array $payload = null) $this->assertErrors($response); } - protected function standardTestBatchEndpoints(array $batch = null, $callback = null) + protected function standardTestBatchEndpoints(?array $batch = null, $callback = null) { if (method_exists($this, 'setUpPayloadClass')) { $this->setUpPayloadClass(); @@ -302,7 +306,7 @@ public function standardTestEditPatch(array $editTo) { $response = $this->api->edit(10000, $this->testPayload); - //there should be an error as the item shouldn't exist + // there should be an error as the item shouldn't exist $this->assertTrue(isset($response['errors'][0]), $response['errors'][0]['message']); $response = $this->api->create($this->testPayload); @@ -320,7 +324,7 @@ public function standardTestEditPut() $response = $this->api->edit(10000, $this->testPayload, true); $this->assertPayload($response); - //now delete the entity + // now delete the entity $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } diff --git a/tests/Api/MessagesTest.php b/tests/Api/MessagesTest.php index 18614777..67004e45 100644 --- a/tests/Api/MessagesTest.php +++ b/tests/Api/MessagesTest.php @@ -1,4 +1,5 @@ api = $this->getContext('pointGroups'); + $this->testPayload = [ + 'name' => 'New Point Group', + 'description' => 'Description of the new point group', + ]; + } + + public function testGetList(): void + { + $this->standardTestGetList(); + } + + public function testGetListOfSpecificIds(): void + { + $this->standardTestGetListOfSpecificIds(); + } + + public function testCreateGetAndDelete(): void + { + $this->standardTestCreateGetAndDelete(); + } + + public function testEditPatch(): void + { + $editTo = [ + 'name' => 'Updated Point Group Name', + ]; + $this->standardTestEditPatch($editTo); + } + + public function testEditPut(): void + { + $this->standardTestEditPut(); + } + + public function testBatchEndpoints(): void + { + $this->standardTestBatchEndpoints(); + } +} diff --git a/tests/Api/PointTriggersTest.php b/tests/Api/PointTriggersTest.php index e7379e94..a05cf467 100644 --- a/tests/Api/PointTriggersTest.php +++ b/tests/Api/PointTriggersTest.php @@ -1,4 +1,5 @@ assertErrors($response); $this->assertTrue(empty($response[$this->api->itemName()]['events']), 'Trigger events were not deleted via PUT request'); - //now delete the form + // now delete the form $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } @@ -131,7 +132,7 @@ public function testDeleteEvents() $this->assertErrors($response); $this->assertTrue(empty($response[$this->api->itemName()]['events']), 'Events were not deleted'); - //now delete the trigger + // now delete the trigger $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } diff --git a/tests/Api/PointsTest.php b/tests/Api/PointsTest.php index 003336d6..c027fa59 100644 --- a/tests/Api/PointsTest.php +++ b/tests/Api/PointsTest.php @@ -1,4 +1,5 @@ assertMatchesRegularExpression("/^(\d+\.)?(\d+\.)?(.+|\d+)$/", $version); } - public function testResponseInfo() - { - $info = $this->api->getResponseInfo(); - $this->assertEquals($info['content_type'], 'application/json'); - } - public function testResponseHeaders() { $headers = $this->api->getResponseHeaders(); - $this->assertEquals($headers[0], 'HTTP/1.1 200 OK'); + $this->assertEquals($headers['Content-Type'], 'application/json'); } } diff --git a/tests/Api/RolesTest.php b/tests/Api/RolesTest.php index 19ad974a..bda930a9 100644 --- a/tests/Api/RolesTest.php +++ b/tests/Api/RolesTest.php @@ -1,4 +1,5 @@ api->edit(10000, $this->testPayload); - //there should be an error as the segment shouldn't exist + // there should be an error as the segment shouldn't exist $this->assertTrue(isset($response['errors'][0]), $response['errors'][0]['message']); $response = $this->api->create($this->testPayload); @@ -109,7 +110,7 @@ public function testEditPatch() $response = $this->api->edit($response[$this->api->itemName()]['id'], $update); $this->assertPayload($response, $update); - //now delete the segment + // now delete the segment $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } @@ -119,7 +120,7 @@ public function testEditPut() $response = $this->api->edit(10000, $this->testPayload, true); $this->assertPayload($response); - //now delete the segment + // now delete the segment $response = $this->api->delete($response[$this->api->itemName()]['id']); $this->assertErrors($response); } diff --git a/tests/Api/SmsesTest.php b/tests/Api/SmsesTest.php index e619bff4..49cfb6eb 100644 --- a/tests/Api/SmsesTest.php +++ b/tests/Api/SmsesTest.php @@ -1,4 +1,5 @@ mauticVersion) { + $expectedTables[] = 'plugin_citrix_events'; + sort($expectedTables); + } + $tables = $this->getTableList(); $this->assertTrue(!empty($tables)); $this->assertSame( @@ -118,7 +124,7 @@ public function testGetStartLimit() foreach ($this->getTableList() as $table) { $response = $this->api->get($table, 1, 2); $this->assertPayload($response); - $this->assertTrue((count($response[$this->api->listName()])) <= 2); + $this->assertTrue(count($response[$this->api->listName()]) <= 2); } } @@ -127,7 +133,7 @@ public function testGetOrderSimple() list($tables, $columns) = $this->getTableList(true); foreach ($tables as $table) { - $hasId = (in_array('id', $columns[$table])); + $hasId = in_array('id', $columns[$table]); $response = $this->api->get( $table, @@ -152,7 +158,7 @@ public function testGetOrderAsc() list($tables, $columns) = $this->getTableList(true); foreach ($tables as $table) { - $hasId = (in_array('id', $columns[$table])); + $hasId = in_array('id', $columns[$table]); $response = $this->api->get( $table, 0, @@ -177,7 +183,7 @@ public function testGetOrderDesc() list($tables, $columns) = $this->getTableList(true); foreach ($tables as $table) { - $hasId = (in_array('id', $columns[$table])); + $hasId = in_array('id', $columns[$table]); $response = $this->api->get( $table, @@ -217,10 +223,10 @@ public function testGetWhereEqual() $response = $this->api->get($table, 0, 2, [], $where); $this->assertPayload($response); - $this->assertTrue((count($response[$this->api->listName()])) <= 1); + $this->assertTrue(count($response[$this->api->listName()]) <= 1); // The record might not exist in the database, but in case it does... - if (1 === (count($response[$this->api->listName()]))) { + if (1 === count($response[$this->api->listName()])) { $this->assertSame((int) $response[$this->api->listName()][0]['id'], $where[0]['val']); } } @@ -246,7 +252,7 @@ public function testGetWhereGreaterThan() $this->assertPayload($response); // The record might not exist in the database, but in case it does... - if ((count($response[$this->api->listName()])) > 0) { + if (count($response[$this->api->listName()]) > 0) { $this->assertGreaterThan($where[0]['val'], (int) $response[$this->api->listName()][0]['id']); } } diff --git a/tests/Api/TagsTest.php b/tests/Api/TagsTest.php index 9c46ff6e..b1ec5df1 100644 --- a/tests/Api/TagsTest.php +++ b/tests/Api/TagsTest.php @@ -1,4 +1,5 @@ addUtmTags(); // check the date; Should be empty - $this->assertTrue(empty($response[$this->api->itemName()]['lastActive'])); + $this->assertTrue(empty($utmIds[$this->api->itemName()]['lastActive'])); // Now add the payload with a known date $this->testPayload['lastActive'] = '2017-01-17T00:30:08+00:00'; @@ -150,7 +151,11 @@ protected function removeUtmTags($utmIds) } } - $this->assertSame(0, count($response[$this->api->itemName()]['utmtags']), 'Should be no more items'); + if (!empty($response)) { + $this->assertSame(0, count($response[$this->api->itemName()]['utmtags']), 'Should be no more items'); + } else { + $this->fail('Expected a response object'); + } } protected function addUtmTags() diff --git a/tests/Api/WebhooksTest.php b/tests/Api/WebhooksTest.php index eba3cdd6..5ba4a1c1 100644 --- a/tests/Api/WebhooksTest.php +++ b/tests/Api/WebhooksTest.php @@ -1,4 +1,5 @@ ['Fri, 17 Nov 2017 14:09:31 GMT'], + 'Server' => ['Apache/2.4.25 (Unix) OpenSSL/0.9.8zh PHP/7.0.15'], + 'X-Powered-By' => ['PHP/7.0.15'], + 'Set-Cookie' => ['9743595cf0a472cb3ec0272949ffe7e8=psh2rh9cam538t1u3e1gd3d8l3; path=/; HttpOnly'], + 'Transfer-Encoding' => ['chunked'], + 'Content-Type' => ['text/html; charset=UTF-8'], + ]; private $htmlBody = ' @@ -64,76 +43,39 @@ class ResponseTest extends TestCase private $urlParamBody = 'first=value&arr[]=foo+bar&arr[]=baz'; - private $curlInfo = [ - 'url' => 'http://mautic.dev/index_dev.php', - 'content_type' => null, - 'http_code' => 200, - 'header_size' => 0, - 'request_size' => 0, - 'filetime' => -1, - 'ssl_verify_result' => 0, - 'redirect_count' => 0, - 'total_time' => 0, - 'namelookup_time' => 0, - 'connect_time' => 0, - 'pretransfer_time' => 0, - 'size_upload' => 0, - 'size_download' => 0, - 'speed_download' => 0, - 'speed_upload' => 0, - 'download_content_length' => -1, - 'upload_content_length' => -1, - 'starttransfer_time' => 0, - 'redirect_time' => 0, - 'redirect_url' => null, - 'primary_ip' => null, - 'certinfo' => [], - 'primary_port' => 0, - 'local_ip' => null, - 'local_port' => 0, - ]; - - private function getHtmlResponse() + private function getHtmlResponse($code = 200) { - return $this->headersWithRedirects.$this->space.$this->htmlBody; + return new HttpResponse($code, $this->headers, $this->htmlBody); } - private function getJsonResponse() + private function getJsonResponse($code = 200) { - return $this->headersWithRedirects.$this->space.$this->jsonBody; + return new HttpResponse($code, $this->headers, $this->jsonBody); } - private function getUrlParamResponse() + private function getUrlParamResponse($code = 200) { - return $this->headersWithRedirects.$this->space.$this->urlParamBody; - } - - private function getInfo($code = 200) - { - $info = $this->curlInfo; - $info['http_code'] = $code; - - return $info; + return new HttpResponse($code, $this->headers, $this->urlParamBody); } public function testParseResponse() { - $response = new Response($this->getJsonResponse(), $this->getInfo()); - $this->assertSame($this->headersWithRedirects, $response->getHeaders()); + $response = new Response($this->getJsonResponse()); + $this->assertSame(200, $response->getStatusCode()); + $this->assertSame($this->headers, $response->getHeaders()); $this->assertSame($this->jsonBody, $response->getBody()); - $this->assertSame($this->getInfo(), $response->getInfo()); } public function testValidation() { $this->expectException(UnexpectedResponseFormatException::class); - $response = new Response($this->getHtmlResponse(), $this->getInfo(500)); + $response = new Response($this->getHtmlResponse(500)); } public function testValidation404() { try { - $response = new Response($this->getHtmlResponse(), $this->getInfo(404)); + $response = new Response($this->getHtmlResponse(404)); } catch (UnexpectedResponseFormatException $e) { $this->assertSame(404, $e->getCode()); } @@ -141,56 +83,56 @@ public function testValidation404() public function testDecodeFromUrlParamsWithParams() { - $response = new Response($this->getUrlParamResponse(), $this->getInfo()); + $response = new Response($this->getUrlParamResponse()); $responseParams = $response->decodeFromUrlParams(); $this->assertSame('value', $responseParams['first']); } public function testDecodeFromUrlParamsWithNoParams() { - $response = new Response($this->headersWithRedirects.$this->space, $this->getInfo()); + $response = new Response(new HttpResponse(200, $this->headers)); $this->expectException(UnexpectedResponseFormatException::class); $response->decodeFromUrlParams(); } public function testDecodeFromJsonWithJson() { - $response = new Response($this->getJsonResponse(), $this->getInfo()); + $response = new Response($this->getJsonResponse()); $json = $response->decodeFromJson(); $this->assertSame('world', $json['hello']); } public function testDecodeFromJsonWithEmptyJson() { - $response = new Response($this->headersWithRedirects.$this->space, $this->getInfo()); + $response = new Response(new HttpResponse(200, $this->headers)); $this->expectException(UnexpectedResponseFormatException::class); $response->decodeFromUrlParams(); } public function testGetDecodedBodyWithJson() { - $response = new Response($this->getJsonResponse(), $this->getInfo()); + $response = new Response($this->getJsonResponse()); $body = $response->getDecodedBody(); $this->assertSame('world', $body['hello']); } public function testDecodeFromJsonWithEmptyResponse() { - $response = new Response($this->headersWithRedirects.$this->space, $this->getInfo()); + $response = new Response(new HttpResponse(200, $this->headers)); $this->expectException(UnexpectedResponseFormatException::class); $body = $response->getDecodedBody(); } public function testDecodeFromJsonWithTextResponse() { - $response = new Response($this->headersWithRedirects.$this->space.'OK', $this->getInfo()); + $response = new Response(new HttpResponse(200, $this->headers, 'OK')); $this->expectException(UnexpectedResponseFormatException::class); $body = $response->getDecodedBody(); } public function testSaveToFile() { - $response = new Response($this->getJsonResponse(), $this->getInfo()); + $response = new Response($this->getJsonResponse()); $result = $response->saveToFile(sys_get_temp_dir()); $this->assertFalse(empty($result['file'])); $this->assertTrue(file_exists($result['file'])); @@ -200,13 +142,13 @@ public function testSaveToFile() public function testIsZip() { - $response = new Response($this->getJsonResponse(), ['http_code' => 200, 'content_type' => 'application/zip']); + $response = new Response($this->getJsonResponse()->withHeader('Content-Type', 'application/zip')); $this->assertTrue($response->isZip()); } public function testIsNotZip() { - $response = new Response($this->getJsonResponse(), ['http_code' => 200, 'content_type' => 'application/json']); + $response = new Response($this->getJsonResponse()->withHeader('Content-Type', 'application/json')); $this->assertFalse($response->isZip()); } } diff --git a/tests/local.config.php.dist b/tests/local.config.php.dist index 78f2a32a..80be5e0f 100644 --- a/tests/local.config.php.dist +++ b/tests/local.config.php.dist @@ -52,10 +52,10 @@ return [ 'version' => 'OAuth1a', // Required for OAuth1a and OAuth2 - 'baseUrl' => 'http://example.com/index_dev.php', + 'baseUrl' => 'http://example.com/index.php', // Required for All tests - 'apiUrl' => 'http://example.com/index_dev.php/api/', + 'apiUrl' => 'http://example.com/index.php/api/', // Required for EmailsTest 'testEmail' => 'notexisting@email.com',