diff --git a/.github/workflows/lint-info-xml.yml b/.github/workflows/lint-info-xml.yml new file mode 100644 index 0000000..bf6f178 --- /dev/null +++ b/.github/workflows/lint-info-xml.yml @@ -0,0 +1,33 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization + +name: Lint info.xml + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-info-xml-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + xml-linters: + runs-on: ubuntu-latest + + name: info.xml lint + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Download schema + run: wget https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd + + - name: Lint info.xml + uses: ChristophWurst/xmllint-action@36f2a302f84f8c83fceea0b9c59e1eb4a616d3c1 # v1.2 + with: + xml-file: ./appinfo/info.xml + xml-schema-file: ./info.xsd diff --git a/.github/workflows/lint-php-cs.yml b/.github/workflows/lint-php-cs.yml new file mode 100644 index 0000000..c23fc7c --- /dev/null +++ b/.github/workflows/lint-php-cs.yml @@ -0,0 +1,40 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization + +name: Lint php-cs + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-php-cs-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + + name: php-cs + + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Set up php8.2 + uses: shivammathur/setup-php@81cd5ae0920b34eef300e1775313071038a53429 # v2 + with: + php-version: 8.2 + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: composer i + + - name: Lint + run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 ) diff --git a/.github/workflows/lint-php.yml b/.github/workflows/lint-php.yml new file mode 100644 index 0000000..92769c4 --- /dev/null +++ b/.github/workflows/lint-php.yml @@ -0,0 +1,54 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization + +name: Lint php + +on: pull_request + +permissions: + contents: read + +concurrency: + group: lint-php-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + php-lint: + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: [ '8.0', '8.1', '8.2', '8.3' ] + + name: php-lint + + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@81cd5ae0920b34eef300e1775313071038a53429 # v2 + with: + php-version: ${{ matrix.php-versions }} + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Lint + run: composer run lint + + summary: + permissions: + contents: none + runs-on: ubuntu-latest + needs: php-lint + + if: always() + + name: php-lint-summary + + steps: + - name: Summary status + run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi diff --git a/.github/workflows/node-build.yml b/.github/workflows/node-build.yml new file mode 100644 index 0000000..46be448 --- /dev/null +++ b/.github/workflows/node-build.yml @@ -0,0 +1,53 @@ +name: Node Build + +on: + pull_request: + paths: + - src/** + - .eslintrc.js + - stylelint.config.js + - webpack.js + push: + branches: + - main + paths: + - src/** + - .eslintrc.js + - stylelint.config.js + - webpack.js + +env: + APP_NAME: integration_jira + +jobs: + build: + name: node-build + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 + with: + path: ${{ env.APP_NAME }} + + - name: Read package.json node and npm engines version + uses: skjnldsv/read-package-engines-version-actions@0ce2ed60f6df073a62a77c0a4958dd0fc68e32e7 # v2.1 + id: versions + with: + path: ${{ env.APP_NAME }} + fallbackNode: "^20" + fallbackNpm: "^9" + + - name: Set up node ${{ steps.versions.outputs.nodeVersion }} + uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c # v3 + with: + node-version: ${{ steps.versions.outputs.nodeVersion }} + + - name: Set up npm ${{ steps.versions.outputs.npmVersion }} + run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}" + + - name: Build ${{ env.APP_NAME }} + run: | + cd ${{ env.APP_NAME }} + npm ci + npm run build diff --git a/.github/workflows/phpunit-sqlite.yml b/.github/workflows/phpunit-sqlite.yml new file mode 100644 index 0000000..605addd --- /dev/null +++ b/.github/workflows/phpunit-sqlite.yml @@ -0,0 +1,157 @@ +# This workflow is provided via the organization template repository +# +# https://github.com/nextcloud/.github +# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization + +name: PHPUnit sqlite + +on: pull_request + +permissions: + contents: read + +concurrency: + group: phpunit-sqlite-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + changes: + runs-on: ubuntu-latest + + outputs: + src: ${{ steps.changes.outputs.src}} + + steps: + - uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2.11.1 + id: changes + continue-on-error: true + with: + filters: | + src: + - '.github/workflows/**' + - 'appinfo/**' + - 'lib/**' + - 'templates/**' + - 'tests/**' + - 'vendor/**' + - 'vendor-bin/**' + - '.php-cs-fixer.dist.php' + - 'composer.json' + - 'composer.lock' + + phpunit-sqlite: + runs-on: ubuntu-latest + + needs: changes + if: needs.changes.outputs.src != 'false' + + strategy: + matrix: + php-versions: ['8.1', '8.2'] + server-versions: ['stable26', 'stable27', 'master'] + + steps: + - name: Set app env + run: | + # Split and keep last + echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV + + - name: Checkout server + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + submodules: true + repository: nextcloud/server + ref: ${{ matrix.server-versions }} + + - name: Checkout app + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + path: apps/${{ env.APP_NAME }} + + - name: Set up php ${{ matrix.php-versions }} + uses: shivammathur/setup-php@81cd5ae0920b34eef300e1775313071038a53429 # v2 + with: + php-version: ${{ matrix.php-versions }} + # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation + extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite + coverage: none + ini-file: development + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Check composer file existence + id: check_composer + uses: andstor/file-existence-action@20b4d2e596410855db8f9ca21e96fbe18e12930b # v2 + with: + files: apps/${{ env.APP_NAME }}/composer.json + + - name: Set up dependencies + # Only run if phpunit config file exists + if: steps.check_composer.outputs.files_exists == 'true' + working-directory: apps/${{ env.APP_NAME }} + run: composer i + + - name: Set up Nextcloud + env: + DB_PORT: 4444 + run: | + mkdir data + ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin + ./occ app:enable --force ${{ env.APP_NAME }} + + - name: Check PHPUnit script is defined + id: check_phpunit + continue-on-error: true + working-directory: apps/${{ env.APP_NAME }} + run: | + composer run --list | grep "^ test:unit " | wc -l | grep 1 + + - name: PHPUnit + # Only run if phpunit config file exists + if: steps.check_phpunit.outcome == 'success' + working-directory: apps/${{ env.APP_NAME }} + run: composer run test:unit + + - name: Check PHPUnit integration script is defined + id: check_integration + continue-on-error: true + working-directory: apps/${{ env.APP_NAME }} + run: | + composer run --list | grep "^ test:integration " | wc -l | grep 1 + + - name: Run Nextcloud + # Only run if phpunit integration config file exists + if: steps.check_integration.outcome == 'success' + run: php -S localhost:8080 & + + - name: PHPUnit integration + # Only run if phpunit integration config file exists + if: steps.check_integration.outcome == 'success' + working-directory: apps/${{ env.APP_NAME }} + run: composer run test:integration + + - name: Print logs + if: always() + run: | + cat data/nextcloud.log + + - name: Skipped + # Fail the action when neither unit nor integration tests ran + if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure' + run: | + echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts' + exit 1 + + summary: + permissions: + contents: none + runs-on: ubuntu-latest + needs: [changes, phpunit-sqlite] + + if: always() + + name: phpunit-sqlite-summary + + steps: + - name: Summary status + run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-sqlite.result != 'success' }}; then exit 1; fi diff --git a/.gitignore b/.gitignore index b4fccbf..70c55d4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ js/ .vscode-upload.json .*.sw* node_modules +/vendor/ +.php-cs-fixer.cache +tests/.phpunit.cache/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..f7bbdd8 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,18 @@ +getFinder() + ->ignoreVCSIgnored(true) + ->notPath('build') + ->notPath('l10n') + ->notPath('src') + ->notPath('vendor') + ->in(__DIR__); +return $config; diff --git a/CHANGELOG.md b/CHANGELOG.md index bc1300b..3729d56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## 1.0.7 – 202x-xx-xx +### Changed +- minimum required NC raised from v25 to v26. +- bump js libs + ## 1.0.6 – 2023-10-24 ### Changed - added support of NC28 diff --git a/appinfo/info.xml b/appinfo/info.xml index df9c13e..117cb5f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -6,7 +6,7 @@ - 1.0.6 + 1.0.7 agpl Julien Veyssier Jira @@ -19,7 +19,7 @@ and notifications about recent activity related to your assigned issues.]]>https://github.com/nextcloud/integration_jira/issues https://github.com/nextcloud/integration_jira/raw/master/img/screenshot1.jpg - + OCA\Jira\BackgroundJob\CheckOpenTickets diff --git a/appinfo/routes.php b/appinfo/routes.php index e102dab..9c8aad4 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -10,13 +10,13 @@ */ return [ - 'routes' => [ - ['name' => 'config#oauthRedirect', 'url' => '/oauth-redirect', 'verb' => 'GET'], - ['name' => 'config#connectToSoftware', 'url' => '/soft-connect', 'verb' => 'PUT'], - ['name' => 'config#setConfig', 'url' => '/config', 'verb' => 'PUT'], - ['name' => 'config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'], - ['name' => 'jiraAPI#getNotifications', 'url' => '/notifications', 'verb' => 'GET'], - ['name' => 'jiraAPI#getJiraUrl', 'url' => '/url', 'verb' => 'GET'], - ['name' => 'jiraAPI#getJiraAvatar', 'url' => '/avatar', 'verb' => 'GET'], - ] + 'routes' => [ + ['name' => 'config#oauthRedirect', 'url' => '/oauth-redirect', 'verb' => 'GET'], + ['name' => 'config#connectToSoftware', 'url' => '/soft-connect', 'verb' => 'PUT'], + ['name' => 'config#setConfig', 'url' => '/config', 'verb' => 'PUT'], + ['name' => 'config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'], + ['name' => 'jiraAPI#getNotifications', 'url' => '/notifications', 'verb' => 'GET'], + ['name' => 'jiraAPI#getJiraUrl', 'url' => '/url', 'verb' => 'GET'], + ['name' => 'jiraAPI#getJiraAvatar', 'url' => '/avatar', 'verb' => 'GET'], + ] ]; diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2586878 --- /dev/null +++ b/composer.json @@ -0,0 +1,22 @@ +{ + "name": "nextcloud/integration_jira", + "type": "project", + "license": "APL-3.0-or-later", + "autoload": { + "psr-4": { + "OCA\\Jira\\": "lib/" + } + }, + "minimum-stability": "stable", + "require-dev": { + "nextcloud/ocp": "^26.0", + "nextcloud/coding-standard": "^1.1", + "phpunit/phpunit": "^10" + }, + "scripts": { + "lint": "find . -name \\*.php -not -path './vendor/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l", + "cs:check": "php-cs-fixer fix --dry-run --diff", + "cs:fix": "php-cs-fixer fix", + "test:unit": "vendor/bin/phpunit -c tests/phpunit.xml --colors=always --fail-on-warning --fail-on-risky" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..54b3a42 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1916 @@ +{ + "_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": "0e34d4e4dfb82de6e27049972faf5242", + "packages": [], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "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.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "nextcloud/coding-standard", + "version": "v1.1.1", + "source": { + "type": "git", + "url": "https://github.com/nextcloud/coding-standard.git", + "reference": "55def702fb9a37a219511e1d8c6fe8e37164c1fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nextcloud/coding-standard/zipball/55def702fb9a37a219511e1d8c6fe8e37164c1fb", + "reference": "55def702fb9a37a219511e1d8c6fe8e37164c1fb", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0", + "php-cs-fixer/shim": "^3.17" + }, + "type": "library", + "autoload": { + "psr-4": { + "Nextcloud\\CodingStandard\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christoph Wurst", + "email": "christoph@winzerhof-wurst.at" + } + ], + "description": "Nextcloud coding standards for the php cs fixer", + "support": { + "issues": "https://github.com/nextcloud/coding-standard/issues", + "source": "https://github.com/nextcloud/coding-standard/tree/v1.1.1" + }, + "time": "2023-06-01T12:05:01+00:00" + }, + { + "name": "nextcloud/ocp", + "version": "v26.0.8", + "source": { + "type": "git", + "url": "https://github.com/nextcloud-deps/ocp.git", + "reference": "2b24e15929d27f4bd800e38fd3a3cf3dcefd00a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/2b24e15929d27f4bd800e38fd3a3cf3dcefd00a4", + "reference": "2b24e15929d27f4bd800e38fd3a3cf3dcefd00a4", + "shasum": "" + }, + "require": { + "php": "^7.4 || ~8.0 || ~8.1", + "psr/container": "^1.1.1", + "psr/event-dispatcher": "^1.0", + "psr/log": "^1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "26.0.0-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "AGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Christoph Wurst", + "email": "christoph@winzerhof-wurst.at" + } + ], + "description": "Composer package containing Nextcloud's public API (classes, interfaces)", + "support": { + "issues": "https://github.com/nextcloud-deps/ocp/issues", + "source": "https://github.com/nextcloud-deps/ocp/tree/v26.0.8" + }, + "time": "2023-10-11T12:57:20+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.17.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "reference": "a6303e50c90c355c7eeee2c4a8b27fe8dc8fef1d", + "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.17.1" + }, + "time": "2023-08-13T19:53:39+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/shim", + "version": "v3.38.2", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/shim.git", + "reference": "b00e057155e00bbfaf3e753590dec01e549770f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/shim/zipball/b00e057155e00bbfaf3e753590dec01e549770f7", + "reference": "b00e057155e00bbfaf3e753590dec01e549770f7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "replace": { + "friendsofphp/php-cs-fixer": "self.version" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer", + "php-cs-fixer.phar" + ], + "type": "application", + "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/PHP-CS-Fixer/shim/issues", + "source": "https://github.com/PHP-CS-Fixer/shim/tree/v3.38.2" + }, + "time": "2023-11-14T00:19:50+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "355324ca4980b8916c18b9db29f3ef484078f26e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/355324ca4980b8916c18b9db29f3ef484078f26e", + "reference": "355324ca4980b8916c18b9db29f3ef484078f26e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.15", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-text-template": "^3.0", + "sebastian/code-unit-reverse-lookup": "^3.0", + "sebastian/complexity": "^3.0", + "sebastian/environment": "^6.0", + "sebastian/lines-of-code": "^2.0", + "sebastian/version": "^4.0", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.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": "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", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-10-04T15:34:17+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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": "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/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.4.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "cacd8b9dd224efa8eb28beb69004126c7ca1a1a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/cacd8b9dd224efa8eb28beb69004126c7ca1a1a1", + "reference": "cacd8b9dd224efa8eb28beb69004126c7ca1a1a1", + "shasum": "" + }, + "require": { + "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": ">=8.1", + "phpunit/php-code-coverage": "^10.1.5", + "phpunit/php-file-iterator": "^4.0", + "phpunit/php-invoker": "^4.0", + "phpunit/php-text-template": "^3.0", + "phpunit/php-timer": "^6.0", + "sebastian/cli-parser": "^2.0", + "sebastian/code-unit": "^2.0", + "sebastian/comparator": "^5.0", + "sebastian/diff": "^5.0", + "sebastian/environment": "^6.0", + "sebastian/exporter": "^5.1", + "sebastian/global-state": "^6.0.1", + "sebastian/object-enumerator": "^5.0", + "sebastian/recursion-context": "^5.0", + "sebastian/type": "^4.0", + "sebastian/version": "^4.0" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.4-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", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.4.2" + }, + "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-10-26T07:21:45+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": "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" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/efdc130dbbbb8ef0b545a994fd811725c5282cae", + "reference": "efdc130dbbbb8ef0b545a994fd811725c5282cae", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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 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/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:15+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "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/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "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/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2db5010a484d53ebf536087a70b4a5423c102372" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2db5010a484d53ebf536087a70b4a5423c102372", + "reference": "2db5010a484d53ebf536087a70b4a5423c102372", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.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", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-14T13:18:12+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68cfb347a44871f01e33ab0ef8215966432f6957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68cfb347a44871f01e33ab0ef8215966432f6957", + "reference": "68cfb347a44871f01e33ab0ef8215966432f6957", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.10", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-09-28T11:50:59+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/912dc2fbe3e3c1e7873313cc801b100b6c68c87b", + "reference": "912dc2fbe3e3c1e7873313cc801b100b6c68c87b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.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", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-05-01T07:48:21+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/43c751b41d74f96cbbd4e07b7aec9675651e2951", + "reference": "43c751b41d74f96cbbd4e07b7aec9675651e2951", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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 functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-04-11T05:39:26+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/64f51654862e0f5e318db7e9dcc2292c63cdbddc", + "reference": "64f51654862e0f5e318db7e9dcc2292c63cdbddc", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-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", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-09-24T13:22:09+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/7ea9ead78f6d380d2a667864c132c2f7b83055e4", + "reference": "7ea9ead78f6d380d2a667864c132c2f7b83055e4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.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", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-07-19T07:19:23+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "649e40d279e243d985aa8fb6e74dd5bb28dc185d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/649e40d279e243d985aa8fb6e74dd5bb28dc185d", + "reference": "649e40d279e243d985aa8fb6e74dd5bb28dc185d", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.10", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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 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", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T09:25:50+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "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/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "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": "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/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.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": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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 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/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.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/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+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": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index c7fe0cf..6d6c77c 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -9,16 +9,16 @@ namespace OCA\Jira\AppInfo; +use OCA\Jira\Dashboard\JiraWidget; +use OCA\Jira\Notification\Notifier; +use OCA\Jira\Search\JiraSearchProvider; use OCP\AppFramework\App; -use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\Bootstrap\IBootContext; + use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\Notification\IManager as INotificationManager; -use OCA\Jira\Dashboard\JiraWidget; -use OCA\Jira\Search\JiraSearchProvider; -use OCA\Jira\Notification\Notifier; - /** * Class Application * @@ -27,8 +27,9 @@ class Application extends App implements IBootstrap { public const APP_ID = 'integration_jira'; - public const JIRA_API_URL = 'https://api.atlassian.com'; - public const JIRA_AUTH_URL = 'https://auth.atlassian.com'; + public const INTEGRATION_USER_AGENT = 'Nextcloud Jira Integration'; + public const INTEGRATION_API_URL = 'https://api.atlassian.com/'; + public const JIRA_AUTH_URL = 'https://auth.atlassian.com/'; /** * Constructor @@ -51,4 +52,3 @@ public function register(IRegistrationContext $context): void { public function boot(IBootContext $context): void { } } - diff --git a/lib/BackgroundJob/CheckOpenTickets.php b/lib/BackgroundJob/CheckOpenTickets.php index bbc7a35..1e9a7cc 100644 --- a/lib/BackgroundJob/CheckOpenTickets.php +++ b/lib/BackgroundJob/CheckOpenTickets.php @@ -23,11 +23,11 @@ namespace OCA\Jira\BackgroundJob; -use OCP\BackgroundJob\TimedJob; +use OCA\Jira\Service\JiraAPIService; use OCP\AppFramework\Utility\ITimeFactory; -use Psr\Log\LoggerInterface; +use OCP\BackgroundJob\TimedJob; -use OCA\Jira\Service\JiraAPIService; +use Psr\Log\LoggerInterface; /** * Class CheckOpenTickets @@ -36,15 +36,13 @@ */ class CheckOpenTickets extends TimedJob { - /** @var JiraAPIService */ - protected $jiraAPIService; + protected JiraAPIService $jiraAPIService; - /** @var LoggerInterface */ - protected $logger; + protected LoggerInterface $logger; public function __construct(ITimeFactory $time, - JiraAPIService $jiraAPIService, - LoggerInterface $logger) { + JiraAPIService $jiraAPIService, + LoggerInterface $logger) { parent::__construct($time); // Every 15 minutes $this->setInterval(60 * 15); diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index fcc88e4..7ee2d3a 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -12,53 +12,38 @@ namespace OCA\Jira\Controller; use DateTime; -use OCP\IURLGenerator; +use OCA\Jira\AppInfo\Application; +use OCA\Jira\Service\NetworkService; +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\RedirectResponse; use OCP\IConfig; use OCP\IL10N; -use OCP\AppFramework\Http\RedirectResponse; -use OCP\IRequest; -use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Controller; -use OCA\Jira\Service\JiraAPIService; -use OCA\Jira\AppInfo\Application; +use OCP\IRequest; +use OCP\IURLGenerator; class ConfigController extends Controller { - /** - * @var IConfig - */ - private $config; - /** - * @var IURLGenerator - */ - private $urlGenerator; - /** - * @var IL10N - */ - private $l; - /** - * @var JiraAPIService - */ - private $jiraAPIService; - /** - * @var string|null - */ - private $userId; + private IConfig $config; + private IURLGenerator $urlGenerator; + private IL10N $l; + private ?string $userId; + private NetworkService $networkService; public function __construct(string $appName, - IRequest $request, - IConfig $config, - IURLGenerator $urlGenerator, - IL10N $l, - JiraAPIService $jiraAPIService, - ?string $userId) { + IRequest $request, + IConfig $config, + IURLGenerator $urlGenerator, + IL10N $l, + NetworkService $networkService, + ?string $userId) { parent::__construct($appName, $request); $this->config = $config; $this->urlGenerator = $urlGenerator; $this->l = $l; - $this->jiraAPIService = $jiraAPIService; $this->userId = $userId; + $this->networkService = $networkService; } /** @@ -121,7 +106,7 @@ public function connectToSoftware(string $url, string $login, string $password): $basicAuthHeader = base64_encode($login . ':' . $password); - $info = $this->jiraAPIService->basicRequest($targetInstanceUrl, $basicAuthHeader, 'rest/api/2/myself'); + $info = $this->networkService->basicRequest($targetInstanceUrl, $basicAuthHeader, 'rest/api/2/myself'); if (isset($info['displayName'])) { $this->config->setUserValue($this->userId, Application::APP_ID, 'user_name', $info['displayName']); // in self hosted version, key is the only account identifier @@ -153,13 +138,13 @@ public function oauthRedirect(string $code = '', string $state = ''): RedirectRe if ($clientID && $clientSecret && $configState !== '' && $configState === $state) { $redirect_uri = $this->config->getUserValue($this->userId, Application::APP_ID, 'redirect_uri'); - $result = $this->jiraAPIService->requestOAuthAccessToken([ + $result = $this->networkService->requestOAuthAccessToken([ 'client_id' => $clientID, 'client_secret' => $clientSecret, 'code' => $code, 'redirect_uri' => $redirect_uri, 'grant_type' => 'authorization_code' - ], 'POST'); + ]); if (isset($result['access_token'])) { $accessToken = $result['access_token']; $this->config->setUserValue($this->userId, Application::APP_ID, 'token', $accessToken); @@ -171,13 +156,13 @@ public function oauthRedirect(string $code = '', string $state = ''): RedirectRe $this->config->setUserValue($this->userId, Application::APP_ID, 'token_expires_at', $expiresAt); } // get accessible resources - $resources = $this->jiraAPIService->oauthRequest($this->userId, 'oauth/token/accessible-resources'); + $resources = $this->networkService->oauthRequest($this->userId, 'oauth/token/accessible-resources'); if (!isset($resources['error']) && count($resources) > 0) { $encodedResources = json_encode($resources); $this->config->setUserValue($this->userId, Application::APP_ID, 'resources', $encodedResources); // get user info $cloudId = $resources[0]['id']; - $info = $this->jiraAPIService->oauthRequest($this->userId, 'ex/jira/' . $cloudId . '/rest/api/2/myself'); + $info = $this->networkService->oauthRequest($this->userId, 'ex/jira/' . $cloudId . '/rest/api/2/myself'); if (isset($info['accountId'], $info['displayName'])) { $this->config->setUserValue($this->userId, Application::APP_ID, 'user_name', $info['displayName']); // in cloud version, accountId is there and key is not diff --git a/lib/Controller/JiraAPIController.php b/lib/Controller/JiraAPIController.php index 2135739..94d5daa 100644 --- a/lib/Controller/JiraAPIController.php +++ b/lib/Controller/JiraAPIController.php @@ -11,28 +11,23 @@ namespace OCA\Jira\Controller; +use OCA\Jira\Service\JiraAPIService; +use OCP\AppFramework\Controller; use OCP\AppFramework\Http\DataDisplayResponse; -use OCP\IRequest; use OCP\AppFramework\Http\DataResponse; -use OCP\AppFramework\Controller; -use OCA\Jira\Service\JiraAPIService; +use OCP\IRequest; class JiraAPIController extends Controller { - /** - * @var JiraAPIService - */ - private $jiraAPIService; - /** - * @var string|null - */ - private $userId; + private JiraAPIService $jiraAPIService; + + private ?string $userId; public function __construct(string $appName, - IRequest $request, - JiraAPIService $jiraAPIService, - ?string $userId) { + IRequest $request, + JiraAPIService $jiraAPIService, + ?string $userId) { parent::__construct($appName, $request); $this->jiraAPIService = $jiraAPIService; $this->userId = $userId; @@ -53,7 +48,7 @@ public function getJiraAvatar(string $accountId = '', string $accountKey = ''): return new DataDisplayResponse('', 401); } else { $response = new DataDisplayResponse($avatarContent); - $response->cacheFor(60*60*24); + $response->cacheFor(60 * 60 * 24); return $response; } } diff --git a/lib/Dashboard/JiraWidget.php b/lib/Dashboard/JiraWidget.php index 76f137d..50a2ee6 100644 --- a/lib/Dashboard/JiraWidget.php +++ b/lib/Dashboard/JiraWidget.php @@ -23,24 +23,20 @@ namespace OCA\Jira\Dashboard; +use OCA\Jira\AppInfo\Application; use OCP\Dashboard\IWidget; use OCP\IL10N; -use OCP\Util; use OCP\IURLGenerator; -use OCA\Jira\AppInfo\Application; +use OCP\Util; class JiraWidget implements IWidget { - /** @var IL10N */ - private $l10n; - /** - * @var IURLGenerator - */ - private $url; + private IL10N $l10n; + private IURLGenerator $url; public function __construct(IL10N $l10n, - IURLGenerator $url) { + IURLGenerator $url) { $this->l10n = $l10n; $this->url = $url; } @@ -57,7 +53,7 @@ public function getId(): string { */ public function getTitle(): string { return $this->l10n->t('Jira notifications'); - } + } /** * @inheritDoc diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index b76b488..02433e8 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -12,27 +12,23 @@ namespace OCA\Jira\Notification; use InvalidArgumentException; +use OCA\Jira\AppInfo\Application; use OCP\IURLGenerator; use OCP\IUserManager; use OCP\L10N\IFactory; use OCP\Notification\IManager as INotificationManager; use OCP\Notification\INotification; use OCP\Notification\INotifier; -use OCA\Jira\AppInfo\Application; class Notifier implements INotifier { - /** @var IFactory */ - protected $factory; + protected IFactory $factory; - /** @var IUserManager */ - protected $userManager; + protected IUserManager $userManager; - /** @var INotificationManager */ - protected $notificationManager; + protected INotificationManager $notificationManager; - /** @var IURLGenerator */ - protected $url; + protected IURLGenerator $url; /** * @param IFactory $factory @@ -41,9 +37,9 @@ class Notifier implements INotifier { * @param IURLGenerator $urlGenerator */ public function __construct(IFactory $factory, - IUserManager $userManager, - INotificationManager $notificationManager, - IURLGenerator $urlGenerator) { + IUserManager $userManager, + INotificationManager $notificationManager, + IURLGenerator $urlGenerator) { $this->factory = $factory; $this->userManager = $userManager; $this->notificationManager = $notificationManager; @@ -85,25 +81,25 @@ public function prepare(INotification $notification, string $languageCode): INot $l = $this->factory->get('integration_jira', $languageCode); switch ($notification->getSubject()) { - case 'new_open_tickets': - $p = $notification->getSubjectParameters(); - $nbOpen = (int) ($p['nbOpen'] ?? 0); - $content = $l->n('You have %s open issue with recent activity in Jira.', 'You have %s open issues with recent activity in Jira.', $nbOpen, [$nbOpen]); + case 'new_open_tickets': + $p = $notification->getSubjectParameters(); + $nbOpen = (int) ($p['nbOpen'] ?? 0); + $content = $l->n('You have %s open issue with recent activity in Jira.', 'You have %s open issues with recent activity in Jira.', $nbOpen, [$nbOpen]); - //$theme = $this->config->getUserValue($userId, 'accessibility', 'theme', ''); - //$iconUrl = ($theme === 'dark') - // ? $this->url->imagePath(Application::APP_ID, 'app.svg') - // : $this->url->imagePath(Application::APP_ID, 'app-dark.svg'); + //$theme = $this->config->getUserValue($userId, 'accessibility', 'theme', ''); + //$iconUrl = ($theme === 'dark') + // ? $this->url->imagePath(Application::APP_ID, 'app.svg') + // : $this->url->imagePath(Application::APP_ID, 'app-dark.svg'); - $notification->setParsedSubject($content) - ->setLink($p['link'] ?? '') - ->setIcon($this->url->getAbsoluteURL($this->url->imagePath(Application::APP_ID, 'app-dark.svg'))); + $notification->setParsedSubject($content) + ->setLink($p['link'] ?? '') + ->setIcon($this->url->getAbsoluteURL($this->url->imagePath(Application::APP_ID, 'app-dark.svg'))); //->setIcon($this->url->getAbsoluteURL($iconUrl)); - return $notification; + return $notification; - default: - // Unknown subject => Unknown notification => throw - throw new InvalidArgumentException(); + default: + // Unknown subject => Unknown notification => throw + throw new InvalidArgumentException(); } } } diff --git a/lib/Search/JiraSearchProvider.php b/lib/Search/JiraSearchProvider.php index 888c6cf..064c85a 100644 --- a/lib/Search/JiraSearchProvider.php +++ b/lib/Search/JiraSearchProvider.php @@ -24,11 +24,11 @@ */ namespace OCA\Jira\Search; -use OCA\Jira\Service\JiraAPIService; use OCA\Jira\AppInfo\Application; +use OCA\Jira\Service\JiraAPIService; use OCP\App\IAppManager; -use OCP\IL10N; use OCP\IConfig; +use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUser; use OCP\Search\IProvider; @@ -37,22 +37,11 @@ class JiraSearchProvider implements IProvider { - /** @var IAppManager */ - private $appManager; - - /** @var IL10N */ - private $l10n; - - /** @var IURLGenerator */ - private $urlGenerator; - /** - * @var IConfig - */ - private $config; - /** - * @var JiraAPIService - */ - private $service; + private IAppManager $appManager; + private IL10N $l10n; + private IURLGenerator $urlGenerator; + private IConfig $config; + private JiraAPIService $service; /** * CospendSearchProvider constructor. @@ -64,10 +53,10 @@ class JiraSearchProvider implements IProvider { * @param JiraAPIService $service */ public function __construct(IAppManager $appManager, - IL10N $l10n, - IConfig $config, - IURLGenerator $urlGenerator, - JiraAPIService $service) { + IL10N $l10n, + IConfig $config, + IURLGenerator $urlGenerator, + JiraAPIService $service) { $this->appManager = $appManager; $this->l10n = $l10n; $this->config = $config; diff --git a/lib/Search/JiraSearchResultEntry.php b/lib/Search/JiraSearchResultEntry.php index d01817d..5632c9b 100644 --- a/lib/Search/JiraSearchResultEntry.php +++ b/lib/Search/JiraSearchResultEntry.php @@ -27,4 +27,4 @@ use OCP\Search\SearchResultEntry; class JiraSearchResultEntry extends SearchResultEntry { -} \ No newline at end of file +} diff --git a/lib/Service/JiraAPIService.php b/lib/Service/JiraAPIService.php index 7753e6b..b22f07a 100644 --- a/lib/Service/JiraAPIService.php +++ b/lib/Service/JiraAPIService.php @@ -12,63 +12,45 @@ namespace OCA\Jira\Service; use DateTime; -use Exception; -use OCP\IL10N; -use OCP\PreConditionNotMetException; -use Psr\Log\LoggerInterface; +use OCA\Jira\AppInfo\Application; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; use OCP\IConfig; -use OCP\IUserManager; +use OCP\IL10N; use OCP\IUser; -use OCP\Http\Client\IClientService; +use OCP\IUserManager; use OCP\Notification\IManager as INotificationManager; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ServerException; -use GuzzleHttp\Exception\ConnectException; +use OCP\PreConditionNotMetException; -use OCA\Jira\AppInfo\Application; -use Throwable; +use Psr\Log\LoggerInterface; class JiraAPIService { - /** - * @var IUserManager - */ - private $userManager; - /** - * @var LoggerInterface - */ - private $logger; - /** - * @var IL10N - */ - private $l10n; - /** - * @var IConfig - */ - private $config; - /** - * @var INotificationManager - */ - private $notificationManager; - /** - * @var \OCP\Http\Client\IClient - */ - private $client; + private IUserManager $userManager; + private LoggerInterface $logger; + private IL10N $l10n; + private IConfig $config; + private INotificationManager $notificationManager; + private NetworkService $networkService; + private IClient $client; /** * Service to make requests to Jira v3 (JSON) API */ - public function __construct (string $appName, - IUserManager $userManager, - LoggerInterface $logger, - IL10N $l10n, - IConfig $config, - INotificationManager $notificationManager, - IClientService $clientService) { + public function __construct( + IUserManager $userManager, + LoggerInterface $logger, + IL10N $l10n, + IConfig $config, + INotificationManager $notificationManager, + NetworkService $networkService, + IClientService $clientService + ) { $this->userManager = $userManager; $this->logger = $logger; $this->l10n = $l10n; $this->config = $config; $this->notificationManager = $notificationManager; + $this->networkService = $networkService; $this->client = $clientService->newClient(); } @@ -110,15 +92,15 @@ private function checkOpenTicketsForUser(string $userId): void { $status_key = $n['fields']['status']['statusCategory']['key'] ?? ''; $assigneeKey = $n['fields']['assignee']['key'] ?? ''; $assigneeId = $n['fields']['assignee']['accountId'] ?? ''; - $embededAccountId = $n['my_account_id'] ?? ''; - // from what i saw, key is used in self hosted and accountId in cloud version - // embededAccountId can be usefull when accessing multiple cloud resources, it being specific to the resource - if ( ( - ($myAccountKey !== '' && $assigneeKey === $myAccountKey) - || ($myAccountId !== '' && $myAccountId === $assigneeId) - || ($embededAccountId !== '' && $embededAccountId === $assigneeId) - ) - && $status_key !== 'done') { + $embeddedAccountId = $n['my_account_id'] ?? ''; + // from what I saw, key is used in self-hosted and accountId in cloud version + // embeddedAccountId can be usefull when accessing multiple cloud resources, it being specific to the resource + if (( + ($myAccountKey !== '' && $assigneeKey === $myAccountKey) + || ($myAccountId !== '' && $myAccountId === $assigneeId) + || ($embeddedAccountId !== '' && $embeddedAccountId === $assigneeId) + ) + && $status_key !== 'done') { $nbOpen++; } } @@ -173,7 +155,7 @@ public function getNotifications(string $userId, ?string $since = null, ?int $li $endPoint = 'rest/api/2/search'; $basicAuthHeader = $this->config->getUserValue($userId, Application::APP_ID, 'basic_auth_header'); - // self hosted Jira + // self-hosted Jira if ($basicAuthHeader !== '') { $jiraUrl = $this->config->getUserValue($userId, Application::APP_ID, 'url'); @@ -185,7 +167,7 @@ public function getNotifications(string $userId, ?string $since = null, ?int $li ]; } - $issuesResult = $this->basicRequest($jiraUrl, $basicAuthHeader, $endPoint); + $issuesResult = $this->networkService->basicRequest($jiraUrl, $basicAuthHeader, $endPoint); if (isset($issuesResult['error'])) { return $issuesResult; } @@ -201,7 +183,7 @@ public function getNotifications(string $userId, ?string $since = null, ?int $li foreach ($resources as $resource) { $cloudId = $resource['id']; $jiraUrl = $resource['url']; - $issuesResult = $this->oauthRequest($userId, 'ex/jira/' . $cloudId . '/' . $endPoint); + $issuesResult = $this->networkService->oauthRequest($userId, 'ex/jira/' . $cloudId . '/' . $endPoint); if (!isset($issuesResult['error']) && isset($issuesResult['issues'])) { foreach ($issuesResult['issues'] as $k => $issue) { $issuesResult['issues'][$k]['jiraUrl'] = $jiraUrl; @@ -217,7 +199,7 @@ public function getNotifications(string $userId, ?string $since = null, ?int $li if (!is_null($since)) { $sinceDate = new Datetime($since); $sinceTimestamp = $sinceDate->getTimestamp(); - $myIssues = array_filter($myIssues, function($elem) use ($sinceTimestamp) { + $myIssues = array_filter($myIssues, function ($elem) use ($sinceTimestamp) { $date = new Datetime($elem['fields']['updated']); $elemTs = $date->getTimestamp(); return $elemTs > $sinceTimestamp; @@ -225,7 +207,7 @@ public function getNotifications(string $userId, ?string $since = null, ?int $li } // sort by updated - usort($myIssues, function($a, $b) { + usort($myIssues, function ($a, $b) { $a = new Datetime($a['fields']['updated']); $ta = $a->getTimestamp(); $b = new Datetime($b['fields']['updated']); @@ -249,7 +231,7 @@ public function search(string $userId, string $query, int $offset = 0, int $limi $endPoint = 'rest/api/2/search'; // jira cloud does not support "*TERM*" but just "TERM*" - // self hosted jira is fine with "*TERM*"... + // self-hosted jira is fine with "*TERM*"... // other problem, '*' does not work with japanese chars (for example) $words = preg_split('/\s+/', $query); $searchString = ''; @@ -270,7 +252,7 @@ public function search(string $userId, string $query, int $offset = 0, int $limi ]; $basicAuthHeader = $this->config->getUserValue($userId, Application::APP_ID, 'basic_auth_header'); - // self hosted Jira + // self-hosted Jira if ($basicAuthHeader !== '') { $jiraUrl = $this->config->getUserValue($userId, Application::APP_ID, 'url'); @@ -282,7 +264,7 @@ public function search(string $userId, string $query, int $offset = 0, int $limi ]; } - $issuesResult = $this->basicRequest($jiraUrl, $basicAuthHeader, $endPoint, $params); + $issuesResult = $this->networkService->basicRequest($jiraUrl, $basicAuthHeader, $endPoint, $params); if (isset($issuesResult['error'])) { return $issuesResult; } @@ -293,12 +275,11 @@ public function search(string $userId, string $query, int $offset = 0, int $limi } else { // Jira cloud $resources = $this->getJiraResources($userId); - $myIssues = []; foreach ($resources as $resource) { $cloudId = $resource['id']; $jiraUrl = $resource['url']; - $issuesResult = $this->oauthRequest($userId, 'ex/jira/' . $cloudId . '/' . $endPoint, $params); + $issuesResult = $this->networkService->oauthRequest($userId, 'ex/jira/' . $cloudId . '/' . $endPoint, $params); if (!isset($issuesResult['error']) && isset($issuesResult['issues'])) { foreach ($issuesResult['issues'] as $k => $issue) { $issuesResult['issues'][$k]['jiraUrl'] = $jiraUrl; @@ -341,12 +322,12 @@ public function getAccountInfo(string $userId, string $accountId, string $accoun ]; } - return $this->basicRequest($jiraUrl, $basicAuthHeader, $endPoint, $params); + return $this->networkService->basicRequest($jiraUrl, $basicAuthHeader, $endPoint, $params); } else { $accessToken = $this->config->getUserValue($userId, Application::APP_ID, 'token'); $refreshToken = $this->config->getUserValue($userId, Application::APP_ID, 'refresh_token'); - $clientID = $this->config->getAppValue(Application::APP_ID, 'client_id'); - $clientSecret = $this->config->getAppValue(Application::APP_ID, 'client_secret'); + // $clientID = $this->config->getAppValue(Application::APP_ID, 'client_id'); + // $clientSecret = $this->config->getAppValue(Application::APP_ID, 'client_secret'); if ($accessToken === '' || $refreshToken === '') { return ['error' => 'no credentials']; } @@ -355,8 +336,8 @@ public function getAccountInfo(string $userId, string $accountId, string $accoun foreach ($resources as $resource) { $cloudId = $resource['id']; -// $jiraUrl = $resource['url']; - $result = $this->oauthRequest($userId, 'ex/jira/' . $cloudId . '/' . $endPoint, $params); + // $jiraUrl = $resource['url']; + $result = $this->networkService->oauthRequest($userId, 'ex/jira/' . $cloudId . '/' . $endPoint, $params); if (!isset($result['error'])) { return $result; } @@ -402,255 +383,4 @@ public function getJiraAvatar(string $userId, string $accountId, string $account return $this->client->get($imageUrl, $options)->getBody(); } - - /** - * @param string $url - * @param string $authHeader - * @param string $endPoint - * @param array $params - * @param string $method - * @return array - */ - public function basicRequest(string $url, string $authHeader, - string $endPoint, array $params = [], string $method = 'GET'): array { - try { - $url = $url . '/' . $endPoint; - $options = [ - 'headers' => [ - 'Authorization' => 'Basic ' . $authHeader, - 'User-Agent' => 'Nextcloud Jira integration', - ] - ]; - if ($method === 'POST') { - $options['headers']['Content-Type'] = 'application/json'; - } - - if (count($params) > 0) { - if ($method === 'GET') { - // manage array parameters - $paramsContent = ''; - foreach ($params as $key => $value) { - if (is_array($value)) { - foreach ($value as $oneArrayValue) { - $paramsContent .= $key . '[]=' . urlencode($oneArrayValue) . '&'; - } - unset($params[$key]); - } - } - $paramsContent .= http_build_query($params); - $url .= '?' . $paramsContent; - } else { - $options['body'] = json_encode($params, JSON_UNESCAPED_UNICODE); - } - } - - if ($method === 'GET') { - $response = $this->client->get($url, $options); - } else if ($method === 'POST') { - $response = $this->client->post($url, $options); - } else if ($method === 'PUT') { - $response = $this->client->put($url, $options); - } else if ($method === 'DELETE') { - $response = $this->client->delete($url, $options); - } else { - return ['error' => $this->l10n->t('Bad HTTP method')]; - } - $body = $response->getBody(); - $respCode = $response->getStatusCode(); -// $headers = $response->getHeaders(); - - if ($respCode >= 400) { - return ['error' => $this->l10n->t('Bad credentials')]; - } else { - return json_decode($body, true); - } - } catch (ServerException | ClientException $e) { - $this->logger->warning('Jira API error : '.$e->getMessage(), ['app' => Application::APP_ID]); - return ['error' => $e->getMessage()]; - } catch (ConnectException $e) { - $this->logger->warning('Jira API connection error : '.$e->getMessage(), ['app' => Application::APP_ID]); - return ['error' => $e->getMessage()]; - } - } - - /** - * @param string $userId - * @param string $endPoint - * @param array $params - * @param string $method - * @return array - * @throws PreConditionNotMetException - */ - public function oauthRequest(string $userId, string $endPoint, array $params = [], string $method = 'GET'): array { - $this->checkTokenExpiration($userId); - $accessToken = $this->config->getUserValue($userId, Application::APP_ID, 'token'); - try { - $url = Application::JIRA_API_URL . '/' . $endPoint; - $options = [ - 'headers' => [ - 'Authorization' => 'Bearer ' . $accessToken, - 'User-Agent' => 'Nextcloud Jira integration', - ] - ]; - - if (count($params) > 0) { - if ($method === 'GET') { - // manage array parameters - $paramsContent = ''; - foreach ($params as $key => $value) { - if (is_array($value)) { - foreach ($value as $oneArrayValue) { - $paramsContent .= $key . '[]=' . urlencode($oneArrayValue) . '&'; - } - unset($params[$key]); - } - } - $paramsContent .= http_build_query($params); - $url .= '?' . $paramsContent; - } else { - $options['body'] = $params; - } - } - - if ($method === 'GET') { - $response = $this->client->get($url, $options); - } else if ($method === 'POST') { - $response = $this->client->post($url, $options); - } else if ($method === 'PUT') { - $response = $this->client->put($url, $options); - } else if ($method === 'DELETE') { - $response = $this->client->delete($url, $options); - } else { - return ['error' => $this->l10n->t('Bad HTTP method')]; - } - $body = $response->getBody(); - $respCode = $response->getStatusCode(); - $headers = $response->getHeaders(); - - if ($respCode >= 400) { - return ['error' => $this->l10n->t('Bad credentials')]; - } else { - $decodedResult = json_decode($body, true); - if (isset($headers['x-aaccountid']) && is_array($headers['x-aaccountid']) && count($headers['x-aaccountid']) > 0) { - $decodedResult['my_account_id'] = $headers['x-aaccountid'][0]; - } - return $decodedResult; - } - } catch (ServerException | ClientException $e) { - $this->logger->warning('Jira API error : '.$e->getMessage(), ['app' => Application::APP_ID]); - return ['error' => $e->getMessage()]; - } catch (ConnectException $e) { - $this->logger->warning('Jira API connection error : '.$e->getMessage(), ['app' => Application::APP_ID]); - return ['error' => $e->getMessage()]; - } - } - - /** - * @param string $userId - * @return void - * @throws PreConditionNotMetException - */ - private function checkTokenExpiration(string $userId): void { - $refreshToken = $this->config->getUserValue($userId, Application::APP_ID, 'refresh_token'); - $expireAt = $this->config->getUserValue($userId, Application::APP_ID, 'token_expires_at'); - if ($refreshToken !== '' && $expireAt !== '') { - $nowTs = (new Datetime())->getTimestamp(); - $expireAt = (int) $expireAt; - // if token expires in less than a minute or is already expired - if ($nowTs > $expireAt - 60) { - $this->refreshToken($userId); - } - } - } - - /** - * @param string $userId - * @return bool - * @throws PreConditionNotMetException - */ - private function refreshToken(string $userId): bool { - $clientID = $this->config->getAppValue(Application::APP_ID, 'client_id'); - $clientSecret = $this->config->getAppValue(Application::APP_ID, 'client_secret'); - $refreshToken = $this->config->getUserValue($userId, Application::APP_ID, 'refresh_token'); - if (!$refreshToken) { - $this->logger->error('No Jira refresh token found', ['app' => Application::APP_ID]); - return false; - } - - $result = $this->requestOAuthAccessToken([ - 'client_id' => $clientID, - 'client_secret' => $clientSecret, - 'grant_type' => 'refresh_token', - 'refresh_token' => $refreshToken, - ], 'POST'); - if (isset($result['access_token'], $result['refresh_token'])) { - $accessToken = $result['access_token']; - $refreshToken = $result['refresh_token']; - $this->config->setUserValue($userId, Application::APP_ID, 'token', $accessToken); - $this->config->setUserValue($userId, Application::APP_ID, 'refresh_token', $refreshToken); - if (isset($result['expires_in'])) { - $nowTs = (new Datetime())->getTimestamp(); - $expiresAt = $nowTs + (int) $result['expires_in']; - $this->config->setUserValue($userId, Application::APP_ID, 'token_expires_at', $expiresAt); - } - return true; - } else { - // impossible to refresh the token - $this->logger->error( - 'Token is not valid anymore. Impossible to refresh it. ' - . $result['error'] . ' ' - . $result['error_description'] ?? '[no error description]', - ['app' => Application::APP_ID] - ); - return false; - } - } - - /** - * @param array $params - * @param string $method - * @return array - */ - public function requestOAuthAccessToken(array $params = [], string $method = 'GET'): array { - try { - $url = Application::JIRA_AUTH_URL . '/oauth/token'; - $options = [ - 'headers' => [ - 'User-Agent' => 'Nextcloud Jira integration', - ] - ]; - - if (count($params) > 0) { - if ($method === 'GET') { - $paramsContent = http_build_query($params); - $url .= '?' . $paramsContent; - } else { - $options['body'] = $params; - } - } - - if ($method === 'GET') { - $response = $this->client->get($url, $options); - } else if ($method === 'POST') { - $response = $this->client->post($url, $options); - } else if ($method === 'PUT') { - $response = $this->client->put($url, $options); - } else if ($method === 'DELETE') { - $response = $this->client->delete($url, $options); - } else { - return ['error' => $this->l10n->t('Bad HTTP method')]; - } - $body = $response->getBody(); - $respCode = $response->getStatusCode(); - - if ($respCode >= 400) { - return ['error' => $this->l10n->t('OAuth access token refused')]; - } else { - return json_decode($body, true); - } - } catch (Exception | Throwable $e) { - $this->logger->warning('Jira OAuth error : '.$e->getMessage(), ['app' => Application::APP_ID]); - return ['error' => $e->getMessage()]; - } - } } diff --git a/lib/Service/NetworkService.php b/lib/Service/NetworkService.php new file mode 100644 index 0000000..e54ea1b --- /dev/null +++ b/lib/Service/NetworkService.php @@ -0,0 +1,263 @@ +client = $clientService->newClient(); + } + + /** + * @param string $authHeader + * @param string $endPoint + * @param array $params + * @param string $method + * @param string $contentType + * @param bool $jsonResponse + * @return array|mixed|resource|string|string[]|IResponse + * @throws PreConditionNotMetException + */ + public function request_integration(string $authHeader, string $endPoint, array $params = [], string $method = 'GET', + string $contentType = '', bool $jsonResponse = true, bool $returnRaw = false) { + return $this->request( + $authHeader, + Application::INTEGRATION_API_URL . $endPoint, + $params, + $method, + $contentType, + $jsonResponse, + $returnRaw); + } + + /** + * @param string $authHeader + * @param string $url + * @param array $params + * @param string $method + * @param string $contentType + * @param bool $jsonResponse + * @return array|mixed|resource|string|string[]|IResponse + * @throws PreConditionNotMetException + */ + public function request(string $authHeader, string $url, array $params = [], string $method = 'GET', + string $contentType = '', bool $jsonResponse = true, bool $returnRaw = false) { + try { + $options = [ + 'headers' => [ + 'User-Agent' => Application::INTEGRATION_USER_AGENT, + ], + ]; + if ($contentType !== '') { + $options['headers']['Content-Type'] = $contentType; + } + if ($authHeader !== '') { + $options['headers']['Authorization'] = $authHeader; + } + + if (count($params) > 0) { + if ($method === 'GET') { + // manage array parameters + $paramsContent = ''; + foreach ($params as $key => $value) { + if (is_array($value)) { + foreach ($value as $oneArrayValue) { + $paramsContent .= $key . '[]=' . urlencode($oneArrayValue) . '&'; + } + unset($params[$key]); + } + } + $paramsContent .= http_build_query($params); + + $url .= '?' . $paramsContent; + } else { + $options['body'] = $params; + } + } + + if ($method === 'GET') { + $response = $this->client->get($url, $options); + } elseif ($method === 'POST') { + $response = $this->client->post($url, $options); + } elseif ($method === 'PUT') { + $response = $this->client->put($url, $options); + } elseif ($method === 'DELETE') { + $response = $this->client->delete($url, $options); + } else { + return ['error' => $this->l10n->t('Bad HTTP method')]; + } + if ($returnRaw) { + return $response; + } + $body = $response->getBody(); + $respCode = $response->getStatusCode(); + + if ($respCode >= 400) { + return ['error' => $this->l10n->t('Bad credentials')]; + } + if ($jsonResponse) { + return json_decode($body, true, flags: JSON_UNESCAPED_UNICODE); + } + return $body; + } catch (ServerException | ClientException $e) { + $body = $e->getResponse()->getBody(); + $this->logger->warning('Network API error : ' . $body, ['app' => Application::APP_ID]); + return ['error' => $e->getMessage()]; + } catch (Exception | Throwable $e) { + $this->logger->warning('Network API error', ['exception' => $e, 'app' => Application::APP_ID]); + return ['error' => $e->getMessage()]; + } + } + + /** + * @param string $userId + * @param string $endPoint + * @param array $params + * @param string $method + * @return array + * @throws PreConditionNotMetException + */ + public function oauthRequest(string $userId, string $endPoint, array $params = [], string $method = 'GET'): array { + $this->checkTokenExpiration($userId); + $accessToken = $this->config->getUserValue($userId, Application::APP_ID, 'token'); + + $response = $this->request_integration( + 'Bearer ' . $accessToken, + $endPoint, + $params, + $method, + returnRaw: true, + ); + $body = $response->getBody(); + $respCode = $response->getStatusCode(); + $headers = $response->getHeaders(); + + if ($respCode >= 400) { + return ['error' => $this->l10n->t('Bad credentials')]; + } + $decodedResult = json_decode($body, true); + if (isset($headers['x-aaccountid']) && is_array($headers['x-aaccountid']) && count($headers['x-aaccountid']) > 0) { + $decodedResult['my_account_id'] = $headers['x-aaccountid'][0]; + } + return $decodedResult; + } + + /** + * @param string $userId + * @return void + * @throws PreConditionNotMetException + */ + private function checkTokenExpiration(string $userId): void { + $refreshToken = $this->config->getUserValue($userId, Application::APP_ID, 'refresh_token'); + $expireAt = $this->config->getUserValue($userId, Application::APP_ID, 'token_expires_at'); + if ($refreshToken !== '' && $expireAt !== '') { + $nowTs = (new Datetime())->getTimestamp(); + $expireAt = (int) $expireAt; + // if token expires in less than a minute or is already expired + if ($nowTs > $expireAt - 60) { + $this->refreshToken($userId); + } + } + } + + /** + * @param string $userId + * @return bool + * @throws PreConditionNotMetException + */ + private function refreshToken(string $userId): bool { + $clientID = $this->config->getAppValue(Application::APP_ID, 'client_id'); + $clientSecret = $this->config->getAppValue(Application::APP_ID, 'client_secret'); + $refreshToken = $this->config->getUserValue($userId, Application::APP_ID, 'refresh_token'); + if (!$refreshToken) { + $this->logger->error('No Jira refresh token found', ['app' => Application::APP_ID]); + return false; + } + + $result = $this->requestOAuthAccessToken([ + 'client_id' => $clientID, + 'client_secret' => $clientSecret, + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + ]); + if (isset($result['access_token'], $result['refresh_token'])) { + $accessToken = $result['access_token']; + $refreshToken = $result['refresh_token']; + $this->config->setUserValue($userId, Application::APP_ID, 'token', $accessToken); + $this->config->setUserValue($userId, Application::APP_ID, 'refresh_token', $refreshToken); + if (isset($result['expires_in'])) { + $nowTs = (new Datetime())->getTimestamp(); + $expiresAt = $nowTs + (int) $result['expires_in']; + $this->config->setUserValue($userId, Application::APP_ID, 'token_expires_at', $expiresAt); + } + return true; + } else { + // impossible to refresh the token + $this->logger->error( + 'Token is not valid anymore. Impossible to refresh it. ' + . $result['error'] . ' ' + . $result['error_description'] ?? '[no error description]', + ['app' => Application::APP_ID] + ); + return false; + } + } + + /** + * @param array $params + * @return array + */ + public function requestOAuthAccessToken(array $params = []): array { + return $this->request( + '', + Application::JIRA_AUTH_URL . 'oauth/token', + $params, + method: 'POST', + ); + } + + /** + * @param string $url + * @param string $authHeader + * @param string $endPoint + * @param array $params + * @param string $method + * @return array + */ + public function basicRequest(string $url, string $authHeader, + string $endPoint, array $params = [], string $method = 'GET'): array { + return $this->request( + 'Basic ' . $authHeader, + $url . '/' . $endPoint, + $params, + $method, + contentType: $method === 'POST' ? 'application/json' : '', + ); + } +} diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index e9243b4..30f4f99 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -1,26 +1,21 @@ config = $config; $this->initialStateService = $initialStateService; } diff --git a/lib/Settings/AdminSection.php b/lib/Settings/AdminSection.php index 1f628c3..68f6598 100644 --- a/lib/Settings/AdminSection.php +++ b/lib/Settings/AdminSection.php @@ -1,20 +1,18 @@ l = $l; $this->urlGenerator = $urlGenerator; } diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php index ff4fa57..297b2b6 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -1,31 +1,23 @@ config = $config; $this->initialStateService = $initialStateService; $this->userId = $userId; diff --git a/lib/Settings/PersonalSection.php b/lib/Settings/PersonalSection.php index e3bda58..76cb1a9 100644 --- a/lib/Settings/PersonalSection.php +++ b/lib/Settings/PersonalSection.php @@ -1,20 +1,18 @@ l = $l; $this->urlGenerator = $urlGenerator; } diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..2aa34bf --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,9 @@ + + + + . + + + + ../appinfo + ../lib + + + diff --git a/tests/unit/Service/JiraAPIServiceTest.php b/tests/unit/Service/JiraAPIServiceTest.php new file mode 100644 index 0000000..2363611 --- /dev/null +++ b/tests/unit/Service/JiraAPIServiceTest.php @@ -0,0 +1,88 @@ +assertEquals('integration_jira', $app::APP_ID); + } + + public function setUp(): void { + parent::setUp(); + + $this->setupDummies(); + } + + private function setupDummies(): void { + $this->userManager = $this->createMock(IUserManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->l10n = $this->createMock(L10N::class); + $this->config = $this->createMock(IConfig::class); + $this->notificationManager = $this->createMock(INotificationManager::class); + $this->networkService = $this->createMock(NetworkService::class); + $this->clientService = $this->createMock(ClientService::class); + + $this->apiService = new JiraAPIService( + $this->userManager, + $this->logger, + $this->l10n, + $this->config, + $this->notificationManager, + $this->networkService, + $this->clientService + ); + } + + public function testSearch() { + $this->networkService->method('oauthRequest')->willReturnCallback(function ( + string $userId, string $endPoint, array $params = [], string $method = 'GET' + ) { + if (str_contains($endPoint, 'rest/api/2/search')) { + return json_decode(file_get_contents('tests/data/search.json'), true); + } + return 'dummy'; + }); + + $this->config->method('getUserValue')->willReturnCallback(function ( + $userId, $appName, $key, $default = '' + ) { + if ($key === 'url') { + return 'jira_url'; + } + if ($key == 'resources') { + return "[{\"id\":\"7dc26f20-c097-4ca6-8d41-d8617d9b258e\",\"url\":\"https:\\\/\\\/ncintegration.atlassian.net\",\"name\":\"ncintegration\",\"scopes\":[\"manage:jira-project\",\"manage:jira-configuration\",\"manage:jira-data-provider\",\"read:jira-work\",\"write:jira-work\",\"read:jira-user\"],\"avatarUrl\":\"https:\\\/\\\/site-admin-avatar-cdn.prod.public.atl-paas.net\\\/avatars\\\/240\\\/koala.png\"}]"; + } + return ''; + }); + + $expected = $this->apiService->search('admin', 'zop', 0, 5); + $this->assertEquals(1, sizeof($expected)); + $this->assertEquals('FIRST-1', $expected[0]['key']); + } +}