diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..63ede231b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +/.gitattributes export-ignore +/.github export-ignore +/.gitignore export-ignore +/.travis.yml export-ignore +/CODE_OF_CONDUCT.md export-ignore +/docs export-ignore +/examples export-ignore +/phpunit.xml.dist export-ignore +/phpcs.xml.dist export-ignore +/style export-ignore +/tests export-ignore +/UPGRADING.md export-ignore +/phpstan.neon.dist export-ignore diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..c20bc3167 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,7 @@ +# Code owners file. +# This file controls who is tagged for review for any given pull request. +# +# For syntax help see: +# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners#codeowners-syntax + +* @googleapis/yoshi-php diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 95% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md index e852a93ac..d8deaf3a0 100644 --- a/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -7,7 +7,7 @@ We'd love to accept your code patches! However, before we can take them, we have Please fill out either the individual or corporate Contributor License Agreement (CLA). * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA](http://code.google.com/legal/individual-cla-v1.0.html). - * If you work for a company that wants to allow you to contribute your work to this client library, then you'll need to sign a[corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html). + * If you work for a company that wants to allow you to contribute your work to this client library, then you'll need to sign a [corporate CLA](http://code.google.com/legal/corporate-cla-v1.0.html). Follow either of the two links above to access the appropriate CLA and instructions for how to sign and return it. Once we receive it, we'll add you to the official list of contributors and be able to accept your patches. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..e70487ba1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,36 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +Thanks for stopping by to let us know something could be better! + +**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. + +Please run down the following list and make sure you've tried the usual "quick fixes": + + - Search the issues already opened: https://github.com/googleapis/google-api-php-client/issues + - Search StackOverflow: http://stackoverflow.com/questions/tagged/google-cloud-platform+php + +If you are still having issues, please be sure to include as much information as possible: + +#### Environment details + + - OS: + - PHP version: + - Package name and version: + +#### Steps to reproduce + + 1. ... + +#### Code example + +```php +# example +``` + +Making sure to follow these steps will guarantee the quickest resolution possible. + +Thanks! diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..20d075c25 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Suggest an idea for this library + +--- + +Thanks for stopping by to let us know something could be better! + +**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. + + **Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + + **Describe the solution you'd like** +A clear and concise description of what you want to happen. + + **Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + + **Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/support_request.md b/.github/ISSUE_TEMPLATE/support_request.md new file mode 100644 index 000000000..995869032 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/support_request.md @@ -0,0 +1,7 @@ +--- +name: Support request +about: If you have a support contract with Google, please create an issue in the Google Cloud Support console. + +--- + +**PLEASE READ**: If you have a support contract with Google, please create an issue in the [support console](https://cloud.google.com/support/) instead of filing on GitHub. This will ensure a timely response. diff --git a/.github/release-please.yml b/.github/release-please.yml new file mode 100644 index 000000000..0a6e0cc27 --- /dev/null +++ b/.github/release-please.yml @@ -0,0 +1,3 @@ +releaseType: simple +handleGHRelease: true +primaryBranch: main diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml new file mode 100644 index 000000000..ed17d0705 --- /dev/null +++ b/.github/release-trigger.yml @@ -0,0 +1,2 @@ +enabled: true +multiScmName: google-api-php-client diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml new file mode 100644 index 000000000..a4444366a --- /dev/null +++ b/.github/sync-repo-settings.yaml @@ -0,0 +1,24 @@ +rebaseMergeAllowed: true +squashMergeAllowed: true +mergeCommitAllowed: false +branchProtectionRules: +- pattern: master + isAdminEnforced: true + requiredStatusCheckContexts: + - 'PHP 8.0 Unit Test' + - 'PHP 8.0 --prefer-lowest Unit Test' + - 'PHP 8.1 Unit Test' + - 'PHP 8.2 Unit Test' + - 'PHP 8.3 Unit Test' + - 'PHP 8.4' + - 'PHP 8.4 --prefer-lowest Unit Test' + - 'PHP Style Check' + - 'cla/google' + requiredApprovingReviewCount: 1 + requiresCodeOwnerReviews: true + requiresStrictStatusChecks: true +permissionRules: + - team: yoshi-php-admins + permission: admin + - team: yoshi-php + permission: push diff --git a/.github/workflows/asset-release.yml b/.github/workflows/asset-release.yml new file mode 100644 index 000000000..1f553fbb9 --- /dev/null +++ b/.github/workflows/asset-release.yml @@ -0,0 +1,64 @@ +name: Add Release Assets + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + asset: + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ ubuntu-latest ] + php: [ "8.0", "8.3" ] + + name: Upload Release Assets + steps: + - id: getTag + name: Get Tag + run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} + + - name: Get release + id: get_release + uses: bruceadams/get-release@v1.2.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Install Dependencies + uses: nick-invision/retry@v1 + with: + timeout_minutes: 10 + max_attempts: 3 + command: composer remove --no-update --dev cache/filesystem-adapter && composer install --no-dev --prefer-dist + + - name: Create Archive + run: | + zip -r ${fileName} . && + zip -d ${fileName} ".git*" || true && + zip -d ${fileName} "tests*" || true && + zip -d ${fileName} "docs*" || true && + zip -d ${fileName} "phpcs.xml.dist" || true && + zip -d ${fileName} "phpunit.xml.dist" || true && + zip -d ${fileName} "tests*" || true && + zip -d ${fileName} "examples*" || true + env: + fileName: google-api-php-client-${{ steps.tagName.outputs.tag }}-PHP${{ matrix.php }}.zip + + - name: Upload Release Archive + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + with: + upload_url: ${{ steps.get_release.outputs.upload_url }} + asset_path: ./google-api-php-client-${{ steps.tagName.outputs.tag }}-PHP${{ matrix.php }}.zip + asset_name: google-api-php-client-${{ steps.tagName.outputs.tag }}-PHP${{ matrix.php }}.zip + asset_content_type: application/zip diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..5d09dabb0 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,26 @@ +name: Generate Documentation +on: + push: + tags: + - "*" + workflow_dispatch: + inputs: + tag: + description: 'Tag to generate documentation for' + required: false + pull_request: + +permissions: + contents: write + +jobs: + docs: + name: "Generate and Deploy Documentation" + uses: GoogleCloudPlatform/php-tools/.github/workflows/doctum.yml@main + with: + title: "Google Cloud PHP Client" + default_version: ${{ inputs.tag || github.head_ref || github.ref_name }} + exclude_file: aliases.php + tag_pattern: "v2.*" + dry_run: ${{ github.event_name == 'pull_request' }} + diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..9f86e493a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,68 @@ +name: Test Suite +on: + push: + branches: + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [ "8.0", "8.1", "8.2", "8.3", "8.4" ] + composer-flags: [""] + include: + - php: "8.0" + composer-flags: "--prefer-lowest " + - php: "8.4" + composer-flags: "--prefer-lowest " + name: PHP ${{ matrix.php }} ${{ matrix.composer-flags }}Unit Test + steps: + - uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + - name: Install Dependencies + uses: nick-invision/retry@v2 + with: + timeout_minutes: 10 + max_attempts: 3 + command: composer update ${{ matrix.composer-flags }} + - name: Run Script + run: vendor/bin/phpunit + + style: + runs-on: ubuntu-latest + name: PHP Style Check + steps: + - uses: actions/checkout@v2 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + - name: Install Dependencies + uses: nick-invision/retry@v2 + with: + timeout_minutes: 10 + max_attempts: 3 + command: composer install + - name: Run Script + run: vendor/bin/phpcs src tests examples --standard=phpcs.xml.dist -nps + staticanalysis: + runs-on: ubuntu-latest + name: PHPStan Static Analysis + steps: + - uses: actions/checkout@v2 + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + - name: Run Script + run: | + composer install + composer global require phpstan/phpstan + ~/.composer/vendor/bin/phpstan analyse + diff --git a/.gitignore b/.gitignore index 0a70c65d2..fc8518be0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,10 @@ .DS_Store phpunit.xml +phpcs.xml composer.lock vendor examples/testfile-small.txt examples/testfile.txt tests/.apiKey +.phpunit.result.cache +.idea/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 11b8eeddd..000000000 --- a/.travis.yml +++ /dev/null @@ -1,47 +0,0 @@ -language: php - -services: - - memcached - -env: - global: - - MEMCACHE_HOST=127.0.0.1 - - MEMCACHE_PORT=11211 - matrix: - - GUZZLE_VERSION=~5.2 - - GUZZLE_VERSION=~6.0 - -sudo: false - -cache: - directories: - - $HOME/.composer/cache - -php: - - 5.4 - - 5.5 - - 5.6 - - 7.0 - - hhvm - -# Guzzle 6.0 is not compatible with PHP 5.4 -matrix: - exclude: - - php: 5.4 - env: GUZZLE_VERSION=~6.0 - -before_install: - - composer self-update - -install: - - composer install - - composer require guzzlehttp/guzzle:$GUZZLE_VERSION - -before_script: - - phpenv version-name | grep ^5.[34] && echo "extension=apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true - - phpenv version-name | grep ^5.[34] && echo "apc.enable_cli=1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini ; true - -script: - - vendor/bin/phpunit - - if [[ "$TRAVIS_PHP_VERSION" == "5.4" ]]; then vendor/bin/phpcs src --standard=style/ruleset.xml -np; fi - diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..9fd015477 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,123 @@ +# Changelog + +## [2.18.3](https://github.com/googleapis/google-api-php-client/compare/v2.18.2...v2.18.3) (2025-04-08) + + +### Bug Fixes + +* Convert Finder lazy iterator to array before deletion ([#2663](https://github.com/googleapis/google-api-php-client/issues/2663)) ([c699405](https://github.com/googleapis/google-api-php-client/commit/c6994051af1568359c97d267d9ef34ccbda31387)) + +## [2.18.2](https://github.com/googleapis/google-api-php-client/compare/v2.18.1...v2.18.2) (2024-12-16) + + +### Bug Fixes + +* Correct type for jwt constructor arg ([#2648](https://github.com/googleapis/google-api-php-client/issues/2648)) ([31a9861](https://github.com/googleapis/google-api-php-client/commit/31a9861af02a8e9070b395f05caed7ffce0ef8be)) + +## [2.18.1](https://github.com/googleapis/google-api-php-client/compare/v2.18.0...v2.18.1) (2024-11-24) + + +### Bug Fixes + +* Implicitly marking parameter as nullable is deprecated ([#2638](https://github.com/googleapis/google-api-php-client/issues/2638)) ([de57db2](https://github.com/googleapis/google-api-php-client/commit/de57db2fdc0d56de1abbf778b28b77c3347eb3fd)) + +## [2.18.0](https://github.com/googleapis/google-api-php-client/compare/v2.17.0...v2.18.0) (2024-10-16) + + +### Features + +* **docs:** Use doctum shared workflow for reference docs ([#2618](https://github.com/googleapis/google-api-php-client/issues/2618)) ([242e2cb](https://github.com/googleapis/google-api-php-client/commit/242e2cb09ad5b25b047a862b4d521037e74cae29)) + + +### Bug Fixes + +* Explicit token caching issue ([#2358](https://github.com/googleapis/google-api-php-client/issues/2358)) ([dc13e5e](https://github.com/googleapis/google-api-php-client/commit/dc13e5e3f517148d3c66d151a5ab133b7840d8fb)) + +## [2.17.0](https://github.com/googleapis/google-api-php-client/compare/v2.16.0...v2.17.0) (2024-07-10) + + +### Features + +* Add logger to client constructor config ([#2606](https://github.com/googleapis/google-api-php-client/issues/2606)) ([1f47133](https://github.com/googleapis/google-api-php-client/commit/1f4713329d71111a317cda8ef8603fa1bdc88858)) +* Add the protected apiVersion property ([#2588](https://github.com/googleapis/google-api-php-client/issues/2588)) ([7e79f3d](https://github.com/googleapis/google-api-php-client/commit/7e79f3d7be4811f760e19cc4a2c558e04196ec1d)) + +## [2.16.0](https://github.com/googleapis/google-api-php-client/compare/v2.15.4...v2.16.0) (2024-04-24) + + +### Features + +* Add universe domain support ([#2563](https://github.com/googleapis/google-api-php-client/issues/2563)) ([35895de](https://github.com/googleapis/google-api-php-client/commit/35895ded90b507074b3430a94a5790ddd01f39f0)) + +## [2.15.4](https://github.com/googleapis/google-api-php-client/compare/v2.15.3...v2.15.4) (2024-03-06) + + +### Bug Fixes + +* Updates phpseclib because of a security issue ([#2574](https://github.com/googleapis/google-api-php-client/issues/2574)) ([633d41f](https://github.com/googleapis/google-api-php-client/commit/633d41f1b65fdb71a83bf747f7a3ad9857f6d02a)) + +## [2.15.3](https://github.com/googleapis/google-api-php-client/compare/v2.15.2...v2.15.3) (2024-01-04) + + +### Bug Fixes + +* Guzzle dependency version ([#2546](https://github.com/googleapis/google-api-php-client/issues/2546)) ([c270f28](https://github.com/googleapis/google-api-php-client/commit/c270f28b00594a151a887edd3cfd205594a1256a)) + +## [2.15.2](https://github.com/googleapis/google-api-php-client/compare/v2.15.1...v2.15.2) (2024-01-03) + + +### Bug Fixes + +* Disallow vulnerable guzzle versions ([#2536](https://github.com/googleapis/google-api-php-client/issues/2536)) ([d1830ed](https://github.com/googleapis/google-api-php-client/commit/d1830ede17114a4951ab9e60b3b9bcd9393b8668)) +* Php 8.3 deprecated get_class method call without argument ([#2509](https://github.com/googleapis/google-api-php-client/issues/2509)) ([8c66021](https://github.com/googleapis/google-api-php-client/commit/8c6602119b631e1a9da4dbe219af18d51c8dab8e)) +* Phpseclib security vulnerability ([#2524](https://github.com/googleapis/google-api-php-client/issues/2524)) ([73705c2](https://github.com/googleapis/google-api-php-client/commit/73705c2a65bfc01fa6d7717b7f401b8288fe0587)) + +## [2.15.1](https://github.com/googleapis/google-api-php-client/compare/v2.15.0...v2.15.1) (2023-09-12) + + +### Bug Fixes + +* Upgrade min phpseclib version ([#2499](https://github.com/googleapis/google-api-php-client/issues/2499)) ([8e7fae2](https://github.com/googleapis/google-api-php-client/commit/8e7fae2b79cfc1b72026347abf6314d91442a018)) + +## [2.15.0](https://github.com/googleapis/google-api-php-client/compare/v2.14.0...v2.15.0) (2023-05-18) + + +### Features + +* Add pkce support and upgrade examples ([#2438](https://github.com/googleapis/google-api-php-client/issues/2438)) ([bded223](https://github.com/googleapis/google-api-php-client/commit/bded223ece445a6130cde82417b20180b1d6698a)) +* Drop support for 7.3 and below ([#2431](https://github.com/googleapis/google-api-php-client/issues/2431)) ([c765b37](https://github.com/googleapis/google-api-php-client/commit/c765b379e95ab272b6a87aa802d9f5507eaeb2e7)) + +## [2.14.0](https://github.com/googleapis/google-api-php-client/compare/v2.13.2...v2.14.0) (2023-05-11) + + +### Features + +* User-supplied query params for auth url ([#2432](https://github.com/googleapis/google-api-php-client/issues/2432)) ([74a7d7b](https://github.com/googleapis/google-api-php-client/commit/74a7d7b838acb08afc02b449f338fbe6577cb03c)) + +## [2.13.2](https://github.com/googleapis/google-api-php-client/compare/v2.13.1...v2.13.2) (2023-03-23) + + +### Bug Fixes + +* Calling class_exists with null in Google\Model ([#2405](https://github.com/googleapis/google-api-php-client/issues/2405)) ([5ed4edc](https://github.com/googleapis/google-api-php-client/commit/5ed4edc9315110a715e9763d27ee6761e1aaa00a)) + +## [2.13.1](https://github.com/googleapis/google-api-php-client/compare/v2.13.0...v2.13.1) (2023-03-13) + + +### Bug Fixes + +* Allow dynamic properties on model classes ([#2408](https://github.com/googleapis/google-api-php-client/issues/2408)) ([11080d5](https://github.com/googleapis/google-api-php-client/commit/11080d5e85a040751a13aca8131f93c7d910db11)) + +## [2.13.0](https://github.com/googleapis/google-api-php-client/compare/v2.12.6...v2.13.0) (2022-12-19) + + +### Features + +* Make auth http client config extends from default client config ([#2348](https://github.com/googleapis/google-api-php-client/issues/2348)) ([2640250](https://github.com/googleapis/google-api-php-client/commit/2640250c7bab479f378972733dcc0a3e9b2e14f8)) + + +### Bug Fixes + +* Don't send content-type header if no post body exists ([#2288](https://github.com/googleapis/google-api-php-client/issues/2288)) ([654c0e2](https://github.com/googleapis/google-api-php-client/commit/654c0e29ab78aba8bfef52fd3d06a3b2b39c4e0d)) +* Ensure new redirect_uri propogates to OAuth2 class ([#2282](https://github.com/googleapis/google-api-php-client/issues/2282)) ([a69131b](https://github.com/googleapis/google-api-php-client/commit/a69131b6488735d112a529a278cfc8b875e18647)) +* Lint errors ([#2315](https://github.com/googleapis/google-api-php-client/issues/2315)) ([88cc63c](https://github.com/googleapis/google-api-php-client/commit/88cc63c38b0cf88629f66fdf8ba6006f6c6d5a2c)) +* Update accounts.google.com authorization URI ([#2275](https://github.com/googleapis/google-api-php-client/issues/2275)) ([b2624d2](https://github.com/googleapis/google-api-php-client/commit/b2624d21fce894126b9975a872cf5cda8038b254)) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..46b2a08ea --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,43 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, +and in the interest of fostering an open and welcoming community, +we pledge to respect all people who contribute through reporting issues, +posting feature requests, updating documentation, +submitting pull requests or patches, and other activities. + +We are committed to making participation in this project +a harassment-free experience for everyone, +regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, +body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, +such as physical or electronic +addresses, without explicit permission +* Other unethical or unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct. +By adopting this Code of Conduct, +project maintainers commit themselves to fairly and consistently +applying these principles to every aspect of managing this project. +Project maintainers who do not follow or enforce the Code of Conduct +may be permanently removed from the project team. + +This code of conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior +may be reported by opening an issue +or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.2.0, +available at [http://contributor-covenant.org/version/1/2/0/](http://contributor-covenant.org/version/1/2/0/) diff --git a/README.md b/README.md index e307595e6..7bac1824f 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,35 @@ -[![Build Status](https://travis-ci.org/google/google-api-php-client.svg?branch=master)](https://travis-ci.org/google/google-api-php-client) +![](https://github.com/googleapis/google-api-php-client/workflows/.github/workflows/tests.yml/badge.svg) # Google APIs Client Library for PHP # -## Description ## -The Google API Client Library enables you to work with Google APIs such as Google+, Drive, or YouTube on your server. +**NOTE**: please check to see if the package you'd like to install is available in our +list of [Google cloud packages](https://cloud.google.com/php/docs/reference) first, as +these are the recommended libraries. -## Beta ## -This library is in Beta. We're comfortable enough with the stability and features of the library that we want you to build real production applications on it. We will make an effort to support the public and protected surface of the library and maintain backwards compatibility in the future. While we are still in Beta, we reserve the right to make incompatible changes. +
+
Reference Docs
https://googleapis.github.io/google-api-php-client/
+
License
Apache 2.0
+
+ +The Google API Client Library enables you to work with Google APIs such as Gmail, Drive or YouTube on your server. + +These client libraries are officially supported by Google. However, the libraries are considered complete and are in maintenance mode. This means that we will address critical bugs and security issues but will not add any new features. + +## Google Cloud Platform + +For Google Cloud Platform APIs such as [Datastore][cloud-datastore], [Cloud Storage][cloud-storage], [Pub/Sub][cloud-pubsub], and [Compute Engine][cloud-compute], we recommend using the Google Cloud client libraries. For a complete list of supported Google Cloud client libraries, see [googleapis/google-cloud-php](https://github.com/googleapis/google-cloud-php). + +[cloud-datastore]: https://github.com/googleapis/google-cloud-php-datastore +[cloud-pubsub]: https://github.com/googleapis/google-cloud-php-pubsub +[cloud-storage]: https://github.com/googleapis/google-cloud-php-storage +[cloud-compute]: https://github.com/googleapis/google-cloud-php-compute ## Requirements ## -* [PHP 5.4.0 or higher](http://www.php.net/) +* [PHP 8.0 or higher](https://www.php.net/) ## Developer Documentation ## -http://developers.google.com/api-client-library/php + +The [docs folder](docs/) provides detailed guides for using this library. ## Installation ## @@ -20,14 +37,23 @@ You can use **Composer** or simply **Download the Release** ### Composer -The preferred method is via [composer](https://getcomposer.org). Follow the +The preferred method is via [composer](https://getcomposer.org/). Follow the [installation instructions](https://getcomposer.org/doc/00-intro.md) if you do not already have composer installed. Once composer is installed, execute the following command in your project root to install this library: ```sh -composer require google/apiclient:^2.0 +composer require google/apiclient +``` + +If you're facing a timeout error then either increase the timeout for composer by adding the env flag as `COMPOSER_PROCESS_TIMEOUT=600 composer install` or you can put this in the `config` section of the composer schema: +``` +{ + "config": { + "process-timeout": 600 + } +} ``` Finally, be sure to include the autoloader: @@ -36,9 +62,64 @@ Finally, be sure to include the autoloader: require_once '/path/to/your-project/vendor/autoload.php'; ``` +This library relies on `google/apiclient-services`. That library provides up-to-date API wrappers for a large number of Google APIs. In order that users may make use of the latest API clients, this library does not pin to a specific version of `google/apiclient-services`. **In order to prevent the accidental installation of API wrappers with breaking changes**, it is highly recommended that you pin to the [latest version](https://github.com/googleapis/google-api-php-client-services/releases) yourself prior to using this library in production. + +#### Cleaning up unused services + +There are over 200 Google API services. The chances are good that you will not +want them all. In order to avoid shipping these dependencies with your code, +you can run the `Google\Task\Composer::cleanup` task and specify the services +you want to keep in `composer.json`: + +```json +{ + "require": { + "google/apiclient": "^2.15.0" + }, + "scripts": { + "pre-autoload-dump": "Google\\Task\\Composer::cleanup" + }, + "extra": { + "google/apiclient-services": [ + "Drive", + "YouTube" + ] + } +} +``` + +This example will remove all services other than "Drive" and "YouTube" when +`composer update` or a fresh `composer install` is run. + +**IMPORTANT**: If you add any services back in `composer.json`, you will need to +remove the `vendor/google/apiclient-services` directory explicitly for the +change you made to have effect: + +```sh +rm -r vendor/google/apiclient-services +composer update +``` + +**NOTE**: This command performs an exact match on the service name, so to keep +`YouTubeReporting` and `YouTubeAnalytics` as well, you'd need to add each of +them explicitly: + +```json +{ + "extra": { + "google/apiclient-services": [ + "Drive", + "YouTube", + "YouTubeAnalytics", + "YouTubeReporting" + ] + } +} +``` + ### Download the Release -If you abhor using composer, you can download the package in its entirety. The [Releases](https://github.com/google/google-api-php-client/releases) page lists all stable versions. Download any file +If you prefer not to use composer, you can download the package in its entirety. The [Releases](https://github.com/googleapis/google-api-php-client/releases) page lists all stable versions. Download any file with the name `google-api-php-client-[RELEASE_NAME].zip` for a package including this library and its dependencies. Uncompress the zip file you download, and include the autoloader in your project: @@ -47,7 +128,7 @@ Uncompress the zip file you download, and include the autoloader in your project require_once '/path/to/google-api-php-client/vendor/autoload.php'; ``` -For additional installation and setup instructions, see [the documentation](https://developers.google.com/api-client-library/php/start/installation). +For additional installation and setup instructions, see [the documentation](docs/). ## Examples ## See the [`examples/`](examples) directory for examples of the key client features. You can @@ -66,15 +147,18 @@ And then browsing to the host and port you specified // include your composer dependencies require_once 'vendor/autoload.php'; -$client = new Google_Client(); +$client = new Google\Client(); $client->setApplicationName("Client_Library_Examples"); $client->setDeveloperKey("YOUR_APP_KEY"); -$service = new Google_Service_Books($client); -$optParams = array('filter' => 'free-ebooks'); -$results = $service->volumes->listVolumes('Henry David Thoreau', $optParams); +$service = new Google\Service\Books($client); +$query = 'Henry David Thoreau'; +$optParams = [ + 'filter' => 'free-ebooks', +]; +$results = $service->volumes->listVolumes($query, $optParams); -foreach ($results as $item) { +foreach ($results->getItems() as $item) { echo $item['volumeInfo']['title'], "
\n"; } ``` @@ -83,34 +167,48 @@ foreach ($results as $item) { > An example of this can be seen in [`examples/simple-file-upload.php`](examples/simple-file-upload.php). -**NOTE:** If you are using Google App Engine or Google Compute Engine, you can skip steps 1-3, as Application Default Credentials are included automatically when `useApplicationDefaultCredentials` is called. - -1. Follow the instructions to [Create Web Application Credentials](https://developers.google.com/api-client-library/php/auth/web-app#creatingcred) +1. Follow the instructions to [Create Web Application Credentials](docs/oauth-web.md#create-authorization-credentials) 1. Download the JSON credentials -1. Set the path to these credentials using the `GOOGLE_APPLICATION_CREDENTIALS` environment variable: +1. Set the path to these credentials using `Google\Client::setAuthConfig`: ```php - putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json'); + $client = new Google\Client(); + $client->setAuthConfig('/path/to/client_credentials.json'); ``` -1. Tell the Google client to use your service account credentials to authenticate: +1. Set the scopes required for the API you are going to call ```php - $client = new Google_Client(); - $client->useApplicationDefaultCredentials(); + $client->addScope(Google\Service\Drive::DRIVE); ``` -1. If you have delegated domain-wide access to the service account and you want to impersonate a user account, specify the email address of the user account using the method setSubject: +1. Set your application's redirect URI + + ```php + // Your redirect URI can be any registered URI, but in this example + // we redirect back to this same page + $redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']; + $client->setRedirectUri($redirect_uri); + ``` + +1. In the script handling the redirect URI, exchange the authorization code for an access token: ```php - $ client->setSubject($user_to_impersonate); + if (isset($_GET['code'])) { + $token = $client->fetchAccessTokenWithAuthCode($_GET['code']); + } ``` ### Authentication with Service Accounts ### > An example of this can be seen in [`examples/service-account.php`](examples/service-account.php). -1. Follow the instructions to [Create a Service Account](https://developers.google.com/api-client-library/php/auth/service-accounts#creatinganaccount) +Some APIs +(such as the [YouTube Data API](https://developers.google.com/youtube/v3/)) do +not support service accounts. Check with the specific API documentation if API +calls return unexpected 401 or 403 errors. + +1. Follow the instructions to [Create a Service Account](docs/oauth-server.md#creating-a-service-account) 1. Download the JSON credentials 1. Set the path to these credentials using the `GOOGLE_APPLICATION_CREDENTIALS` environment variable: @@ -121,14 +219,14 @@ foreach ($results as $item) { 1. Tell the Google client to use your service account credentials to authenticate: ```php - $client = new Google_Client(); + $client = new Google\Client(); $client->useApplicationDefaultCredentials(); ``` 1. Set the scopes required for the API you are going to call ```php - $client->addScope(Google_Service_Drive::DRIVE); + $client->addScope(Google\Service\Drive::DRIVE); ``` 1. If you have delegated domain-wide access to the service account and you want to impersonate a user account, specify the email address of the user account using the method setSubject: @@ -137,15 +235,29 @@ foreach ($results as $item) { $client->setSubject($user_to_impersonate); ``` +#### How to use a specific JSON key + +If you want to a specific JSON key instead of using `GOOGLE_APPLICATION_CREDENTIALS` environment variable, you can do this: + +```php +$jsonKey = [ + 'type' => 'service_account', + // ... +]; +$client = new Google\Client(); +$client->setAuthConfig($jsonKey); +``` + ### Making Requests ### -The classes used to call the API in [google-api-php-client-services](https://github.com/Google/google-api-php-client-services) are autogenerated. They map directly to the JSON requests and responses found in the [APIs Explorer](https://developers.google.com/apis-explorer/#p/). +The classes used to call the API in [google-api-php-client-services](https://github.com/googleapis/google-api-php-client-services) are autogenerated. They map directly to the JSON requests and responses found in the [APIs Explorer](https://developers.google.com/apis-explorer/#p/). A JSON request to the [Datastore API](https://developers.google.com/apis-explorer/#p/datastore/v1beta3/datastore.projects.runQuery) would look like this: -```json +``` POST https://datastore.googleapis.com/v1beta3/projects/YOUR_PROJECT_ID:runQuery?key=YOUR_API_KEY - +``` +```json { "query": { "kind": [{ @@ -166,10 +278,10 @@ Using this library, the same call would look something like this: ```php // create the datastore service class -$datastore = new Google_Service_Datastore($client) +$datastore = new Google\Service\Datastore($client); // build the query - this maps directly to the JSON -$query = new Google_Service_Datastore_Query([ +$query = new Google\Service\Datastore\Query([ 'kind' => [ [ 'name' => 'Book', @@ -185,28 +297,28 @@ $query = new Google_Service_Datastore_Query([ ]); // build the request and response -$request = new Google_Service_Datastore_RunQueryRequest(['query' => $query]); +$request = new Google\Service\Datastore\RunQueryRequest(['query' => $query]); $response = $datastore->projects->runQuery('YOUR_DATASET_ID', $request); ``` -However, as each property of the JSON API has a corresponding generated class, the above code could also be written lile this: +However, as each property of the JSON API has a corresponding generated class, the above code could also be written like this: ```php // create the datastore service class -$datastore = new Google_Service_Datastore($client) +$datastore = new Google\Service\Datastore($client); // build the query -$request = new Google_Service_Datastore_RunQueryRequest(); -$query = new Google_Service_Datastore_Query(); +$request = new Google\Service\Datastore_RunQueryRequest(); +$query = new Google\Service\Datastore\Query(); // - set the order -$order = new Google_Service_Datastore_PropertyOrder(); +$order = new Google\Service\Datastore_PropertyOrder(); $order->setDirection('descending'); -$property = new Google_Service_Datastore_PropertyReference(); +$property = new Google\Service\Datastore\PropertyReference(); $property->setName('title'); $order->setProperty($property); $query->setOrder([$order]); // - set the kinds -$kind = new Google_Service_Datastore_KindExpression(); +$kind = new Google\Service\Datastore\KindExpression(); $kind->setName('Book'); $query->setKinds([$kind]); // - set the limit @@ -223,11 +335,13 @@ The method used is a matter of preference, but *it will be very difficult to use If Google Authentication is desired for external applications, or a Google API is not available yet in this library, HTTP requests can be made directly. +If you are installing this client only to authenticate your own HTTP client requests, you should use [`google/auth`](https://github.com/googleapis/google-auth-library-php#call-the-apis) instead. + The `authorize` method returns an authorized [Guzzle Client](http://docs.guzzlephp.org/), so any request made using the client will contain the corresponding authorization. ```php // create the Google client -$client = new Google_Client(); +$client = new Google\Client(); /** * Set your method for authentication. Depending on the API, This could be @@ -235,7 +349,7 @@ $client = new Google_Client(); * Application Default Credentials. */ $client->useApplicationDefaultCredentials(); -$client->addScope(Google_Service_Plus::PLUS_ME); +$client->addScope(Google\Service\Plus::PLUS_ME); // returns a Guzzle HTTP Client $httpClient = $client->authorize(); @@ -246,25 +360,32 @@ $response = $httpClient->get('https://www.googleapis.com/plus/v1/people/me'); ### Caching ### -It is recommended to use another caching library to improve performance. This can be done by passing a [PSR-6](http://www.php-fig.org/psr/psr-6/) compatible library to the client: +It is recommended to use another caching library to improve performance. This can be done by passing a [PSR-6](https://www.php-fig.org/psr/psr-6/) compatible library to the client: ```php -$cache = new Stash\Pool(new Stash\Driver\FileSystem); +use League\Flysystem\Adapter\Local; +use League\Flysystem\Filesystem; +use Cache\Adapter\Filesystem\FilesystemCachePool; + +$filesystemAdapter = new Local(__DIR__.'/'); +$filesystem = new Filesystem($filesystemAdapter); + +$cache = new FilesystemCachePool($filesystem); $client->setCache($cache); ``` -In this example we use [StashPHP](http://www.stashphp.com/). Add this to your project with composer: +In this example we use [PHP Cache](http://www.php-cache.com/). Add this to your project with composer: ``` -composer require tedivm/stash +composer require cache/filesystem-adapter ``` ### Updating Tokens ### -When using [Refresh Tokens](https://developers.google.com/identity/protocols/OAuth2InstalledApp#refresh) or [Service Account Credentials](https://developers.google.com/identity/protocols/OAuth2ServiceAccount#overview), it may be useful to perform some action when a new access token is granted. To do this, pass a callable to the `setTokenCallback` method on the client: +When using [Refresh Tokens](https://developers.google.com/identity/protocols/OAuth2InstalledApp#offline) or [Service Account Credentials](https://developers.google.com/identity/protocols/OAuth2ServiceAccount#overview), it may be useful to perform some action when a new access token is granted. To do this, pass a callable to the `setTokenCallback` method on the client: ```php -$logger = new Monolog\Logger; +$logger = new Monolog\Logger(); $tokenCallback = function ($cacheKey, $accessToken) use ($logger) { $logger->debug(sprintf('new access token received at cache key %s', $cacheKey)); }; @@ -282,7 +403,7 @@ $httpClient = new GuzzleHttp\Client([ 'verify' => false, // otherwise HTTPS requests will fail. ]); -$client = new Google_Client(); +$client = new Google\Client(); $client->setHttpClient($httpClient); ``` @@ -290,39 +411,78 @@ Now all calls made by this library will appear in the Charles UI. One additional step is required in Charles to view SSL requests. Go to **Charles > Proxy > SSL Proxying Settings** and add the domain you'd like captured. In the case of the Google APIs, this is usually `*.googleapis.com`. +### Controlling HTTP Client Configuration Directly + +Google API Client uses [Guzzle](http://docs.guzzlephp.org/) as its default HTTP client. That means that you can control your HTTP requests in the same manner you would for any application using Guzzle. + +Let's say, for instance, we wished to apply a referrer to each request. + +```php +use GuzzleHttp\Client; + +$httpClient = new Client([ + 'headers' => [ + 'referer' => 'mysite.com' + ] +]); + +$client = new Google\Client(); +$client->setHttpClient($httpClient); +``` + +Other Guzzle features such as [Handlers and Middleware](http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html) offer even more control. + +### Partial Consent and Granted Scopes + +When using OAuth2 3LO (e.g. you're a client requesting credentials from a 3rd +party, such as in the [simple file upload example](examples/simple-file-upload.php)), +you may want to take advantage of Partial Consent. + +To allow clients to only grant certain scopes in the OAuth2 screen, pass the +querystring parameter for `enable_serial_consent` when generating the +authorization URL: + +```php +$authUrl = $client->createAuthUrl($scope, ['enable_serial_consent' => 'true']); +``` + +Once the flow is completed, you can see which scopes were granted by calling +`getGrantedScope` on the OAuth2 object: + +```php +// Space-separated string of granted scopes if it exists, otherwise null. +echo $client->getOAuth2Service()->getGrantedScope(); +``` + ### Service Specific Examples ### YouTube: https://github.com/youtube/api-samples/tree/master/php -## Frequently Asked Questions ## +## How Do I Contribute? ## -### What do I do if something isn't working? ### +Please see the [contributing](.github/CONTRIBUTING.md) page for more information. In particular, we love pull requests - but please make sure to sign the contributor license agreement. -For support with the library the best place to ask is via the google-api-php-client tag on StackOverflow: http://stackoverflow.com/questions/tagged/google-api-php-client +## Frequently Asked Questions ## -If there is a specific bug with the library, please [file a issue](/Google/google-api-php-client/issues) in the Github issues tracker, including an example of the failing code and any specific errors retrieved. Feature requests can also be filed, as long as they are core library requests, and not-API specific: for those, refer to the documentation for the individual APIs for the best place to file requests. Please try to provide a clear statement of the problem that the feature would address. +### What do I do if something isn't working? ### -### How do I contribute? ### +For support with the library the best place to ask is via the google-api-php-client tag on StackOverflow: https://stackoverflow.com/questions/tagged/google-api-php-client -We accept contributions via Github Pull Requests, but all contributors need to be covered by the standard Google Contributor License Agreement. You can find links, and more instructions, in the documentation: https://developers.google.com/api-client-library/php/contribute +If there is a specific bug with the library, please [file an issue](https://github.com/googleapis/google-api-php-client/issues) in the GitHub issues tracker, including an example of the failing code and any specific errors retrieved. Feature requests can also be filed, as long as they are core library requests, and not-API specific: for those, refer to the documentation for the individual APIs for the best place to file requests. Please try to provide a clear statement of the problem that the feature would address. ### I want an example of X! ### If X is a feature of the library, file away! If X is an example of using a specific service, the best place to go is to the teams for those specific APIs - our preference is to link to their examples rather than add them to the library, as they can then pin to specific versions of the library. If you have any examples for other APIs, let us know and we will happily add a link to the README above! -### Why do you still support 5.2? ### - -When we started working on the 1.0.0 branch we knew there were several fundamental issues to fix with the 0.6 releases of the library. At that time we looked at the usage of the library, and other related projects, and determined that there was still a large and active base of PHP 5.2 installs. You can see this in statistics such as the PHP versions chart in the WordPress stats: http://wordpress.org/about/stats/. We will keep looking at the types of usage we see, and try to take advantage of newer PHP features where possible. - -### Why does Google_..._Service have weird names? ### +### Why do some Google\Service classes have weird names? ### -The _Service classes are generally automatically generated from the API discovery documents: https://developers.google.com/discovery/. Sometimes new features are added to APIs with unusual names, which can cause some unexpected or non-standard style naming in the PHP classes. +The _Google\Service_ classes are generally automatically generated from the API discovery documents: https://developers.google.com/discovery/. Sometimes new features are added to APIs with unusual names, which can cause some unexpected or non-standard style naming in the PHP classes. ### How do I deal with non-JSON response types? ### Some services return XML or similar by default, rather than JSON, which is what the library supports. You can request a JSON response by adding an 'alt' argument to optional params that is normally the last argument to a method call: -``` +```php $opt_params = array( 'alt' => "json" ); @@ -330,7 +490,7 @@ $opt_params = array( ### How do I set a field to null? ### -The library strips out nulls from the objects sent to the Google APIs as its the default value of all of the uninitialised properties. To work around this, set the field you want to null to Google_Model::NULL_VALUE. This is a placeholder that will be replaced with a true null when sent over the wire. +The library strips out nulls from the objects sent to the Google APIs as it is the default value of all of the uninitialized properties. To work around this, set the field you want to null to `Google\Model::NULL_VALUE`. This is a placeholder that will be replaced with a true null when sent over the wire. ## Code Quality ## diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..8b58ae9c0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). + +The Google Security Team will respond within 5 working days of your report on g.co/vulnz. + +We use g.co/vulnz for our intake, and do coordination and disclosure here using GitHub Security Advisory to privately discuss and fix the issue. diff --git a/UPGRADING.md b/UPGRADING.md index f654570a4..ef939e943 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,6 +1,53 @@ Google API Client Upgrade Guide =============================== +2.x to 2.10.0 +------------- + +### Namespaces + +The Google API Client for PHP now uses namespaces for all classes. Code using +the legacy classnames will continue to work, but it is advised to upgrade to the +underspaced names, as the legacy classnames will be deprecated some time in the +future. + +**Before** + +```php +$client = new Google_Client(); +$service = new Google_Service_Books($client); +``` + +**After** +```php +$client = new Google\Client(); +$service = new Google\Service\Books($client); +``` + +### Service class constructors + +Service class constructors now accept an optional `Google\Client|array` parameter +as their first argument, rather than requiring an instance of `Google\Client`. + +**Before** + +```php +$client = new Google_Client(); +$client->setApplicationName("Client_Library_Examples"); +$client->setDeveloperKey("YOUR_APP_KEY"); + +$service = new Google_Service_Books($client); +``` + +**After** + +```php +$service = new Google\Service\Books([ + 'application_name' => "Client_Library_Examples", + 'developer_key' => "YOUR_APP_KEY", +]); +``` + 1.0 to 2.0 ---------- @@ -175,7 +222,7 @@ $client->getAuth() **After** ```php -$client->useDefaultApplicationCredentials(); +$client->useApplicationDefaultCredentials(); $client->addScope('https://www.googleapis.com/auth/sqlservice.admin'); ``` @@ -244,7 +291,7 @@ $client->revokeToken($token); $client->isAccessTokenExpired(); ``` -## PHP 5.4 is now the minimum supported PHP version +## PHP 5.6 is now the minimum supported PHP version This was previously `PHP 5.2`. If you still need to use PHP 5.2, please continue to use the [v1-master](https://github.com/google/google-api-php-client/tree/v1-master) branch. @@ -308,20 +355,23 @@ setting the Guzzle `GuzzleHttp\ClientInterface` object. 1. Automatically refreshes access tokens if one is set and the access token is expired - Removed `Google_Config` - Removed `Google_Utils` - - [`Google\Auth\CacheInterface`][Google Auth CacheInterface] is used for all caching. As a result: + - [`PSR-6`][PSR 6] cache is used for all caching. As a result: 1. Removed `Google_Cache_Abstract` 1. Classes `Google_Cache_Apc`, `Google_Cache_File`, `Google_Cache_Memcache`, and `Google_Cache_Null` now implement `Google\Auth\CacheInterface`. + 1. Google Auth provides simple [caching utilities][Google Auth Cache] which + are used by default unless you provide alternatives. - Removed `$boundary` constructor argument for `Google_Http_MediaFileUpload` -[PSR 3]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md +[PSR 3]: https://www.php-fig.org/psr/psr-3/ +[PSR 6]: https://www.php-fig.org/psr/psr-6/ [Guzzle 5]: https://github.com/guzzle/guzzle [Guzzle 6]: http://docs.guzzlephp.org/en/latest/psr7.html [Monolog]: https://github.com/Seldaek/monolog [Google Auth]: https://github.com/google/google-auth-library-php +[Google Auth Cache]: https://github.com/googleapis/google-auth-library-php/tree/master/src/Cache [Google Auth GCE]: https://github.com/google/google-auth-library-php/blob/master/src/GCECredentials.php [Google Auth OAuth2]: https://github.com/google/google-auth-library-php/blob/master/src/OAuth2.php [Google Auth Simple]: https://github.com/google/google-auth-library-php/blob/master/src/Simple.php [Google Auth AppIdentity]: https://github.com/google/google-auth-library-php/blob/master/src/AppIdentityCredentials.php -[Google Auth CacheInterface]: https://github.com/google/google-auth-library-php/blob/master/src/CacheInterface.php [Firebase JWT]: https://github.com/firebase/php-jwt diff --git a/composer.json b/composer.json index be5bdaf1e..abacbe84b 100644 --- a/composer.json +++ b/composer.json @@ -6,36 +6,42 @@ "homepage": "http://developers.google.com/api-client-library/php", "license": "Apache-2.0", "require": { - "php": ">=5.4", - "google/auth": "0.9", - "google/apiclient-services": "*@dev", - "firebase/php-jwt": "~2.0|~3.0", - "monolog/monolog": "^1.17", - "phpseclib/phpseclib": "~2.0", - "guzzlehttp/guzzle": "~5.2|~6.0", - "guzzlehttp/psr7": "^1.2" + "php": "^8.0", + "google/auth": "^1.37", + "google/apiclient-services": "~0.350", + "firebase/php-jwt": "^6.0", + "monolog/monolog": "^2.9||^3.0", + "phpseclib/phpseclib": "^3.0.36", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.6" }, "require-dev": { - "phpunit/phpunit": "~4", - "squizlabs/php_codesniffer": "~2.3", + "squizlabs/php_codesniffer": "^3.8", "symfony/dom-crawler": "~2.1", "symfony/css-selector": "~2.1", - "tedivm/stash": "^0.14.1" + "cache/filesystem-adapter": "^1.1", + "phpcompatibility/php-compatibility": "^9.2", + "composer/composer": "^1.10.23", + "phpspec/prophecy-phpunit": "^2.1", + "phpunit/phpunit": "^9.6" }, "suggest": { - "tedivm/stash": "For caching certs and tokens (using Google_Client::setCache)" + "cache/filesystem-adapter": "For caching certs and tokens (using Google\\Client::setCache)" }, "autoload": { - "psr-0": { - "Google_": "src/" + "psr-4": { + "Google\\": "src/" }, + "files": [ + "src/aliases.php" + ], "classmap": [ - "src/Google/Service/" + "src/aliases.php" ] }, "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-main": "2.x-dev" } } } diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..c97236175 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,15 @@ +# Google API Client Library for PHP Docs + +The Google API Client Library for PHP offers simple, flexible access to many Google APIs. + +## Documentation + +- [Getting Started](start.md) +- [API Keys](api-keys.md) +- [Auth](auth.md) +- [Installation](install.md) +- [Media](media.md) +- [OAuth Server](oauth-server.md) +- [OAuth Web](oauth-web.md) +- [Pagination](pagination.md) +- [Parameters](parameters.md) diff --git a/docs/api-keys.md b/docs/api-keys.md new file mode 100644 index 000000000..2e7d771e5 --- /dev/null +++ b/docs/api-keys.md @@ -0,0 +1,13 @@ +# API Keys + +When calling APIs that do not access private user data, you can use simple API keys. These keys are used to authenticate your application for accounting purposes. The Google Developers Console documentation also describes [API keys](https://developers.google.com/console/help/using-keys). + +> Note: If you do need to access private user data, you must use OAuth 2.0. See [Using OAuth 2.0 for Web Server Applications](/docs/oauth-web.md) and [Using OAuth 2.0 for Server to Server Applications](/docs/oauth-server.md) for more information. + +## Using API Keys + +To use API keys, call the `setDeveloperKey()` method of the `Google\Client` object before making any API calls. For example: + +```php +$client->setDeveloperKey($api_key); +``` diff --git a/docs/auth.md b/docs/auth.md new file mode 100644 index 000000000..65dc24630 --- /dev/null +++ b/docs/auth.md @@ -0,0 +1,11 @@ +# Authorization + +The Google PHP Client Library supports several methods for making authenticated calls to the Google APIs. + +- [API Keys](api-keys.md) +- [OAuth 2.0 For Webservers](oauth-web.md) +- [OAuth 2.0 Service Accounts](oauth-server.md) + +In addition, it supports a method of identifying users without granting access to make Google API calls. + +- [ID Token Verification](id-token.md) \ No newline at end of file diff --git a/docs/id-token.md b/docs/id-token.md new file mode 100644 index 000000000..56456fa88 --- /dev/null +++ b/docs/id-token.md @@ -0,0 +1,22 @@ +# ID Token Authentication + +ID tokens allow authenticating a user securely without requiring a network call (in many cases), and without granting the server access to request user information from the Google APIs. + +> For a complete example, see the [idtoken.php](https://github.com/googleapis/google-api-php-client/blob/master/examples/idtoken.php) sample in the examples/ directory of the client library. + +This is accomplished because each ID token is a cryptographically signed, base64 encoded JSON structure. The token payload includes the Google user ID, the client ID of the application the user signed in to, and the issuer (in this case, Google). It also contains a cryptographic signature which can be verified with the public Google certificates to ensure that the token was created by Google. If the user has granted permission to view their email address to the application, the ID token will additionally include their email address. + +The token can be easily and securely verified with the PHP client library + +```php +function getUserFromToken($token) { + $ticket = $client->verifyIdToken($token); + if ($ticket) { + $data = $ticket->getAttributes(); + return $data['payload']['sub']; // user ID + } + return false +} +``` + +The library will automatically download and cache the certificate required for verification, and refresh it if it has expired. diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 000000000..8218978fb --- /dev/null +++ b/docs/install.md @@ -0,0 +1,39 @@ +# Installation + +This page contains information about installing the Google APIs Client Library for PHP. + +## Requirements + +* PHP version 5.6 or greater. + +## Obtaining the client library + +There are two options for obtaining the files for the client library. + +### Using Composer + +You can install the library by adding it as a dependency to your composer.json. + +```json +"require": { + "google/apiclient": "^2.0" +} +``` + +### Downloading from GitHub + +Follow [the instructions in the README](https://github.com/google/google-api-php-client#download-the-release) to download the package locally. + +### What to do with the files + +After obtaining the files, include the autoloader. If you used Composer, your require statement will look like this: + +```php +require_once '/path/to/your-project/vendor/autoload.php'; +``` + +If you downloaded the package separately, your require statement will look like this: + +```php +require_once '/path/to/google-api-php-client/vendor/autoload.php'; +``` diff --git a/docs/media.md b/docs/media.md new file mode 100644 index 000000000..6e35bb2cf --- /dev/null +++ b/docs/media.md @@ -0,0 +1,75 @@ +# Media Upload + +The PHP client library allows for uploading large files for use with APIs such as Drive or YouTube. There are three different methods available. + +## Simple Upload + +In the simple upload case, the data is passed as the body of the request made to the server. This limits the ability to specify metadata, but is very easy to use. + +```php +$file = new Google\Service\Drive\DriveFile(); +$result = $service->files->insert($file, array( + 'data' => file_get_contents("path/to/file"), + 'mimeType' => 'application/octet-stream', + 'uploadType' => 'media' +)); +``` + +## Multipart File Upload + +With multipart file uploads, the uploaded file is sent as one part of a multipart form post. This allows metadata about the file object to be sent as part of the post as well. This is triggered by specifying the _multipart_ uploadType. + +```php +$file = new Google\Service\Drive\DriveFile(); +$file->setTitle("Hello World!"); +$result = $service->files->insert($file, array( + 'data' => file_get_contents("path/to/file"), + 'mimeType' => 'application/octet-stream', + 'uploadType' => 'multipart' +)); +``` + +## Resumable File Upload + +It is also possible to split the upload across multiple requests. This is convenient for larger files, and allows resumption of the upload if there is a problem. Resumable uploads can be sent with separate metadata. + +```php +$file = new Google\Service\Drive\DriveFile(); +$file->title = "Big File"; +$chunkSizeBytes = 1 * 1024 * 1024; + +// Call the API with the media upload, defer so it doesn't immediately return. +$client->setDefer(true); +$request = $service->files->insert($file); + +// Create a media file upload to represent our upload process. +$media = new Google\Http\MediaFileUpload( + $client, + $request, + 'text/plain', + null, + true, + $chunkSizeBytes +); +$media->setFileSize(filesize("path/to/file")); + +// Upload the various chunks. $status will be false until the process is +// complete. +$status = false; +$handle = fopen("path/to/file", "rb"); +while (!$status && !feof($handle)) { + $chunk = fread($handle, $chunkSizeBytes); + $status = $media->nextChunk($chunk); + } + +// The final value of $status will be the data from the API for the object +// that has been uploaded. +$result = false; +if($status != false) { + $result = $status; +} + +fclose($handle); +// Reset to the client to execute requests immediately in the future. +$client->setDefer(false); +``` \ No newline at end of file diff --git a/docs/oauth-server.md b/docs/oauth-server.md new file mode 100644 index 000000000..a4c403eb0 --- /dev/null +++ b/docs/oauth-server.md @@ -0,0 +1,140 @@ +# Using OAuth 2.0 for Server to Server Applications + +The Google APIs Client Library for PHP supports using OAuth 2.0 for server-to-server interactions such as those between a web application and a Google service. For this scenario you need a service account, which is an account that belongs to your application instead of to an individual end user. Your application calls Google APIs on behalf of the service account, so users aren't directly involved. This scenario is sometimes called "two-legged OAuth," or "2LO." (The related term "three-legged OAuth" refers to scenarios in which your application calls Google APIs on behalf of end users, and in which user consent is sometimes required.) + +Typically, an application uses a service account when the application uses Google APIs to work with its own data rather than a user's data. For example, an application that uses [Google Cloud Datastore](https://cloud.google.com/datastore/) for data persistence would use a service account to authenticate its calls to the Google Cloud Datastore API. + +If you have a G Suite domain—if you use [G Suite Business](https://gsuite.google.com), for example—an administrator of the G Suite domain can authorize an application to access user data on behalf of users in the G Suite domain. For example, an application that uses the [Google Calendar API](https://developers.google.com/google-apps/calendar/) to add events to the calendars of all users in a G Suite domain would use a service account to access the Google Calendar API on behalf of users. Authorizing a service account to access data on behalf of users in a domain is sometimes referred to as "delegating domain-wide authority" to a service account. + +> **Note:** When you use [G Suite Marketplace](https://www.google.com/enterprise/marketplace/) to install an application for your domain, the required permissions are automatically granted to the application. You do not need to manually authorize the service accounts that the application uses. + +> **Note:** Although you can use service accounts in applications that run from a G Suite domain, service accounts are not members of your G Suite account and aren't subject to domain policies set by G Suite administrators. For example, a policy set in the G Suite admin console to restrict the ability of Apps end users to share documents outside of the domain would not apply to service accounts. + +This document describes how an application can complete the server-to-server OAuth 2.0 flow by using the Google APIs Client Library for PHP. + +## Overview + +To support server-to-server interactions, first create a service account for your project in the Developers Console. If you want to access user data for users in your G Suite domain, then delegate domain-wide access to the service account. + +Then, your application prepares to make authorized API calls by using the service account's credentials to request an access token from the OAuth 2.0 auth server. + +Finally, your application can use the access token to call Google APIs. + +## Creating a service account + +A service account's credentials include a generated email address that is unique, a client ID, and at least one public/private key pair. + +If your application runs on Google App Engine, a service account is set up automatically when you create your project. + +If your application doesn't run on Google App Engine or Google Compute Engine, you must obtain these credentials in the Google Developers Console. To generate service-account credentials, or to view the public credentials that you've already generated, do the following: + +1. Open the [**Service accounts** section](https://console.cloud.google.com/iam-admin/serviceaccounts) of the Developers Console's **IAM & Admin** page. +2. Click **Create service account**. +3. In the **Create service account** window, type a name for the service account and select **Furnish a new private key**. If you want to [grant G Suite domain-wide authority](https://developers.google.com/identity/protocols/OAuth2ServiceAccount#delegatingauthority) to the service account, also select **Enable G Suite Domain-wide Delegation**. Then, click **Create**. + +Your new public/private key pair is generated and downloaded to your machine; it serves as the only copy of this key. You are responsible for storing it securely. + +You can return to the [Developers Console](https://console.developers.google.com/) at any time to view the client ID, email address, and public key fingerprints, or to generate additional public/private key pairs. For more details about service account credentials in the Developers Console, see [Service accounts](https://developers.google.com/console/help/service-accounts) in the Developers Console help file. + +Take note of the service account's email address and store the service account's private key file in a location accessible to your application. Your application needs them to make authorized API calls. + +**Note:** You must store and manage private keys securely in both development and production environments. Google does not keep a copy of your private keys, only your public keys. + +### Delegating domain-wide authority to the service account + +If your application runs in a G Suite domain and accesses user data, the service account that you created needs to be granted access to the user data that you want to access. + +The following steps must be performed by an administrator of the G Suite domain: + +1. Go to your G Suite domain’s [Admin console](http://admin.google.com). +2. Select **Security** from the list of controls. If you don't see **Security** listed, select **More controls** from the gray bar at the bottom of the page, then select **Security** from the list of controls. If you can't see the controls, make sure you're signed in as an administrator for the domain. +3. Select **Advanced settings** from the list of options. +4. Select **Manage third party OAuth Client access** in the **Authentication** section. +5. In the **Client name** field enter the service account's **Client ID**. +6. In the **One or More API Scopes** field enter the list of scopes that your application should be granted access to. For example, if your application needs domain-wide access to the Google Drive API and the Google Calendar API, enter: https://www.googleapis.com/auth/drive, https://www.googleapis.com/auth/calendar. +7. Click **Authorize**. + +Your application now has the authority to make API calls as users in your domain (to "impersonate" users). When you prepare to make authorized API calls, you specify the user to impersonate. + +[](#top_of_page)Preparing to make an authorized API call +-------------------------------------------------------- + +After you have obtained the client email address and private key from the Developers Console, set the path to these credentials in the `GOOGLE_APPLICATION_CREDENTIALS` environment variable ( **Note:** This is not required in the App Engine environment): + +```php +putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json'); +``` + +Call the `useApplicationDefaultCredentials` to use your service account credentials to authenticate: + +```php +$client = new Google\Client(); +$client->useApplicationDefaultCredentials(); +``` + +If you have delegated domain-wide access to the service account and you want to impersonate a user account, specify the email address of the user account using the method `setSubject`: + +```php +$client->setSubject($user_to_impersonate); +``` + +Use the authorized `Google\Client` object to call Google APIs in your application. + +## Calling Google APIs + +Use the authorized `Google\Client` object to call Google APIs by completing the following steps: + +1. Build a service object for the API that you want to call, providing the authorized `Google\Client` object. For example, to call the Cloud SQL Administration API: + + ```php + $sqladmin = new Google\Service\SQLAdmin($client); + ``` + +2. Make requests to the API service using the [interface provided by the service object](https://github.com/googleapis/google-api-php-client/blob/master/docs/start.md#build-the-service-object). For example, to list the instances of Cloud SQL databases in the examinable-example-123 project: + + ```php + $response = $sqladmin->instances->listInstances('examinable-example-123')->getItems(); + ``` + +## Complete example + +The following example prints a JSON-formatted list of Cloud SQL instances in a project. + +To run this example: + +1. Create a new directory and change to it. For example: + + ```sh + mkdir ~/php-oauth2-example + cd ~/php-oauth2-example + ``` + +2. Install the [Google API Client Library](https://github.com/google/google-api-php-client) for PHP using [Composer](https://getcomposer.org): + + ```sh + composer require google/apiclient:^2.0 + ``` + +3. Create the file sqlinstances.php with the content below. +4. Run the example from the command line: + + ``` + php ~/php-oauth2-example/sqlinstances.php + ``` + +### sqlinstances.php + +```php +useApplicationDefaultCredentials(); + +$sqladmin = new Google\Service\SQLAdmin($client); +$response = $sqladmin->instances + ->listInstances('examinable-example-123')->getItems(); +echo json_encode($response) . "\n"; +``` diff --git a/docs/oauth-web.md b/docs/oauth-web.md new file mode 100644 index 000000000..904745ed5 --- /dev/null +++ b/docs/oauth-web.md @@ -0,0 +1,424 @@ +# Using OAuth 2.0 for Web Server Applications + +This document explains how web server applications use the Google API Client Library for PHP to implement OAuth 2.0 authorization to access Google APIs. OAuth 2.0 allows users to share specific data with an application while keeping their usernames, passwords, and other information private. For example, an application can use OAuth 2.0 to obtain permission from users to store files in their Google Drives. + +This OAuth 2.0 flow is specifically for user authorization. It is designed for applications that can store confidential information and maintain state. A properly authorized web server application can access an API while the user interacts with the application or after the user has left the application. + +Web server applications frequently also use [service accounts](oauth-server.md) to authorize API requests, particularly when calling Cloud APIs to access project-based data rather than user-specific data. Web server applications can use service accounts in conjunction with user authorization. + +## Prerequisites + +### Enable APIs for your project + +Any application that calls Google APIs needs to enable those APIs in the API Console. To enable the appropriate APIs for your project: + +1. Open the [Library](https://console.developers.google.com/apis/library) page in the API Console. +2. Select the project associated with your application. Create a project if you do not have one already. +3. Use the **Library** page to find each API that your application will use. Click on each API and enable it for your project. + +### Create authorization credentials + +Any application that uses OAuth 2.0 to access Google APIs must have authorization credentials that identify the application to Google's OAuth 2.0 server. The following steps explain how to create credentials for your project. Your applications can then use the credentials to access APIs that you have enabled for that project. + +1. Open the [Credentials page](https://console.developers.google.com/apis/credentials) in the API Console. +2. Click **Create credentials > OAuth client ID**. +3. Complete the form. Set the application type to `Web application`. Applications that use languages and frameworks like PHP, Java, Python, Ruby, and .NET must specify authorized **redirect URIs**. The redirect URIs are the endpoints to which the OAuth 2.0 server can send responses. + + For testing, you can specify URIs that refer to the local machine, such as `http://localhost:8080`. With that in mind, please note that all of the examples in this document use `http://localhost:8080` as the redirect URI. + + We recommend that you design your app's auth endpoints so that your application does not expose authorization codes to other resources on the page. + +After creating your credentials, download the **client_secret.json** file from the API Console. Securely store the file in a location that only your application can access. + +> **Important:** Do not store the **client_secret.json** file in a publicly-accessible location. In addition, if you share the source code to your application—for example, on GitHub—store the **client_secret.json** file outside of your source tree to avoid inadvertently sharing your client credentials. + +### Identify access scopes + +Scopes enable your application to only request access to the resources that it needs while also enabling users to control the amount of access that they grant to your application. Thus, there may be an inverse relationship between the number of scopes requested and the likelihood of obtaining user consent. + +Before you start implementing OAuth 2.0 authorization, we recommend that you identify the scopes that your app will need permission to access. + +We also recommend that your application request access to authorization scopes via an [incremental authorization](#incremental-authorization) process, in which your application requests access to user data in context. This best practice helps users to more easily understand why your application needs the access it is requesting. + +The [OAuth 2.0 API Scopes](https://developers.google.com/identity/protocols/googlescopes) document contains a full list of scopes that you might use to access Google APIs. + +> If your public application uses scopes that permit access to certain user data, it must pass review. If you see **unverified app** on the screen when testing your application, you must submit a verification request to remove it. Find out more about [unverified apps](https://support.google.com/cloud/answer/7454865) and get answers to [frequently asked questions about app verification](https://support.google.com/cloud/answer/9110914) in the Help Center. + +### Language-specific requirements + +To run any of the code samples in this document, you'll need a Google account, access to the Internet, and a web browser. If you are using one of the API client libraries, also see the language-specific requirements below. + +To run the PHP code samples in this document, you'll need: + +* PHP 5.6 or greater with the command-line interface (CLI) and JSON extension installed. +* The [Composer](https://getcomposer.org/) dependency management tool. +* The Google APIs Client Library for PHP: + ```sh + php composer.phar require google/apiclient:^2.0 + ``` + +## Obtaining OAuth 2.0 access tokens + +The following steps show how your application interacts with Google's OAuth 2.0 server to obtain a user's consent to perform an API request on the user's behalf. Your application must have that consent before it can execute a Google API request that requires user authorization. + +The list below quickly summarizes these steps: + +1. Your application identifies the permissions it needs. +2. Your application redirects the user to Google along with the list of requested permissions. +3. The user decides whether to grant the permissions to your application. +4. Your application finds out what the user decided. +5. If the user granted the requested permissions, your application retrieves tokens needed to make API requests on the user's behalf. + +### Step 1: Set authorization parameters + +Your first step is to create the authorization request. That request sets parameters that identify your application and define the permissions that the user will be asked to grant to your application. + +The code snippet below creates a `Google\Client()` object, which defines the parameters in the authorization request. + +That object uses information from your **client_secret.json** file to identify your application. The object also identifies the scopes that your application is requesting permission to access and the URL to your application's auth endpoint, which will handle the response from Google's OAuth 2.0 server. Finally, the code sets the optional access_type and include_granted_scopes parameters. + +For example, this code requests read-only, offline access to a user's Google Drive: + +```php +$client = new Google\Client(); +$client->setAuthConfig('client_secret.json'); +$client->addScope(Google\Service\Drive::DRIVE_METADATA_READONLY); +$client->setRedirectUri('http://' . $_SERVER['HTTP_HOST'] . '/oauth2callback.php'); +$client->setAccessType('offline'); // offline access +$client->setIncludeGrantedScopes(true); // incremental auth +``` + +The request specifies the following information: + +#### Parameters + +##### `client_id` + +**Required**. The client ID for your application. You can find this value in the [API Console](https://console.developers.google.com/). In PHP, call the `setAuthConfig` function to load authorization credentials from a **client_secret.json** file. + +```php +$client = new Google\Client(); +$client->setAuthConfig('client_secret.json'); +``` + +##### `redirect_uri` + +**Required**. Determines where the API server redirects the user after the user completes the authorization flow. The value must exactly match one of the authorized redirect URIs for the OAuth 2.0 client, which you configured in the [API Console](https://console.developers.google.com/). If this value doesn't match an authorized URI, you will get a 'redirect_uri_mismatch' error. Note that the `http` or `https` scheme, case, and trailing slash ('`/`') must all match. + +To set this value in PHP, call the `setRedirectUri` function. Note that you must specify a valid redirect URI for your API Console project. + +```php +$client->setRedirectUri('http://localhost:8080/oauth2callback.php'); +``` + +##### `scope` + +**Required**. A space-delimited list of scopes that identify the resources that your application could access on the user's behalf. These values inform the consent screen that Google displays to the user. + +Scopes enable your application to only request access to the resources that it needs while also enabling users to control the amount of access that they grant to your application. Thus, there is an inverse relationship between the number of scopes requested and the likelihood of obtaining user consent. To set this value in PHP, call the `addScope` function: + +```php +$client->addScope(Google\Service\Drive::DRIVE_METADATA_READONLY); +``` + +The [OAuth 2.0 API Scopes](https://developers.google.com/identity/protocols/googlescopes) document provides a full list of scopes that you might use to access Google APIs. + +We recommend that your application request access to authorization scopes in context whenever possible. By requesting access to user data in context, via [incremental authorization](#Incremental-authorization), you help users to more easily understand why your application needs the access it is requesting. + +##### `access_type` + +**Recommended**. Indicates whether your application can refresh access tokens when the user is not present at the browser. Valid parameter values are `online`, which is the default value, and `offline`. + +Set the value to `offline` if your application needs to refresh access tokens when the user is not present at the browser. This is the method of refreshing access tokens described later in this document. This value instructs the Google authorization server to return a refresh token _and_ an access token the first time that your application exchanges an authorization code for tokens. + +To set this value in PHP, call the `setAccessType` function: + +```php +$client->setAccessType('offline'); +``` + +##### `state` + +**Recommended**. Specifies any string value that your application uses to maintain state between your authorization request and the authorization server's response. The server returns the exact value that you send as a `name=value` pair in the hash (`#`) fragment of the `redirect_uri` after the user consents to or denies your application's access request. + +You can use this parameter for several purposes, such as directing the user to the correct resource in your application, sending nonces, and mitigating cross-site request forgery. Since your `redirect_uri` can be guessed, using a `state` value can increase your assurance that an incoming connection is the result of an authentication request. If you generate a random string or encode the hash of a cookie or another value that captures the client's state, you can validate the response to additionally ensure that the request and response originated in the same browser, providing protection against attacks such as cross-site request forgery. See the [OpenID Connect](https://developers.google.com/identity/protocols/OpenIDConnect#createxsrftoken) documentation for an example of how to create and confirm a `state` token. + +To set this value in PHP, call the `setState` function: + +```php +$client->setState($sample_passthrough_value); +``` + +##### `include_granted_scopes` + +**Optional**. Enables applications to use incremental authorization to request access to additional scopes in context. If you set this parameter's value to `true` and the authorization request is granted, then the new access token will also cover any scopes to which the user previously granted the application access. See the [incremental authorization](#Incremental-authorization) section for examples. + +To set this value in PHP, call the `setIncludeGrantedScopes` function: + +```php +$client->setIncludeGrantedScopes(true); +``` + +##### `login_hint` + +**Optional**. If your application knows which user is trying to authenticate, it can use this parameter to provide a hint to the Google Authentication Server. The server uses the hint to simplify the login flow either by prefilling the email field in the sign-in form or by selecting the appropriate multi-login session. + +Set the parameter value to an email address or `sub` identifier, which is equivalent to the user's Google ID. + +To set this value in PHP, call the `setLoginHint` function: + +```php +$client->setLoginHint('timmerman@google.com'); +``` + +##### `prompt` + +**Optional**. A space-delimited, case-sensitive list of prompts to present the user. If you don't specify this parameter, the user will be prompted only the first time your app requests access. + +To set this value in PHP, call the `setPrompt` function: + +```php +$client->setPrompt('consent'); +``` + +Possible values are: + +`none` + +Do not display any authentication or consent screens. Must not be specified with other values. + +`consent` + +Prompt the user for consent. + +`select_account` + +Prompt the user to select an account. + +### Step 2: Redirect to Google's OAuth 2.0 server + +Redirect the user to Google's OAuth 2.0 server to initiate the authentication and authorization process. Typically, this occurs when your application first needs to access the user's data. In the case of [incremental authorization](#incremental-authorization), this step also occurs when your application first needs to access additional resources that it does not yet have permission to access. + +1. Generate a URL to request access from Google's OAuth 2.0 server: + + ```php + $auth_url = $client->createAuthUrl(); + ``` + +2. Redirect the user to `$auth_url`: + + ```php + header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL)); + ``` + +Google's OAuth 2.0 server authenticates the user and obtains consent from the user for your application to access the requested scopes. The response is sent back to your application using the redirect URL you specified. + +### Step 3: Google prompts user for consent + +In this step, the user decides whether to grant your application the requested access. At this stage, Google displays a consent window that shows the name of your application and the Google API services that it is requesting permission to access with the user's authorization credentials. The user can then consent or refuse to grant access to your application. + +Your application doesn't need to do anything at this stage as it waits for the response from Google's OAuth 2.0 server indicating whether the access was granted. That response is explained in the following step. + +### Step 4: Handle the OAuth 2.0 server response + +The OAuth 2.0 server responds to your application's access request by using the URL specified in the request. + +If the user approves the access request, then the response contains an authorization code. If the user does not approve the request, the response contains an error message. The authorization code or error message that is returned to the web server appears on the query string, as shown below: + +An error response: + + https://oauth2.example.com/auth?error=access_denied + +An authorization code response: + + https://oauth2.example.com/auth?code=4/P7q7W91a-oMsCeLvIaQm6bTrgtp7 + +> **Important**: If your response endpoint renders an HTML page, any resources on that page will be able to see the authorization code in the URL. Scripts can read the URL directly, and the URL in the `Referer` HTTP header may be sent to any or all resources on the page. +> +> Carefully consider whether you want to send authorization credentials to all resources on that page (especially third-party scripts such as social plugins and analytics). To avoid this issue, we recommend that the server first handle the request, then redirect to another URL that doesn't include the response parameters. + +#### Sample OAuth 2.0 server response + +You can test this flow by clicking on the following sample URL, which requests read-only access to view metadata for files in your Google Drive: + +``` +https://accounts.google.com/o/oauth2/v2/auth? + scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive.metadata.readonly& + access_type=offline& + include_granted_scopes=true& + state=state_parameter_passthrough_value& + redirect_uri=http%3A%2F%2Foauth2.example.com%2Fcallback& + response_type=code& + client_id=client_id +``` + +After completing the OAuth 2.0 flow, you should be redirected to `http://localhost/oauth2callback`, which will likely yield a `404 NOT FOUND` error unless your local machine serves a file at that address. The next step provides more detail about the information returned in the URI when the user is redirected back to your application. + +### Step 5: Exchange authorization code for refresh and access tokens + +After the web server receives the authorization code, it can exchange the authorization code for an access token. + +To exchange an authorization code for an access token, use the `authenticate` method: + +```php +$client->authenticate($_GET['code']); +``` + +You can retrieve the access token with the `getAccessToken` method: + +```php +$access_token = $client->getAccessToken(); +``` + +[](#top_of_page)Calling Google APIs +----------------------------------- + +Use the access token to call Google APIs by completing the following steps: + +1. If you need to apply an access token to a new `Google\Client` object—for example, if you stored the access token in a user session—use the `setAccessToken` method: + + ```php + $client->setAccessToken($access_token); + ``` + +2. Build a service object for the API that you want to call. You build a a service object by providing an authorized `Google\Client` object to the constructor for the API you want to call. For example, to call the Drive API: + + ```php + $drive = new Google\Service\Drive($client); + ``` + +3. Make requests to the API service using the [interface provided by the service object](start.md). For example, to list the files in the authenticated user's Google Drive: + + ```php + $files = $drive->files->listFiles(array())->getItems(); + ``` + +[](#top_of_page)Complete example +-------------------------------- + +The following example prints a JSON-formatted list of files in a user's Google Drive after the user authenticates and gives consent for the application to access the user's Drive files. + +To run this example: + +1. In the API Console, add the URL of the local machine to the list of redirect URLs. For example, add `http://localhost:8080`. +2. Create a new directory and change to it. For example: + + ```sh + mkdir ~/php-oauth2-example + cd ~/php-oauth2-example + ``` + +3. Install the [Google API Client Library](https://github.com/google/google-api-php-client) for PHP using [Composer](https://getcomposer.org): + + ```sh + composer require google/apiclient:^2.0 + ``` + +4. Create the files `index.php` and `oauth2callback.php` with the content below. +5. Run the example with a web server configured to serve PHP. If you use PHP 5.6 or newer, you can use PHP's built-in test web server: + + ```sh + php -S localhost:8080 ~/php-oauth2-example + ``` + +#### index.php + +```php +setAuthConfig('client_secrets.json'); +$client->addScope(Google\Service\Drive::DRIVE_METADATA_READONLY); + +if (isset($_SESSION['access_token']) && $_SESSION['access_token']) { + $client->setAccessToken($_SESSION['access_token']); + $drive = new Google\Service\Drive($client); + $files = $drive->files->listFiles(array())->getItems(); + echo json_encode($files); +} else { + $redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . '/oauth2callback.php'; + header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); +} +``` + +#### oauth2callback.php + +```php +setAuthConfigFile('client_secrets.json'); +$client->setRedirectUri('http://' . $_SERVER['HTTP_HOST'] . '/oauth2callback.php'); +$client->addScope(Google\Service\Drive::DRIVE_METADATA_READONLY); + +if (! isset($_GET['code'])) { + $auth_url = $client->createAuthUrl(); + header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL)); +} else { + $client->authenticate($_GET['code']); + $_SESSION['access_token'] = $client->getAccessToken(); + $redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . '/'; + header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); +} +``` + +## Incremental authorization + +In the OAuth 2.0 protocol, your app requests authorization to access resources, which are identified by scopes. It is considered a best user-experience practice to request authorization for resources at the time you need them. To enable that practice, Google's authorization server supports incremental authorization. This feature lets you request scopes as they are needed and, if the user grants permission, add those scopes to your existing access token for that user. + +For example, an app that lets people sample music tracks and create mixes might need very few resources at sign-in time, perhaps nothing more than the name of the person signing in. However, saving a completed mix would require access to their Google Drive. Most people would find it natural if they only were asked for access to their Google Drive at the time the app actually needed it. + +In this case, at sign-in time the app might request the `profile` scope to perform basic sign-in, and then later request the `https://www.googleapis.com/auth/drive.file` scope at the time of the first request to save a mix. + +To implement incremental authorization, you complete the normal flow for requesting an access token but make sure that the authorization request includes previously granted scopes. This approach allows your app to avoid having to manage multiple access tokens. + +The following rules apply to an access token obtained from an incremental authorization: + +* The token can be used to access resources corresponding to any of the scopes rolled into the new, combined authorization. +* When you use the refresh token for the combined authorization to obtain an access token, the access token represents the combined authorization and can be used for any of its scopes. +* The combined authorization includes all scopes that the user granted to the API project even if the grants were requested from different clients. For example, if a user granted access to one scope using an application's desktop client and then granted another scope to the same application via a mobile client, the combined authorization would include both scopes. +* If you revoke a token that represents a combined authorization, access to all of that authorization's scopes on behalf of the associated user are revoked simultaneously. + +The example for [setting authorization parameters](#Step-1-Set-authorization-parameters) demonstrates how to ensure authorization requests follow this best practice. The code snippet below also shows the code that you need to add to use incremental authorization. + +```php +$client->setIncludeGrantedScopes(true); +``` + +## Refreshing an access token (offline access) + +Access tokens periodically expire. You can refresh an access token without prompting the user for permission (including when the user is not present) if you requested offline access to the scopes associated with the token. + +If you use a Google API Client Library, the [client object](#Step-1-Set-authorization-parameters) refreshes the access token as needed as long as you configure that object for offline access. + +Requesting offline access is a requirement for any application that needs to access a Google API when the user is not present. For example, an app that performs backup services or executes actions at predetermined times needs to be able to refresh its access token when the user is not present. The default style of access is called `online`. + +Server-side web applications, installed applications, and devices all obtain refresh tokens during the authorization process. Refresh tokens are not typically used in client-side (JavaScript) web applications. + +If your application needs offline access to a Google API, set the API client's access type to `offline`: + +```php +$client->setAccessType("offline"); +``` + +After a user grants offline access to the requested scopes, you can continue to use the API client to access Google APIs on the user's behalf when the user is offline. The client object will refresh the access token as needed. + +## Revoking a token + +In some cases a user may wish to revoke access given to an application. A user can revoke access by visiting [Account Settings](https://security.google.com/settings/security/permissions). It is also possible for an application to programmatically revoke the access given to it. Programmatic revocation is important in instances where a user unsubscribes or removes an application. In other words, part of the removal process can include an API request to ensure the permissions granted to the application are removed. + +To programmatically revoke a token, call `revokeToken()`: + +```php +$client->revokeToken(); +``` + +**Note:** Following a successful revocation response, it might take some time before the revocation has full effect. + +Except as otherwise noted, the content of this page is licensed under the [Creative Commons Attribution 4.0 License](https://creativecommons.org/licenses/by/4.0/), and code samples are licensed under the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0). For details, see our [Site Policies](https://developers.google.com/terms/site-policies). Java is a registered trademark of Oracle and/or its affiliates. \ No newline at end of file diff --git a/docs/pagination.md b/docs/pagination.md new file mode 100644 index 000000000..22bf78721 --- /dev/null +++ b/docs/pagination.md @@ -0,0 +1,10 @@ +# Pagination + +Most list API calls have a maximum limit of results they will return in a single response. To allow retrieving more than this number of results, responses may return a pagination token which can be passed with a request in order to access subsequent pages. + +The token for the page will normally be found on list response objects, normally `nextPageToken`. This can be passed in the optional params. + +```php +$token = $results->getNextPageToken(); +$server->listActivities('me', 'public', array('pageToken' => $token)); +``` \ No newline at end of file diff --git a/docs/parameters.md b/docs/parameters.md new file mode 100644 index 000000000..183a0c298 --- /dev/null +++ b/docs/parameters.md @@ -0,0 +1,14 @@ +# Standard Parameters + +Many API methods include support for certain optional parameters. In addition to these there are several standard parameters that can be applied to any API call. These are defined in the `Google\Service\Resource` class. + +## Parameters + +- **alt**: Specify an alternative response type, for example csv. +- **fields**: A comma separated list of fields that should be included in the response. Nested parameters can be specified with parens, e.g. key,parent(child/subsection). +- **userIp**: The IP of the end-user making the request. This is used in per-user request quotas, as defined in the Google Developers Console +- **quotaUser**: A user ID for the end user, an alternative to userIp for applying per-user request quotas +- **data**: Used as part of [media](media.md) +- **mimeType**: Used as part of [media](media.md) +- **uploadType**: Used as part of [media](media.md) +- **mediaUpload**: Used as part of [media](media.md) \ No newline at end of file diff --git a/docs/start.md b/docs/start.md new file mode 100644 index 000000000..9c97991b7 --- /dev/null +++ b/docs/start.md @@ -0,0 +1,93 @@ +# Getting Started + +This document provides all the basic information you need to start using the library. It covers important library concepts, shows examples for various use cases, and gives links to more information. + +## Setup + +There are a few setup steps you need to complete before you can use this library: + +1. If you don't already have a Google account, [sign up](https://www.google.com/accounts). +2. If you have never created a Google API project, read the [Managing Projects page](https://developers.google.com/console/help/#managingprojects) and create a project in the [Google Developers Console](https://console.developers.google.com/) +3. [Install](install.md) the library. + +## Authentication and authorization + +It is important to understand the basics of how API authentication and authorization are handled. All API calls must use either simple or authorized access (defined below). Many API methods require authorized access, but some can use either. Some API methods that can use either behave differently, depending on whether you use simple or authorized access. See the API's method documentation to determine the appropriate access type. + +### 1. Simple API access (API keys) + +These API calls do not access any private user data. Your application must authenticate itself as an application belonging to your Google Cloud project. This is needed to measure project usage for accounting purposes. + +#### Important concepts + +* **API key**: To authenticate your application, use an [API key](https://cloud.google.com/docs/authentication/api-keys) for your Google Cloud Console project. Every simple access call your application makes must include this key. + +> **Warning**: Keep your API key private. If someone obtains your key, they could use it to consume your quota or incur charges against your Google Cloud project. + + +### 2. Authorized API access (OAuth 2.0) + +These API calls access private user data. Before you can call them, the user that has access to the private data must grant your application access. Therefore, your application must be authenticated, the user must grant access for your application, and the user must be authenticated in order to grant that access. All of this is accomplished with [OAuth 2.0](https://developers.google.com/identity/protocols/OAuth2) and libraries written for it. + +#### Important concepts + +* **Scope**: Each API defines one or more scopes that declare a set of operations permitted. For example, an API might have read-only and read-write scopes. When your application requests access to user data, the request must include one or more scopes. The user needs to approve the scope of access your application is requesting. +* **Refresh and access tokens**: When a user grants your application access, the OAuth 2.0 authorization server provides your application with refresh and access tokens. These tokens are only valid for the scope requested. Your application uses access tokens to authorize API calls. Access tokens expire, but refresh tokens do not. Your application can use a refresh token to acquire a new access token. + + > **Warning**: Keep refresh and access tokens private. If someone obtains your tokens, they could use them to access private user data. + +* **Client ID and client secret**: These strings uniquely identify your application and are used to acquire tokens. They are created for your Google Cloud project on the [API Access pane](https://code.google.com/apis/console#:access) of the Google Cloud. There are three types of client IDs, so be sure to get the correct type for your application: + + * Web application client IDs + * Installed application client IDs + * [Service Account](https://developers.google.com/identity/protocols/OAuth2ServiceAccount) client IDs + + > **Warning**: Keep your client secret private. If someone obtains your client secret, they could use it to consume your quota, incur charges against your Google Cloud project, and request access to user data. + + +## Building and calling a service + +This section described how to build an API-specific service object, make calls to the service, and process the response. + +### Build the client object + +The client object is the primary container for classes and configuration in the library. + +```php +$client = new Google\Client(); +$client->setApplicationName("My Application"); +$client->setDeveloperKey("MY_SIMPLE_API_KEY"); +``` + +### Build the service object + +Services are called through queries to service specific objects. These are created by constructing the service object, and passing an instance of `Google\Client` to it. `Google\Client` contains the IO, authentication and other classes required by the service to function, and the service informs the client which scopes it uses to provide a default when authenticating a user. + +```php +$service = new Google\Service\Books($client); +``` + +### Calling an API + +Each API provides resources and methods, usually in a chain. These can be accessed from the service object in the form `$service->resource->method(args)`. Most method require some arguments, then accept a final parameter of an array containing optional parameters. For example, with the Google Books API, we can make a call to list volumes matching a certain string, and add an optional _filter_ parameter. + +```php +$optParams = array('filter' => 'free-ebooks'); +$results = $service->volumes->listVolumes('Henry David Thoreau', $optParams); +``` + +### Handling the result + +There are two main types of response - items and collections of items. Each can be accessed either as an object or as an array. Collections implement the `Iterator` interface so can be used in foreach and other constructs. + +```php +foreach ($results as $item) { + echo $item['volumeInfo']['title'], "
\n"; +} +``` + +**Properties are hydrated according to the value in the Response. If a property is not present in the Response it will be set to null. Some fields won't be in the Response if you didn't ask for them in the Request using the `fields` property. Therefore, watchout for null properties, maybe they have a value already but are null because they're not present in the Response.** + +## Google App Engine support + +This library works well with Google App Engine applications. The Memcache class is automatically used for caching, and the file IO is implemented with the use of the Streams API. diff --git a/examples/README.md b/examples/README.md index 447a93a22..b3f62d4cb 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,4 +10,3 @@ ``` 1. Point your browser to the host and port you specified, i.e `http://localhost:8000`. - diff --git a/examples/batch.php b/examples/batch.php index 94118185f..3fea6085f 100644 --- a/examples/batch.php +++ b/examples/batch.php @@ -32,17 +32,17 @@ setDeveloperKey, the request may still succeed using the anonymous quota. ************************************************/ -$client = new Google_Client(); +$client = new Google\Client(); $client->setApplicationName("Client_Library_Examples"); // Warn if the API key isn't set. if (!$apiKey = getApiKey()) { - echo missingApiKeyWarning(); - return; + echo missingApiKeyWarning(); + return; } $client->setDeveloperKey($apiKey); -$service = new Google_Service_Books($client); +$service = new Google\Service\Books($client); /************************************************ To actually make the batch call we need to @@ -58,30 +58,36 @@ want to execute with keys of our choice - these keys will be reflected in the returned array. ************************************************/ -$batch = new Google_Http_Batch($client); -$optParams = array('filter' => 'free-ebooks'); -$req1 = $service->volumes->listVolumes('Henry David Thoreau', $optParams); + +// NOTE: Some services use `new Google\Http\Batch($client);` instead +$batch = $service->createBatch(); + +$query = 'Henry David Thoreau'; +$optParams = ['filter' => 'free-ebooks']; +$req1 = $service->volumes->listVolumes($query, $optParams); $batch->add($req1, "thoreau"); -$req2 = $service->volumes->listVolumes('George Bernard Shaw', $optParams); +$query = 'George Bernard Shaw'; +$req2 = $service->volumes->listVolumes($query, $optParams); $batch->add($req2, "shaw"); /************************************************ Executing the batch will send all requests off at once. ************************************************/ + $results = $batch->execute(); ?>

Results Of Call 1:

- - + +

Results Of Call 2:

- - + +
- +setAuthConfig($oauth_credentials); $client->setRedirectUri($redirect_uri); $client->setScopes('email'); @@ -45,26 +45,26 @@ * local access token in this case ************************************************/ if (isset($_REQUEST['logout'])) { - unset($_SESSION['id_token_token']); + unset($_SESSION['id_token_token']); } /************************************************ * If we have a code back from the OAuth 2.0 flow, * we need to exchange that with the - * Google_Client::fetchAccessTokenWithAuthCode() + * Google\Client::fetchAccessTokenWithAuthCode() * function. We store the resultant access token * bundle in the session, and redirect to ourself. ************************************************/ if (isset($_GET['code'])) { - $token = $client->fetchAccessTokenWithAuthCode($_GET['code']); - $client->setAccessToken($token); + $token = $client->fetchAccessTokenWithAuthCode($_GET['code'], $_SESSION['code_verifier']); - // store in the session also - $_SESSION['id_token_token'] = $token; + // store in the session also + $_SESSION['id_token_token'] = $token; - // redirect back to the example - header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); + // redirect back to the example + header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); + return; } /************************************************ @@ -75,9 +75,10 @@ !empty($_SESSION['id_token_token']) && isset($_SESSION['id_token_token']['id_token']) ) { - $client->setAccessToken($_SESSION['id_token_token']); + $client->setAccessToken($_SESSION['id_token_token']); } else { - $authUrl = $client->createAuthUrl(); + $_SESSION['code_verifier'] = $client->getOAuth2Service()->generateCodeVerifier(); + $authUrl = $client->createAuthUrl(); } /************************************************ @@ -89,16 +90,16 @@ and that can be cached. ************************************************/ if ($client->getAccessToken()) { - $token_data = $client->verifyIdToken(); + $token_data = $client->verifyIdToken(); } ?>
- +
- +

Here is the data from your Id Token:

@@ -106,4 +107,4 @@
- + - - To view this example, run the following command from the root directory of this repository: + + To view this example, run the following command from the root directory of this repository: - php -S localhost:8080 -t examples/ + php -S localhost:8080 -t examples/ - And then browse to "localhost:8080" in your web browser - + And then browse to "localhost:8080" in your web browser + - - - - API Key set! - + + + + API Key set! + - +
You have not entered your API key -
- API Key: - + " method="POST"> + API Key: +
This can be found in the Google API Console
@@ -31,13 +31,13 @@ - +setAuthConfig($oauth_credentials); +$client->setRedirectUri($redirect_uri); +$client->addScope("https://www.googleapis.com/auth/drive"); +$service = new Google\Service\Drive($client); + +/************************************************ + * If we have a code back from the OAuth 2.0 flow, + * we need to exchange that with the + * Google\Client::fetchAccessTokenWithAuthCode() + * function. We store the resultant access token + * bundle in the session, and redirect to ourself. + ************************************************/ +if (isset($_GET['code'])) { + $token = $client->fetchAccessTokenWithAuthCode($_GET['code'], $_SESSION['code_verifier']); + $client->setAccessToken($token); + + // store in the session also + $_SESSION['upload_token'] = $token; + + // redirect back to the example + header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); +} + +// set the access token as part of the client +if (!empty($_SESSION['upload_token'])) { + $client->setAccessToken($_SESSION['upload_token']); + if ($client->isAccessTokenExpired()) { + unset($_SESSION['upload_token']); + } +} else { + $_SESSION['code_verifier'] = $client->getOAuth2Service()->generateCodeVerifier(); + $authUrl = $client->createAuthUrl(); +} + +/************************************************ + * If we're signed in then lets try to download our + * file. + ************************************************/ +if ($client->getAccessToken()) { + // Check for "Big File" and include the file ID and size + $files = $service->files->listFiles([ + 'q' => "name='Big File'", + 'fields' => 'files(id,size)' + ]); + + if (count($files) == 0) { + echo " +

+ Before you can use this sample, you need to + upload a large file to Drive. +

"; + return; + } + + // If this is a POST, download the file + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + // Determine the file's size and ID + $fileId = $files[0]->id; + $fileSize = intval($files[0]->size); + + // Get the authorized Guzzle HTTP client + $http = $client->authorize(); + + // Open a file for writing + $fp = fopen('Big File (downloaded)', 'w'); + + // Download in 1 MB chunks + $chunkSizeBytes = 1 * 1024 * 1024; + $chunkStart = 0; + + // Iterate over each chunk and write it to our file + while ($chunkStart < $fileSize) { + $chunkEnd = $chunkStart + $chunkSizeBytes; + $response = $http->request( + 'GET', + sprintf('/drive/v3/files/%s', $fileId), + [ + 'query' => ['alt' => 'media'], + 'headers' => [ + 'Range' => sprintf('bytes=%s-%s', $chunkStart, $chunkEnd) + ] + ] + ); + $chunkStart = $chunkEnd + 1; + fwrite($fp, $response->getBody()->getContents()); + } + // close the file pointer + fclose($fp); + + // redirect back to this example + header('Location: ' . filter_var($redirect_uri . '?downloaded', FILTER_SANITIZE_URL)); + } +} +?> + +
+ + + +
+

Your call was successful! Check your filesystem for the file:

+

Big File (downloaded)

+
+ +
+ +
+ +
+ +setAuthConfig($oauth_credentials); $client->setRedirectUri($redirect_uri); $client->addScope("https://www.googleapis.com/auth/drive"); -$service = new Google_Service_Drive($client); +$service = new Google\Service\Drive($client); // add "?logout" to the URL to remove a token from the session if (isset($_REQUEST['logout'])) { - unset($_SESSION['upload_token']); + unset($_SESSION['upload_token']); } /************************************************ * If we have a code back from the OAuth 2.0 flow, * we need to exchange that with the - * Google_Client::fetchAccessTokenWithAuthCode() + * Google\Client::fetchAccessTokenWithAuthCode() * function. We store the resultant access token * bundle in the session, and redirect to ourself. ************************************************/ if (isset($_GET['code'])) { - $token = $client->fetchAccessTokenWithAuthCode($_GET['code']); - $client->setAccessToken($token); + $token = $client->fetchAccessTokenWithAuthCode($_GET['code'], $_SESSION['code_verifier']); + $client->setAccessToken($token); - // store in the session also - $_SESSION['upload_token'] = $token; + // store in the session also + $_SESSION['upload_token'] = $token; - // redirect back to the example - header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); + // redirect back to the example + header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); } // set the access token as part of the client if (!empty($_SESSION['upload_token'])) { - $client->setAccessToken($_SESSION['upload_token']); - if ($client->isAccessTokenExpired()) { - unset($_SESSION['upload_token']); - } + $client->setAccessToken($_SESSION['upload_token']); + if ($client->isAccessTokenExpired()) { + unset($_SESSION['upload_token']); + } } else { - $authUrl = $client->createAuthUrl(); + $_SESSION['code_verifier'] = $client->getOAuth2Service()->generateCodeVerifier(); + $authUrl = $client->createAuthUrl(); } /************************************************ @@ -78,69 +79,70 @@ * file. ************************************************/ if ($_SERVER['REQUEST_METHOD'] == 'POST' && $client->getAccessToken()) { - /************************************************ - * We'll setup an empty 20MB file to upload. - ************************************************/ - DEFINE("TESTFILE", 'testfile.txt'); - if (!file_exists(TESTFILE)) { - $fh = fopen(TESTFILE, 'w'); - fseek($fh, 1024*1024*20); - fwrite($fh, "!", 1); - fclose($fh); - } - - $file = new Google_Service_Drive_DriveFile(); - $file->name = "Big File"; - $chunkSizeBytes = 1 * 1024 * 1024; - - // Call the API with the media upload, defer so it doesn't immediately return. - $client->setDefer(true); - $request = $service->files->create($file); - - // Create a media file upload to represent our upload process. - $media = new Google_Http_MediaFileUpload( - $client, - $request, - 'text/plain', - null, - true, - $chunkSizeBytes - ); - $media->setFileSize(filesize(TESTFILE)); - - // Upload the various chunks. $status will be false until the process is - // complete. - $status = false; - $handle = fopen(TESTFILE, "rb"); - while (!$status && !feof($handle)) { - // read until you get $chunkSizeBytes from TESTFILE - // fread will never return more than 8192 bytes if the stream is read buffered and it does not represent a plain file - // An example of a read buffered file is when reading from a URL - $chunk = readVideoChunk($handle, $chunkSizeBytes); - $status = $media->nextChunk($chunk); - } - - // The final value of $status will be the data from the API for the object - // that has been uploaded. - $result = false; - if ($status != false) { - $result = $status; - } - - fclose($handle); + /************************************************ + * We'll setup an empty 20MB file to upload. + ************************************************/ + DEFINE("TESTFILE", 'testfile.txt'); + if (!file_exists(TESTFILE)) { + $fh = fopen(TESTFILE, 'w'); + fseek($fh, 1024*1024*20); + fwrite($fh, "!", 1); + fclose($fh); + } + + $file = new Google\Service\Drive\DriveFile(); + $file->name = "Big File"; + $chunkSizeBytes = 1 * 1024 * 1024; + + // Call the API with the media upload, defer so it doesn't immediately return. + $client->setDefer(true); + $request = $service->files->create($file); + + // Create a media file upload to represent our upload process. + $media = new Google\Http\MediaFileUpload( + $client, + $request, + 'text/plain', + null, + true, + $chunkSizeBytes + ); + $media->setFileSize(filesize(TESTFILE)); + + // Upload the various chunks. $status will be false until the process is + // complete. + $status = false; + $handle = fopen(TESTFILE, "rb"); + while (!$status && !feof($handle)) { + // read until you get $chunkSizeBytes from TESTFILE + // fread will never return more than 8192 bytes if the stream is read + // buffered and it does not represent a plain file + // An example of a read buffered file is when reading from a URL + $chunk = readVideoChunk($handle, $chunkSizeBytes); + $status = $media->nextChunk($chunk); + } + + // The final value of $status will be the data from the API for the object + // that has been uploaded. + $result = false; + if ($status != false) { + $result = $status; + } + + fclose($handle); } -function readVideoChunk ($handle, $chunkSize) +function readVideoChunk($handle, $chunkSize) { $byteCount = 0; $giantChunk = ""; while (!feof($handle)) { - // fread will never return more than 8192 bytes if the stream is read buffered and it does not represent a plain file + // fread will never return more than 8192 bytes if the stream is read + // buffered and it does not represent a plain file $chunk = fread($handle, 8192); $byteCount += strlen($chunk); $giantChunk .= $chunk; - if ($byteCount >= $chunkSize) - { + if ($byteCount >= $chunkSize) { return $giantChunk; } } @@ -149,20 +151,21 @@ function readVideoChunk ($handle, $chunkSize) ?>
- + - +

Your call was successful! Check your drive for this file:

name ?>

+

Now try downloading a large file from Drive.

- +
- +setAuthConfig($oauth_credentials); $client->setRedirectUri($redirect_uri); $client->addScope("https://www.googleapis.com/auth/drive"); @@ -43,78 +43,79 @@ // add "?logout" to the URL to remove a token from the session if (isset($_REQUEST['logout'])) { - unset($_SESSION['multi-api-token']); + unset($_SESSION['multi-api-token']); } /************************************************ * If we have a code back from the OAuth 2.0 flow, * we need to exchange that with the - * Google_Client::fetchAccessTokenWithAuthCode() + * Google\Client::fetchAccessTokenWithAuthCode() * function. We store the resultant access token * bundle in the session, and redirect to ourself. ************************************************/ if (isset($_GET['code'])) { - $token = $client->fetchAccessTokenWithAuthCode($_GET['code']); - $client->setAccessToken($token); + $token = $client->fetchAccessTokenWithAuthCode($_GET['code'], $_SESSION['code_verifier']); + $client->setAccessToken($token); - // store in the session also - $_SESSION['multi-api-token'] = $token; + // store in the session also + $_SESSION['multi-api-token'] = $token; - // redirect back to the example - header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); + // redirect back to the example + header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); } // set the access token as part of the client if (!empty($_SESSION['multi-api-token'])) { - $client->setAccessToken($_SESSION['multi-api-token']); - if ($client->isAccessTokenExpired()) { - unset($_SESSION['multi-api-token']); - } + $client->setAccessToken($_SESSION['multi-api-token']); + if ($client->isAccessTokenExpired()) { + unset($_SESSION['multi-api-token']); + } } else { - $authUrl = $client->createAuthUrl(); + $_SESSION['code_verifier'] = $client->getOAuth2Service()->generateCodeVerifier(); + $authUrl = $client->createAuthUrl(); } /************************************************ We are going to create both YouTube and Drive services, and query both. ************************************************/ -$yt_service = new Google_Service_YouTube($client); -$dr_service = new Google_Service_Drive($client); +$yt_service = new Google\Service\YouTube($client); +$dr_service = new Google\Service\Drive($client); /************************************************ If we're signed in, retrieve channels from YouTube and a list of files from Drive. ************************************************/ if ($client->getAccessToken()) { - $_SESSION['multi-api-token'] = $client->getAccessToken(); + $_SESSION['multi-api-token'] = $client->getAccessToken(); - $dr_results = $dr_service->files->listFiles(array('pageSize' => 10)); + $dr_results = $dr_service->files->listFiles(['pageSize' => 10]); - $yt_channels = $yt_service->channels->listChannels('contentDetails', array("mine" => true)); - $likePlaylist = $yt_channels[0]->contentDetails->relatedPlaylists->likes; - $yt_results = $yt_service->playlistItems->listPlaylistItems( - "snippet", - array("playlistId" => $likePlaylist) - ); + $yt_channels = $yt_service->channels->listChannels('contentDetails', ["mine" => true]); + $likePlaylist = $yt_channels[0]->contentDetails->relatedPlaylists->likes; + $yt_results = $yt_service->playlistItems->listPlaylistItems( + "snippet", + ["playlistId" => $likePlaylist] + ); } ?>
- + - +

Results Of Drive List:

- - name ?>
- + + name ?>
+

Results Of YouTube Likes:

- -
- + +
+
- +setAuthConfig($credentials_file); + // set the location manually + $client->setAuthConfig($credentials_file); } elseif (getenv('GOOGLE_APPLICATION_CREDENTIALS')) { - // use the application default credentials - $client->useApplicationDefaultCredentials(); + // use the application default credentials + $client->useApplicationDefaultCredentials(); } else { - echo missingServiceAccountDetailsWarning(); - return; + echo missingServiceAccountDetailsWarning(); + return; } $client->setApplicationName("Client_Library_Examples"); $client->setScopes(['https://www.googleapis.com/auth/books']); -$service = new Google_Service_Books($client); +$service = new Google\Service\Books($client); /************************************************ We're just going to make the same call as in the simple query as an example. ************************************************/ -$optParams = array('filter' => 'free-ebooks'); -$results = $service->volumes->listVolumes('Henry David Thoreau', $optParams); +$query = 'Henry David Thoreau'; +$optParams = [ + 'filter' => 'free-ebooks', +]; +$results = $service->volumes->listVolumes($query, $optParams); ?>

Results Of Call:

- - + +
- +setAuthConfig($oauth_credentials); $client->setRedirectUri($redirect_uri); $client->addScope("https://www.googleapis.com/auth/drive"); -$service = new Google_Service_Drive($client); +$service = new Google\Service\Drive($client); // add "?logout" to the URL to remove a token from the session if (isset($_REQUEST['logout'])) { - unset($_SESSION['upload_token']); + unset($_SESSION['upload_token']); } /************************************************ * If we have a code back from the OAuth 2.0 flow, * we need to exchange that with the - * Google_Client::fetchAccessTokenWithAuthCode() + * Google\Client::fetchAccessTokenWithAuthCode() * function. We store the resultant access token * bundle in the session, and redirect to ourself. ************************************************/ if (isset($_GET['code'])) { - $token = $client->fetchAccessTokenWithAuthCode($_GET['code']); - $client->setAccessToken($token); + $token = $client->fetchAccessTokenWithAuthCode($_GET['code'], $_SESSION['code_verifier']); + $client->setAccessToken($token); - // store in the session also - $_SESSION['upload_token'] = $token; + // store in the session also + $_SESSION['upload_token'] = $token; - // redirect back to the example - header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); + // redirect back to the example + header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); } // set the access token as part of the client if (!empty($_SESSION['upload_token'])) { - $client->setAccessToken($_SESSION['upload_token']); - if ($client->isAccessTokenExpired()) { - unset($_SESSION['upload_token']); - } + $client->setAccessToken($_SESSION['upload_token']); + if ($client->isAccessTokenExpired()) { + unset($_SESSION['upload_token']); + } } else { - $authUrl = $client->createAuthUrl(); + $_SESSION['code_verifier'] = $client->getOAuth2Service()->generateCodeVerifier(); + $authUrl = $client->createAuthUrl(); } /************************************************ @@ -78,46 +79,46 @@ * file. For larger files, see fileupload.php. ************************************************/ if ($_SERVER['REQUEST_METHOD'] == 'POST' && $client->getAccessToken()) { - // We'll setup an empty 1MB file to upload. - DEFINE("TESTFILE", 'testfile-small.txt'); - if (!file_exists(TESTFILE)) { - $fh = fopen(TESTFILE, 'w'); - fseek($fh, 1024 * 1024); - fwrite($fh, "!", 1); - fclose($fh); - } + // We'll setup an empty 1MB file to upload. + DEFINE("TESTFILE", 'testfile-small.txt'); + if (!file_exists(TESTFILE)) { + $fh = fopen(TESTFILE, 'w'); + fseek($fh, 1024 * 1024); + fwrite($fh, "!", 1); + fclose($fh); + } - // This is uploading a file directly, with no metadata associated. - $file = new Google_Service_Drive_DriveFile(); - $result = $service->files->create( - $file, - array( - 'data' => file_get_contents(TESTFILE), - 'mimeType' => 'application/octet-stream', - 'uploadType' => 'media' - ) - ); + // This is uploading a file directly, with no metadata associated. + $file = new Google\Service\Drive\DriveFile(); + $result = $service->files->create( + $file, + [ + 'data' => file_get_contents(TESTFILE), + 'mimeType' => 'application/octet-stream', + 'uploadType' => 'media' + ] + ); - // Now lets try and send the metadata as well using multipart! - $file = new Google_Service_Drive_DriveFile(); - $file->setName("Hello World!"); - $result2 = $service->files->create( - $file, - array( - 'data' => file_get_contents(TESTFILE), - 'mimeType' => 'application/octet-stream', - 'uploadType' => 'multipart' - ) - ); + // Now lets try and send the metadata as well using multipart! + $file = new Google\Service\Drive\DriveFile(); + $file->setName("Hello World!"); + $result2 = $service->files->create( + $file, + [ + 'data' => file_get_contents(TESTFILE), + 'mimeType' => 'application/octet-stream', + 'uploadType' => 'multipart' + ] + ); } ?>
- + - +

Your call was successful! Check your drive for the following files:

- +
- +setApplicationName("Client_Library_Examples"); // Warn if the API key isn't set. if (!$apiKey = getApiKey()) { - echo missingApiKeyWarning(); - return; + echo missingApiKeyWarning(); + return; } $client->setDeveloperKey($apiKey); -$service = new Google_Service_Books($client); +$service = new Google\Service\Books($client); /************************************************ We make a call to our service, which will @@ -47,15 +47,21 @@ (the query), and an array of named optional parameters. ************************************************/ -$optParams = array('filter' => 'free-ebooks'); -$results = $service->volumes->listVolumes('Henry David Thoreau', $optParams); +$query = 'Henry David Thoreau'; +$optParams = [ + 'filter' => 'free-ebooks', +]; +$results = $service->volumes->listVolumes($query, $optParams); /************************************************ This is an example of deferring a call. ***********************************************/ $client->setDefer(true); -$optParams = array('filter' => 'free-ebooks'); -$request = $service->volumes->listVolumes('Henry David Thoreau', $optParams); +$query = 'Henry David Thoreau'; +$optParams = [ + 'filter' => 'free-ebooks', +]; +$request = $service->volumes->listVolumes($query, $optParams); $resultsDeferred = $client->execute($request); /************************************************ @@ -64,21 +70,21 @@ array. Some calls will return a single item which we can immediately use. The individual responses - are typed as Google_Service_Books_Volume, but + are typed as Google\Service\Books_Volume, but can be treated as an array. ************************************************/ ?>

Results Of Call:

- - + +

Results Of Deferred Call:

- - + +
- + + $ret = " " . $title . " \n"; - if ($_SERVER['PHP_SELF'] != "/index.php") { - $ret .= "

Back

"; - } - $ret .= "

" . $title . "

"; + if ($_SERVER['PHP_SELF'] != "/index.php") { + $ret .= "

Back

"; + } + $ret .= "

" . $title . "

"; - // Start the session (for storing access tokens and things) - if (!headers_sent()) { - session_start(); - } + // Start the session (for storing access tokens and things) + if (!headers_sent()) { + session_start(); + } - return $ret; + return $ret; } function pageFooter($file = null) { - $ret = ""; - if ($file) { - $ret .= "

Code:

"; - $ret .= "
";
-    $ret .= htmlspecialchars(file_get_contents($file));
-    $ret .= "
"; - } - $ret .= ""; - - return $ret; + $ret = ""; + if ($file) { + $ret .= "

Code:

"; + $ret .= "
";
+        $ret .= htmlspecialchars(file_get_contents($file));
+        $ret .= "
"; + } + $ret .= ""; + + return $ret; } function missingApiKeyWarning() { - $ret = " + $ret = "

Warning: You need to set a Simple API Access key from the Google API console

"; - return $ret; + return $ret; } function missingClientSecretsWarning() { - $ret = " + $ret = "

Warning: You need to set Client ID, Client Secret and Redirect URI from the Google API console

"; - return $ret; + return $ret; } function missingServiceAccountDetailsWarning() { - $ret = " + $ret = "

Warning: You need download your Service Account Credentials JSON from the Google API console. @@ -81,12 +81,12 @@ function missingServiceAccountDetailsWarning() as the path to this file, but in the context of this example we will do this for you.

"; - return $ret; + return $ret; } function missingOAuth2CredentialsWarning() { - $ret = " + $ret = "

Warning: You need to set the location of your OAuth2 Client Credentials from the Google API console. @@ -96,46 +96,72 @@ function missingOAuth2CredentialsWarning() rename them 'oauth-credentials.json'.

"; - return $ret; + return $ret; +} + +function invalidCsrfTokenWarning() +{ + $ret = " +

+ The CSRF token is invalid, your session probably expired. Please refresh the page. +

"; + + return $ret; } function checkServiceAccountCredentialsFile() { - // service account creds - $application_creds = __DIR__ . '/../../service-account-credentials.json'; + // service account creds + $application_creds = __DIR__ . '/../../service-account-credentials.json'; - return file_exists($application_creds) ? $application_creds : false; + return file_exists($application_creds) ? $application_creds : false; +} + +function getCsrfToken() +{ + if (!isset($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(openssl_random_pseudo_bytes(32)); + } + + return $_SESSION['csrf_token']; +} + +function validateCsrfToken() +{ + return isset($_REQUEST['csrf_token']) + && isset($_SESSION['csrf_token']) + && $_REQUEST['csrf_token'] === $_SESSION['csrf_token']; } function getOAuthCredentialsFile() { - // oauth2 creds - $oauth_creds = __DIR__ . '/../../oauth-credentials.json'; + // oauth2 creds + $oauth_creds = __DIR__ . '/../../oauth-credentials.json'; - if (file_exists($oauth_creds)) { - return $oauth_creds; - } + if (file_exists($oauth_creds)) { + return $oauth_creds; + } - return false; + return false; } function setClientCredentialsFile($apiKey) { - $file = __DIR__ . '/../../tests/.apiKey'; - file_put_contents($file, $apiKey); + $file = __DIR__ . '/../../tests/.apiKey'; + file_put_contents($file, $apiKey); } function getApiKey() { - $file = __DIR__ . '/../../tests/.apiKey'; - if (file_exists($file)) { - return file_get_contents($file); - } + $file = __DIR__ . '/../../tests/.apiKey'; + if (file_exists($file)) { + return file_get_contents($file); + } } function setApiKey($apiKey) { - $file = __DIR__ . '/../../tests/.apiKey'; - file_put_contents($file, $apiKey); + $file = __DIR__ . '/../../tests/.apiKey'; + file_put_contents($file, $apiKey); } diff --git a/examples/url-shortener.php b/examples/url-shortener.php deleted file mode 100644 index f9110984f..000000000 --- a/examples/url-shortener.php +++ /dev/null @@ -1,133 +0,0 @@ -setAuthConfig($oauth_credentials); -$client->setRedirectUri($redirect_uri); -$client->addScope("https://www.googleapis.com/auth/urlshortener"); - -/************************************************ - * When we create the service here, we pass the - * client to it. The client then queries the service - * for the required scopes, and uses that when - * generating the authentication URL later. - ************************************************/ -$service = new Google_Service_Urlshortener($client); - -/************************************************ - * If we're logging out we just need to clear our - * local access token in this case - ************************************************/ -if (isset($_REQUEST['logout'])) { - unset($_SESSION['access_token']); -} - -/************************************************ - * If we have a code back from the OAuth 2.0 flow, - * we need to exchange that with the - * Google_Client::fetchAccessTokenWithAuthCode() - * function. We store the resultant access token - * bundle in the session, and redirect to ourself. - ************************************************/ -if (isset($_GET['code'])) { - $token = $client->fetchAccessTokenWithAuthCode($_GET['code']); - $client->setAccessToken($token); - - // store in the session also - $_SESSION['access_token'] = $token; - - // redirect back to the example - header('Location: ' . filter_var($redirect_uri, FILTER_SANITIZE_URL)); -} - -/************************************************ - If we have an access token, we can make - requests, else we generate an authentication URL. - ************************************************/ -if (isset($_SESSION['access_token']) && $_SESSION['access_token']) { - $client->setAccessToken($_SESSION['access_token']); -} else { - $authUrl = $client->createAuthUrl(); -} - -/************************************************ - If we're signed in and have a request to shorten - a URL, then we create a new URL object, set the - unshortened URL, and call the 'insert' method on - the 'url' resource. Note that we re-store the - access_token bundle, just in case anything - changed during the request - the main thing that - might happen here is the access token itself is - refreshed if the application has offline access. - ************************************************/ -if ($client->getAccessToken() && isset($_GET['url'])) { - $url = new Google_Service_Urlshortener_Url(); - $url->longUrl = $_GET['url']; - $short = $service->url->insert($url); - $_SESSION['access_token'] = $client->getAccessToken(); -} -?> - -
- - - -
- - -
- Logout - - You created a short link!
- -
-
-
- Create another - -
- - - - Service/*.php + + *Test.php @@ -64,13 +64,16 @@ 0 + + + - + - + @@ -84,7 +87,9 @@ - + + src/aliases\.php + - - - - + + + src/aliases\.php - @@ -151,9 +160,5 @@ - - - - - + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 000000000..dcef11342 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,5 @@ +parameters: + treatPhpDocTypesAsCertain: false + level: 5 + paths: + - src diff --git a/phpunit.xml.dist b/phpunit.xml.dist index da9664ea9..1e07db961 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,19 +1,16 @@ - - - - tests/Google - - - tests/examples - - - - - ./src/Google - - + + + + ./src + + + + + tests/Google + + + tests/examples + + diff --git a/src/AccessToken/Revoke.php b/src/AccessToken/Revoke.php new file mode 100644 index 000000000..dc8b0c351 --- /dev/null +++ b/src/AccessToken/Revoke.php @@ -0,0 +1,81 @@ +http = $http; + } + + /** + * Revoke an OAuth2 access token or refresh token. This method will revoke the current access + * token, if a token isn't provided. + * + * @param string|array $token The token (access token or a refresh token) that should be revoked. + * @return boolean Returns True if the revocation was successful, otherwise False. + */ + public function revokeToken($token) + { + if (is_array($token)) { + if (isset($token['refresh_token'])) { + $token = $token['refresh_token']; + } else { + $token = $token['access_token']; + } + } + + $body = Psr7\Utils::streamFor(http_build_query(['token' => $token])); + $request = new Request( + 'POST', + Client::OAUTH2_REVOKE_URI, + [ + 'Cache-Control' => 'no-store', + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + $body + ); + + $httpHandler = HttpHandlerFactory::build($this->http); + + $response = $httpHandler($request); + + return $response->getStatusCode() == 200; + } +} diff --git a/src/AccessToken/Verify.php b/src/AccessToken/Verify.php new file mode 100644 index 000000000..5529450e5 --- /dev/null +++ b/src/AccessToken/Verify.php @@ -0,0 +1,264 @@ +http = $http; + $this->cache = $cache; + $this->jwt = $jwt ?: $this->getJwtService(); + } + + /** + * Verifies an id token and returns the authenticated apiLoginTicket. + * Throws an exception if the id token is not valid. + * The audience parameter can be used to control which id tokens are + * accepted. By default, the id token must have been issued to this OAuth2 client. + * + * @param string $idToken the ID token in JWT format + * @param string $audience Optional. The audience to verify against JWt "aud" + * @return array|false the token payload, if successful + */ + public function verifyIdToken($idToken, $audience = null) + { + if (empty($idToken)) { + throw new LogicException('id_token cannot be null'); + } + + // set phpseclib constants if applicable + $this->setPhpsecConstants(); + + // Check signature + $certs = $this->getFederatedSignOnCerts(); + foreach ($certs as $cert) { + try { + $args = [$idToken]; + $publicKey = $this->getPublicKey($cert); + if (class_exists(Key::class)) { + $args[] = new Key($publicKey, 'RS256'); + } else { + $args[] = $publicKey; + $args[] = ['RS256']; + } + $payload = \call_user_func_array([$this->jwt, 'decode'], $args); + + if (property_exists($payload, 'aud')) { + if ($audience && $payload->aud != $audience) { + return false; + } + } + + // support HTTP and HTTPS issuers + // @see https://developers.google.com/identity/sign-in/web/backend-auth + $issuers = [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS]; + if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) { + return false; + } + + return (array)$payload; + } catch (ExpiredException $e) { // @phpstan-ignore-line + return false; + } catch (ExpiredExceptionV3 $e) { + return false; + } catch (SignatureInvalidException $e) { + // continue + } catch (DomainException $e) { + // continue + } + } + + return false; + } + + private function getCache() + { + return $this->cache; + } + + /** + * Retrieve and cache a certificates file. + * + * @param string $url location + * @return array certificates + * @throws \Google\Exception + */ + private function retrieveCertsFromLocation($url) + { + // If we're retrieving a local file, just grab it. + if (0 !== strpos($url, 'http')) { + if (!$file = file_get_contents($url)) { + throw new GoogleException( + "Failed to retrieve verification certificates: '". + $url."'." + ); + } + + return json_decode($file, true); + } + + // @phpstan-ignore-next-line + $response = $this->http->get($url); + + if ($response->getStatusCode() == 200) { + return json_decode((string)$response->getBody(), true); + } + throw new GoogleException( + sprintf( + 'Failed to retrieve verification certificates: "%s".', + $response->getBody()->getContents() + ), + $response->getStatusCode() + ); + } + + // Gets federated sign-on certificates to use for verifying identity tokens. + // Returns certs as array structure, where keys are key ids, and values + // are PEM encoded certificates. + private function getFederatedSignOnCerts() + { + $certs = null; + if ($cache = $this->getCache()) { + $cacheItem = $cache->getItem('federated_signon_certs_v3'); + $certs = $cacheItem->get(); + } + + + if (!$certs) { + $certs = $this->retrieveCertsFromLocation( + self::FEDERATED_SIGNON_CERT_URL + ); + + if ($cache) { + $cacheItem->expiresAt(new DateTime('+1 hour')); + $cacheItem->set($certs); + $cache->save($cacheItem); + } + } + + if (!isset($certs['keys'])) { + throw new InvalidArgumentException( + 'federated sign-on certs expects "keys" to be set' + ); + } + + return $certs['keys']; + } + + private function getJwtService() + { + $jwt = new JWT(); + if ($jwt::$leeway < 1) { + // Ensures JWT leeway is at least 1 + // @see https://github.com/google/google-api-php-client/issues/827 + $jwt::$leeway = 1; + } + + return $jwt; + } + + private function getPublicKey($cert) + { + $modulus = new BigInteger($this->jwt->urlsafeB64Decode($cert['n']), 256); + $exponent = new BigInteger($this->jwt->urlsafeB64Decode($cert['e']), 256); + $component = ['n' => $modulus, 'e' => $exponent]; + + $loader = PublicKeyLoader::load($component); + + return $loader->toString('PKCS8'); + } + + /** + * phpseclib calls "phpinfo" by default, which requires special + * whitelisting in the AppEngine VM environment. This function + * sets constants to bypass the need for phpseclib to check phpinfo + * + * @see phpseclib/Math/BigInteger + * @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85 + */ + private function setPhpsecConstants() + { + if (filter_var(getenv('GAE_VM'), FILTER_VALIDATE_BOOLEAN)) { + if (!defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) { + define('MATH_BIGINTEGER_OPENSSL_ENABLED', true); + } + if (!defined('CRYPT_RSA_MODE')) { + define('CRYPT_RSA_MODE', AES::ENGINE_OPENSSL); + } + } + } +} diff --git a/src/AuthHandler/AuthHandlerFactory.php b/src/AuthHandler/AuthHandlerFactory.php new file mode 100644 index 000000000..98a0ab166 --- /dev/null +++ b/src/AuthHandler/AuthHandlerFactory.php @@ -0,0 +1,49 @@ +cache = $cache; + $this->cacheConfig = $cacheConfig; + } + + public function attachCredentials( + ClientInterface $http, + CredentialsLoader $credentials, + ?callable $tokenCallback = null + ) { + // use the provided cache + if ($this->cache) { + $credentials = new FetchAuthTokenCache( + $credentials, + $this->cacheConfig, + $this->cache + ); + } + + return $this->attachCredentialsCache($http, $credentials, $tokenCallback); + } + + public function attachCredentialsCache( + ClientInterface $http, + FetchAuthTokenCache $credentials, + ?callable $tokenCallback = null + ) { + // if we end up needing to make an HTTP request to retrieve credentials, we + // can use our existing one, but we need to throw exceptions so the error + // bubbles up. + $authHttp = $this->createAuthHttp($http); + $authHttpHandler = HttpHandlerFactory::build($authHttp); + $middleware = new AuthTokenMiddleware( + $credentials, + $authHttpHandler, + $tokenCallback + ); + + $config = $http->getConfig(); + $config['handler']->remove('google_auth'); + $config['handler']->push($middleware, 'google_auth'); + $config['auth'] = 'google_auth'; + $http = new Client($config); + + return $http; + } + + public function attachToken(ClientInterface $http, array $token, array $scopes) + { + $tokenFunc = function ($scopes) use ($token) { + return $token['access_token']; + }; + + // Derive a cache prefix from the token, to ensure setting a new token + // results in a cache-miss. + // Note: Supplying a custom "prefix" will bust this behavior. + $cacheConfig = $this->cacheConfig; + if (!isset($cacheConfig['prefix']) && isset($token['access_token'])) { + $cacheConfig['prefix'] = substr(sha1($token['access_token']), -10); + } + + $middleware = new ScopedAccessTokenMiddleware( + $tokenFunc, + $scopes, + $cacheConfig, + $this->cache + ); + + $config = $http->getConfig(); + $config['handler']->remove('google_auth'); + $config['handler']->push($middleware, 'google_auth'); + $config['auth'] = 'scoped'; + $http = new Client($config); + + return $http; + } + + public function attachKey(ClientInterface $http, $key) + { + $middleware = new SimpleMiddleware(['key' => $key]); + + $config = $http->getConfig(); + $config['handler']->remove('google_auth'); + $config['handler']->push($middleware, 'google_auth'); + $config['auth'] = 'simple'; + $http = new Client($config); + + return $http; + } + + private function createAuthHttp(ClientInterface $http) + { + return new Client(['http_errors' => true] + $http->getConfig()); + } +} diff --git a/src/AuthHandler/Guzzle7AuthHandler.php b/src/AuthHandler/Guzzle7AuthHandler.php new file mode 100644 index 000000000..310f8b12c --- /dev/null +++ b/src/AuthHandler/Guzzle7AuthHandler.php @@ -0,0 +1,25 @@ +config = array_merge([ + 'application_name' => '', + 'base_path' => self::API_BASE_PATH, + 'client_id' => '', + 'client_secret' => '', + 'credentials' => null, + 'scopes' => null, + 'quota_project' => null, + 'redirect_uri' => null, + 'state' => null, + 'developer_key' => '', + 'use_application_default_credentials' => false, + 'signing_key' => null, + 'signing_algorithm' => null, + 'subject' => null, + 'hd' => '', + 'prompt' => '', + 'openid.realm' => '', + 'include_granted_scopes' => null, + 'logger' => null, + 'login_hint' => '', + 'request_visible_actions' => '', + 'access_type' => 'online', + 'approval_prompt' => 'auto', + 'retry' => [], + 'retry_map' => null, + 'cache' => null, + 'cache_config' => [], + 'token_callback' => null, + 'jwt' => null, + 'api_format_v2' => false, + 'universe_domain' => getenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN') + ?: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN, + ], $config); + + if (!is_null($this->config['credentials'])) { + if ($this->config['credentials'] instanceof CredentialsLoader) { + $this->credentials = $this->config['credentials']; + } else { + $this->setAuthConfig($this->config['credentials']); + } + unset($this->config['credentials']); + } + + if (!is_null($this->config['scopes'])) { + $this->setScopes($this->config['scopes']); + unset($this->config['scopes']); + } + + // Set a default token callback to update the in-memory access token + if (is_null($this->config['token_callback'])) { + $this->config['token_callback'] = function ($cacheKey, $newAccessToken) { + $this->setAccessToken( + [ + 'access_token' => $newAccessToken, + 'expires_in' => 3600, // Google default + 'created' => time(), + ] + ); + }; + } + + if (!is_null($this->config['cache'])) { + $this->setCache($this->config['cache']); + unset($this->config['cache']); + } + + if (!is_null($this->config['logger'])) { + $this->setLogger($this->config['logger']); + unset($this->config['logger']); + } + } + + /** + * Get a string containing the version of the library. + * + * @return string + */ + public function getLibraryVersion() + { + return self::LIBVER; + } + + /** + * For backwards compatibility + * alias for fetchAccessTokenWithAuthCode + * + * @param string $code string code from accounts.google.com + * @return array access token + * @deprecated + */ + public function authenticate($code) + { + return $this->fetchAccessTokenWithAuthCode($code); + } + + /** + * Attempt to exchange a code for an valid authentication token. + * Helper wrapped around the OAuth 2.0 implementation. + * + * @param string $code code from accounts.google.com + * @param string $codeVerifier the code verifier used for PKCE (if applicable) + * @return array access token + */ + public function fetchAccessTokenWithAuthCode($code, $codeVerifier = null) + { + if (strlen($code) == 0) { + throw new InvalidArgumentException("Invalid code"); + } + + $auth = $this->getOAuth2Service(); + $auth->setCode($code); + $auth->setRedirectUri($this->getRedirectUri()); + if ($codeVerifier) { + $auth->setCodeVerifier($codeVerifier); + } + + $httpHandler = HttpHandlerFactory::build($this->getHttpClient()); + $creds = $auth->fetchAuthToken($httpHandler); + if ($creds && isset($creds['access_token'])) { + $creds['created'] = time(); + $this->setAccessToken($creds); + } + + return $creds; + } + + /** + * For backwards compatibility + * alias for fetchAccessTokenWithAssertion + * + * @return array access token + * @deprecated + */ + public function refreshTokenWithAssertion() + { + return $this->fetchAccessTokenWithAssertion(); + } + + /** + * Fetches a fresh access token with a given assertion token. + * @param ClientInterface $authHttp optional. + * @return array access token + */ + public function fetchAccessTokenWithAssertion(?ClientInterface $authHttp = null) + { + if (!$this->isUsingApplicationDefaultCredentials()) { + throw new DomainException( + 'set the JSON service account credentials using' + . ' Google\Client::setAuthConfig or set the path to your JSON file' + . ' with the "GOOGLE_APPLICATION_CREDENTIALS" environment variable' + . ' and call Google\Client::useApplicationDefaultCredentials to' + . ' refresh a token with assertion.' + ); + } + + $this->getLogger()->log( + 'info', + 'OAuth2 access token refresh with Signed JWT assertion grants.' + ); + + $credentials = $this->createApplicationDefaultCredentials(); + + $httpHandler = HttpHandlerFactory::build($authHttp); + $creds = $credentials->fetchAuthToken($httpHandler); + if ($creds && isset($creds['access_token'])) { + $creds['created'] = time(); + $this->setAccessToken($creds); + } + + return $creds; + } + + /** + * For backwards compatibility + * alias for fetchAccessTokenWithRefreshToken + * + * @param string $refreshToken + * @return array access token + */ + public function refreshToken($refreshToken) + { + return $this->fetchAccessTokenWithRefreshToken($refreshToken); + } + + /** + * Fetches a fresh OAuth 2.0 access token with the given refresh token. + * @param string $refreshToken + * @return array access token + */ + public function fetchAccessTokenWithRefreshToken($refreshToken = null) + { + if (null === $refreshToken) { + if (!isset($this->token['refresh_token'])) { + throw new LogicException( + 'refresh token must be passed in or set as part of setAccessToken' + ); + } + $refreshToken = $this->token['refresh_token']; + } + $this->getLogger()->info('OAuth2 access token refresh'); + $auth = $this->getOAuth2Service(); + $auth->setRefreshToken($refreshToken); + + $httpHandler = HttpHandlerFactory::build($this->getHttpClient()); + $creds = $auth->fetchAuthToken($httpHandler); + if ($creds && isset($creds['access_token'])) { + $creds['created'] = time(); + if (!isset($creds['refresh_token'])) { + $creds['refresh_token'] = $refreshToken; + } + $this->setAccessToken($creds); + } + + return $creds; + } + + /** + * Create a URL to obtain user authorization. + * The authorization endpoint allows the user to first + * authenticate, and then grant/deny the access request. + * @param string|array $scope The scope is expressed as an array or list of space-delimited strings. + * @param array $queryParams Querystring params to add to the authorization URL. + * @return string + */ + public function createAuthUrl($scope = null, array $queryParams = []) + { + if (empty($scope)) { + $scope = $this->prepareScopes(); + } + if (is_array($scope)) { + $scope = implode(' ', $scope); + } + + // only accept one of prompt or approval_prompt + $approvalPrompt = $this->config['prompt'] + ? null + : $this->config['approval_prompt']; + + // include_granted_scopes should be string "true", string "false", or null + $includeGrantedScopes = $this->config['include_granted_scopes'] === null + ? null + : var_export($this->config['include_granted_scopes'], true); + + $params = array_filter([ + 'access_type' => $this->config['access_type'], + 'approval_prompt' => $approvalPrompt, + 'hd' => $this->config['hd'], + 'include_granted_scopes' => $includeGrantedScopes, + 'login_hint' => $this->config['login_hint'], + 'openid.realm' => $this->config['openid.realm'], + 'prompt' => $this->config['prompt'], + 'redirect_uri' => $this->config['redirect_uri'], + 'response_type' => 'code', + 'scope' => $scope, + 'state' => $this->config['state'], + ]) + $queryParams; + + // If the list of scopes contains plus.login, add request_visible_actions + // to auth URL. + $rva = $this->config['request_visible_actions']; + if (strlen($rva) > 0 && false !== strpos($scope, 'plus.login')) { + $params['request_visible_actions'] = $rva; + } + + $auth = $this->getOAuth2Service(); + + return (string) $auth->buildFullAuthorizationUri($params); + } + + /** + * Adds auth listeners to the HTTP client based on the credentials + * set in the Google API Client object + * + * @param ClientInterface $http the http client object. + * @return ClientInterface the http client object + */ + public function authorize(?ClientInterface $http = null) + { + $http = $http ?: $this->getHttpClient(); + $authHandler = $this->getAuthHandler(); + + // These conditionals represent the decision tree for authentication + // 1. Check if a Google\Auth\CredentialsLoader instance has been supplied via the "credentials" option + // 2. Check for Application Default Credentials + // 3a. Check for an Access Token + // 3b. If access token exists but is expired, try to refresh it + // 4. Check for API Key + if ($this->credentials) { + $this->checkUniverseDomain($this->credentials); + return $authHandler->attachCredentials( + $http, + $this->credentials, + $this->config['token_callback'] + ); + } + + if ($this->isUsingApplicationDefaultCredentials()) { + $credentials = $this->createApplicationDefaultCredentials(); + $this->checkUniverseDomain($credentials); + return $authHandler->attachCredentialsCache( + $http, + $credentials, + $this->config['token_callback'] + ); + } + + if ($token = $this->getAccessToken()) { + $scopes = $this->prepareScopes(); + // add refresh subscriber to request a new token + if (isset($token['refresh_token']) && $this->isAccessTokenExpired()) { + $credentials = $this->createUserRefreshCredentials( + $scopes, + $token['refresh_token'] + ); + $this->checkUniverseDomain($credentials); + return $authHandler->attachCredentials( + $http, + $credentials, + $this->config['token_callback'] + ); + } + + return $authHandler->attachToken($http, $token, (array) $scopes); + } + + if ($key = $this->config['developer_key']) { + return $authHandler->attachKey($http, $key); + } + + return $http; + } + + /** + * Set the configuration to use application default credentials for + * authentication + * + * @see https://developers.google.com/identity/protocols/application-default-credentials + * @param boolean $useAppCreds + */ + public function useApplicationDefaultCredentials($useAppCreds = true) + { + $this->config['use_application_default_credentials'] = $useAppCreds; + } + + /** + * To prevent useApplicationDefaultCredentials from inappropriately being + * called in a conditional + * + * @see https://developers.google.com/identity/protocols/application-default-credentials + */ + public function isUsingApplicationDefaultCredentials() + { + return $this->config['use_application_default_credentials']; + } + + /** + * Set the access token used for requests. + * + * Note that at the time requests are sent, tokens are cached. A token will be + * cached for each combination of service and authentication scopes. If a + * cache pool is not provided, creating a new instance of the client will + * allow modification of access tokens. If a persistent cache pool is + * provided, in order to change the access token, you must clear the cached + * token by calling `$client->getCache()->clear()`. (Use caution in this case, + * as calling `clear()` will remove all cache items, including any items not + * related to Google API PHP Client.) + * + * **NOTE:** The universe domain is assumed to be "googleapis.com" unless + * explicitly set. When setting an access token directly via this method, there + * is no way to verify the universe domain. Be sure to set the "universe_domain" + * option if "googleapis.com" is not intended. + * + * @param string|array $token + * @throws InvalidArgumentException + */ + public function setAccessToken($token) + { + if (is_string($token)) { + if ($json = json_decode($token, true)) { + $token = $json; + } else { + // assume $token is just the token string + $token = [ + 'access_token' => $token, + ]; + } + } + if ($token == null) { + throw new InvalidArgumentException('invalid json token'); + } + if (!isset($token['access_token'])) { + throw new InvalidArgumentException("Invalid token format"); + } + $this->token = $token; + } + + public function getAccessToken() + { + return $this->token; + } + + /** + * @return string|null + */ + public function getRefreshToken() + { + if (isset($this->token['refresh_token'])) { + return $this->token['refresh_token']; + } + + return null; + } + + /** + * Returns if the access_token is expired. + * @return bool Returns True if the access_token is expired. + */ + public function isAccessTokenExpired() + { + if (!$this->token) { + return true; + } + + $created = 0; + if (isset($this->token['created'])) { + $created = $this->token['created']; + } elseif (isset($this->token['id_token'])) { + // check the ID token for "iat" + // signature verification is not required here, as we are just + // using this for convenience to save a round trip request + // to the Google API server + $idToken = $this->token['id_token']; + if (substr_count($idToken, '.') == 2) { + $parts = explode('.', $idToken); + $payload = json_decode(base64_decode($parts[1]), true); + if ($payload && isset($payload['iat'])) { + $created = $payload['iat']; + } + } + } + if (!isset($this->token['expires_in'])) { + // if the token does not have an "expires_in", then it's considered expired + return true; + } + + // If the token is set to expire in the next 30 seconds. + return ($created + ($this->token['expires_in'] - 30)) < time(); + } + + /** + * @deprecated See UPGRADING.md for more information + */ + public function getAuth() + { + throw new BadMethodCallException( + 'This function no longer exists. See UPGRADING.md for more information' + ); + } + + /** + * @deprecated See UPGRADING.md for more information + */ + public function setAuth($auth) + { + throw new BadMethodCallException( + 'This function no longer exists. See UPGRADING.md for more information' + ); + } + + /** + * Set the OAuth 2.0 Client ID. + * @param string $clientId + */ + public function setClientId($clientId) + { + $this->config['client_id'] = $clientId; + } + + public function getClientId() + { + return $this->config['client_id']; + } + + /** + * Set the OAuth 2.0 Client Secret. + * @param string $clientSecret + */ + public function setClientSecret($clientSecret) + { + $this->config['client_secret'] = $clientSecret; + } + + public function getClientSecret() + { + return $this->config['client_secret']; + } + + /** + * Set the OAuth 2.0 Redirect URI. + * @param string $redirectUri + */ + public function setRedirectUri($redirectUri) + { + $this->config['redirect_uri'] = $redirectUri; + } + + public function getRedirectUri() + { + return $this->config['redirect_uri']; + } + + /** + * Set OAuth 2.0 "state" parameter to achieve per-request customization. + * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.1.2.2 + * @param string $state + */ + public function setState($state) + { + $this->config['state'] = $state; + } + + /** + * @param string $accessType Possible values for access_type include: + * {@code "offline"} to request offline access from the user. + * {@code "online"} to request online access from the user. + */ + public function setAccessType($accessType) + { + $this->config['access_type'] = $accessType; + } + + /** + * @param string $approvalPrompt Possible values for approval_prompt include: + * {@code "force"} to force the approval UI to appear. + * {@code "auto"} to request auto-approval when possible. (This is the default value) + */ + public function setApprovalPrompt($approvalPrompt) + { + $this->config['approval_prompt'] = $approvalPrompt; + } + + /** + * Set the login hint, email address or sub id. + * @param string $loginHint + */ + public function setLoginHint($loginHint) + { + $this->config['login_hint'] = $loginHint; + } + + /** + * Set the application name, this is included in the User-Agent HTTP header. + * @param string $applicationName + */ + public function setApplicationName($applicationName) + { + $this->config['application_name'] = $applicationName; + } + + /** + * If 'plus.login' is included in the list of requested scopes, you can use + * this method to define types of app activities that your app will write. + * You can find a list of available types here: + * @link https://developers.google.com/+/api/moment-types + * + * @param array $requestVisibleActions Array of app activity types + */ + public function setRequestVisibleActions($requestVisibleActions) + { + if (is_array($requestVisibleActions)) { + $requestVisibleActions = implode(" ", $requestVisibleActions); + } + $this->config['request_visible_actions'] = $requestVisibleActions; + } + + /** + * Set the developer key to use, these are obtained through the API Console. + * @see http://code.google.com/apis/console-help/#generatingdevkeys + * @param string $developerKey + */ + public function setDeveloperKey($developerKey) + { + $this->config['developer_key'] = $developerKey; + } + + /** + * Set the hd (hosted domain) parameter streamlines the login process for + * Google Apps hosted accounts. By including the domain of the user, you + * restrict sign-in to accounts at that domain. + * @param string $hd the domain to use. + */ + public function setHostedDomain($hd) + { + $this->config['hd'] = $hd; + } + + /** + * Set the prompt hint. Valid values are none, consent and select_account. + * If no value is specified and the user has not previously authorized + * access, then the user is shown a consent screen. + * @param string $prompt + * {@code "none"} Do not display any authentication or consent screens. Must not be specified with other values. + * {@code "consent"} Prompt the user for consent. + * {@code "select_account"} Prompt the user to select an account. + */ + public function setPrompt($prompt) + { + $this->config['prompt'] = $prompt; + } + + /** + * openid.realm is a parameter from the OpenID 2.0 protocol, not from OAuth + * 2.0. It is used in OpenID 2.0 requests to signify the URL-space for which + * an authentication request is valid. + * @param string $realm the URL-space to use. + */ + public function setOpenidRealm($realm) + { + $this->config['openid.realm'] = $realm; + } + + /** + * If this is provided with the value true, and the authorization request is + * granted, the authorization will include any previous authorizations + * granted to this user/application combination for other scopes. + * @param bool $include the URL-space to use. + */ + public function setIncludeGrantedScopes($include) + { + $this->config['include_granted_scopes'] = $include; + } + + /** + * sets function to be called when an access token is fetched + * @param callable $tokenCallback - function ($cacheKey, $accessToken) + */ + public function setTokenCallback(callable $tokenCallback) + { + $this->config['token_callback'] = $tokenCallback; + } + + /** + * Revoke an OAuth2 access token or refresh token. This method will revoke the current access + * token, if a token isn't provided. + * + * @param string|array|null $token The token (access token or a refresh token) that should be revoked. + * @return boolean Returns True if the revocation was successful, otherwise False. + */ + public function revokeToken($token = null) + { + $tokenRevoker = new Revoke($this->getHttpClient()); + + return $tokenRevoker->revokeToken($token ?: $this->getAccessToken()); + } + + /** + * Verify an id_token. This method will verify the current id_token, if one + * isn't provided. + * + * @throws LogicException If no token was provided and no token was set using `setAccessToken`. + * @throws UnexpectedValueException If the token is not a valid JWT. + * @param string|null $idToken The token (id_token) that should be verified. + * @return array|false Returns the token payload as an array if the verification was + * successful, false otherwise. + */ + public function verifyIdToken($idToken = null) + { + $tokenVerifier = new Verify( + $this->getHttpClient(), + $this->getCache(), + $this->config['jwt'] + ); + + if (null === $idToken) { + $token = $this->getAccessToken(); + if (!isset($token['id_token'])) { + throw new LogicException( + 'id_token must be passed in or set as part of setAccessToken' + ); + } + $idToken = $token['id_token']; + } + + return $tokenVerifier->verifyIdToken( + $idToken, + $this->getClientId() + ); + } + + /** + * Set the scopes to be requested. Must be called before createAuthUrl(). + * Will remove any previously configured scopes. + * @param string|array $scope_or_scopes, ie: + * array( + * 'https://www.googleapis.com/auth/plus.login', + * 'https://www.googleapis.com/auth/moderator' + * ); + */ + public function setScopes($scope_or_scopes) + { + $this->requestedScopes = []; + $this->addScope($scope_or_scopes); + } + + /** + * This functions adds a scope to be requested as part of the OAuth2.0 flow. + * Will append any scopes not previously requested to the scope parameter. + * A single string will be treated as a scope to request. An array of strings + * will each be appended. + * @param string|string[] $scope_or_scopes e.g. "profile" + */ + public function addScope($scope_or_scopes) + { + if (is_string($scope_or_scopes) && !in_array($scope_or_scopes, $this->requestedScopes)) { + $this->requestedScopes[] = $scope_or_scopes; + } elseif (is_array($scope_or_scopes)) { + foreach ($scope_or_scopes as $scope) { + $this->addScope($scope); + } + } + } + + /** + * Returns the list of scopes requested by the client + * @return array the list of scopes + * + */ + public function getScopes() + { + return $this->requestedScopes; + } + + /** + * @return string|null + * @visible For Testing + */ + public function prepareScopes() + { + if (empty($this->requestedScopes)) { + return null; + } + + return implode(' ', $this->requestedScopes); + } + + /** + * Helper method to execute deferred HTTP requests. + * + * @template T + * @param RequestInterface $request + * @param class-string|false|null $expectedClass + * @throws \Google\Exception + * @return mixed|T|ResponseInterface + */ + public function execute(RequestInterface $request, $expectedClass = null) + { + $request = $request + ->withHeader( + 'User-Agent', + sprintf( + '%s %s%s', + $this->config['application_name'], + self::USER_AGENT_SUFFIX, + $this->getLibraryVersion() + ) + ) + ->withHeader( + 'x-goog-api-client', + sprintf( + 'gl-php/%s gdcl/%s', + phpversion(), + $this->getLibraryVersion() + ) + ); + + if ($this->config['api_format_v2']) { + $request = $request->withHeader( + 'X-GOOG-API-FORMAT-VERSION', + '2' + ); + } + + // call the authorize method + // this is where most of the grunt work is done + $http = $this->authorize(); + + return REST::execute( + $http, + $request, + $expectedClass, + $this->config['retry'], + $this->config['retry_map'] + ); + } + + /** + * Declare whether batch calls should be used. This may increase throughput + * by making multiple requests in one connection. + * + * @param boolean $useBatch True if the batch support should + * be enabled. Defaults to False. + */ + public function setUseBatch($useBatch) + { + // This is actually an alias for setDefer. + $this->setDefer($useBatch); + } + + /** + * Are we running in Google AppEngine? + * return bool + */ + public function isAppEngine() + { + return (isset($_SERVER['SERVER_SOFTWARE']) && + strpos($_SERVER['SERVER_SOFTWARE'], 'Google App Engine') !== false); + } + + public function setConfig($name, $value) + { + $this->config[$name] = $value; + } + + public function getConfig($name, $default = null) + { + return isset($this->config[$name]) ? $this->config[$name] : $default; + } + + /** + * For backwards compatibility + * alias for setAuthConfig + * + * @param string $file the configuration file + * @throws \Google\Exception + * @deprecated + */ + public function setAuthConfigFile($file) + { + $this->setAuthConfig($file); + } + + /** + * Set the auth config from new or deprecated JSON config. + * This structure should match the file downloaded from + * the "Download JSON" button on in the Google Developer + * Console. + * @param string|array $config the configuration json + * @throws \Google\Exception + */ + public function setAuthConfig($config) + { + if (is_string($config)) { + if (!file_exists($config)) { + throw new InvalidArgumentException(sprintf('file "%s" does not exist', $config)); + } + + $json = file_get_contents($config); + + if (!$config = json_decode($json, true)) { + throw new LogicException('invalid json for auth config'); + } + } + + $key = isset($config['installed']) ? 'installed' : 'web'; + if (isset($config['type']) && $config['type'] == 'service_account') { + // @TODO(v3): Remove this, as it isn't accurate. ADC applies only to determining + // credentials based on the user's environment. + $this->useApplicationDefaultCredentials(); + + // set the information from the config + $this->setClientId($config['client_id']); + $this->config['client_email'] = $config['client_email']; + $this->config['signing_key'] = $config['private_key']; + $this->config['signing_algorithm'] = 'HS256'; + } elseif (isset($config[$key])) { + // old-style + $this->setClientId($config[$key]['client_id']); + $this->setClientSecret($config[$key]['client_secret']); + if (isset($config[$key]['redirect_uris'])) { + $this->setRedirectUri($config[$key]['redirect_uris'][0]); + } + } else { + // new-style + $this->setClientId($config['client_id']); + $this->setClientSecret($config['client_secret']); + if (isset($config['redirect_uris'])) { + $this->setRedirectUri($config['redirect_uris'][0]); + } + } + } + + /** + * Use when the service account has been delegated domain wide access. + * + * @param string $subject an email address account to impersonate + */ + public function setSubject($subject) + { + $this->config['subject'] = $subject; + } + + /** + * Declare whether making API calls should make the call immediately, or + * return a request which can be called with ->execute(); + * + * @param boolean $defer True if calls should not be executed right away. + */ + public function setDefer($defer) + { + $this->deferExecution = $defer; + } + + /** + * Whether or not to return raw requests + * @return boolean + */ + public function shouldDefer() + { + return $this->deferExecution; + } + + /** + * @return OAuth2 implementation + */ + public function getOAuth2Service() + { + if (!isset($this->auth)) { + $this->auth = $this->createOAuth2Service(); + } + + return $this->auth; + } + + /** + * create a default google auth object + */ + protected function createOAuth2Service() + { + $auth = new OAuth2([ + 'clientId' => $this->getClientId(), + 'clientSecret' => $this->getClientSecret(), + 'authorizationUri' => self::OAUTH2_AUTH_URL, + 'tokenCredentialUri' => self::OAUTH2_TOKEN_URI, + 'redirectUri' => $this->getRedirectUri(), + 'issuer' => $this->config['client_id'], + 'signingKey' => $this->config['signing_key'], + 'signingAlgorithm' => $this->config['signing_algorithm'], + ]); + + return $auth; + } + + /** + * Set the Cache object + * @param CacheItemPoolInterface $cache + */ + public function setCache(CacheItemPoolInterface $cache) + { + $this->cache = $cache; + } + + /** + * @return CacheItemPoolInterface + */ + public function getCache() + { + if (!$this->cache) { + $this->cache = $this->createDefaultCache(); + } + + return $this->cache; + } + + /** + * @param array $cacheConfig + */ + public function setCacheConfig(array $cacheConfig) + { + $this->config['cache_config'] = $cacheConfig; + } + + /** + * Set the Logger object + * @param LoggerInterface $logger + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * @return LoggerInterface + */ + public function getLogger() + { + if (!isset($this->logger)) { + $this->logger = $this->createDefaultLogger(); + } + + return $this->logger; + } + + protected function createDefaultLogger() + { + $logger = new Logger('google-api-php-client'); + if ($this->isAppEngine()) { + $handler = new MonologSyslogHandler('app', LOG_USER, Logger::NOTICE); + } else { + $handler = new MonologStreamHandler('php://stderr', Logger::NOTICE); + } + $logger->pushHandler($handler); + + return $logger; + } + + protected function createDefaultCache() + { + return new MemoryCacheItemPool(); + } + + /** + * Set the Http Client object + * @param ClientInterface $http + */ + public function setHttpClient(ClientInterface $http) + { + $this->http = $http; + } + + /** + * @return ClientInterface + */ + public function getHttpClient() + { + if (null === $this->http) { + $this->http = $this->createDefaultHttpClient(); + } + + return $this->http; + } + + /** + * Set the API format version. + * + * `true` will use V2, which may return more useful error messages. + * + * @param bool $value + */ + public function setApiFormatV2($value) + { + $this->config['api_format_v2'] = (bool) $value; + } + + protected function createDefaultHttpClient() + { + $guzzleVersion = null; + if (defined('\GuzzleHttp\ClientInterface::MAJOR_VERSION')) { + $guzzleVersion = ClientInterface::MAJOR_VERSION; + } elseif (defined('\GuzzleHttp\ClientInterface::VERSION')) { + $guzzleVersion = (int)substr(ClientInterface::VERSION, 0, 1); + } + + if (5 === $guzzleVersion) { + $options = [ + 'base_url' => $this->config['base_path'], + 'defaults' => ['exceptions' => false], + ]; + if ($this->isAppEngine()) { + if (class_exists(StreamHandler::class)) { + // set StreamHandler on AppEngine by default + $options['handler'] = new StreamHandler(); + $options['defaults']['verify'] = '/etc/ca-certificates.crt'; + } + } + } elseif (6 === $guzzleVersion || 7 === $guzzleVersion) { + // guzzle 6 or 7 + $options = [ + 'base_uri' => $this->config['base_path'], + 'http_errors' => false, + ]; + } else { + throw new LogicException('Could not find supported version of Guzzle.'); + } + + return new GuzzleClient($options); + } + + /** + * @return FetchAuthTokenCache + */ + private function createApplicationDefaultCredentials() + { + $scopes = $this->prepareScopes(); + $sub = $this->config['subject']; + $signingKey = $this->config['signing_key']; + + // create credentials using values supplied in setAuthConfig + if ($signingKey) { + $serviceAccountCredentials = [ + 'client_id' => $this->config['client_id'], + 'client_email' => $this->config['client_email'], + 'private_key' => $signingKey, + 'type' => 'service_account', + 'quota_project_id' => $this->config['quota_project'], + ]; + $credentials = CredentialsLoader::makeCredentials( + $scopes, + $serviceAccountCredentials + ); + } else { + // When $sub is provided, we cannot pass cache classes to ::getCredentials + // because FetchAuthTokenCache::setSub does not exist. + // The result is when $sub is provided, calls to ::onGce are not cached. + $credentials = ApplicationDefaultCredentials::getCredentials( + $scopes, + null, + $sub ? null : $this->config['cache_config'], + $sub ? null : $this->getCache(), + $this->config['quota_project'] + ); + } + + // for service account domain-wide authority (impersonating a user) + // @see https://developers.google.com/identity/protocols/OAuth2ServiceAccount + if ($sub) { + if (!$credentials instanceof ServiceAccountCredentials) { + throw new DomainException('domain-wide authority requires service account credentials'); + } + + $credentials->setSub($sub); + } + + // If we are not using FetchAuthTokenCache yet, create it now + if (!$credentials instanceof FetchAuthTokenCache) { + $credentials = new FetchAuthTokenCache( + $credentials, + $this->config['cache_config'], + $this->getCache() + ); + } + return $credentials; + } + + protected function getAuthHandler() + { + // Be very careful using the cache, as the underlying auth library's cache + // implementation is naive, and the cache keys do not account for user + // sessions. + // + // @see https://github.com/google/google-api-php-client/issues/821 + return AuthHandlerFactory::build( + $this->getCache(), + $this->config['cache_config'] + ); + } + + private function createUserRefreshCredentials($scope, $refreshToken) + { + $creds = array_filter([ + 'client_id' => $this->getClientId(), + 'client_secret' => $this->getClientSecret(), + 'refresh_token' => $refreshToken, + ]); + + return new UserRefreshCredentials($scope, $creds); + } + + private function checkUniverseDomain($credentials) + { + $credentialsUniverse = $credentials instanceof GetUniverseDomainInterface + ? $credentials->getUniverseDomain() + : GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN; + if ($credentialsUniverse !== $this->getUniverseDomain()) { + throw new DomainException(sprintf( + 'The configured universe domain (%s) does not match the credential universe domain (%s)', + $this->getUniverseDomain(), + $credentialsUniverse + )); + } + } + + public function getUniverseDomain() + { + return $this->config['universe_domain']; + } +} diff --git a/src/Collection.php b/src/Collection.php new file mode 100644 index 000000000..fe2c62fec --- /dev/null +++ b/src/Collection.php @@ -0,0 +1,122 @@ +{$this->collection_key}) + && is_array($this->{$this->collection_key}) + ) { + reset($this->{$this->collection_key}); + } + } + + /** @return mixed */ + #[\ReturnTypeWillChange] + public function current() + { + $this->coerceType($this->key()); + if (is_array($this->{$this->collection_key})) { + return current($this->{$this->collection_key}); + } + } + + /** @return mixed */ + #[\ReturnTypeWillChange] + public function key() + { + if ( + isset($this->{$this->collection_key}) + && is_array($this->{$this->collection_key}) + ) { + return key($this->{$this->collection_key}); + } + } + + /** @return mixed */ + #[\ReturnTypeWillChange] + public function next() + { + return next($this->{$this->collection_key}); + } + + /** @return bool */ + #[\ReturnTypeWillChange] + public function valid() + { + $key = $this->key(); + return $key !== null && $key !== false; + } + + /** @return int */ + #[\ReturnTypeWillChange] + public function count() + { + if (!isset($this->{$this->collection_key})) { + return 0; + } + return count($this->{$this->collection_key}); + } + + /** @return bool */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + if (!is_numeric($offset)) { + return parent::offsetExists($offset); + } + return isset($this->{$this->collection_key}[$offset]); + } + + /** @return mixed */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + if (!is_numeric($offset)) { + return parent::offsetGet($offset); + } + $this->coerceType($offset); + return $this->{$this->collection_key}[$offset]; + } + + /** @return void */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + if (!is_numeric($offset)) { + parent::offsetSet($offset, $value); + } + $this->{$this->collection_key}[$offset] = $value; + } + + /** @return void */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + if (!is_numeric($offset)) { + parent::offsetUnset($offset); + } + unset($this->{$this->collection_key}[$offset]); + } + + private function coerceType($offset) + { + $keyType = $this->keyType($this->collection_key); + if ($keyType && !is_object($this->{$this->collection_key}[$offset])) { + $this->{$this->collection_key}[$offset] = + new $keyType($this->{$this->collection_key}[$offset]); + } + } +} diff --git a/src/Google/Exception.php b/src/Exception.php similarity index 87% rename from src/Google/Exception.php rename to src/Exception.php index af8026971..de4508d92 100644 --- a/src/Google/Exception.php +++ b/src/Exception.php @@ -15,6 +15,10 @@ * limitations under the License. */ -class Google_Exception extends Exception +namespace Google; + +use Exception as BaseException; + +class Exception extends BaseException { } diff --git a/src/Google/AccessToken/Revoke.php b/src/Google/AccessToken/Revoke.php deleted file mode 100644 index 807a6d90f..000000000 --- a/src/Google/AccessToken/Revoke.php +++ /dev/null @@ -1,79 +0,0 @@ -http = $http; - } - - /** - * Revoke an OAuth2 access token or refresh token. This method will revoke the current access - * token, if a token isn't provided. - * - * @param string|null $token The token (access token or a refresh token) that should be revoked. - * @return boolean Returns True if the revocation was successful, otherwise False. - */ - public function revokeToken(array $token) - { - if (isset($token['refresh_token'])) { - $tokenString = $token['refresh_token']; - } else { - $tokenString = $token['access_token']; - } - - $body = Psr7\stream_for(http_build_query(array('token' => $tokenString))); - $request = new Request( - 'POST', - Google_Client::OAUTH2_REVOKE_URI, - [ - 'Cache-Control' => 'no-store', - 'Content-Type' => 'application/x-www-form-urlencoded', - ], - $body - ); - - $httpHandler = HttpHandlerFactory::build($this->http); - - $response = $httpHandler($request); - if ($response->getStatusCode() == 200) { - return true; - } - - return false; - } -} diff --git a/src/Google/AccessToken/Verify.php b/src/Google/AccessToken/Verify.php deleted file mode 100644 index 2e8d4770d..000000000 --- a/src/Google/AccessToken/Verify.php +++ /dev/null @@ -1,234 +0,0 @@ -http = $http; - $this->cache = $cache; - $this->jwt = $this->getJwtService(); - } - - /** - * Verifies an id token and returns the authenticated apiLoginTicket. - * Throws an exception if the id token is not valid. - * The audience parameter can be used to control which id tokens are - * accepted. By default, the id token must have been issued to this OAuth2 client. - * - * @param $audience - * @return array the token payload, if successful - */ - public function verifyIdToken($idToken, $audience = null) - { - if (empty($idToken)) { - throw new LogicException('id_token cannot be null'); - } - - // set phpseclib constants if applicable - $this->setPhpsecConstants(); - - // Check signature - $certs = $this->getFederatedSignOnCerts(); - foreach ($certs as $cert) { - $modulus = new BigInteger($this->jwt->urlsafeB64Decode($cert['n']), 256); - $exponent = new BigInteger($this->jwt->urlsafeB64Decode($cert['e']), 256); - - $rsa = new RSA(); - $rsa->loadKey(array('n' => $modulus, 'e' => $exponent)); - - try { - $payload = $this->jwt->decode( - $idToken, - $rsa->getPublicKey(), - array('RS256') - ); - - if (property_exists($payload, 'aud')) { - if ($audience && $payload->aud != $audience) { - return false; - } - } - - // support HTTP and HTTPS issuers - // @see https://developers.google.com/identity/sign-in/web/backend-auth - $issuers = array(self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS); - if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) { - return false; - } - - return (array) $payload; - } catch (ExpiredException $e) { - return false; - } catch (ExpiredExceptionV3 $e) { - return false; - } catch (DomainException $e) { - // continue - } - } - - return false; - } - - private function getCache() - { - return $this->cache; - } - - /** - * Retrieve and cache a certificates file. - * - * @param $url string location - * @throws Google_Exception - * @return array certificates - */ - private function retrieveCertsFromLocation($url) - { - // If we're retrieving a local file, just grab it. - if (0 !== strpos($url, 'http')) { - if (!$file = file_get_contents($url)) { - throw new Google_Exception( - "Failed to retrieve verification certificates: '" . - $url . "'." - ); - } - - return json_decode($file, true); - } - - $response = $this->http->get($url); - - if ($response->getStatusCode() == 200) { - return json_decode((string) $response->getBody(), true); - } - throw new Google_Exception( - sprintf( - 'Failed to retrieve verification certificates: "%s".', - $response->getBody()->getContents() - ), - $response->getStatusCode() - ); - } - - // Gets federated sign-on certificates to use for verifying identity tokens. - // Returns certs as array structure, where keys are key ids, and values - // are PEM encoded certificates. - private function getFederatedSignOnCerts() - { - $certs = null; - if ($cache = $this->getCache()) { - $cacheItem = $cache->getItem('federated_signon_certs_v3', 3600); - $certs = $cacheItem->get(); - } - - - if (!$certs) { - $certs = $this->retrieveCertsFromLocation( - self::FEDERATED_SIGNON_CERT_URL - ); - - if ($cache) { - $cacheItem->set($certs); - $cache->save($cacheItem); - } - } - - if (!isset($certs['keys'])) { - throw new InvalidArgumentException( - 'federated sign-on certs expects "keys" to be set' - ); - } - - return $certs['keys']; - } - - private function getJwtService() - { - $jwtClass = 'JWT'; - if (class_exists('\Firebase\JWT\JWT')) { - $jwtClass = 'Firebase\JWT\JWT'; - } - - if (property_exists($jwtClass, 'leeway')) { - // adds 1 second to JWT leeway - // @see https://github.com/google/google-api-php-client/issues/827 - $jwtClass::$leeway = 1; - } - - return new $jwtClass; - } - - /** - * phpseclib calls "phpinfo" by default, which requires special - * whitelisting in the AppEngine VM environment. This function - * sets constants to bypass the need for phpseclib to check phpinfo - * - * @see phpseclib/Math/BigInteger - * @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85 - */ - private function setPhpsecConstants() - { - if (filter_var(getenv('GAE_VM'), FILTER_VALIDATE_BOOLEAN)) { - if (!defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) { - define('MATH_BIGINTEGER_OPENSSL_ENABLED', true); - } - if (!defined('CRYPT_RSA_MODE')) { - define('CRYPT_RSA_MODE', RSA::MODE_OPENSSL); - } - } - } -} diff --git a/src/Google/AuthHandler/AuthHandlerFactory.php b/src/Google/AuthHandler/AuthHandlerFactory.php deleted file mode 100644 index f1a3229ae..000000000 --- a/src/Google/AuthHandler/AuthHandlerFactory.php +++ /dev/null @@ -1,42 +0,0 @@ -cache = $cache; - $this->cacheConfig = $cacheConfig; - } - - public function attachCredentials( - ClientInterface $http, - CredentialsLoader $credentials, - callable $tokenCallback = null - ) { - // if we end up needing to make an HTTP request to retrieve credentials, we - // can use our existing one, but we need to throw exceptions so the error - // bubbles up. - $authHttp = $this->createAuthHttp($http); - $authHttpHandler = HttpHandlerFactory::build($authHttp); - $subscriber = new AuthTokenSubscriber( - $credentials, - $this->cacheConfig, - $this->cache, - $authHttpHandler, - $tokenCallback - ); - - $http->setDefaultOption('auth', 'google_auth'); - $http->getEmitter()->attach($subscriber); - - return $http; - } - - public function attachToken(ClientInterface $http, array $token, array $scopes) - { - $tokenFunc = function ($scopes) use ($token) { - return $token['access_token']; - }; - - $subscriber = new ScopedAccessTokenSubscriber( - $tokenFunc, - $scopes, - [], - $this->cache - ); - - $http->setDefaultOption('auth', 'scoped'); - $http->getEmitter()->attach($subscriber); - - return $http; - } - - public function attachKey(ClientInterface $http, $key) - { - $subscriber = new SimpleSubscriber(['key' => $key]); - - $http->setDefaultOption('auth', 'simple'); - $http->getEmitter()->attach($subscriber); - - return $http; - } - - private function createAuthHttp(ClientInterface $http) - { - return new Client( - [ - 'base_url' => $http->getBaseUrl(), - 'defaults' => [ - 'exceptions' => true, - 'verify' => $http->getDefaultOption('verify'), - 'proxy' => $http->getDefaultOption('proxy'), - ] - ] - ); - } -} diff --git a/src/Google/AuthHandler/Guzzle6AuthHandler.php b/src/Google/AuthHandler/Guzzle6AuthHandler.php deleted file mode 100644 index fb743e601..000000000 --- a/src/Google/AuthHandler/Guzzle6AuthHandler.php +++ /dev/null @@ -1,99 +0,0 @@ -cache = $cache; - $this->cacheConfig = $cacheConfig; - } - - public function attachCredentials( - ClientInterface $http, - CredentialsLoader $credentials, - callable $tokenCallback = null - ) { - // if we end up needing to make an HTTP request to retrieve credentials, we - // can use our existing one, but we need to throw exceptions so the error - // bubbles up. - $authHttp = $this->createAuthHttp($http); - $authHttpHandler = HttpHandlerFactory::build($authHttp); - $middleware = new AuthTokenMiddleware( - $credentials, - $this->cacheConfig, - $this->cache, - $authHttpHandler, - $tokenCallback - ); - - $config = $http->getConfig(); - $config['handler']->remove('google_auth'); - $config['handler']->push($middleware, 'google_auth'); - $config['auth'] = 'google_auth'; - $http = new Client($config); - - return $http; - } - - public function attachToken(ClientInterface $http, array $token, array $scopes) - { - $tokenFunc = function ($scopes) use ($token) { - return $token['access_token']; - }; - - $middleware = new ScopedAccessTokenMiddleware( - $tokenFunc, - $scopes, - [], - $this->cache - ); - - $config = $http->getConfig(); - $config['handler']->remove('google_auth'); - $config['handler']->push($middleware, 'google_auth'); - $config['auth'] = 'scoped'; - $http = new Client($config); - - return $http; - } - - public function attachKey(ClientInterface $http, $key) - { - $middleware = new SimpleMiddleware(['key' => $key]); - - $config = $http->getConfig(); - $config['handler']->remove('google_auth'); - $config['handler']->push($middleware, 'google_auth'); - $config['auth'] = 'simple'; - $http = new Client($config); - - return $http; - } - - private function createAuthHttp(ClientInterface $http) - { - return new Client( - [ - 'base_uri' => $http->getConfig('base_uri'), - 'exceptions' => true, - 'verify' => $http->getConfig('verify'), - 'proxy' => $http->getConfig('proxy'), - ] - ); - } -} diff --git a/src/Google/Client.php b/src/Google/Client.php deleted file mode 100644 index c8592c70e..000000000 --- a/src/Google/Client.php +++ /dev/null @@ -1,1094 +0,0 @@ -config = array_merge( - [ - 'application_name' => '', - - // Don't change these unless you're working against a special development - // or testing environment. - 'base_path' => self::API_BASE_PATH, - - // https://developers.google.com/console - 'client_id' => '', - 'client_secret' => '', - 'redirect_uri' => null, - 'state' => null, - - // Simple API access key, also from the API console. Ensure you get - // a Server key, and not a Browser key. - 'developer_key' => '', - - // For use with Google Cloud Platform - // fetch the ApplicationDefaultCredentials, if applicable - // @see https://developers.google.com/identity/protocols/application-default-credentials - 'use_application_default_credentials' => false, - 'signing_key' => null, - 'signing_algorithm' => null, - 'subject' => null, - - // Other OAuth2 parameters. - 'hd' => '', - 'prompt' => '', - 'openid.realm' => '', - 'include_granted_scopes' => null, - 'login_hint' => '', - 'request_visible_actions' => '', - 'access_type' => 'online', - 'approval_prompt' => 'auto', - - // Task Runner retry configuration - // @see Google_Task_Runner - 'retry' => array(), - - // cache config for downstream auth caching - 'cache_config' => [], - - // function to be called when an access token is fetched - // follows the signature function ($cacheKey, $accessToken) - 'token_callback' => null, - ], - $config - ); - } - - /** - * Get a string containing the version of the library. - * - * @return string - */ - public function getLibraryVersion() - { - return self::LIBVER; - } - - /** - * For backwards compatibility - * alias for fetchAccessTokenWithAuthCode - * - * @param $code string code from accounts.google.com - * @return array access token - */ - public function authenticate($code) - { - return $this->fetchAccessTokenWithAuthCode($code); - } - - /** - * Attempt to exchange a code for an valid authentication token. - * Helper wrapped around the OAuth 2.0 implementation. - * - * @param $code string code from accounts.google.com - * @return array access token - */ - public function fetchAccessTokenWithAuthCode($code) - { - if (strlen($code) == 0) { - throw new InvalidArgumentException("Invalid code"); - } - - $auth = $this->getOAuth2Service(); - $auth->setCode($code); - $auth->setRedirectUri($this->getRedirectUri()); - - $httpHandler = HttpHandlerFactory::build($this->getHttpClient()); - $creds = $auth->fetchAuthToken($httpHandler); - if ($creds && isset($creds['access_token'])) { - $creds['created'] = time(); - $this->setAccessToken($creds); - } - - return $creds; - } - - /** - * For backwards compatibility - * alias for fetchAccessTokenWithAssertion - * - * @return array access token - */ - public function refreshTokenWithAssertion() - { - return $this->fetchAccessTokenWithAssertion(); - } - - /** - * Fetches a fresh access token with a given assertion token. - * @param $assertionCredentials optional. - * @return array access token - */ - public function fetchAccessTokenWithAssertion(ClientInterface $authHttp = null) - { - if (!$this->isUsingApplicationDefaultCredentials()) { - throw new DomainException( - 'set the JSON service account credentials using' - . ' Google_Client::setAuthConfig or set the path to your JSON file' - . ' with the "GOOGLE_APPLICATION_CREDENTIALS" environment variable' - . ' and call Google_Client::useApplicationDefaultCredentials to' - . ' refresh a token with assertion.' - ); - } - - $this->getLogger()->log( - 'info', - 'OAuth2 access token refresh with Signed JWT assertion grants.' - ); - - $credentials = $this->createApplicationDefaultCredentials(); - - $httpHandler = HttpHandlerFactory::build($authHttp); - $accessToken = $credentials->fetchAuthToken($httpHandler); - if ($accessToken && isset($accessToken['access_token'])) { - $this->setAccessToken($accessToken); - } - - return $accessToken; - } - - /** - * For backwards compatibility - * alias for fetchAccessTokenWithRefreshToken - * - * @param string $refreshToken - * @return array access token - */ - public function refreshToken($refreshToken) - { - return $this->fetchAccessTokenWithRefreshToken($refreshToken); - } - - /** - * Fetches a fresh OAuth 2.0 access token with the given refresh token. - * @param string $refreshToken - * @return array access token - */ - public function fetchAccessTokenWithRefreshToken($refreshToken = null) - { - if (is_null($refreshToken)) { - if (!isset($this->token['refresh_token'])) { - throw new LogicException( - 'refresh token must be passed in or set as part of setAccessToken' - ); - } - $refreshToken = $this->token['refresh_token']; - } - $this->getLogger()->info('OAuth2 access token refresh'); - $auth = $this->getOAuth2Service(); - $auth->setRefreshToken($refreshToken); - - $httpHandler = HttpHandlerFactory::build($this->getHttpClient()); - $creds = $auth->fetchAuthToken($httpHandler); - if ($creds && isset($creds['access_token'])) { - $creds['created'] = time(); - $this->setAccessToken($creds); - } - - return $creds; - } - - /** - * Create a URL to obtain user authorization. - * The authorization endpoint allows the user to first - * authenticate, and then grant/deny the access request. - * @param string|array $scope The scope is expressed as an array or list of space-delimited strings. - * @return string - */ - public function createAuthUrl($scope = null) - { - if (empty($scope)) { - $scope = $this->prepareScopes(); - } - if (is_array($scope)) { - $scope = implode(' ', $scope); - } - - // only accept one of prompt or approval_prompt - $approvalPrompt = $this->config['prompt'] - ? null - : $this->config['approval_prompt']; - - // include_granted_scopes should be string "true", string "false", or null - $includeGrantedScopes = $this->config['include_granted_scopes'] === null - ? null - : var_export($this->config['include_granted_scopes'], true); - - $params = array_filter( - [ - 'access_type' => $this->config['access_type'], - 'approval_prompt' => $approvalPrompt, - 'hd' => $this->config['hd'], - 'include_granted_scopes' => $includeGrantedScopes, - 'login_hint' => $this->config['login_hint'], - 'openid.realm' => $this->config['openid.realm'], - 'prompt' => $this->config['prompt'], - 'response_type' => 'code', - 'scope' => $scope, - 'state' => $this->config['state'], - ] - ); - - // If the list of scopes contains plus.login, add request_visible_actions - // to auth URL. - $rva = $this->config['request_visible_actions']; - if (strlen($rva) > 0 && false !== strpos($scope, 'plus.login')) { - $params['request_visible_actions'] = $rva; - } - - $auth = $this->getOAuth2Service(); - - return (string) $auth->buildFullAuthorizationUri($params); - } - - /** - * Adds auth listeners to the HTTP client based on the credentials - * set in the Google API Client object - * - * @param GuzzleHttp\ClientInterface $http the http client object. - * @param GuzzleHttp\ClientInterface $authHttp an http client for authentication. - * @return GuzzleHttp\ClientInterface the http client object - */ - public function authorize(ClientInterface $http = null, ClientInterface $authHttp = null) - { - $credentials = null; - $token = null; - $scopes = null; - if (is_null($http)) { - $http = $this->getHttpClient(); - } - - // These conditionals represent the decision tree for authentication - // 1. Check for Application Default Credentials - // 2. Check for API Key - // 3a. Check for an Access Token - // 3b. If access token exists but is expired, try to refresh it - if ($this->isUsingApplicationDefaultCredentials()) { - $credentials = $this->createApplicationDefaultCredentials(); - } elseif ($token = $this->getAccessToken()) { - $scopes = $this->prepareScopes(); - // add refresh subscriber to request a new token - if ($this->isAccessTokenExpired() && isset($token['refresh_token'])) { - $credentials = $this->createUserRefreshCredentials( - $scopes, - $token['refresh_token'] - ); - } - } - - $authHandler = $this->getAuthHandler(); - - if ($credentials) { - $callback = $this->config['token_callback']; - $http = $authHandler->attachCredentials($http, $credentials, $callback); - } elseif ($token) { - $http = $authHandler->attachToken($http, $token, (array) $scopes); - } elseif ($key = $this->config['developer_key']) { - $http = $authHandler->attachKey($http, $key); - } - - return $http; - } - - /** - * Set the configuration to use application default credentials for - * authentication - * - * @see https://developers.google.com/identity/protocols/application-default-credentials - * @param boolean $useAppCreds - */ - public function useApplicationDefaultCredentials($useAppCreds = true) - { - $this->config['use_application_default_credentials'] = $useAppCreds; - } - - /** - * To prevent useApplicationDefaultCredentials from inappropriately being - * called in a conditional - * - * @see https://developers.google.com/identity/protocols/application-default-credentials - */ - public function isUsingApplicationDefaultCredentials() - { - return $this->config['use_application_default_credentials']; - } - - /** - * @param string|array $token - * @throws InvalidArgumentException - */ - public function setAccessToken($token) - { - if (is_string($token)) { - if ($json = json_decode($token, true)) { - $token = $json; - } else { - // assume $token is just the token string - $token = array( - 'access_token' => $token, - ); - } - } - if ($token == null) { - throw new InvalidArgumentException('invalid json token'); - } - if (!isset($token['access_token'])) { - throw new InvalidArgumentException("Invalid token format"); - } - $this->token = $token; - } - - public function getAccessToken() - { - return $this->token; - } - - public function getRefreshToken() - { - if (isset($this->token['refresh_token'])) { - return $this->token['refresh_token']; - } - } - - /** - * Returns if the access_token is expired. - * @return bool Returns True if the access_token is expired. - */ - public function isAccessTokenExpired() - { - if (!$this->token) { - return true; - } - - $created = 0; - if (isset($this->token['created'])) { - $created = $this->token['created']; - } elseif (isset($this->token['id_token'])) { - // check the ID token for "iat" - // signature verification is not required here, as we are just - // using this for convenience to save a round trip request - // to the Google API server - $idToken = $this->token['id_token']; - if (substr_count($idToken, '.') == 2) { - $parts = explode('.', $idToken); - $payload = json_decode(base64_decode($parts[1]), true); - if ($payload && isset($payload['iat'])) { - $created = $payload['iat']; - } - } - } - - // If the token is set to expire in the next 30 seconds. - $expired = ($created - + ($this->token['expires_in'] - 30)) < time(); - - return $expired; - } - - public function getAuth() - { - throw new BadMethodCallException( - 'This function no longer exists. See UPGRADING.md for more information' - ); - } - - public function setAuth($auth) - { - throw new BadMethodCallException( - 'This function no longer exists. See UPGRADING.md for more information' - ); - } - - /** - * Set the OAuth 2.0 Client ID. - * @param string $clientId - */ - public function setClientId($clientId) - { - $this->config['client_id'] = $clientId; - } - - public function getClientId() - { - return $this->config['client_id']; - } - - /** - * Set the OAuth 2.0 Client Secret. - * @param string $clientSecret - */ - public function setClientSecret($clientSecret) - { - $this->config['client_secret'] = $clientSecret; - } - - public function getClientSecret() - { - return $this->config['client_secret']; - } - - /** - * Set the OAuth 2.0 Redirect URI. - * @param string $redirectUri - */ - public function setRedirectUri($redirectUri) - { - $this->config['redirect_uri'] = $redirectUri; - } - - public function getRedirectUri() - { - return $this->config['redirect_uri']; - } - - /** - * Set OAuth 2.0 "state" parameter to achieve per-request customization. - * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.1.2.2 - * @param string $state - */ - public function setState($state) - { - $this->config['state'] = $state; - } - - /** - * @param string $accessType Possible values for access_type include: - * {@code "offline"} to request offline access from the user. - * {@code "online"} to request online access from the user. - */ - public function setAccessType($accessType) - { - $this->config['access_type'] = $accessType; - } - - /** - * @param string $approvalPrompt Possible values for approval_prompt include: - * {@code "force"} to force the approval UI to appear. (This is the default value) - * {@code "auto"} to request auto-approval when possible. - */ - public function setApprovalPrompt($approvalPrompt) - { - $this->config['approval_prompt'] = $approvalPrompt; - } - - /** - * Set the login hint, email address or sub id. - * @param string $loginHint - */ - public function setLoginHint($loginHint) - { - $this->config['login_hint'] = $loginHint; - } - - /** - * Set the application name, this is included in the User-Agent HTTP header. - * @param string $applicationName - */ - public function setApplicationName($applicationName) - { - $this->config['application_name'] = $applicationName; - } - - /** - * If 'plus.login' is included in the list of requested scopes, you can use - * this method to define types of app activities that your app will write. - * You can find a list of available types here: - * @link https://developers.google.com/+/api/moment-types - * - * @param array $requestVisibleActions Array of app activity types - */ - public function setRequestVisibleActions($requestVisibleActions) - { - if (is_array($requestVisibleActions)) { - $requestVisibleActions = join(" ", $requestVisibleActions); - } - $this->config['request_visible_actions'] = $requestVisibleActions; - } - - /** - * Set the developer key to use, these are obtained through the API Console. - * @see http://code.google.com/apis/console-help/#generatingdevkeys - * @param string $developerKey - */ - public function setDeveloperKey($developerKey) - { - $this->config['developer_key'] = $developerKey; - } - - /** - * Set the hd (hosted domain) parameter streamlines the login process for - * Google Apps hosted accounts. By including the domain of the user, you - * restrict sign-in to accounts at that domain. - * @param $hd string - the domain to use. - */ - public function setHostedDomain($hd) - { - $this->config['hd'] = $hd; - } - - /** - * Set the prompt hint. Valid values are none, consent and select_account. - * If no value is specified and the user has not previously authorized - * access, then the user is shown a consent screen. - * @param $prompt string - */ - public function setPrompt($prompt) - { - $this->config['prompt'] = $prompt; - } - - /** - * openid.realm is a parameter from the OpenID 2.0 protocol, not from OAuth - * 2.0. It is used in OpenID 2.0 requests to signify the URL-space for which - * an authentication request is valid. - * @param $realm string - the URL-space to use. - */ - public function setOpenidRealm($realm) - { - $this->config['openid.realm'] = $realm; - } - - /** - * If this is provided with the value true, and the authorization request is - * granted, the authorization will include any previous authorizations - * granted to this user/application combination for other scopes. - * @param $include boolean - the URL-space to use. - */ - public function setIncludeGrantedScopes($include) - { - $this->config['include_granted_scopes'] = $include; - } - - /** - * sets function to be called when an access token is fetched - * @param callable $tokenCallback - function ($cacheKey, $accessToken) - */ - public function setTokenCallback(callable $tokenCallback) - { - $this->config['token_callback'] = $tokenCallback; - } - - /** - * Revoke an OAuth2 access token or refresh token. This method will revoke the current access - * token, if a token isn't provided. - * - * @param string|null $token The token (access token or a refresh token) that should be revoked. - * @return boolean Returns True if the revocation was successful, otherwise False. - */ - public function revokeToken($token = null) - { - $tokenRevoker = new Google_AccessToken_Revoke( - $this->getHttpClient() - ); - - return $tokenRevoker->revokeToken($token ?: $this->getAccessToken()); - } - - /** - * Verify an id_token. This method will verify the current id_token, if one - * isn't provided. - * - * @throws Google_Exception - * @param string|null $idToken The token (id_token) that should be verified. - * @return array|false Returns the token payload as an array if the verification was - * successful, false otherwise. - */ - public function verifyIdToken($idToken = null) - { - $tokenVerifier = new Google_AccessToken_Verify( - $this->getHttpClient(), - $this->getCache() - ); - - if (is_null($idToken)) { - $token = $this->getAccessToken(); - if (!isset($token['id_token'])) { - throw new LogicException( - 'id_token must be passed in or set as part of setAccessToken' - ); - } - $idToken = $token['id_token']; - } - - return $tokenVerifier->verifyIdToken( - $idToken, - $this->getClientId() - ); - } - - /** - * Set the scopes to be requested. Must be called before createAuthUrl(). - * Will remove any previously configured scopes. - * @param array $scopes, ie: array('https://www.googleapis.com/auth/plus.login', - * 'https://www.googleapis.com/auth/moderator') - */ - public function setScopes($scopes) - { - $this->requestedScopes = array(); - $this->addScope($scopes); - } - - /** - * This functions adds a scope to be requested as part of the OAuth2.0 flow. - * Will append any scopes not previously requested to the scope parameter. - * A single string will be treated as a scope to request. An array of strings - * will each be appended. - * @param $scope_or_scopes string|array e.g. "profile" - */ - public function addScope($scope_or_scopes) - { - if (is_string($scope_or_scopes) && !in_array($scope_or_scopes, $this->requestedScopes)) { - $this->requestedScopes[] = $scope_or_scopes; - } else if (is_array($scope_or_scopes)) { - foreach ($scope_or_scopes as $scope) { - $this->addScope($scope); - } - } - } - - /** - * Returns the list of scopes requested by the client - * @return array the list of scopes - * - */ - public function getScopes() - { - return $this->requestedScopes; - } - - /** - * @return array - * @visible For Testing - */ - public function prepareScopes() - { - if (empty($this->requestedScopes)) { - return null; - } - $scopes = implode(' ', $this->requestedScopes); - return $scopes; - } - - /** - * Helper method to execute deferred HTTP requests. - * - * @param $request Psr\Http\Message\RequestInterface|Google_Http_Batch - * @throws Google_Exception - * @return object of the type of the expected class or Psr\Http\Message\ResponseInterface. - */ - public function execute(RequestInterface $request, $expectedClass = null) - { - $request = $request->withHeader( - 'User-Agent', - $this->config['application_name'] - . " " . self::USER_AGENT_SUFFIX - . $this->getLibraryVersion() - ); - - // call the authorize method - // this is where most of the grunt work is done - $http = $this->authorize(); - - return Google_Http_REST::execute($http, $request, $expectedClass, $this->config['retry']); - } - - /** - * Declare whether batch calls should be used. This may increase throughput - * by making multiple requests in one connection. - * - * @param boolean $useBatch True if the batch support should - * be enabled. Defaults to False. - */ - public function setUseBatch($useBatch) - { - // This is actually an alias for setDefer. - $this->setDefer($useBatch); - } - - /** - * Are we running in Google AppEngine? - * return bool - */ - public function isAppEngine() - { - return (isset($_SERVER['SERVER_SOFTWARE']) && - strpos($_SERVER['SERVER_SOFTWARE'], 'Google App Engine') !== false); - } - - public function setConfig($name, $value) - { - $this->config[$name] = $value; - } - - public function getConfig($name, $default = null) - { - return isset($this->config[$name]) ? $this->config[$name] : $default; - } - - /** - * For backwards compatibility - * alias for setAuthConfig - * - * @param string $file the configuration file - * @throws Google_Exception - */ - public function setAuthConfigFile($file) - { - $this->setAuthConfig($file); - } - - /** - * Set the auth config from new or deprecated JSON config. - * This structure should match the file downloaded from - * the "Download JSON" button on in the Google Developer - * Console. - * @param string|array $json the configuration json - * @throws Google_Exception - */ - public function setAuthConfig($config) - { - if (is_string($config)) { - if (!file_exists($config)) { - throw new InvalidArgumentException('file does not exist'); - } - - $json = file_get_contents($config); - - if (!$config = json_decode($json, true)) { - throw new LogicException('invalid json for auth config'); - } - } - - $key = isset($config['installed']) ? 'installed' : 'web'; - if (isset($config['type']) && $config['type'] == 'service_account') { - // application default credentials - $this->useApplicationDefaultCredentials(); - - // set the information from the config - $this->setClientId($config['client_id']); - $this->config['client_email'] = $config['client_email']; - $this->config['signing_key'] = $config['private_key']; - $this->config['signing_algorithm'] = 'HS256'; - } elseif (isset($config[$key])) { - // old-style - $this->setClientId($config[$key]['client_id']); - $this->setClientSecret($config[$key]['client_secret']); - if (isset($config[$key]['redirect_uris'])) { - $this->setRedirectUri($config[$key]['redirect_uris'][0]); - } - } else { - // new-style - $this->setClientId($config['client_id']); - $this->setClientSecret($config['client_secret']); - if (isset($config['redirect_uris'])) { - $this->setRedirectUri($config['redirect_uris'][0]); - } - } - } - - /** - * Use when the service account has been delegated domain wide access. - * - * @param string subject an email address account to impersonate - */ - public function setSubject($subject) - { - $this->config['subject'] = $subject; - } - - /** - * Declare whether making API calls should make the call immediately, or - * return a request which can be called with ->execute(); - * - * @param boolean $defer True if calls should not be executed right away. - */ - public function setDefer($defer) - { - $this->deferExecution = $defer; - } - - /** - * Whether or not to return raw requests - * @return boolean - */ - public function shouldDefer() - { - return $this->deferExecution; - } - - /** - * @return Google\Auth\OAuth2 implementation - */ - public function getOAuth2Service() - { - if (!isset($this->auth)) { - $this->auth = $this->createOAuth2Service(); - } - - return $this->auth; - } - - /** - * create a default google auth object - */ - protected function createOAuth2Service() - { - $auth = new OAuth2( - [ - 'clientId' => $this->getClientId(), - 'clientSecret' => $this->getClientSecret(), - 'authorizationUri' => self::OAUTH2_AUTH_URL, - 'tokenCredentialUri' => self::OAUTH2_TOKEN_URI, - 'redirectUri' => $this->getRedirectUri(), - 'issuer' => $this->config['client_id'], - 'signingKey' => $this->config['signing_key'], - 'signingAlgorithm' => $this->config['signing_algorithm'], - ] - ); - - return $auth; - } - - /** - * Set the Cache object - * @param Psr\Cache\CacheItemPoolInterface $cache - */ - public function setCache(CacheItemPoolInterface $cache) - { - $this->cache = $cache; - } - - /** - * @return Psr\Cache\CacheItemPoolInterface Cache implementation - */ - public function getCache() - { - return $this->cache; - } - - /** - * @return Google\Auth\CacheInterface Cache implementation - */ - public function setCacheConfig(array $cacheConfig) - { - $this->config['cache_config'] = $cacheConfig; - } - - /** - * Set the Logger object - * @param Psr\Log\LoggerInterface $logger - */ - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - } - - /** - * @return Psr\Log\LoggerInterface implementation - */ - public function getLogger() - { - if (!isset($this->logger)) { - $this->logger = $this->createDefaultLogger(); - } - - return $this->logger; - } - - protected function createDefaultLogger() - { - $logger = new Logger('google-api-php-client'); - $logger->pushHandler(new MonologStreamHandler('php://stderr', Logger::NOTICE)); - - return $logger; - } - - /** - * Set the Http Client object - * @param GuzzleHttp\ClientInterface $http - */ - public function setHttpClient(ClientInterface $http) - { - $this->http = $http; - } - - /** - * @return GuzzleHttp\ClientInterface implementation - */ - public function getHttpClient() - { - if (is_null($this->http)) { - $this->http = $this->createDefaultHttpClient(); - } - - return $this->http; - } - - protected function createDefaultHttpClient() - { - $options = ['exceptions' => false]; - - $version = ClientInterface::VERSION; - if ('5' === $version[0]) { - $options = [ - 'base_url' => $this->config['base_path'], - 'defaults' => $options, - ]; - if ($this->isAppEngine()) { - // set StreamHandler on AppEngine by default - $options['handler'] = new StreamHandler(); - $options['defaults']['verify'] = '/etc/ca-certificates.crt'; - } - } else { - // guzzle 6 - $options['base_uri'] = $this->config['base_path']; - } - - return new Client($options); - } - - private function createApplicationDefaultCredentials() - { - $scopes = $this->prepareScopes(); - $sub = $this->config['subject']; - $signingKey = $this->config['signing_key']; - - // create credentials using values supplied in setAuthConfig - if ($signingKey) { - $serviceAccountCredentials = array( - 'client_id' => $this->config['client_id'], - 'client_email' => $this->config['client_email'], - 'private_key' => $signingKey, - 'type' => 'service_account', - ); - $credentials = CredentialsLoader::makeCredentials($scopes, $serviceAccountCredentials); - } else { - $credentials = ApplicationDefaultCredentials::getCredentials($scopes); - } - - // for service account domain-wide authority (impersonating a user) - // @see https://developers.google.com/identity/protocols/OAuth2ServiceAccount - if ($sub) { - if (!$credentials instanceof ServiceAccountCredentials) { - throw new DomainException('domain-wide authority requires service account credentials'); - } - - $credentials->setSub($sub); - } - - return $credentials; - } - - protected function getAuthHandler() - { - // Be very careful using the cache, as the underlying auth library's cache - // implementation is naive, and the cache keys do not account for user - // sessions. - // - // @see https://github.com/google/google-api-php-client/issues/821 - return Google_AuthHandler_AuthHandlerFactory::build( - $this->getCache(), - $this->config['cache_config'] - ); - } - - private function createUserRefreshCredentials($scope, $refreshToken) - { - $creds = array_filter( - array( - 'client_id' => $this->getClientId(), - 'client_secret' => $this->getClientSecret(), - 'refresh_token' => $refreshToken, - ) - ); - - return new UserRefreshCredentials($scope, $creds); - } -} diff --git a/src/Google/Collection.php b/src/Google/Collection.php deleted file mode 100644 index b26e9e51d..000000000 --- a/src/Google/Collection.php +++ /dev/null @@ -1,101 +0,0 @@ -modelData[$this->collection_key]) - && is_array($this->modelData[$this->collection_key])) { - reset($this->modelData[$this->collection_key]); - } - } - - public function current() - { - $this->coerceType($this->key()); - if (is_array($this->modelData[$this->collection_key])) { - return current($this->modelData[$this->collection_key]); - } - } - - public function key() - { - if (isset($this->modelData[$this->collection_key]) - && is_array($this->modelData[$this->collection_key])) { - return key($this->modelData[$this->collection_key]); - } - } - - public function next() - { - return next($this->modelData[$this->collection_key]); - } - - public function valid() - { - $key = $this->key(); - return $key !== null && $key !== false; - } - - public function count() - { - if (!isset($this->modelData[$this->collection_key])) { - return 0; - } - return count($this->modelData[$this->collection_key]); - } - - public function offsetExists($offset) - { - if (!is_numeric($offset)) { - return parent::offsetExists($offset); - } - return isset($this->modelData[$this->collection_key][$offset]); - } - - public function offsetGet($offset) - { - if (!is_numeric($offset)) { - return parent::offsetGet($offset); - } - $this->coerceType($offset); - return $this->modelData[$this->collection_key][$offset]; - } - - public function offsetSet($offset, $value) - { - if (!is_numeric($offset)) { - return parent::offsetSet($offset, $value); - } - $this->modelData[$this->collection_key][$offset] = $value; - } - - public function offsetUnset($offset) - { - if (!is_numeric($offset)) { - return parent::offsetUnset($offset); - } - unset($this->modelData[$this->collection_key][$offset]); - } - - private function coerceType($offset) - { - $typeKey = $this->keyType($this->collection_key); - if (isset($this->$typeKey) && !is_object($this->modelData[$this->collection_key][$offset])) { - $type = $this->$typeKey; - $this->modelData[$this->collection_key][$offset] = - new $type($this->modelData[$this->collection_key][$offset]); - } - } -} diff --git a/src/Google/Http/Batch.php b/src/Google/Http/Batch.php deleted file mode 100644 index e344777b3..000000000 --- a/src/Google/Http/Batch.php +++ /dev/null @@ -1,243 +0,0 @@ -client = $client; - $this->boundary = mt_rand(); - } - - public function add(RequestInterface $request, $key = false) - { - if (false == $key) { - $key = mt_rand(); - } - - $this->requests[$key] = $request; - } - - public function execute() - { - $body = ''; - $classes = array(); - $batchHttpTemplate = <<requests as $key => $request) { - $firstLine = sprintf( - '%s %s HTTP/%s', - $request->getMethod(), - $request->getRequestTarget(), - $request->getProtocolVersion() - ); - - $content = (string) $request->getBody(); - - $headers = ''; - foreach ($request->getHeaders() as $name => $values) { - $headers .= sprintf("%s:%s\r\n", $name, implode(', ', $values)); - } - - $body .= sprintf( - $batchHttpTemplate, - $this->boundary, - $key, - $firstLine, - $headers, - $content ? "\n".$content : '' - ); - - $classes['response-' . $key] = $request->getHeaderLine('X-Php-Expected-Class'); - } - - $body .= "--{$this->boundary}--"; - $body = trim($body); - $url = Google_Client::API_BASE_PATH . '/' . self::BATCH_PATH; - $headers = array( - 'Content-Type' => sprintf('multipart/mixed; boundary=%s', $this->boundary), - 'Content-Length' => strlen($body), - ); - - $request = new Request( - 'POST', - $url, - $headers, - $body - ); - - $response = $this->client->execute($request); - - return $this->parseResponse($response, $classes); - } - - public function parseResponse(ResponseInterface $response, $classes = array()) - { - $contentType = $response->getHeaderLine('content-type'); - $contentType = explode(';', $contentType); - $boundary = false; - foreach ($contentType as $part) { - $part = (explode('=', $part, 2)); - if (isset($part[0]) && 'boundary' == trim($part[0])) { - $boundary = $part[1]; - } - } - - $body = (string) $response->getBody(); - if (!empty($body)) { - $body = str_replace("--$boundary--", "--$boundary", $body); - $parts = explode("--$boundary", $body); - $responses = array(); - $requests = array_values($this->requests); - - foreach ($parts as $i => $part) { - $part = trim($part); - if (!empty($part)) { - list($rawHeaders, $part) = explode("\r\n\r\n", $part, 2); - $headers = $this->parseRawHeaders($rawHeaders); - - $status = substr($part, 0, strpos($part, "\n")); - $status = explode(" ", $status); - $status = $status[1]; - - list($partHeaders, $partBody) = $this->parseHttpResponse($part, false); - $response = new Response( - $status, - $partHeaders, - Psr7\stream_for($partBody) - ); - - // Need content id. - $key = $headers['content-id']; - $class = ''; - if (!empty($this->expected_classes[$key])) { - $class = $this->expected_classes[$key]; - } - - try { - $response = Google_Http_REST::decodeHttpResponse($response, $requests[$i-1]); - } catch (Google_Service_Exception $e) { - // Store the exception as the response, so successful responses - // can be processed. - $response = $e; - } - - $responses[$key] = $response; - } - } - - return $responses; - } - - return null; - } - - private function parseRawHeaders($rawHeaders) - { - $headers = array(); - $responseHeaderLines = explode("\r\n", $rawHeaders); - foreach ($responseHeaderLines as $headerLine) { - if ($headerLine && strpos($headerLine, ':') !== false) { - list($header, $value) = explode(': ', $headerLine, 2); - $header = strtolower($header); - if (isset($headers[$header])) { - $headers[$header] .= "\n" . $value; - } else { - $headers[$header] = $value; - } - } - } - return $headers; - } - - /** - * Used by the IO lib and also the batch processing. - * - * @param $respData - * @param $headerSize - * @return array - */ - private function parseHttpResponse($respData, $headerSize) - { - // check proxy header - foreach (self::$CONNECTION_ESTABLISHED_HEADERS as $established_header) { - if (stripos($respData, $established_header) !== false) { - // existed, remove it - $respData = str_ireplace($established_header, '', $respData); - // Subtract the proxy header size unless the cURL bug prior to 7.30.0 - // is present which prevented the proxy header size from being taken into - // account. - // @TODO look into this - // if (!$this->needsQuirk()) { - // $headerSize -= strlen($established_header); - // } - break; - } - } - - if ($headerSize) { - $responseBody = substr($respData, $headerSize); - $responseHeaders = substr($respData, 0, $headerSize); - } else { - $responseSegments = explode("\r\n\r\n", $respData, 2); - $responseHeaders = $responseSegments[0]; - $responseBody = isset($responseSegments[1]) ? $responseSegments[1] : - null; - } - - $responseHeaders = $this->parseRawHeaders($responseHeaders); - - return array($responseHeaders, $responseBody); - } -} diff --git a/src/Google/Http/MediaFileUpload.php b/src/Google/Http/MediaFileUpload.php deleted file mode 100644 index 72a6eeaf8..000000000 --- a/src/Google/Http/MediaFileUpload.php +++ /dev/null @@ -1,351 +0,0 @@ -client = $client; - $this->request = $request; - $this->mimeType = $mimeType; - $this->data = $data; - $this->resumable = $resumable; - $this->chunkSize = $chunkSize; - $this->progress = 0; - - $this->process(); - } - - /** - * Set the size of the file that is being uploaded. - * @param $size - int file size in bytes - */ - public function setFileSize($size) - { - $this->size = $size; - } - - /** - * Return the progress on the upload - * @return int progress in bytes uploaded. - */ - public function getProgress() - { - return $this->progress; - } - - /** - * Send the next part of the file to upload. - * @param [$chunk] the next set of bytes to send. If false will used $data passed - * at construct time. - */ - public function nextChunk($chunk = false) - { - $resumeUri = $this->getResumeUri(); - - if (false == $chunk) { - $chunk = substr($this->data, $this->progress, $this->chunkSize); - } - - $lastBytePos = $this->progress + strlen($chunk) - 1; - $headers = array( - 'content-range' => "bytes $this->progress-$lastBytePos/$this->size", - 'content-length' => strlen($chunk), - 'expect' => '', - ); - - $request = new Request( - 'PUT', - $resumeUri, - $headers, - Psr7\stream_for($chunk) - ); - - return $this->makePutRequest($request); - } - - /** - * Return the HTTP result code from the last call made. - * @return int code - */ - public function getHttpResultCode() - { - return $this->httpResultCode; - } - - /** - * Sends a PUT-Request to google drive and parses the response, - * setting the appropiate variables from the response() - * - * @param Google_Http_Request $httpRequest the Reuqest which will be send - * - * @return false|mixed false when the upload is unfinished or the decoded http response - * - */ - private function makePutRequest(RequestInterface $request) - { - $response = $this->client->execute($request); - $this->httpResultCode = $response->getStatusCode(); - - if (308 == $this->httpResultCode) { - // Track the amount uploaded. - $range = explode('-', $response->getHeaderLine('range')); - $this->progress = $range[1] + 1; - - // Allow for changing upload URLs. - $location = $response->getHeaderLine('location'); - if ($location) { - $this->resumeUri = $location; - } - - // No problems, but upload not complete. - return false; - } - - return Google_Http_REST::decodeHttpResponse($response, $this->request); - } - - /** - * Resume a previously unfinished upload - * @param $resumeUri the resume-URI of the unfinished, resumable upload. - */ - public function resume($resumeUri) - { - $this->resumeUri = $resumeUri; - $headers = array( - 'content-range' => "bytes */$this->size", - 'content-length' => 0, - ); - $httpRequest = new Request( - 'PUT', - $this->resumeUri, - $headers - ); - - return $this->makePutRequest($httpRequest); - } - - /** - * @return Psr\Http\Message\RequestInterface $request - * @visible for testing - */ - private function process() - { - $this->transformToUploadUrl(); - $request = $this->request; - - $postBody = ''; - $contentType = false; - - $meta = (string) $request->getBody(); - $meta = is_string($meta) ? json_decode($meta, true) : $meta; - - $uploadType = $this->getUploadType($meta); - $request = $request->withUri( - Uri::withQueryValue($request->getUri(), 'uploadType', $uploadType) - ); - - $mimeType = $this->mimeType ? - $this->mimeType : - $request->getHeaderLine('content-type'); - - if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) { - $contentType = $mimeType; - $postBody = is_string($meta) ? $meta : json_encode($meta); - } else if (self::UPLOAD_MEDIA_TYPE == $uploadType) { - $contentType = $mimeType; - $postBody = $this->data; - } else if (self::UPLOAD_MULTIPART_TYPE == $uploadType) { - // This is a multipart/related upload. - $boundary = $this->boundary ? $this->boundary : mt_rand(); - $boundary = str_replace('"', '', $boundary); - $contentType = 'multipart/related; boundary=' . $boundary; - $related = "--$boundary\r\n"; - $related .= "Content-Type: application/json; charset=UTF-8\r\n"; - $related .= "\r\n" . json_encode($meta) . "\r\n"; - $related .= "--$boundary\r\n"; - $related .= "Content-Type: $mimeType\r\n"; - $related .= "Content-Transfer-Encoding: base64\r\n"; - $related .= "\r\n" . base64_encode($this->data) . "\r\n"; - $related .= "--$boundary--"; - $postBody = $related; - } - - $request = $request->withBody(Psr7\stream_for($postBody)); - - if (isset($contentType) && $contentType) { - $request = $request->withHeader('content-type', $contentType); - } - - return $this->request = $request; - } - - /** - * Valid upload types: - * - resumable (UPLOAD_RESUMABLE_TYPE) - * - media (UPLOAD_MEDIA_TYPE) - * - multipart (UPLOAD_MULTIPART_TYPE) - * @param $meta - * @return string - * @visible for testing - */ - public function getUploadType($meta) - { - if ($this->resumable) { - return self::UPLOAD_RESUMABLE_TYPE; - } - - if (false == $meta && $this->data) { - return self::UPLOAD_MEDIA_TYPE; - } - - return self::UPLOAD_MULTIPART_TYPE; - } - - public function getResumeUri() - { - if (is_null($this->resumeUri)) { - $this->resumeUri = $this->fetchResumeUri(); - } - - return $this->resumeUri; - } - - private function fetchResumeUri() - { - $result = null; - $body = $this->request->getBody(); - if ($body) { - $headers = array( - 'content-type' => 'application/json; charset=UTF-8', - 'content-length' => $body->getSize(), - 'x-upload-content-type' => $this->mimeType, - 'x-upload-content-length' => $this->size, - 'expect' => '', - ); - foreach ($headers as $key => $value) { - $this->request = $this->request->withHeader($key, $value); - } - } - - $response = $this->client->execute($this->request, false); - $location = $response->getHeaderLine('location'); - $code = $response->getStatusCode(); - - if (200 == $code && true == $location) { - return $location; - } - - $message = $code; - $body = json_decode((string) $this->request->getBody(), true); - if (isset($body['error']['errors'])) { - $message .= ': '; - foreach ($body['error']['errors'] as $error) { - $message .= "{$error[domain]}, {$error[message]};"; - } - $message = rtrim($message, ';'); - } - - $error = "Failed to start the resumable upload (HTTP {$message})"; - $this->client->getLogger()->error($error); - - throw new Google_Exception($error); - } - - private function transformToUploadUrl() - { - $parts = parse_url(http://github.com/(string) $this->request->getUri()); - if (!isset($parts['path'])) { - $parts['path'] = ''; - } - $parts['path'] = '/upload' . $parts['path']; - $uri = Uri::fromParts($parts); - $this->request = $this->request->withUri($uri); - } - - public function setChunkSize($chunkSize) - { - $this->chunkSize = $chunkSize; - } - - public function getRequest() - { - return $this->request; - } -} diff --git a/src/Google/Http/REST.php b/src/Google/Http/REST.php deleted file mode 100644 index 7eccc9bee..000000000 --- a/src/Google/Http/REST.php +++ /dev/null @@ -1,182 +0,0 @@ -getMethod(), (string) $request->getUri()), - array(get_class(), 'doExecute'), - array($client, $request, $expectedClass) - ); - - if (!is_null($retryMap)) { - $runner->setRetryMap($retryMap); - } - - return $runner->run(); - } - - /** - * Executes a Psr\Http\Message\RequestInterface - * - * @param Google_Client $client - * @param Psr\Http\Message\RequestInterface $request - * @return array decoded result - * @throws Google_Service_Exception on server side error (ie: not authenticated, - * invalid or malformed post body, invalid url) - */ - public static function doExecute(ClientInterface $client, RequestInterface $request, $expectedClass = null) - { - try { - $httpHandler = HttpHandlerFactory::build($client); - $response = $httpHandler($request); - } catch (RequestException $e) { - // if Guzzle throws an exception, catch it and handle the response - if (!$e->hasResponse()) { - throw $e; - } - - $response = $e->getResponse(); - // specific checking for Guzzle 5: convert to PSR7 response - if ($response instanceof \GuzzleHttp\Message\ResponseInterface) { - $response = new Response( - $response->getStatusCode(), - $response->getHeaders() ?: [], - $response->getBody(), - $response->getProtocolVersion(), - $response->getReasonPhrase() - ); - } - } - - return self::decodeHttpResponse($response, $request, $expectedClass); - } - - /** - * Decode an HTTP Response. - * @static - * @throws Google_Service_Exception - * @param Psr\Http\Message\RequestInterface $response The http response to be decoded. - * @param Psr\Http\Message\ResponseInterface $response - * @return mixed|null - */ - public static function decodeHttpResponse( - ResponseInterface $response, - RequestInterface $request = null, - $expectedClass = null - ) { - $code = $response->getStatusCode(); - - // retry strategy - if ((intVal($code)) >= 400) { - // if we errored out, it should be safe to grab the response body - $body = (string) $response->getBody(); - - // Check if we received errors, and add those to the Exception for convenience - throw new Google_Service_Exception($body, $code, null, self::getResponseErrors($body)); - } - - // Ensure we only pull the entire body into memory if the request is not - // of media type - $body = self::decodeBody($response, $request); - - if ($expectedClass = self::determineExpectedClass($expectedClass, $request)) { - $json = json_decode($body, true); - - return new $expectedClass($json); - } - - return $response; - } - - private static function decodeBody(ResponseInterface $response, RequestInterface $request = null) - { - if (self::isAltMedia($request)) { - // don't decode the body, it's probably a really long string - return ''; - } - - return (string) $response->getBody(); - } - - private static function determineExpectedClass($expectedClass, RequestInterface $request = null) - { - // "false" is used to explicitly prevent an expected class from being returned - if (false === $expectedClass) { - return null; - } - - // if we don't have a request, we just use what's passed in - if (is_null($request)) { - return $expectedClass; - } - - // return what we have in the request header if one was not supplied - return $expectedClass ?: $request->getHeaderLine('X-Php-Expected-Class'); - } - - private static function getResponseErrors($body) - { - $json = json_decode($body, true); - - if (isset($json['error']['errors'])) { - return $json['error']['errors']; - } - - return null; - } - - private static function isAltMedia(RequestInterface $request = null) - { - if ($request && $qs = $request->getUri()->getQuery()) { - parse_str($qs, $query); - if (isset($query['alt']) && $query['alt'] == 'media') { - return true; - } - } - - return false; - } -} diff --git a/src/Google/Model.php b/src/Google/Model.php deleted file mode 100644 index 7168c945b..000000000 --- a/src/Google/Model.php +++ /dev/null @@ -1,308 +0,0 @@ -mapTypes($array); - } - $this->gapiInit(); - } - - /** - * Getter that handles passthrough access to the data array, and lazy object creation. - * @param string $key Property name. - * @return mixed The value if any, or null. - */ - public function __get($key) - { - $keyTypeName = $this->keyType($key); - $keyDataType = $this->dataType($key); - if (isset($this->$keyTypeName) && !isset($this->processed[$key])) { - if (isset($this->modelData[$key])) { - $val = $this->modelData[$key]; - } else if (isset($this->$keyDataType) && - ($this->$keyDataType == 'array' || $this->$keyDataType == 'map')) { - $val = array(); - } else { - $val = null; - } - - if ($this->isAssociativeArray($val)) { - if (isset($this->$keyDataType) && 'map' == $this->$keyDataType) { - foreach ($val as $arrayKey => $arrayItem) { - $this->modelData[$key][$arrayKey] = - $this->createObjectFromName($keyTypeName, $arrayItem); - } - } else { - $this->modelData[$key] = $this->createObjectFromName($keyTypeName, $val); - } - } else if (is_array($val)) { - $arrayObject = array(); - foreach ($val as $arrayIndex => $arrayItem) { - $arrayObject[$arrayIndex] = - $this->createObjectFromName($keyTypeName, $arrayItem); - } - $this->modelData[$key] = $arrayObject; - } - $this->processed[$key] = true; - } - - return isset($this->modelData[$key]) ? $this->modelData[$key] : null; - } - - /** - * Initialize this object's properties from an array. - * - * @param array $array Used to seed this object's properties. - * @return void - */ - protected function mapTypes($array) - { - // Hard initialise simple types, lazy load more complex ones. - foreach ($array as $key => $val) { - if ( !property_exists($this, $this->keyType($key)) && - property_exists($this, $key)) { - $this->$key = $val; - unset($array[$key]); - } elseif (property_exists($this, $camelKey = $this->camelCase($key))) { - // This checks if property exists as camelCase, leaving it in array as snake_case - // in case of backwards compatibility issues. - $this->$camelKey = $val; - } - } - $this->modelData = $array; - } - - /** - * Blank initialiser to be used in subclasses to do post-construction initialisation - this - * avoids the need for subclasses to have to implement the variadics handling in their - * constructors. - */ - protected function gapiInit() - { - return; - } - - /** - * Create a simplified object suitable for straightforward - * conversion to JSON. This is relatively expensive - * due to the usage of reflection, but shouldn't be called - * a whole lot, and is the most straightforward way to filter. - */ - public function toSimpleObject() - { - $object = new stdClass(); - - // Process all other data. - foreach ($this->modelData as $key => $val) { - $result = $this->getSimpleValue($val); - if ($result !== null) { - $object->$key = $this->nullPlaceholderCheck($result); - } - } - - // Process all public properties. - $reflect = new ReflectionObject($this); - $props = $reflect->getProperties(ReflectionProperty::IS_PUBLIC); - foreach ($props as $member) { - $name = $member->getName(); - $result = $this->getSimpleValue($this->$name); - if ($result !== null) { - $name = $this->getMappedName($name); - $object->$name = $this->nullPlaceholderCheck($result); - } - } - - return $object; - } - - /** - * Handle different types of values, primarily - * other objects and map and array data types. - */ - private function getSimpleValue($value) - { - if ($value instanceof Google_Model) { - return $value->toSimpleObject(); - } else if (is_array($value)) { - $return = array(); - foreach ($value as $key => $a_value) { - $a_value = $this->getSimpleValue($a_value); - if ($a_value !== null) { - $key = $this->getMappedName($key); - $return[$key] = $this->nullPlaceholderCheck($a_value); - } - } - return $return; - } - return $value; - } - - /** - * Check whether the value is the null placeholder and return true null. - */ - private function nullPlaceholderCheck($value) - { - if ($value === self::NULL_VALUE) { - return null; - } - return $value; - } - - /** - * If there is an internal name mapping, use that. - */ - private function getMappedName($key) - { - if (isset($this->internal_gapi_mappings) && - isset($this->internal_gapi_mappings[$key])) { - $key = $this->internal_gapi_mappings[$key]; - } - return $key; - } - - /** - * Returns true only if the array is associative. - * @param array $array - * @return bool True if the array is associative. - */ - protected function isAssociativeArray($array) - { - if (!is_array($array)) { - return false; - } - $keys = array_keys($array); - foreach ($keys as $key) { - if (is_string($key)) { - return true; - } - } - return false; - } - - /** - * Given a variable name, discover its type. - * - * @param $name - * @param $item - * @return object The object from the item. - */ - private function createObjectFromName($name, $item) - { - $type = $this->$name; - return new $type($item); - } - - /** - * Verify if $obj is an array. - * @throws Google_Exception Thrown if $obj isn't an array. - * @param array $obj Items that should be validated. - * @param string $method Method expecting an array as an argument. - */ - public function assertIsArray($obj, $method) - { - if ($obj && !is_array($obj)) { - throw new Google_Exception( - "Incorrect parameter type passed to $method(). Expected an array." - ); - } - } - - public function offsetExists($offset) - { - return isset($this->$offset) || isset($this->modelData[$offset]); - } - - public function offsetGet($offset) - { - return isset($this->$offset) ? - $this->$offset : - $this->__get($offset); - } - - public function offsetSet($offset, $value) - { - if (property_exists($this, $offset)) { - $this->$offset = $value; - } else { - $this->modelData[$offset] = $value; - $this->processed[$offset] = true; - } - } - - public function offsetUnset($offset) - { - unset($this->modelData[$offset]); - } - - protected function keyType($key) - { - return $key . "Type"; - } - - protected function dataType($key) - { - return $key . "DataType"; - } - - public function __isset($key) - { - return isset($this->modelData[$key]); - } - - public function __unset($key) - { - unset($this->modelData[$key]); - } - - /** - * Convert a string to camelCase - * @param string $value - * @return string - */ - private function camelCase($value) - { - $value = ucwords(str_replace(array('-', '_'), ' ', $value)); - $value = str_replace(' ', '', $value); - $value[0] = strtolower($value[0]); - return $value; - } -} diff --git a/src/Google/Service.php b/src/Google/Service.php deleted file mode 100644 index d3fd3b49d..000000000 --- a/src/Google/Service.php +++ /dev/null @@ -1,56 +0,0 @@ -client = $client; - } - - /** - * Return the associated Google_Client class. - * @return Google_Client - */ - public function getClient() - { - return $this->client; - } - - /** - * Create a new HTTP Batch handler for this service - * - * @return Google_Http_Batch - */ - public function createBatch() - { - return new Google_Http_Batch( - $this->client, - false, - $this->rootUrl, - $this->batchPath - ); - } -} diff --git a/src/Google/Service/Exception.php b/src/Google/Service/Exception.php deleted file mode 100644 index abfd3f7f1..000000000 --- a/src/Google/Service/Exception.php +++ /dev/null @@ -1,68 +0,0 @@ -= 0) { - parent::__construct($message, $code, $previous); - } else { - parent::__construct($message, $code); - } - - $this->errors = $errors; - } - - /** - * An example of the possible errors returned. - * - * { - * "domain": "global", - * "reason": "authError", - * "message": "Invalid Credentials", - * "locationType": "header", - * "location": "Authorization", - * } - * - * @return [{string, string}] List of errors return in an HTTP response or []. - */ - public function getErrors() - { - return $this->errors; - } -} diff --git a/src/Google/Service/Resource.php b/src/Google/Service/Resource.php deleted file mode 100644 index 96c24fc1a..000000000 --- a/src/Google/Service/Resource.php +++ /dev/null @@ -1,296 +0,0 @@ - array('type' => 'string', 'location' => 'query'), - 'fields' => array('type' => 'string', 'location' => 'query'), - 'trace' => array('type' => 'string', 'location' => 'query'), - 'userIp' => array('type' => 'string', 'location' => 'query'), - 'quotaUser' => array('type' => 'string', 'location' => 'query'), - 'data' => array('type' => 'string', 'location' => 'body'), - 'mimeType' => array('type' => 'string', 'location' => 'header'), - 'uploadType' => array('type' => 'string', 'location' => 'query'), - 'mediaUpload' => array('type' => 'complex', 'location' => 'query'), - 'prettyPrint' => array('type' => 'string', 'location' => 'query'), - ); - - /** @var string $rootUrl */ - private $rootUrl; - - /** @var Google_Client $client */ - private $client; - - /** @var string $serviceName */ - private $serviceName; - - /** @var string $servicePath */ - private $servicePath; - - /** @var string $resourceName */ - private $resourceName; - - /** @var array $methods */ - private $methods; - - public function __construct($service, $serviceName, $resourceName, $resource) - { - $this->rootUrl = $service->rootUrl; - $this->client = $service->getClient(); - $this->servicePath = $service->servicePath; - $this->serviceName = $serviceName; - $this->resourceName = $resourceName; - $this->methods = is_array($resource) && isset($resource['methods']) ? - $resource['methods'] : - array($resourceName => $resource); - } - - /** - * TODO: This function needs simplifying. - * @param $name - * @param $arguments - * @param $expectedClass - optional, the expected class name - * @return Google_Http_Request|expectedClass - * @throws Google_Exception - */ - public function call($name, $arguments, $expectedClass = null) - { - if (! isset($this->methods[$name])) { - $this->client->getLogger()->error( - 'Service method unknown', - array( - 'service' => $this->serviceName, - 'resource' => $this->resourceName, - 'method' => $name - ) - ); - - throw new Google_Exception( - "Unknown function: " . - "{$this->serviceName}->{$this->resourceName}->{$name}()" - ); - } - $method = $this->methods[$name]; - $parameters = $arguments[0]; - - // postBody is a special case since it's not defined in the discovery - // document as parameter, but we abuse the param entry for storing it. - $postBody = null; - if (isset($parameters['postBody'])) { - if ($parameters['postBody'] instanceof Google_Model) { - // In the cases the post body is an existing object, we want - // to use the smart method to create a simple object for - // for JSONification. - $parameters['postBody'] = $parameters['postBody']->toSimpleObject(); - } else if (is_object($parameters['postBody'])) { - // If the post body is another kind of object, we will try and - // wrangle it into a sensible format. - $parameters['postBody'] = - $this->convertToArrayAndStripNulls($parameters['postBody']); - } - $postBody = (array) $parameters['postBody']; - unset($parameters['postBody']); - } - - // TODO: optParams here probably should have been - // handled already - this may well be redundant code. - if (isset($parameters['optParams'])) { - $optParams = $parameters['optParams']; - unset($parameters['optParams']); - $parameters = array_merge($parameters, $optParams); - } - - if (!isset($method['parameters'])) { - $method['parameters'] = array(); - } - - $method['parameters'] = array_merge( - $this->stackParameters, - $method['parameters'] - ); - - foreach ($parameters as $key => $val) { - if ($key != 'postBody' && ! isset($method['parameters'][$key])) { - $this->client->getLogger()->error( - 'Service parameter unknown', - array( - 'service' => $this->serviceName, - 'resource' => $this->resourceName, - 'method' => $name, - 'parameter' => $key - ) - ); - throw new Google_Exception("($name) unknown parameter: '$key'"); - } - } - - foreach ($method['parameters'] as $paramName => $paramSpec) { - if (isset($paramSpec['required']) && - $paramSpec['required'] && - ! isset($parameters[$paramName]) - ) { - $this->client->getLogger()->error( - 'Service parameter missing', - array( - 'service' => $this->serviceName, - 'resource' => $this->resourceName, - 'method' => $name, - 'parameter' => $paramName - ) - ); - throw new Google_Exception("($name) missing required param: '$paramName'"); - } - if (isset($parameters[$paramName])) { - $value = $parameters[$paramName]; - $parameters[$paramName] = $paramSpec; - $parameters[$paramName]['value'] = $value; - unset($parameters[$paramName]['required']); - } else { - // Ensure we don't pass nulls. - unset($parameters[$paramName]); - } - } - - $this->client->getLogger()->info( - 'Service Call', - array( - 'service' => $this->serviceName, - 'resource' => $this->resourceName, - 'method' => $name, - 'arguments' => $parameters, - ) - ); - - // build the service uri - $url = $this->createRequestUri( - $method['path'], - $parameters - ); - - // NOTE: because we're creating the request by hand, - // and because the service has a rootUrl property - // the "base_uri" of the Http Client is not accounted for - $request = new Request( - $method['httpMethod'], - $url, - ['content-type' => 'application/json'], - $postBody ? json_encode($postBody) : '' - ); - - // support uploads - if (isset($parameters['data'])) { - $mimeType = isset($parameters['mimeType']) - ? $parameters['mimeType']['value'] - : 'application/octet-stream'; - $data = $parameters['data']['value']; - $upload = new Google_Http_MediaFileUpload($this->client, $request, $mimeType, $data); - - // pull down the modified request - $request = $upload->getRequest(); - } - - // if this is a media type, we will return the raw response - // rather than using an expected class - if (isset($parameters['alt']) && $parameters['alt']['value'] == 'media') { - $expectedClass = null; - } - - // if the client is marked for deferring, rather than - // execute the request, return the response - if ($this->client->shouldDefer()) { - // @TODO find a better way to do this - $request = $request - ->withHeader('X-Php-Expected-Class', $expectedClass); - - return $request; - } - - return $this->client->execute($request, $expectedClass); - } - - protected function convertToArrayAndStripNulls($o) - { - $o = (array) $o; - foreach ($o as $k => $v) { - if ($v === null) { - unset($o[$k]); - } elseif (is_object($v) || is_array($v)) { - $o[$k] = $this->convertToArrayAndStripNulls($o[$k]); - } - } - return $o; - } - - /** - * Parse/expand request parameters and create a fully qualified - * request uri. - * @static - * @param string $restPath - * @param array $params - * @return string $requestUrl - */ - public function createRequestUri($restPath, $params) - { - // code for leading slash - $requestUrl = $this->servicePath . $restPath; - if ($this->rootUrl) { - if ('/' !== substr($this->rootUrl, -1) && '/' !== substr($requestUrl, 0, 1)) { - $requestUrl = '/' . $requestUrl; - } - $requestUrl = $this->rootUrl . $requestUrl; - } - $uriTemplateVars = array(); - $queryVars = array(); - foreach ($params as $paramName => $paramSpec) { - if ($paramSpec['type'] == 'boolean') { - $paramSpec['value'] = ($paramSpec['value']) ? 'true' : 'false'; - } - if ($paramSpec['location'] == 'path') { - $uriTemplateVars[$paramName] = $paramSpec['value']; - } else if ($paramSpec['location'] == 'query') { - if (isset($paramSpec['repeated']) && is_array($paramSpec['value'])) { - foreach ($paramSpec['value'] as $value) { - $queryVars[] = $paramName . '=' . rawurlencode(rawurldecode($value)); - } - } else { - $queryVars[] = $paramName . '=' . rawurlencode(rawurldecode($paramSpec['value'])); - } - } - } - - if (count($uriTemplateVars)) { - $uriTemplateParser = new Google_Utils_UriTemplate(); - $requestUrl = $uriTemplateParser->parse($requestUrl, $uriTemplateVars); - } - - if (count($queryVars)) { - $requestUrl .= '?' . implode($queryVars, '&'); - } - - return $requestUrl; - } -} diff --git a/src/Google/Task/Runner.php b/src/Google/Task/Runner.php deleted file mode 100644 index 9d7f6c932..000000000 --- a/src/Google/Task/Runner.php +++ /dev/null @@ -1,284 +0,0 @@ - self::TASK_RETRY_ALWAYS, - '503' => self::TASK_RETRY_ALWAYS, - 'rateLimitExceeded' => self::TASK_RETRY_ALWAYS, - 'userRateLimitExceeded' => self::TASK_RETRY_ALWAYS, - 6 => self::TASK_RETRY_ALWAYS, // CURLE_COULDNT_RESOLVE_HOST - 7 => self::TASK_RETRY_ALWAYS, // CURLE_COULDNT_CONNECT - 28 => self::TASK_RETRY_ALWAYS, // CURLE_OPERATION_TIMEOUTED - 35 => self::TASK_RETRY_ALWAYS, // CURLE_SSL_CONNECT_ERROR - 52 => self::TASK_RETRY_ALWAYS // CURLE_GOT_NOTHING - ]; - - /** - * Creates a new task runner with exponential backoff support. - * - * @param array $config The task runner config - * @param string $name The name of the current task (used for logging) - * @param callable $action The task to run and possibly retry - * @param array $arguments The task arguments - * @throws Google_Task_Exception when misconfigured - */ - public function __construct( - $config, - $name, - $action, - array $arguments = array() - ) { - if (isset($config['initial_delay'])) { - if ($config['initial_delay'] < 0) { - throw new Google_Task_Exception( - 'Task configuration `initial_delay` must not be negative.' - ); - } - - $this->delay = $config['initial_delay']; - } - - if (isset($config['max_delay'])) { - if ($config['max_delay'] <= 0) { - throw new Google_Task_Exception( - 'Task configuration `max_delay` must be greater than 0.' - ); - } - - $this->maxDelay = $config['max_delay']; - } - - if (isset($config['factor'])) { - if ($config['factor'] <= 0) { - throw new Google_Task_Exception( - 'Task configuration `factor` must be greater than 0.' - ); - } - - $this->factor = $config['factor']; - } - - if (isset($config['jitter'])) { - if ($config['jitter'] <= 0) { - throw new Google_Task_Exception( - 'Task configuration `jitter` must be greater than 0.' - ); - } - - $this->jitter = $config['jitter']; - } - - if (isset($config['retries'])) { - if ($config['retries'] < 0) { - throw new Google_Task_Exception( - 'Task configuration `retries` must not be negative.' - ); - } - $this->maxAttempts += $config['retries']; - } - - if (!is_callable($action)) { - throw new Google_Task_Exception( - 'Task argument `$action` must be a valid callable.' - ); - } - - $this->name = $name; - $this->action = $action; - $this->arguments = $arguments; - } - - /** - * Checks if a retry can be attempted. - * - * @return boolean - */ - public function canAttempt() - { - return $this->attempts < $this->maxAttempts; - } - - /** - * Runs the task and (if applicable) automatically retries when errors occur. - * - * @return mixed - * @throws Google_Task_Retryable on failure when no retries are available. - */ - public function run() - { - while ($this->attempt()) { - try { - return call_user_func_array($this->action, $this->arguments); - } catch (Google_Service_Exception $exception) { - $allowedRetries = $this->allowedRetries( - $exception->getCode(), - $exception->getErrors() - ); - - if (!$this->canAttempt() || !$allowedRetries) { - throw $exception; - } - - if ($allowedRetries > 0) { - $this->maxAttempts = min( - $this->maxAttempts, - $this->attempts + $allowedRetries - ); - } - } - } - } - - /** - * Runs a task once, if possible. This is useful for bypassing the `run()` - * loop. - * - * NOTE: If this is not the first attempt, this function will sleep in - * accordance to the backoff configurations before running the task. - * - * @return boolean - */ - public function attempt() - { - if (!$this->canAttempt()) { - return false; - } - - if ($this->attempts > 0) { - $this->backOff(); - } - - $this->attempts++; - return true; - } - - /** - * Sleeps in accordance to the backoff configurations. - */ - private function backOff() - { - $delay = $this->getDelay(); - - usleep($delay * 1000000); - } - - /** - * Gets the delay (in seconds) for the current backoff period. - * - * @return float - */ - private function getDelay() - { - $jitter = $this->getJitter(); - $factor = $this->attempts > 1 ? $this->factor + $jitter : 1 + abs($jitter); - - return $this->delay = min($this->maxDelay, $this->delay * $factor); - } - - /** - * Gets the current jitter (random number between -$this->jitter and - * $this->jitter). - * - * @return float - */ - private function getJitter() - { - return $this->jitter * 2 * mt_rand() / mt_getrandmax() - $this->jitter; - } - - /** - * Gets the number of times the associated task can be retried. - * - * NOTE: -1 is returned if the task can be retried indefinitely - * - * @return integer - */ - public function allowedRetries($code, $errors = array()) - { - if (isset($this->retryMap[$code])) { - return $this->retryMap[$code]; - } - - if (!empty($errors) && isset($errors[0]['reason']) && - isset($this->retryMap[$errors[0]['reason']])) { - return $this->retryMap[$errors[0]['reason']]; - } - - return 0; - } - - public function setRetryMap($retryMap) - { - $this->retryMap = $retryMap; - } -} diff --git a/src/Google/Utils/UriTemplate.php b/src/Google/Utils/UriTemplate.php deleted file mode 100644 index e59fe9f21..000000000 --- a/src/Google/Utils/UriTemplate.php +++ /dev/null @@ -1,333 +0,0 @@ - "reserved", - "/" => "segments", - "." => "dotprefix", - "#" => "fragment", - ";" => "semicolon", - "?" => "form", - "&" => "continuation" - ); - - /** - * @var reserved array - * These are the characters which should not be URL encoded in reserved - * strings. - */ - private $reserved = array( - "=", ",", "!", "@", "|", ":", "/", "?", "#", - "[", "]",'$', "&", "'", "(", ")", "*", "+", ";" - ); - private $reservedEncoded = array( - "%3D", "%2C", "%21", "%40", "%7C", "%3A", "%2F", "%3F", - "%23", "%5B", "%5D", "%24", "%26", "%27", "%28", "%29", - "%2A", "%2B", "%3B" - ); - - public function parse($string, array $parameters) - { - return $this->resolveNextSection($string, $parameters); - } - - /** - * This function finds the first matching {...} block and - * executes the replacement. It then calls itself to find - * subsequent blocks, if any. - */ - private function resolveNextSection($string, $parameters) - { - $start = strpos($string, "{"); - if ($start === false) { - return $string; - } - $end = strpos($string, "}"); - if ($end === false) { - return $string; - } - $string = $this->replace($string, $start, $end, $parameters); - return $this->resolveNextSection($string, $parameters); - } - - private function replace($string, $start, $end, $parameters) - { - // We know a data block will have {} round it, so we can strip that. - $data = substr($string, $start + 1, $end - $start - 1); - - // If the first character is one of the reserved operators, it effects - // the processing of the stream. - if (isset($this->operators[$data[0]])) { - $op = $this->operators[$data[0]]; - $data = substr($data, 1); - $prefix = ""; - $prefix_on_missing = false; - - switch ($op) { - case "reserved": - // Reserved means certain characters should not be URL encoded - $data = $this->replaceVars($data, $parameters, ",", null, true); - break; - case "fragment": - // Comma separated with fragment prefix. Bare values only. - $prefix = "#"; - $prefix_on_missing = true; - $data = $this->replaceVars($data, $parameters, ",", null, true); - break; - case "segments": - // Slash separated data. Bare values only. - $prefix = "/"; - $data =$this->replaceVars($data, $parameters, "/"); - break; - case "dotprefix": - // Dot separated data. Bare values only. - $prefix = "."; - $prefix_on_missing = true; - $data = $this->replaceVars($data, $parameters, "."); - break; - case "semicolon": - // Semicolon prefixed and separated. Uses the key name - $prefix = ";"; - $data = $this->replaceVars($data, $parameters, ";", "=", false, true, false); - break; - case "form": - // Standard URL format. Uses the key name - $prefix = "?"; - $data = $this->replaceVars($data, $parameters, "&", "="); - break; - case "continuation": - // Standard URL, but with leading ampersand. Uses key name. - $prefix = "&"; - $data = $this->replaceVars($data, $parameters, "&", "="); - break; - } - - // Add the initial prefix character if data is valid. - if ($data || ($data !== false && $prefix_on_missing)) { - $data = $prefix . $data; - } - - } else { - // If no operator we replace with the defaults. - $data = $this->replaceVars($data, $parameters); - } - // This is chops out the {...} and replaces with the new section. - return substr($string, 0, $start) . $data . substr($string, $end + 1); - } - - private function replaceVars( - $section, - $parameters, - $sep = ",", - $combine = null, - $reserved = false, - $tag_empty = false, - $combine_on_empty = true - ) { - if (strpos($section, ",") === false) { - // If we only have a single value, we can immediately process. - return $this->combine( - $section, - $parameters, - $sep, - $combine, - $reserved, - $tag_empty, - $combine_on_empty - ); - } else { - // If we have multiple values, we need to split and loop over them. - // Each is treated individually, then glued together with the - // separator character. - $vars = explode(",", $section); - return $this->combineList( - $vars, - $sep, - $parameters, - $combine, - $reserved, - false, // Never emit empty strings in multi-param replacements - $combine_on_empty - ); - } - } - - public function combine( - $key, - $parameters, - $sep, - $combine, - $reserved, - $tag_empty, - $combine_on_empty - ) { - $length = false; - $explode = false; - $skip_final_combine = false; - $value = false; - - // Check for length restriction. - if (strpos($key, ":") !== false) { - list($key, $length) = explode(":", $key); - } - - // Check for explode parameter. - if ($key[strlen($key) - 1] == "*") { - $explode = true; - $key = substr($key, 0, -1); - $skip_final_combine = true; - } - - // Define the list separator. - $list_sep = $explode ? $sep : ","; - - if (isset($parameters[$key])) { - $data_type = $this->getDataType($parameters[$key]); - switch ($data_type) { - case self::TYPE_SCALAR: - $value = $this->getValue($parameters[$key], $length); - break; - case self::TYPE_LIST: - $values = array(); - foreach ($parameters[$key] as $pkey => $pvalue) { - $pvalue = $this->getValue($pvalue, $length); - if ($combine && $explode) { - $values[$pkey] = $key . $combine . $pvalue; - } else { - $values[$pkey] = $pvalue; - } - } - $value = implode($list_sep, $values); - if ($value == '') { - return ''; - } - break; - case self::TYPE_MAP: - $values = array(); - foreach ($parameters[$key] as $pkey => $pvalue) { - $pvalue = $this->getValue($pvalue, $length); - if ($explode) { - $pkey = $this->getValue($pkey, $length); - $values[] = $pkey . "=" . $pvalue; // Explode triggers = combine. - } else { - $values[] = $pkey; - $values[] = $pvalue; - } - } - $value = implode($list_sep, $values); - if ($value == '') { - return false; - } - break; - } - } else if ($tag_empty) { - // If we are just indicating empty values with their key name, return that. - return $key; - } else { - // Otherwise we can skip this variable due to not being defined. - return false; - } - - if ($reserved) { - $value = str_replace($this->reservedEncoded, $this->reserved, $value); - } - - // If we do not need to include the key name, we just return the raw - // value. - if (!$combine || $skip_final_combine) { - return $value; - } - - // Else we combine the key name: foo=bar, if value is not the empty string. - return $key . ($value != '' || $combine_on_empty ? $combine . $value : ''); - } - - /** - * Return the type of a passed in value - */ - private function getDataType($data) - { - if (is_array($data)) { - reset($data); - if (key($data) !== 0) { - return self::TYPE_MAP; - } - return self::TYPE_LIST; - } - return self::TYPE_SCALAR; - } - - /** - * Utility function that merges multiple combine calls - * for multi-key templates. - */ - private function combineList( - $vars, - $sep, - $parameters, - $combine, - $reserved, - $tag_empty, - $combine_on_empty - ) { - $ret = array(); - foreach ($vars as $var) { - $response = $this->combine( - $var, - $parameters, - $sep, - $combine, - $reserved, - $tag_empty, - $combine_on_empty - ); - if ($response === false) { - continue; - } - $ret[] = $response; - } - return implode($sep, $ret); - } - - /** - * Utility function to encode and trim values - */ - private function getValue($value, $length) - { - if ($length) { - $value = substr($value, 0, $length); - } - $value = rawurlencode($value); - return $value; - } -} diff --git a/src/Google/autoload.php b/src/Google/autoload.php deleted file mode 100644 index 8b493010e..000000000 --- a/src/Google/autoload.php +++ /dev/null @@ -1,21 +0,0 @@ -client = $client; + $this->boundary = $boundary ?: mt_rand(); + $rootUrl = rtrim($rootUrl ?: $this->client->getConfig('base_path'), '/'); + $this->rootUrl = str_replace( + 'UNIVERSE_DOMAIN', + $this->client->getUniverseDomain(), + $rootUrl + ); + $this->batchPath = $batchPath ?: self::BATCH_PATH; + } + + public function add(RequestInterface $request, $key = false) + { + if (false == $key) { + $key = mt_rand(); + } + + $this->requests[$key] = $request; + } + + public function execute() + { + $body = ''; + $classes = []; + $batchHttpTemplate = <<requests as $key => $request) { + $firstLine = sprintf( + '%s %s HTTP/%s', + $request->getMethod(), + $request->getRequestTarget(), + $request->getProtocolVersion() + ); + + $content = (string) $request->getBody(); + + $headers = ''; + foreach ($request->getHeaders() as $name => $values) { + $headers .= sprintf("%s:%s\r\n", $name, implode(', ', $values)); + } + + $body .= sprintf( + $batchHttpTemplate, + $this->boundary, + $key, + $firstLine, + $headers, + $content ? "\n" . $content : '' + ); + + $classes['response-' . $key] = $request->getHeaderLine('X-Php-Expected-Class'); + } + + $body .= "--{$this->boundary}--"; + $body = trim($body); + $url = $this->rootUrl . '/' . $this->batchPath; + $headers = [ + 'Content-Type' => sprintf('multipart/mixed; boundary=%s', $this->boundary), + 'Content-Length' => (string) strlen($body), + ]; + + $request = new Request( + 'POST', + $url, + $headers, + $body + ); + + $response = $this->client->execute($request); + + return $this->parseResponse($response, $classes); + } + + public function parseResponse(ResponseInterface $response, $classes = []) + { + $contentType = $response->getHeaderLine('content-type'); + $contentType = explode(';', $contentType); + $boundary = false; + foreach ($contentType as $part) { + $part = explode('=', $part, 2); + if (isset($part[0]) && 'boundary' == trim($part[0])) { + $boundary = $part[1]; + } + } + + $body = (string) $response->getBody(); + if (!empty($body)) { + $body = str_replace("--$boundary--", "--$boundary", $body); + $parts = explode("--$boundary", $body); + $responses = []; + $requests = array_values($this->requests); + + foreach ($parts as $i => $part) { + $part = trim($part); + if (!empty($part)) { + list($rawHeaders, $part) = explode("\r\n\r\n", $part, 2); + $headers = $this->parseRawHeaders($rawHeaders); + + $status = substr($part, 0, strpos($part, "\n")); + $status = explode(" ", $status); + $status = $status[1]; + + list($partHeaders, $partBody) = $this->parseHttpResponse($part, 0); + $response = new Response( + (int) $status, + $partHeaders, + Psr7\Utils::streamFor($partBody) + ); + + // Need content id. + $key = $headers['content-id']; + + try { + $response = REST::decodeHttpResponse($response, $requests[$i-1]); + } catch (GoogleServiceException $e) { + // Store the exception as the response, so successful responses + // can be processed. + $response = $e; + } + + $responses[$key] = $response; + } + } + + return $responses; + } + + return null; + } + + private function parseRawHeaders($rawHeaders) + { + $headers = []; + $responseHeaderLines = explode("\r\n", $rawHeaders); + foreach ($responseHeaderLines as $headerLine) { + if ($headerLine && strpos($headerLine, ':') !== false) { + list($header, $value) = explode(': ', $headerLine, 2); + $header = strtolower($header); + if (isset($headers[$header])) { + $headers[$header] = array_merge((array)$headers[$header], (array)$value); + } else { + $headers[$header] = $value; + } + } + } + return $headers; + } + + /** + * Used by the IO lib and also the batch processing. + * + * @param string $respData + * @param int $headerSize + * @return array + */ + private function parseHttpResponse($respData, $headerSize) + { + // check proxy header + foreach (self::$CONNECTION_ESTABLISHED_HEADERS as $established_header) { + if (stripos($respData, $established_header) !== false) { + // existed, remove it + $respData = str_ireplace($established_header, '', $respData); + // Subtract the proxy header size unless the cURL bug prior to 7.30.0 + // is present which prevented the proxy header size from being taken into + // account. + // @TODO look into this + // if (!$this->needsQuirk()) { + // $headerSize -= strlen($established_header); + // } + break; + } + } + + if ($headerSize) { + $responseBody = substr($respData, $headerSize); + $responseHeaders = substr($respData, 0, $headerSize); + } else { + $responseSegments = explode("\r\n\r\n", $respData, 2); + $responseHeaders = $responseSegments[0]; + $responseBody = isset($responseSegments[1]) ? $responseSegments[1] : null; + } + + $responseHeaders = $this->parseRawHeaders($responseHeaders); + + return [$responseHeaders, $responseBody]; + } +} diff --git a/src/Http/MediaFileUpload.php b/src/Http/MediaFileUpload.php new file mode 100644 index 000000000..2713ea415 --- /dev/null +++ b/src/Http/MediaFileUpload.php @@ -0,0 +1,353 @@ +client = $client; + $this->request = $request; + $this->mimeType = $mimeType; + $this->data = $data; + $this->resumable = $resumable; + $this->chunkSize = $chunkSize; + $this->progress = 0; + + $this->process(); + } + + /** + * Set the size of the file that is being uploaded. + * @param int $size - int file size in bytes + */ + public function setFileSize($size) + { + $this->size = $size; + } + + /** + * Return the progress on the upload + * @return int progress in bytes uploaded. + */ + public function getProgress() + { + return $this->progress; + } + + /** + * Send the next part of the file to upload. + * @param string|bool $chunk Optional. The next set of bytes to send. If false will + * use $data passed at construct time. + */ + public function nextChunk($chunk = false) + { + $resumeUri = $this->getResumeUri(); + + if (false == $chunk) { + $chunk = substr($this->data, $this->progress, $this->chunkSize); + } + + $lastBytePos = $this->progress + strlen($chunk) - 1; + $headers = [ + 'content-range' => "bytes $this->progress-$lastBytePos/$this->size", + 'content-length' => (string) strlen($chunk), + 'expect' => '', + ]; + + $request = new Request( + 'PUT', + $resumeUri, + $headers, + Psr7\Utils::streamFor($chunk) + ); + + return $this->makePutRequest($request); + } + + /** + * Return the HTTP result code from the last call made. + * @return int code + */ + public function getHttpResultCode() + { + return $this->httpResultCode; + } + + /** + * Sends a PUT-Request to google drive and parses the response, + * setting the appropiate variables from the response() + * + * @param RequestInterface $request the Request which will be send + * + * @return false|mixed false when the upload is unfinished or the decoded http response + * + */ + private function makePutRequest(RequestInterface $request) + { + $response = $this->client->execute($request); + $this->httpResultCode = $response->getStatusCode(); + + if (308 == $this->httpResultCode) { + // Track the amount uploaded. + $range = $response->getHeaderLine('range'); + if ($range) { + $range_array = explode('-', $range); + $this->progress = ((int) $range_array[1]) + 1; + } + + // Allow for changing upload URLs. + $location = $response->getHeaderLine('location'); + if ($location) { + $this->resumeUri = $location; + } + + // No problems, but upload not complete. + return false; + } + + return REST::decodeHttpResponse($response, $this->request); + } + + /** + * Resume a previously unfinished upload + * @param string $resumeUri the resume-URI of the unfinished, resumable upload. + */ + public function resume($resumeUri) + { + $this->resumeUri = $resumeUri; + $headers = [ + 'content-range' => "bytes */$this->size", + 'content-length' => '0', + ]; + $httpRequest = new Request( + 'PUT', + $this->resumeUri, + $headers + ); + return $this->makePutRequest($httpRequest); + } + + /** + * @return RequestInterface + * @visible for testing + */ + private function process() + { + $this->transformToUploadUrl(); + $request = $this->request; + + $postBody = ''; + $contentType = false; + + $meta = json_decode((string) $request->getBody(), true); + + $uploadType = $this->getUploadType($meta); + $request = $request->withUri( + Uri::withQueryValue($request->getUri(), 'uploadType', $uploadType) + ); + + $mimeType = $this->mimeType ?: $request->getHeaderLine('content-type'); + + if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) { + $contentType = $mimeType; + $postBody = is_string($meta) ? $meta : json_encode($meta); + } elseif (self::UPLOAD_MEDIA_TYPE == $uploadType) { + $contentType = $mimeType; + $postBody = $this->data; + } elseif (self::UPLOAD_MULTIPART_TYPE == $uploadType) { + // This is a multipart/related upload. + $boundary = $this->boundary ?: mt_rand(); + $boundary = str_replace('"', '', $boundary); + $contentType = 'multipart/related; boundary=' . $boundary; + $related = "--$boundary\r\n"; + $related .= "Content-Type: application/json; charset=UTF-8\r\n"; + $related .= "\r\n" . json_encode($meta) . "\r\n"; + $related .= "--$boundary\r\n"; + $related .= "Content-Type: $mimeType\r\n"; + $related .= "Content-Transfer-Encoding: base64\r\n"; + $related .= "\r\n" . base64_encode($this->data) . "\r\n"; + $related .= "--$boundary--"; + $postBody = $related; + } + + $request = $request->withBody(Psr7\Utils::streamFor($postBody)); + + if ($contentType) { + $request = $request->withHeader('content-type', $contentType); + } + + return $this->request = $request; + } + + /** + * Valid upload types: + * - resumable (UPLOAD_RESUMABLE_TYPE) + * - media (UPLOAD_MEDIA_TYPE) + * - multipart (UPLOAD_MULTIPART_TYPE) + * @param string|false $meta + * @return string + * @visible for testing + */ + public function getUploadType($meta) + { + if ($this->resumable) { + return self::UPLOAD_RESUMABLE_TYPE; + } + + if (false == $meta && $this->data) { + return self::UPLOAD_MEDIA_TYPE; + } + + return self::UPLOAD_MULTIPART_TYPE; + } + + public function getResumeUri() + { + if (null === $this->resumeUri) { + $this->resumeUri = $this->fetchResumeUri(); + } + + return $this->resumeUri; + } + + private function fetchResumeUri() + { + $body = $this->request->getBody(); + $headers = [ + 'content-type' => 'application/json; charset=UTF-8', + 'content-length' => $body->getSize(), + 'x-upload-content-type' => $this->mimeType, + 'x-upload-content-length' => $this->size, + 'expect' => '', + ]; + foreach ($headers as $key => $value) { + $this->request = $this->request->withHeader($key, $value); + } + + $response = $this->client->execute($this->request, false); + $location = $response->getHeaderLine('location'); + $code = $response->getStatusCode(); + + if (200 == $code && true == $location) { + return $location; + } + + $message = $code; + $body = json_decode((string) $this->request->getBody(), true); + if (isset($body['error']['errors'])) { + $message .= ': '; + foreach ($body['error']['errors'] as $error) { + $message .= "{$error['domain']}, {$error['message']};"; + } + $message = rtrim($message, ';'); + } + + $error = "Failed to start the resumable upload (HTTP {$message})"; + $this->client->getLogger()->error($error); + + throw new GoogleException($error); + } + + private function transformToUploadUrl() + { + $parts = parse_url(http://github.com/(string) $this->request->getUri()); + if (!isset($parts['path'])) { + $parts['path'] = ''; + } + $parts['path'] = '/upload' . $parts['path']; + $uri = Uri::fromParts($parts); + $this->request = $this->request->withUri($uri); + } + + public function setChunkSize($chunkSize) + { + $this->chunkSize = $chunkSize; + } + + public function getRequest() + { + return $this->request; + } +} diff --git a/src/Http/REST.php b/src/Http/REST.php new file mode 100644 index 000000000..c3d3270db --- /dev/null +++ b/src/Http/REST.php @@ -0,0 +1,198 @@ +|false|null $expectedClass + * @param array $config + * @param array $retryMap + * @return mixed|T|null + * @throws \Google\Service\Exception on server side error (ie: not authenticated, + * invalid or malformed post body, invalid url) + */ + public static function execute( + ClientInterface $client, + RequestInterface $request, + $expectedClass = null, + $config = [], + $retryMap = null + ) { + $runner = new Runner( + $config, + sprintf('%s %s', $request->getMethod(), (string)$request->getUri()), + [self::class, 'doExecute'], + [$client, $request, $expectedClass] + ); + + if (null !== $retryMap) { + $runner->setRetryMap($retryMap); + } + + return $runner->run(); + } + + /** + * Executes a Psr\Http\Message\RequestInterface + * + * @template T + * @param ClientInterface $client + * @param RequestInterface $request + * @param class-string|false|null $expectedClass + * @return mixed|T|null + * @throws \Google\Service\Exception on server side error (ie: not authenticated, + * invalid or malformed post body, invalid url) + */ + public static function doExecute(ClientInterface $client, RequestInterface $request, $expectedClass = null) + { + try { + $httpHandler = HttpHandlerFactory::build($client); + $response = $httpHandler($request); + } catch (RequestException $e) { + // if Guzzle throws an exception, catch it and handle the response + if (!$e->hasResponse()) { + throw $e; + } + + $response = $e->getResponse(); + // specific checking for Guzzle 5: convert to PSR7 response + if ( + interface_exists('\GuzzleHttp\Message\ResponseInterface') + && $response instanceof \GuzzleHttp\Message\ResponseInterface + ) { + $response = new Response( + $response->getStatusCode(), + $response->getHeaders() ?: [], + $response->getBody(), + $response->getProtocolVersion(), + $response->getReasonPhrase() + ); + } + } + + return self::decodeHttpResponse($response, $request, $expectedClass); + } + + /** + * Decode an HTTP Response. + * @static + * + * @template T + * @param RequestInterface $response The http response to be decoded. + * @param ResponseInterface $response + * @param class-string|false|null $expectedClass + * @return mixed|T|null + * @throws \Google\Service\Exception + */ + public static function decodeHttpResponse( + ResponseInterface $response, + ?RequestInterface $request = null, + $expectedClass = null + ) { + $code = $response->getStatusCode(); + + // retry strategy + if (intVal($code) >= 400) { + // if we errored out, it should be safe to grab the response body + $body = (string)$response->getBody(); + + // Check if we received errors, and add those to the Exception for convenience + throw new GoogleServiceException($body, $code, null, self::getResponseErrors($body)); + } + + // Ensure we only pull the entire body into memory if the request is not + // of media type + $body = self::decodeBody($response, $request); + + if ($expectedClass = self::determineExpectedClass($expectedClass, $request)) { + $json = json_decode($body, true); + + return new $expectedClass($json); + } + + return $response; + } + + private static function decodeBody(ResponseInterface $response, ?RequestInterface $request = null) + { + if (self::isAltMedia($request)) { + // don't decode the body, it's probably a really long string + return ''; + } + + return (string)$response->getBody(); + } + + private static function determineExpectedClass($expectedClass, ?RequestInterface $request = null) + { + // "false" is used to explicitly prevent an expected class from being returned + if (false === $expectedClass) { + return null; + } + + // if we don't have a request, we just use what's passed in + if (null === $request) { + return $expectedClass; + } + + // return what we have in the request header if one was not supplied + return $expectedClass ?: $request->getHeaderLine('X-Php-Expected-Class'); + } + + private static function getResponseErrors($body) + { + $json = json_decode($body, true); + + if (isset($json['error']['errors'])) { + return $json['error']['errors']; + } + + return null; + } + + private static function isAltMedia(?RequestInterface $request = null) + { + if ($request && $qs = $request->getUri()->getQuery()) { + parse_str($qs, $query); + if (isset($query['alt']) && $query['alt'] == 'media') { + return true; + } + } + + return false; + } +} diff --git a/src/Model.php b/src/Model.php new file mode 100644 index 000000000..87f437d66 --- /dev/null +++ b/src/Model.php @@ -0,0 +1,333 @@ +mapTypes($array); + } + $this->gapiInit(); + } + + /** + * Getter that handles passthrough access to the data array, and lazy object creation. + * @param string $key Property name. + * @return mixed The value if any, or null. + */ + public function __get($key) + { + $keyType = $this->keyType($key); + $keyDataType = $this->dataType($key); + if ($keyType && !isset($this->processed[$key])) { + if (isset($this->modelData[$key])) { + $val = $this->modelData[$key]; + } elseif ($keyDataType == 'array' || $keyDataType == 'map') { + $val = []; + } else { + $val = null; + } + + if ($this->isAssociativeArray($val)) { + if ($keyDataType && 'map' == $keyDataType) { + foreach ($val as $arrayKey => $arrayItem) { + $this->modelData[$key][$arrayKey] = + new $keyType($arrayItem); + } + } else { + $this->modelData[$key] = new $keyType($val); + } + } elseif (is_array($val)) { + $arrayObject = []; + foreach ($val as $arrayIndex => $arrayItem) { + $arrayObject[$arrayIndex] = new $keyType($arrayItem); + } + $this->modelData[$key] = $arrayObject; + } + $this->processed[$key] = true; + } + + return isset($this->modelData[$key]) ? $this->modelData[$key] : null; + } + + /** + * Initialize this object's properties from an array. + * + * @param array $array Used to seed this object's properties. + * @return void + */ + protected function mapTypes($array) + { + // Hard initialise simple types, lazy load more complex ones. + foreach ($array as $key => $val) { + if ($keyType = $this->keyType($key)) { + $dataType = $this->dataType($key); + if ($dataType == 'array' || $dataType == 'map') { + $this->$key = []; + foreach ($val as $itemKey => $itemVal) { + if ($itemVal instanceof $keyType) { + $this->{$key}[$itemKey] = $itemVal; + } else { + $this->{$key}[$itemKey] = new $keyType($itemVal); + } + } + } elseif ($val instanceof $keyType) { + $this->$key = $val; + } else { + $this->$key = new $keyType($val); + } + unset($array[$key]); + } elseif (property_exists($this, $key)) { + $this->$key = $val; + unset($array[$key]); + } elseif (property_exists($this, $camelKey = $this->camelCase($key))) { + // This checks if property exists as camelCase, leaving it in array as snake_case + // in case of backwards compatibility issues. + $this->$camelKey = $val; + } + } + $this->modelData = $array; + } + + /** + * Blank initialiser to be used in subclasses to do post-construction initialisation - this + * avoids the need for subclasses to have to implement the variadics handling in their + * constructors. + */ + protected function gapiInit() + { + return; + } + + /** + * Create a simplified object suitable for straightforward + * conversion to JSON. This is relatively expensive + * due to the usage of reflection, but shouldn't be called + * a whole lot, and is the most straightforward way to filter. + */ + public function toSimpleObject() + { + $object = new stdClass(); + + // Process all other data. + foreach ($this->modelData as $key => $val) { + $result = $this->getSimpleValue($val); + if ($result !== null) { + $object->$key = $this->nullPlaceholderCheck($result); + } + } + + // Process all public properties. + $reflect = new ReflectionObject($this); + $props = $reflect->getProperties(ReflectionProperty::IS_PUBLIC); + foreach ($props as $member) { + $name = $member->getName(); + $result = $this->getSimpleValue($this->$name); + if ($result !== null) { + $name = $this->getMappedName($name); + $object->$name = $this->nullPlaceholderCheck($result); + } + } + + return $object; + } + + /** + * Handle different types of values, primarily + * other objects and map and array data types. + */ + private function getSimpleValue($value) + { + if ($value instanceof Model) { + return $value->toSimpleObject(); + } elseif (is_array($value)) { + $return = []; + foreach ($value as $key => $a_value) { + $a_value = $this->getSimpleValue($a_value); + if ($a_value !== null) { + $key = $this->getMappedName($key); + $return[$key] = $this->nullPlaceholderCheck($a_value); + } + } + return $return; + } + return $value; + } + + /** + * Check whether the value is the null placeholder and return true null. + */ + private function nullPlaceholderCheck($value) + { + if ($value === self::NULL_VALUE) { + return null; + } + return $value; + } + + /** + * If there is an internal name mapping, use that. + */ + private function getMappedName($key) + { + if (isset($this->internal_gapi_mappings, $this->internal_gapi_mappings[$key])) { + $key = $this->internal_gapi_mappings[$key]; + } + return $key; + } + + /** + * Returns true only if the array is associative. + * @param array $array + * @return bool True if the array is associative. + */ + protected function isAssociativeArray($array) + { + if (!is_array($array)) { + return false; + } + $keys = array_keys($array); + foreach ($keys as $key) { + if (is_string($key)) { + return true; + } + } + return false; + } + + /** + * Verify if $obj is an array. + * @throws \Google\Exception Thrown if $obj isn't an array. + * @param array $obj Items that should be validated. + * @param string $method Method expecting an array as an argument. + */ + public function assertIsArray($obj, $method) + { + if ($obj && !is_array($obj)) { + throw new GoogleException( + "Incorrect parameter type passed to $method(). Expected an array." + ); + } + } + + /** @return bool */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return isset($this->$offset) || isset($this->modelData[$offset]); + } + + /** @return mixed */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return isset($this->$offset) ? + $this->$offset : + $this->__get($offset); + } + + /** @return void */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + if (property_exists($this, $offset)) { + $this->$offset = $value; + } else { + $this->modelData[$offset] = $value; + $this->processed[$offset] = true; + } + } + + /** @return void */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + unset($this->modelData[$offset]); + } + + protected function keyType($key) + { + $keyType = $key . "Type"; + + // ensure keyType is a valid class + if (property_exists($this, $keyType) && $this->$keyType !== null && class_exists($this->$keyType)) { + return $this->$keyType; + } + } + + protected function dataType($key) + { + $dataType = $key . "DataType"; + + if (property_exists($this, $dataType)) { + return $this->$dataType; + } + } + + public function __isset($key) + { + return isset($this->modelData[$key]); + } + + public function __unset($key) + { + unset($this->modelData[$key]); + } + + /** + * Convert a string to camelCase + * @param string $value + * @return string + */ + private function camelCase($value) + { + $value = ucwords(str_replace(['-', '_'], ' ', $value)); + $value = str_replace(' ', '', $value); + $value[0] = strtolower($value[0]); + return $value; + } +} diff --git a/src/Service.php b/src/Service.php new file mode 100644 index 000000000..8c8fe5fa7 --- /dev/null +++ b/src/Service.php @@ -0,0 +1,76 @@ +client = $clientOrConfig; + } elseif (is_array($clientOrConfig)) { + $this->client = new Client($clientOrConfig ?: []); + } else { + $errorMessage = 'constructor must be array or instance of Google\Client'; + if (class_exists('TypeError')) { + throw new TypeError($errorMessage); + } + trigger_error($errorMessage, E_USER_ERROR); + } + } + + /** + * Return the associated Google\Client class. + * @return \Google\Client + */ + public function getClient() + { + return $this->client; + } + + /** + * Create a new HTTP Batch handler for this service + * + * @return Batch + */ + public function createBatch() + { + return new Batch( + $this->client, + false, + $this->rootUrlTemplate ?? $this->rootUrl, + $this->batchPath + ); + } +} diff --git a/src/Service/Exception.php b/src/Service/Exception.php new file mode 100644 index 000000000..d79a2e75d --- /dev/null +++ b/src/Service/Exception.php @@ -0,0 +1,73 @@ +>|null $errors List of errors returned in an HTTP + * response or null. Defaults to []. + */ + public function __construct( + $message, + $code = 0, + ?Exception $previous = null, + $errors = [] + ) { + if (version_compare(PHP_VERSION, '5.3.0') >= 0) { + parent::__construct($message, $code, $previous); + } else { + parent::__construct($message, $code); + } + + $this->errors = $errors; + } + + /** + * An example of the possible errors returned. + * + * [ + * { + * "domain": "global", + * "reason": "authError", + * "message": "Invalid Credentials", + * "locationType": "header", + * "location": "Authorization", + * } + * ] + * + * @return array>|null List of errors returned in an HTTP response or null. + */ + public function getErrors() + { + return $this->errors; + } +} diff --git a/src/Google/Service/README.md b/src/Service/README.md similarity index 100% rename from src/Google/Service/README.md rename to src/Service/README.md diff --git a/src/Service/Resource.php b/src/Service/Resource.php new file mode 100644 index 000000000..693aaa781 --- /dev/null +++ b/src/Service/Resource.php @@ -0,0 +1,320 @@ + ['type' => 'string', 'location' => 'query'], + 'fields' => ['type' => 'string', 'location' => 'query'], + 'trace' => ['type' => 'string', 'location' => 'query'], + 'userIp' => ['type' => 'string', 'location' => 'query'], + 'quotaUser' => ['type' => 'string', 'location' => 'query'], + 'data' => ['type' => 'string', 'location' => 'body'], + 'mimeType' => ['type' => 'string', 'location' => 'header'], + 'uploadType' => ['type' => 'string', 'location' => 'query'], + 'mediaUpload' => ['type' => 'complex', 'location' => 'query'], + 'prettyPrint' => ['type' => 'string', 'location' => 'query'], + ]; + + /** @var string $rootUrlTemplate */ + private $rootUrlTemplate; + + /** @var string $apiVersion */ + protected $apiVersion; + + /** @var \Google\Client $client */ + private $client; + + /** @var string $serviceName */ + private $serviceName; + + /** @var string $servicePath */ + private $servicePath; + + /** @var string $resourceName */ + private $resourceName; + + /** @var array $methods */ + private $methods; + + public function __construct($service, $serviceName, $resourceName, $resource) + { + $this->rootUrlTemplate = $service->rootUrlTemplate ?? $service->rootUrl; + $this->client = $service->getClient(); + $this->servicePath = $service->servicePath; + $this->serviceName = $serviceName; + $this->resourceName = $resourceName; + $this->methods = is_array($resource) && isset($resource['methods']) ? + $resource['methods'] : + [$resourceName => $resource]; + } + + /** + * TODO: This function needs simplifying. + * + * @template T + * @param string $name + * @param array $arguments + * @param class-string $expectedClass - optional, the expected class name + * @return mixed|T|ResponseInterface|RequestInterface + * @throws \Google\Exception + */ + public function call($name, $arguments, $expectedClass = null) + { + if (! isset($this->methods[$name])) { + $this->client->getLogger()->error( + 'Service method unknown', + [ + 'service' => $this->serviceName, + 'resource' => $this->resourceName, + 'method' => $name + ] + ); + + throw new GoogleException( + "Unknown function: " . + "{$this->serviceName}->{$this->resourceName}->{$name}()" + ); + } + $method = $this->methods[$name]; + $parameters = $arguments[0]; + + // postBody is a special case since it's not defined in the discovery + // document as parameter, but we abuse the param entry for storing it. + $postBody = null; + if (isset($parameters['postBody'])) { + if ($parameters['postBody'] instanceof Model) { + // In the cases the post body is an existing object, we want + // to use the smart method to create a simple object for + // for JSONification. + $parameters['postBody'] = $parameters['postBody']->toSimpleObject(); + } elseif (is_object($parameters['postBody'])) { + // If the post body is another kind of object, we will try and + // wrangle it into a sensible format. + $parameters['postBody'] = + $this->convertToArrayAndStripNulls($parameters['postBody']); + } + $postBody = (array) $parameters['postBody']; + unset($parameters['postBody']); + } + + // TODO: optParams here probably should have been + // handled already - this may well be redundant code. + if (isset($parameters['optParams'])) { + $optParams = $parameters['optParams']; + unset($parameters['optParams']); + $parameters = array_merge($parameters, $optParams); + } + + if (!isset($method['parameters'])) { + $method['parameters'] = []; + } + + $method['parameters'] = array_merge( + $this->stackParameters, + $method['parameters'] + ); + + foreach ($parameters as $key => $val) { + if ($key != 'postBody' && !isset($method['parameters'][$key])) { + $this->client->getLogger()->error( + 'Service parameter unknown', + [ + 'service' => $this->serviceName, + 'resource' => $this->resourceName, + 'method' => $name, + 'parameter' => $key + ] + ); + throw new GoogleException("($name) unknown parameter: '$key'"); + } + } + + foreach ($method['parameters'] as $paramName => $paramSpec) { + if ( + isset($paramSpec['required']) && + $paramSpec['required'] && + ! isset($parameters[$paramName]) + ) { + $this->client->getLogger()->error( + 'Service parameter missing', + [ + 'service' => $this->serviceName, + 'resource' => $this->resourceName, + 'method' => $name, + 'parameter' => $paramName + ] + ); + throw new GoogleException("($name) missing required param: '$paramName'"); + } + if (isset($parameters[$paramName])) { + $value = $parameters[$paramName]; + $parameters[$paramName] = $paramSpec; + $parameters[$paramName]['value'] = $value; + unset($parameters[$paramName]['required']); + } else { + // Ensure we don't pass nulls. + unset($parameters[$paramName]); + } + } + + $this->client->getLogger()->info( + 'Service Call', + [ + 'service' => $this->serviceName, + 'resource' => $this->resourceName, + 'method' => $name, + 'arguments' => $parameters, + ] + ); + + // build the service uri + $url = $this->createRequestUri($method['path'], $parameters); + + // NOTE: because we're creating the request by hand, + // and because the service has a rootUrl property + // the "base_uri" of the Http Client is not accounted for + $request = new Request( + $method['httpMethod'], + $url, + $postBody ? ['content-type' => 'application/json'] : [], + $postBody ? json_encode($postBody) : '' + ); + + // support uploads + if (isset($parameters['data'])) { + $mimeType = isset($parameters['mimeType']) + ? $parameters['mimeType']['value'] + : 'application/octet-stream'; + $data = $parameters['data']['value']; + $upload = new MediaFileUpload($this->client, $request, $mimeType, $data); + + // pull down the modified request + $request = $upload->getRequest(); + } + + // if this is a media type, we will return the raw response + // rather than using an expected class + if (isset($parameters['alt']) && $parameters['alt']['value'] == 'media') { + $expectedClass = null; + } + + // If the class which is extending from this one contains + // an Api Version, add it to the header + if ($this->apiVersion) { + $request = $request + ->withHeader('X-Goog-Api-Version', $this->apiVersion); + } + + // if the client is marked for deferring, rather than + // execute the request, return the response + if ($this->client->shouldDefer()) { + // @TODO find a better way to do this + $request = $request + ->withHeader('X-Php-Expected-Class', $expectedClass); + + return $request; + } + + return $this->client->execute($request, $expectedClass); + } + + protected function convertToArrayAndStripNulls($o) + { + $o = (array) $o; + foreach ($o as $k => $v) { + if ($v === null) { + unset($o[$k]); + } elseif (is_object($v) || is_array($v)) { + $o[$k] = $this->convertToArrayAndStripNulls($o[$k]); + } + } + return $o; + } + + /** + * Parse/expand request parameters and create a fully qualified + * request uri. + * @static + * @param string $restPath + * @param array $params + * @return string $requestUrl + */ + public function createRequestUri($restPath, $params) + { + // Override the default servicePath address if the $restPath use a / + if ('/' == substr($restPath, 0, 1)) { + $requestUrl = substr($restPath, 1); + } else { + $requestUrl = $this->servicePath . $restPath; + } + + if ($this->rootUrlTemplate) { + // code for universe domain + $rootUrl = str_replace('UNIVERSE_DOMAIN', $this->client->getUniverseDomain(), $this->rootUrlTemplate); + // code for leading slash + if ('/' !== substr($rootUrl, -1) && '/' !== substr($requestUrl, 0, 1)) { + $requestUrl = '/' . $requestUrl; + } + $requestUrl = $rootUrl . $requestUrl; + } + $uriTemplateVars = []; + $queryVars = []; + foreach ($params as $paramName => $paramSpec) { + if ($paramSpec['type'] == 'boolean') { + $paramSpec['value'] = $paramSpec['value'] ? 'true' : 'false'; + } + if ($paramSpec['location'] == 'path') { + $uriTemplateVars[$paramName] = $paramSpec['value']; + } elseif ($paramSpec['location'] == 'query') { + if (is_array($paramSpec['value'])) { + foreach ($paramSpec['value'] as $value) { + $queryVars[] = $paramName . '=' . rawurlencode(rawurldecode($value)); + } + } else { + $queryVars[] = $paramName . '=' . rawurlencode(rawurldecode($paramSpec['value'])); + } + } + } + + if (count($uriTemplateVars)) { + $uriTemplateParser = new UriTemplate(); + $requestUrl = $uriTemplateParser->parse($requestUrl, $uriTemplateVars); + } + + if (count($queryVars)) { + $requestUrl .= '?' . implode('&', $queryVars); + } + + return $requestUrl; + } +} diff --git a/src/Task/Composer.php b/src/Task/Composer.php new file mode 100644 index 000000000..fff4de22d --- /dev/null +++ b/src/Task/Composer.php @@ -0,0 +1,115 @@ +getComposer(); + $extra = $composer->getPackage()->getExtra(); + $servicesToKeep = $extra['google/apiclient-services'] ?? []; + if (empty($servicesToKeep)) { + return; + } + $vendorDir = $composer->getConfig()->get('vendor-dir'); + $serviceDir = sprintf( + '%s/google/apiclient-services/src/Google/Service', + $vendorDir + ); + if (!is_dir($serviceDir)) { + // path for google/apiclient-services >= 0.200.0 + $serviceDir = sprintf( + '%s/google/apiclient-services/src', + $vendorDir + ); + } + self::verifyServicesToKeep($serviceDir, $servicesToKeep); + $finder = self::getServicesToRemove($serviceDir, $servicesToKeep); + $filesystem = $filesystem ?: new Filesystem(); + $servicesToRemoveCount = $finder->count(); + if (0 === $servicesToRemoveCount) { + return; + } + $event->getIO()->write( + sprintf('Removing %d google services', $servicesToRemoveCount) + ); + $pathsToRemove = iterator_to_array($finder); + foreach ($pathsToRemove as $pathToRemove) { + $realpath = $pathToRemove->getRealPath(); + $filesystem->remove($realpath); + $filesystem->remove($realpath . '.php'); + } + } + + /** + * @throws InvalidArgumentException when the service doesn't exist + */ + private static function verifyServicesToKeep( + $serviceDir, + array $servicesToKeep + ) { + $finder = (new Finder()) + ->directories() + ->depth('== 0'); + + foreach ($servicesToKeep as $service) { + if (!preg_match('/^[a-zA-Z0-9]*$/', $service)) { + throw new InvalidArgumentException( + sprintf( + 'Invalid Google service name "%s"', + $service + ) + ); + } + try { + $finder->in($serviceDir . '/' . $service); + } catch (InvalidArgumentException $e) { + throw new InvalidArgumentException( + sprintf( + 'Google service "%s" does not exist or was removed previously', + $service + ) + ); + } + } + } + + private static function getServicesToRemove( + $serviceDir, + array $servicesToKeep + ) { + // find all files in the current directory + return (new Finder()) + ->directories() + ->depth('== 0') + ->in($serviceDir) + ->exclude($servicesToKeep); + } +} diff --git a/src/Google/Task/Exception.php b/src/Task/Exception.php similarity index 85% rename from src/Google/Task/Exception.php rename to src/Task/Exception.php index 5422e6fc4..9e0d436b5 100644 --- a/src/Google/Task/Exception.php +++ b/src/Task/Exception.php @@ -15,6 +15,10 @@ * limitations under the License. */ -class Google_Task_Exception extends Google_Exception +namespace Google\Task; + +use Google\Exception as GoogleException; + +class Exception extends GoogleException { } diff --git a/src/Google/Task/Retryable.php b/src/Task/Retryable.php similarity index 94% rename from src/Google/Task/Retryable.php rename to src/Task/Retryable.php index 19aa4ddc2..5f67af8ac 100644 --- a/src/Google/Task/Retryable.php +++ b/src/Task/Retryable.php @@ -15,10 +15,12 @@ * limitations under the License. */ +namespace Google\Task; + /** * Interface for checking how many times a given task can be retried following * a failure. */ -interface Google_Task_Retryable +interface Retryable { } diff --git a/src/Task/Runner.php b/src/Task/Runner.php new file mode 100644 index 000000000..1047f1f01 --- /dev/null +++ b/src/Task/Runner.php @@ -0,0 +1,293 @@ + self::TASK_RETRY_ALWAYS, + '503' => self::TASK_RETRY_ALWAYS, + 'rateLimitExceeded' => self::TASK_RETRY_ALWAYS, + 'userRateLimitExceeded' => self::TASK_RETRY_ALWAYS, + 6 => self::TASK_RETRY_ALWAYS, // CURLE_COULDNT_RESOLVE_HOST + 7 => self::TASK_RETRY_ALWAYS, // CURLE_COULDNT_CONNECT + 28 => self::TASK_RETRY_ALWAYS, // CURLE_OPERATION_TIMEOUTED + 35 => self::TASK_RETRY_ALWAYS, // CURLE_SSL_CONNECT_ERROR + 52 => self::TASK_RETRY_ALWAYS, // CURLE_GOT_NOTHING + 'lighthouseError' => self::TASK_RETRY_NEVER + ]; + + /** + * Creates a new task runner with exponential backoff support. + * + * @param array $config The task runner config + * @param string $name The name of the current task (used for logging) + * @param callable $action The task to run and possibly retry + * @param array $arguments The task arguments + * @throws \Google\Task\Exception when misconfigured + */ + // @phpstan-ignore-next-line + public function __construct( + $config, + $name, + $action, + array $arguments = [] + ) { + if (isset($config['initial_delay'])) { + if ($config['initial_delay'] < 0) { + throw new GoogleTaskException( + 'Task configuration `initial_delay` must not be negative.' + ); + } + + $this->delay = $config['initial_delay']; + } + + if (isset($config['max_delay'])) { + if ($config['max_delay'] <= 0) { + throw new GoogleTaskException( + 'Task configuration `max_delay` must be greater than 0.' + ); + } + + $this->maxDelay = $config['max_delay']; + } + + if (isset($config['factor'])) { + if ($config['factor'] <= 0) { + throw new GoogleTaskException( + 'Task configuration `factor` must be greater than 0.' + ); + } + + $this->factor = $config['factor']; + } + + if (isset($config['jitter'])) { + if ($config['jitter'] <= 0) { + throw new GoogleTaskException( + 'Task configuration `jitter` must be greater than 0.' + ); + } + + $this->jitter = $config['jitter']; + } + + if (isset($config['retries'])) { + if ($config['retries'] < 0) { + throw new GoogleTaskException( + 'Task configuration `retries` must not be negative.' + ); + } + $this->maxAttempts += $config['retries']; + } + + if (!is_callable($action)) { + throw new GoogleTaskException( + 'Task argument `$action` must be a valid callable.' + ); + } + + $this->action = $action; + $this->arguments = $arguments; + } + + /** + * Checks if a retry can be attempted. + * + * @return boolean + */ + public function canAttempt() + { + return $this->attempts < $this->maxAttempts; + } + + /** + * Runs the task and (if applicable) automatically retries when errors occur. + * + * @return mixed + * @throws \Google\Service\Exception on failure when no retries are available. + */ + public function run() + { + while ($this->attempt()) { + try { + return call_user_func_array($this->action, $this->arguments); + } catch (GoogleServiceException $exception) { + $allowedRetries = $this->allowedRetries( + $exception->getCode(), + $exception->getErrors() + ); + + if (!$this->canAttempt() || !$allowedRetries) { + throw $exception; + } + + if ($allowedRetries > 0) { + $this->maxAttempts = min( + $this->maxAttempts, + $this->attempts + $allowedRetries + ); + } + } + } + } + + /** + * Runs a task once, if possible. This is useful for bypassing the `run()` + * loop. + * + * NOTE: If this is not the first attempt, this function will sleep in + * accordance to the backoff configurations before running the task. + * + * @return boolean + */ + public function attempt() + { + if (!$this->canAttempt()) { + return false; + } + + if ($this->attempts > 0) { + $this->backOff(); + } + + $this->attempts++; + + return true; + } + + /** + * Sleeps in accordance to the backoff configurations. + */ + private function backOff() + { + $delay = $this->getDelay(); + + usleep((int) ($delay * 1000000)); + } + + /** + * Gets the delay (in seconds) for the current backoff period. + * + * @return int + */ + private function getDelay() + { + $jitter = $this->getJitter(); + $factor = $this->attempts > 1 ? $this->factor + $jitter : 1 + abs($jitter); + + return $this->delay = min($this->maxDelay, $this->delay * $factor); + } + + /** + * Gets the current jitter (random number between -$this->jitter and + * $this->jitter). + * + * @return float + */ + private function getJitter() + { + return $this->jitter * 2 * mt_rand() / mt_getrandmax() - $this->jitter; + } + + /** + * Gets the number of times the associated task can be retried. + * + * NOTE: -1 is returned if the task can be retried indefinitely + * + * @return integer + */ + public function allowedRetries($code, $errors = []) + { + if (isset($this->retryMap[$code])) { + return $this->retryMap[$code]; + } + + if ( + !empty($errors) && + isset($errors[0]['reason'], $this->retryMap[$errors[0]['reason']]) + ) { + return $this->retryMap[$errors[0]['reason']]; + } + + return 0; + } + + public function setRetryMap($retryMap) + { + $this->retryMap = $retryMap; + } +} diff --git a/src/Utils/UriTemplate.php b/src/Utils/UriTemplate.php new file mode 100644 index 000000000..d4691e02c --- /dev/null +++ b/src/Utils/UriTemplate.php @@ -0,0 +1,334 @@ + "reserved", + "/" => "segments", + "." => "dotprefix", + "#" => "fragment", + ";" => "semicolon", + "?" => "form", + "&" => "continuation" + ]; + + /** + * @var array + * These are the characters which should not be URL encoded in reserved + * strings. + */ + private $reserved = [ + "=", ",", "!", "@", "|", ":", "/", "?", "#", + "[", "]", '$', "&", "'", "(", ")", "*", "+", ";" + ]; + private $reservedEncoded = [ + "%3D", "%2C", "%21", "%40", "%7C", "%3A", "%2F", "%3F", + "%23", "%5B", "%5D", "%24", "%26", "%27", "%28", "%29", + "%2A", "%2B", "%3B" + ]; + + public function parse($string, array $parameters) + { + return $this->resolveNextSection($string, $parameters); + } + + /** + * This function finds the first matching {...} block and + * executes the replacement. It then calls itself to find + * subsequent blocks, if any. + */ + private function resolveNextSection($string, $parameters) + { + $start = strpos($string, "{"); + if ($start === false) { + return $string; + } + $end = strpos($string, "}"); + if ($end === false) { + return $string; + } + $string = $this->replace($string, $start, $end, $parameters); + return $this->resolveNextSection($string, $parameters); + } + + private function replace($string, $start, $end, $parameters) + { + // We know a data block will have {} round it, so we can strip that. + $data = substr($string, $start + 1, $end - $start - 1); + + // If the first character is one of the reserved operators, it effects + // the processing of the stream. + if (isset($this->operators[$data[0]])) { + $op = $this->operators[$data[0]]; + $data = substr($data, 1); + $prefix = ""; + $prefix_on_missing = false; + + switch ($op) { + case "reserved": + // Reserved means certain characters should not be URL encoded + $data = $this->replaceVars($data, $parameters, ",", null, true); + break; + case "fragment": + // Comma separated with fragment prefix. Bare values only. + $prefix = "#"; + $prefix_on_missing = true; + $data = $this->replaceVars($data, $parameters, ",", null, true); + break; + case "segments": + // Slash separated data. Bare values only. + $prefix = "/"; + $data =$this->replaceVars($data, $parameters, "/"); + break; + case "dotprefix": + // Dot separated data. Bare values only. + $prefix = "."; + $prefix_on_missing = true; + $data = $this->replaceVars($data, $parameters, "."); + break; + case "semicolon": + // Semicolon prefixed and separated. Uses the key name + $prefix = ";"; + $data = $this->replaceVars($data, $parameters, ";", "=", false, true, false); + break; + case "form": + // Standard URL format. Uses the key name + $prefix = "?"; + $data = $this->replaceVars($data, $parameters, "&", "="); + break; + case "continuation": + // Standard URL, but with leading ampersand. Uses key name. + $prefix = "&"; + $data = $this->replaceVars($data, $parameters, "&", "="); + break; + } + + // Add the initial prefix character if data is valid. + if ($data || ($data !== false && $prefix_on_missing)) { + $data = $prefix . $data; + } + } else { + // If no operator we replace with the defaults. + $data = $this->replaceVars($data, $parameters); + } + // This is chops out the {...} and replaces with the new section. + return substr($string, 0, $start) . $data . substr($string, $end + 1); + } + + private function replaceVars( + $section, + $parameters, + $sep = ",", + $combine = null, + $reserved = false, + $tag_empty = false, + $combine_on_empty = true + ) { + if (strpos($section, ",") === false) { + // If we only have a single value, we can immediately process. + return $this->combine( + $section, + $parameters, + $sep, + $combine, + $reserved, + $tag_empty, + $combine_on_empty + ); + } else { + // If we have multiple values, we need to split and loop over them. + // Each is treated individually, then glued together with the + // separator character. + $vars = explode(",", $section); + return $this->combineList( + $vars, + $sep, + $parameters, + $combine, + $reserved, + false, // Never emit empty strings in multi-param replacements + $combine_on_empty + ); + } + } + + public function combine( + $key, + $parameters, + $sep, + $combine, + $reserved, + $tag_empty, + $combine_on_empty + ) { + $length = false; + $explode = false; + $skip_final_combine = false; + $value = false; + + // Check for length restriction. + if (strpos($key, ":") !== false) { + list($key, $length) = explode(":", $key); + } + + // Check for explode parameter. + if ($key[strlen($key) - 1] == "*") { + $explode = true; + $key = substr($key, 0, -1); + $skip_final_combine = true; + } + + // Define the list separator. + $list_sep = $explode ? $sep : ","; + + if (isset($parameters[$key])) { + $data_type = $this->getDataType($parameters[$key]); + switch ($data_type) { + case self::TYPE_SCALAR: + $value = $this->getValue($parameters[$key], $length); + break; + case self::TYPE_LIST: + $values = []; + foreach ($parameters[$key] as $pkey => $pvalue) { + $pvalue = $this->getValue($pvalue, $length); + if ($combine && $explode) { + $values[$pkey] = $key . $combine . $pvalue; + } else { + $values[$pkey] = $pvalue; + } + } + $value = implode($list_sep, $values); + if ($value == '') { + return ''; + } + break; + case self::TYPE_MAP: + $values = []; + foreach ($parameters[$key] as $pkey => $pvalue) { + $pvalue = $this->getValue($pvalue, $length); + if ($explode) { + $pkey = $this->getValue($pkey, $length); + $values[] = $pkey . "=" . $pvalue; // Explode triggers = combine. + } else { + $values[] = $pkey; + $values[] = $pvalue; + } + } + $value = implode($list_sep, $values); + if ($value == '') { + return false; + } + break; + } + } elseif ($tag_empty) { + // If we are just indicating empty values with their key name, return that. + return $key; + } else { + // Otherwise we can skip this variable due to not being defined. + return false; + } + + if ($reserved) { + $value = str_replace($this->reservedEncoded, $this->reserved, $value); + } + + // If we do not need to include the key name, we just return the raw + // value. + if (!$combine || $skip_final_combine) { + return $value; + } + + // Else we combine the key name: foo=bar, if value is not the empty string. + return $key . ($value != '' || $combine_on_empty ? $combine . $value : ''); + } + + /** + * Return the type of a passed in value + */ + private function getDataType($data) + { + if (is_array($data)) { + reset($data); + if (key($data) !== 0) { + return self::TYPE_MAP; + } + return self::TYPE_LIST; + } + return self::TYPE_SCALAR; + } + + /** + * Utility function that merges multiple combine calls + * for multi-key templates. + */ + private function combineList( + $vars, + $sep, + $parameters, + $combine, + $reserved, + $tag_empty, + $combine_on_empty + ) { + $ret = []; + foreach ($vars as $var) { + $response = $this->combine( + $var, + $parameters, + $sep, + $combine, + $reserved, + $tag_empty, + $combine_on_empty + ); + if ($response === false) { + continue; + } + $ret[] = $response; + } + return implode($sep, $ret); + } + + /** + * Utility function to encode and trim values + */ + private function getValue($value, $length) + { + if ($length) { + $value = substr($value, 0, $length); + } + $value = rawurlencode($value); + return $value; + } +} diff --git a/src/aliases.php b/src/aliases.php new file mode 100644 index 000000000..3224a030f --- /dev/null +++ b/src/aliases.php @@ -0,0 +1,102 @@ + 'Google_Client', + 'Google\\Service' => 'Google_Service', + 'Google\\AccessToken\\Revoke' => 'Google_AccessToken_Revoke', + 'Google\\AccessToken\\Verify' => 'Google_AccessToken_Verify', + 'Google\\Model' => 'Google_Model', + 'Google\\Utils\\UriTemplate' => 'Google_Utils_UriTemplate', + 'Google\\AuthHandler\\Guzzle6AuthHandler' => 'Google_AuthHandler_Guzzle6AuthHandler', + 'Google\\AuthHandler\\Guzzle7AuthHandler' => 'Google_AuthHandler_Guzzle7AuthHandler', + 'Google\\AuthHandler\\AuthHandlerFactory' => 'Google_AuthHandler_AuthHandlerFactory', + 'Google\\Http\\Batch' => 'Google_Http_Batch', + 'Google\\Http\\MediaFileUpload' => 'Google_Http_MediaFileUpload', + 'Google\\Http\\REST' => 'Google_Http_REST', + 'Google\\Task\\Retryable' => 'Google_Task_Retryable', + 'Google\\Task\\Exception' => 'Google_Task_Exception', + 'Google\\Task\\Runner' => 'Google_Task_Runner', + 'Google\\Collection' => 'Google_Collection', + 'Google\\Service\\Exception' => 'Google_Service_Exception', + 'Google\\Service\\Resource' => 'Google_Service_Resource', + 'Google\\Exception' => 'Google_Exception', +]; + +foreach ($classMap as $class => $alias) { + class_alias($class, $alias); +} + +/** + * This class needs to be defined explicitly as scripts must be recognized by + * the autoloader. + */ +class Google_Task_Composer extends \Google\Task\Composer +{ +} + +/** @phpstan-ignore-next-line */ +if (\false) { + class Google_AccessToken_Revoke extends \Google\AccessToken\Revoke + { + } + class Google_AccessToken_Verify extends \Google\AccessToken\Verify + { + } + class Google_AuthHandler_AuthHandlerFactory extends \Google\AuthHandler\AuthHandlerFactory + { + } + class Google_AuthHandler_Guzzle6AuthHandler extends \Google\AuthHandler\Guzzle6AuthHandler + { + } + class Google_AuthHandler_Guzzle7AuthHandler extends \Google\AuthHandler\Guzzle7AuthHandler + { + } + class Google_Client extends \Google\Client + { + } + class Google_Collection extends \Google\Collection + { + } + class Google_Exception extends \Google\Exception + { + } + class Google_Http_Batch extends \Google\Http\Batch + { + } + class Google_Http_MediaFileUpload extends \Google\Http\MediaFileUpload + { + } + class Google_Http_REST extends \Google\Http\REST + { + } + class Google_Model extends \Google\Model + { + } + class Google_Service extends \Google\Service + { + } + class Google_Service_Exception extends \Google\Service\Exception + { + } + class Google_Service_Resource extends \Google\Service\Resource + { + } + class Google_Task_Exception extends \Google\Task\Exception + { + } + interface Google_Task_Retryable extends \Google\Task\Retryable + { + } + class Google_Task_Runner extends \Google\Task\Runner + { + } + class Google_Utils_UriTemplate extends \Google\Utils\UriTemplate + { + } +} diff --git a/tests/BaseTest.php b/tests/BaseTest.php index 905e8b7c3..144c7d789 100644 --- a/tests/BaseTest.php +++ b/tests/BaseTest.php @@ -15,219 +15,193 @@ * limitations under the License. */ +namespace Google\Tests; + +use Google\Client; +use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\ClientInterface; use Symfony\Component\DomCrawler\Crawler; -use Stash\Driver\FileSystem; -use Stash\Pool; +use League\Flysystem\Adapter\Local; +use League\Flysystem\Filesystem; +use Cache\Adapter\Filesystem\FilesystemCachePool; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; -class BaseTest extends PHPUnit_Framework_TestCase +class BaseTest extends TestCase { - private $key; - private $client; - private $memcacheHost; - private $memcachePort; - protected $testDir = __DIR__; - - public function getClient() - { - if (!$this->client) { - $this->client = $this->createClient(); - } + use ProphecyTrait; - return $this->client; - } - - public function getCache($path = null) - { - $path = $path ?: sys_get_temp_dir().'/google-api-php-client-tests'; - return new Pool(new FileSystem(['path' => $path])); - } - - private function createClient() - { - $options = [ - 'auth' => 'google_auth', - 'exceptions' => false, - ]; - - if ($proxy = getenv('HTTP_PROXY')) { - $options['proxy'] = $proxy; - $options['verify'] = false; - } - - // adjust constructor depending on guzzle version - if (!$this->isGuzzle6()) { - $options = ['defaults' => $options]; - } + private $key; + private $client; - $httpClient = new GuzzleHttp\Client($options); - - $client = new Google_Client(); - $client->setApplicationName('google-api-php-client-tests'); - $client->setHttpClient($httpClient); - $client->setScopes([ - "https://www.googleapis.com/auth/plus.me", - "https://www.googleapis.com/auth/urlshortener", - "https://www.googleapis.com/auth/tasks", - "https://www.googleapis.com/auth/adsense", - "https://www.googleapis.com/auth/youtube", - "https://www.googleapis.com/auth/drive", - ]); - - if ($this->key) { - $client->setDeveloperKey($this->key); - } + public function getClient() + { + if (!$this->client) { + $this->client = $this->createClient(); + } - list($clientId, $clientSecret) = $this->getClientIdAndSecret(); - $client->setClientId($clientId); - $client->setClientSecret($clientSecret); - $client->setCache($this->getCache()); - - return $client; - } - - public function checkToken() - { - $client = $this->getClient(); - $cache = $client->getCache(); - $cacheItem = $cache->getItem('access_token'); - - if (!$token = $cacheItem->get()) { - if (!$token = $this->tryToGetAnAccessToken($client)) { - return $this->markTestSkipped("Test requires access token"); - } - $cacheItem->set($token); - $cache->save($cacheItem); + return $this->client; } - $client->setAccessToken($token); + public function getCache($path = null) + { + $path = $path ?: sys_get_temp_dir().'/google-api-php-client-tests/'; + $filesystemAdapter = new Local($path); + $filesystem = new Filesystem($filesystemAdapter); - if ($client->isAccessTokenExpired()) { - // as long as we have client credentials, even if its expired - // our access token will automatically be refreshed - $this->checkClientCredentials(); + return new FilesystemCachePool($filesystem); } - return true; - } - - public function tryToGetAnAccessToken(Google_Client $client) - { - $this->checkClientCredentials(); - - $client->setRedirectUri("urn:ietf:wg:oauth:2.0:oob"); - $client->setConfig('access_type', 'offline'); - $authUrl = $client->createAuthUrl(); - - echo "\nPlease enter the auth code:\n"; - ob_flush(); - `open '$authUrl'`; - $authCode = trim(fgets(STDIN)); - - if ($accessToken = $client->fetchAccessTokenWithAuthCode($authCode)) { - if (isset($accessToken['access_token'])) { - return $accessToken; - } + private function createClient() + { + $options = [ + 'auth' => 'google_auth', + 'exceptions' => false, + ]; + + if ($proxy = getenv('HTTP_PROXY')) { + $options['proxy'] = $proxy; + $options['verify'] = false; + } + + $httpClient = new GuzzleClient($options); + + $client = new Client(); + $client->setApplicationName('google-api-php-client-tests'); + $client->setHttpClient($httpClient); + $client->setScopes( + [ + "https://www.googleapis.com/auth/tasks", + "https://www.googleapis.com/auth/adsense", + "https://www.googleapis.com/auth/youtube", + "https://www.googleapis.com/auth/drive", + ] + ); + + if ($this->key) { + $client->setDeveloperKey($this->key); + } + + list($clientId, $clientSecret) = $this->getClientIdAndSecret(); + $client->setClientId($clientId); + $client->setClientSecret($clientSecret); + if (version_compare(PHP_VERSION, '5.5', '>=')) { + $client->setCache($this->getCache()); + } + + return $client; } - return false; - } - - private function getClientIdAndSecret() - { - $clientId = getenv('GCLOUD_CLIENT_ID') ? getenv('GCLOUD_CLIENT_ID') : null; - $clientSecret = getenv('GCLOUD_CLIENT_SECRET') ? getenv('GCLOUD_CLIENT_SECRET') : null; - - return array($clientId, $clientSecret); - } + public function checkToken() + { + $client = $this->getClient(); + $cache = $client->getCache(); + $cacheItem = $cache->getItem('access_token'); + + if (!$token = $cacheItem->get()) { + if (!$token = $this->tryToGetAnAccessToken($client)) { + return $this->markTestSkipped("Test requires access token"); + } + $cacheItem->set($token); + $cache->save($cacheItem); + } + + $client->setAccessToken($token); + + if ($client->isAccessTokenExpired()) { + // as long as we have client credentials, even if its expired + // our access token will automatically be refreshed + $this->checkClientCredentials(); + } + + return true; + } - public function checkClientCredentials() - { - list($clientId, $clientSecret) = $this->getClientIdAndSecret(); - if (!($clientId && $clientSecret)) { - $this->markTestSkipped("Test requires GCLOUD_CLIENT_ID and GCLOUD_CLIENT_SECRET to be set"); + public function tryToGetAnAccessToken(Client $client) + { + $this->checkClientCredentials(); + + $client->setRedirectUri("urn:ietf:wg:oauth:2.0:oob"); + $client->setConfig('access_type', 'offline'); + $authUrl = $client->createAuthUrl(); + echo "\nGo to: $authUrl\n"; + echo "\nPlease enter the auth code:\n"; + ob_flush(); + `open '$authUrl'`; + $authCode = trim(fgets(STDIN)); + + if ($accessToken = $client->fetchAccessTokenWithAuthCode($authCode)) { + if (isset($accessToken['access_token'])) { + return $accessToken; + } + } + + return false; } - } - public function checkServiceAccountCredentials() - { - if (!$f = getenv('GOOGLE_APPLICATION_CREDENTIALS')) { - $skip = "This test requires the GOOGLE_APPLICATION_CREDENTIALS environment variable to be set\n" - . "see https://developers.google.com/accounts/docs/application-default-credentials"; - $this->markTestSkipped($skip); + private function getClientIdAndSecret() + { + $clientId = getenv('GOOGLE_CLIENT_ID') ?: null; + $clientSecret = getenv('GOOGLE_CLIENT_SECRET') ?: null; - return false; + return [$clientId, $clientSecret]; } - if (!file_exists($f)) { - $this->markTestSkipped('invalid path for GOOGLE_APPLICATION_CREDENTIALS'); + protected function checkClientCredentials() + { + list($clientId, $clientSecret) = $this->getClientIdAndSecret(); + if (!($clientId && $clientSecret)) { + $this->markTestSkipped("Test requires GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET to be set"); + } } - return true; - } + protected function checkServiceAccountCredentials() + { + if (!$f = getenv('GOOGLE_APPLICATION_CREDENTIALS')) { + $skip = "This test requires the GOOGLE_APPLICATION_CREDENTIALS environment variable to be set\n" + . "see https://developers.google.com/accounts/docs/application-default-credentials"; + $this->markTestSkipped($skip); - public function checkKey() - { - $this->key = $this->loadKey(); + return false; + } - if (!strlen($this->key)) { - $this->markTestSkipped("Test requires api key\nYou can create one in your developer console"); - return false; - } - } + if (!file_exists($f)) { + $this->markTestSkipped('invalid path for GOOGLE_APPLICATION_CREDENTIALS'); + } - public function loadKey() - { - if (file_exists($f = dirname(__FILE__) . DIRECTORY_SEPARATOR . '.apiKey')) { - return file_get_contents($f); - } - } - - protected function loadExample($example) - { - // trick app into thinking we are a web server - $_SERVER['HTTP_USER_AGENT'] = 'google-api-php-client-tests'; - $_SERVER['HTTP_HOST'] = 'localhost'; - $_SERVER['REQUEST_METHOD'] = 'GET'; - - // include the file and return an HTML crawler - $file = __DIR__ . '/../examples/' . $example; - if (is_file($file)) { - ob_start(); - include $file; - $html = ob_get_clean(); - - return new Crawler($html); + return true; } - return false; - } - - protected function isGuzzle6() - { - $version = ClientInterface::VERSION; - - return ('6' === $version[0]); - } + protected function checkKey() + { + if (file_exists($apiKeyFile = __DIR__ . DIRECTORY_SEPARATOR . '.apiKey')) { + $apiKey = file_get_contents($apiKeyFile); + } elseif (!$apiKey = getenv('GOOGLE_API_KEY')) { + $this->markTestSkipped( + "Test requires api key\nYou can create one in your developer console" + ); + file_put_contents($apiKeyFile, $apiKey); + } + $this->key = $apiKey; + } - protected function isGuzzle5() - { - $version = ClientInterface::VERSION; + protected function loadExample($example) + { + // trick app into thinking we are a web server + $_SERVER['HTTP_USER_AGENT'] = 'google-api-php-client-tests'; + $_SERVER['HTTP_HOST'] = 'localhost'; + $_SERVER['REQUEST_METHOD'] = 'GET'; - return ('5' === $version[0]); - } + // include the file and return an HTML crawler + $file = __DIR__ . '/../examples/' . $example; + if (is_file($file)) { + ob_start(); + include $file; + $html = ob_get_clean(); - public function onlyGuzzle6() - { - if (!$this->isGuzzle6()) { - $this->markTestSkipped('Guzzle 6 only'); - } - } + return new Crawler($html); + } - public function onlyGuzzle5() - { - if (!$this->isGuzzle5()) { - $this->markTestSkipped('Guzzle 5 only'); + return false; } - } } diff --git a/tests/Google/AccessToken/RevokeTest.php b/tests/Google/AccessToken/RevokeTest.php index 0a2ee11df..c6dd714f7 100644 --- a/tests/Google/AccessToken/RevokeTest.php +++ b/tests/Google/AccessToken/RevokeTest.php @@ -1,7 +1,5 @@ getMock('Psr\Http\Message\ResponseInterface'); - $response->expects($this->exactly(2)) - ->method('getStatusCode') - ->will($this->returnValue(200)); - $http = $this->getMock('GuzzleHttp\ClientInterface'); - $http->expects($this->exactly(2)) - ->method('send') - ->will($this->returnCallback( - function ($request) use (&$token, $response) { - parse_str((string) $request->getBody(), $fields); - $token = isset($fields['token']) ? $fields['token'] : null; + $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); + $response->getStatusCode() + ->shouldBeCalledTimes(3) + ->willReturn(200); - return $response; - } - )); + $http = $this->prophesize('GuzzleHttp\ClientInterface'); + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(3) + ->will(function ($args) use (&$token, $response) { + parse_str((string) $args[0]->getBody(), $fields); + $token = isset($fields['token']) ? $fields['token'] : null; - // adds support for extra "createRequest" step (required for Guzzle 5) - if ($this->isGuzzle5()) { - $requestToken = null; - $request = $this->getMock('GuzzleHttp\Message\RequestInterface'); - $request->expects($this->exactly(2)) - ->method('getBody') - ->will($this->returnCallback( - function () use (&$requestToken) { - return 'token='.$requestToken; - })); - $http->expects($this->exactly(2)) - ->method('createRequest') - ->will($this->returnCallback( - function ($method, $url, $params) use (&$requestToken, $request) { - parse_str((string) $params['body'], $fields); - $requestToken = isset($fields['token']) ? $fields['token'] : null; + return $response->reveal(); + }); - return $request; - } - )); - } + $t = [ + 'access_token' => $accessToken, + 'created' => time(), + 'expires_in' => '3600' + ]; - $t = array( - 'access_token' => $accessToken, - 'created' => time(), - 'expires_in' => '3600' - ); + // Test with access token. + $revoke = new Revoke($http->reveal()); + $this->assertTrue($revoke->revokeToken($t)); + $this->assertEquals($accessToken, $token); - // Test with access token. - $revoke = new Google_AccessToken_Revoke($http); - $this->assertTrue($revoke->revokeToken($t)); - $this->assertEquals($accessToken, $token); + // Test with refresh token. + $revoke = new Revoke($http->reveal()); + $t = [ + 'access_token' => $accessToken, + 'refresh_token' => $refreshToken, + 'created' => time(), + 'expires_in' => '3600' + ]; - // Test with refresh token. - $revoke = new Google_AccessToken_Revoke($http); - $t = array( - 'access_token' => $accessToken, - 'refresh_token' => $refreshToken, - 'created' => time(), - 'expires_in' => '3600' - ); - $this->assertTrue($revoke->revokeToken($t)); - $this->assertEquals($refreshToken, $token); - } + $this->assertTrue($revoke->revokeToken($t)); + $this->assertEquals($refreshToken, $token); - public function testInvalidStringToken() - { - $phpVersion = phpversion(); - if ('7' === $phpVersion[0]) { - // primitive type hints actually throw exceptions in PHP7 - $this->setExpectedException('TypeError'); - } else { - $this->setExpectedException('PHPUnit_Framework_Error'); + // Test with token string. + $revoke = new Revoke($http->reveal()); + $t = $accessToken; + $this->assertTrue($revoke->revokeToken($t)); + $this->assertEquals($accessToken, $token); } - // Test with string token - $revoke = new Google_AccessToken_Revoke(); - $revoke->revokeToken('ACCESS_TOKEN'); - } } diff --git a/tests/Google/AccessToken/VerifyTest.php b/tests/Google/AccessToken/VerifyTest.php index a6f4ef01d..7d37209e4 100644 --- a/tests/Google/AccessToken/VerifyTest.php +++ b/tests/Google/AccessToken/VerifyTest.php @@ -1,7 +1,5 @@ getClient(); - $verify = new Google_AccessToken_Verify($client->getHttpClient()); - - // set these to values that will be changed - if (defined('MATH_BIGINTEGER_OPENSSL_ENABLED') || defined('CRYPT_RSA_MODE')) { - $this->markTestSkipped('Cannot run test - constants already defined'); + /** + * This test needs to run before the other verify tests, + * to ensure the constants are not defined. + */ + public function testPhpsecConstants() + { + $client = $this->getClient(); + $verify = new Verify($client->getHttpClient()); + + // set these to values that will be changed + if (defined('MATH_BIGINTEGER_OPENSSL_ENABLED') || defined('CRYPT_RSA_MODE')) { + $this->markTestSkipped('Cannot run test - constants already defined'); + } + + // Pretend we are on App Engine VMs + putenv('GAE_VM=1'); + + $verify->verifyIdToken('a.b.c'); + + putenv('GAE_VM=0'); + + $openSslEnable = constant('MATH_BIGINTEGER_OPENSSL_ENABLED'); + $rsaMode = constant('CRYPT_RSA_MODE'); + $this->assertTrue($openSslEnable); + $this->assertEquals(AES::ENGINE_OPENSSL, $rsaMode); } - // Pretend we are on App Engine VMs - putenv('GAE_VM=1'); - - $verify->verifyIdToken('a.b.c'); - - putenv('GAE_VM=0'); - - $openSslEnable = constant('MATH_BIGINTEGER_OPENSSL_ENABLED'); - $rsaMode = constant('CRYPT_RSA_MODE'); - $this->assertEquals(true, $openSslEnable); - $this->assertEquals(phpseclib\Crypt\RSA::MODE_OPENSSL, $rsaMode); - } - - /** - * Most of the logic for ID token validation is in AuthTest - - * this is just a general check to ensure we verify a valid - * id token if one exists. - */ - public function testValidateIdToken() - { - $this->checkToken(); - - $jwt = $this->getJwtService(); - $client = $this->getClient(); - $http = $client->getHttpClient(); - $token = $client->getAccessToken(); - if ($client->isAccessTokenExpired()) { - $token = $client->fetchAccessTokenWithRefreshToken(); + /** + * Most of the logic for ID token validation is in AuthTest - + * this is just a general check to ensure we verify a valid + * id token if one exists. + */ + public function testValidateIdToken() + { + $this->checkToken(); + + $jwt = new JWT(); + $client = $this->getClient(); + $http = $client->getHttpClient(); + $token = $client->getAccessToken(); + if ($client->isAccessTokenExpired()) { + $token = $client->fetchAccessTokenWithRefreshToken(); + } + $segments = explode('.', $token['id_token']); + $this->assertCount(3, $segments); + // Extract the client ID in this case as it wont be set on the test client. + $data = json_decode($jwt->urlSafeB64Decode($segments[1])); + $verify = new Verify($http); + $payload = $verify->verifyIdToken($token['id_token'], $data->aud); + $this->assertArrayHasKey('sub', $payload); + $this->assertGreaterThan(0, strlen($payload['sub'])); + + // TODO: Need to be smart about testing/disabling the + // caching for this test to make sense. Not sure how to do that + // at the moment. + $client = $this->getClient(); + $http = $client->getHttpClient(); + $data = json_decode($jwt->urlSafeB64Decode($segments[1])); + $verify = new Verify($http); + $payload = $verify->verifyIdToken($token['id_token'], $data->aud); + $this->assertArrayHasKey('sub', $payload); + $this->assertGreaterThan(0, strlen($payload['sub'])); } - $segments = explode('.', $token['id_token']); - $this->assertEquals(3, count($segments)); - // Extract the client ID in this case as it wont be set on the test client. - $data = json_decode($jwt->urlSafeB64Decode($segments[1])); - $verify = new Google_AccessToken_Verify($http); - $payload = $verify->verifyIdToken($token['id_token'], $data->aud); - $this->assertTrue(isset($payload['sub'])); - $this->assertTrue(strlen($payload['sub']) > 0); - - // TODO: Need to be smart about testing/disabling the - // caching for this test to make sense. Not sure how to do that - // at the moment. - $client = $this->getClient(); - $http = $client->getHttpClient(); - $data = json_decode($jwt->urlSafeB64Decode($segments[1])); - $verify = new Google_AccessToken_Verify($http); - $payload = $verify->verifyIdToken($token['id_token'], $data->aud); - $this->assertTrue(isset($payload['sub'])); - $this->assertTrue(strlen($payload['sub']) > 0); - } - - public function testRetrieveCertsFromLocation() - { - $client = $this->getClient(); - $verify = new Google_AccessToken_Verify($client->getHttpClient()); - - // make this method public for testing purposes - $method = new ReflectionMethod($verify, 'retrieveCertsFromLocation'); - $method->setAccessible(true); - $certs = $method->invoke($verify, Google_AccessToken_Verify::FEDERATED_SIGNON_CERT_URL); - - $this->assertArrayHasKey('keys', $certs); - $this->assertGreaterThan(1, count($certs['keys'])); - $this->assertArrayHasKey('alg', $certs['keys'][0]); - $this->assertEquals('RS256', $certs['keys'][0]['alg']); - } - - private function getJwtService() - { - if (class_exists('\Firebase\JWT\JWT')) { - return new \Firebase\JWT\JWT; + + /** + * Most of the logic for ID token validation is in AuthTest - + * this is just a general check to ensure we verify a valid + * id token if one exists. + */ + public function testLeewayIsUnchangedWhenPassingInJwt() + { + $this->checkToken(); + + $jwt = new JWT(); + // set arbitrary leeway so we can check this later + $jwt::$leeway = $leeway = 1.5; + $client = $this->getClient(); + $token = $client->getAccessToken(); + if ($client->isAccessTokenExpired()) { + $token = $client->fetchAccessTokenWithRefreshToken(); + } + $segments = explode('.', $token['id_token']); + $this->assertCount(3, $segments); + // Extract the client ID in this case as it wont be set on the test client. + $data = json_decode($jwt->urlSafeB64Decode($segments[1])); + $verify = new Verify($client->getHttpClient(), null, $jwt); + $payload = $verify->verifyIdToken($token['id_token'], $data->aud); + // verify the leeway is set as it was + $this->assertEquals($leeway, $jwt::$leeway); } - return new \JWT; - } + public function testRetrieveCertsFromLocation() + { + $client = $this->getClient(); + $verify = new Verify($client->getHttpClient()); + + // make this method public for testing purposes + $method = new ReflectionMethod($verify, 'retrieveCertsFromLocation'); + $method->setAccessible(true); + $certs = $method->invoke($verify, Verify::FEDERATED_SIGNON_CERT_URL); + + $this->assertArrayHasKey('keys', $certs); + $this->assertGreaterThan(1, count($certs['keys'])); + $this->assertArrayHasKey('alg', $certs['keys'][0]); + $this->assertEquals('RS256', $certs['keys'][0]['alg']); + } } diff --git a/tests/Google/AuthHandler/AuthHandlerTest.php b/tests/Google/AuthHandler/AuthHandlerTest.php new file mode 100644 index 000000000..0bc34aa82 --- /dev/null +++ b/tests/Google/AuthHandler/AuthHandlerTest.php @@ -0,0 +1,82 @@ +attachToken( + $client, + ['access_token' => '1234'], + $scopes + ); + + // Call our middleware and verify the token is set + $scopedMiddleware = $this->getGoogleAuthMiddleware($http1); + $request = $scopedMiddleware(new Request('GET', '/'), ['auth' => 'scoped']); + $this->assertEquals(['Bearer 1234'], $request->getHeader('Authorization')); + + // Attach a new token to the HTTP client + $http2 = $authHandler->attachToken( + $client, + ['access_token' => '5678'], + $scopes + ); + + // Call our middleware and verify the NEW token is set + $scopedMiddleware = $this->getGoogleAuthMiddleware($http2); + $request = $scopedMiddleware(new Request('GET', '/'), ['auth' => 'scoped']); + $this->assertEquals(['Bearer 5678'], $request->getHeader('Authorization')); + } + + private function getGoogleAuthMiddleware(Client $http) + { + // All sorts of horrible reflection to get at our middleware + $handler = $http->getConfig()['handler']; + $reflectionMethod = new \ReflectionMethod($handler, 'findByName'); + $reflectionMethod->setAccessible(true); + $authMiddlewareIdx = $reflectionMethod->invoke($handler, 'google_auth'); + + $reflectionProperty = new \ReflectionProperty($handler, 'stack'); + $reflectionProperty->setAccessible(true); + $stack = $reflectionProperty->getValue($handler); + + $callable = $stack[$authMiddlewareIdx][0]; + return $callable(function ($request) { + return $request; + }); + } +} diff --git a/tests/Google/CacheTest.php b/tests/Google/CacheTest.php new file mode 100644 index 000000000..153b2669c --- /dev/null +++ b/tests/Google/CacheTest.php @@ -0,0 +1,102 @@ +checkServiceAccountCredentials(); + + $client = $this->getClient(); + $client->useApplicationDefaultCredentials(); + $client->setAccessType('offline'); + $client->setScopes(['https://www.googleapis.com/auth/drive.readonly']); + $client->setCache(new MemoryCacheItemPool); + + /* Refresh token when expired */ + if ($client->isAccessTokenExpired()) { + $client->refreshTokenWithAssertion(); + } + + /* Make a service call */ + $service = new Drive($client); + $files = $service->files->listFiles(); + $this->assertInstanceOf('Google_Service_Drive_FileList', $files); + } + + public function testFileCache() + { + $this->checkServiceAccountCredentials(); + + $client = new Client(); + $client->useApplicationDefaultCredentials(); + $client->setScopes(['https://www.googleapis.com/auth/drive.readonly']); + // filecache with new cache dir + $cache = $this->getCache(sys_get_temp_dir() . '/cloud-samples-tests-php-cache-test/'); + $client->setCache($cache); + + $token1 = null; + $client->setTokenCallback(function ($cacheKey, $accessToken) use ($cache, &$token1) { + $token1 = $accessToken; + $cacheItem = $cache->getItem($cacheKey); + // expire the item + $cacheItem->expiresAt(new DateTime('now -1 second')); + $cache->save($cacheItem); + + $cacheItem2 = $cache->getItem($cacheKey); + }); + + /* Refresh token when expired */ + if ($client->isAccessTokenExpired()) { + $client->refreshTokenWithAssertion(); + } + + /* Make a service call */ + $service = new Drive($client); + $files = $service->files->listFiles(); + $this->assertInstanceOf(Drive\FileList::class, $files); + + sleep(2); + + // make sure the token expires + $client = new Client(); + $client->useApplicationDefaultCredentials(); + $client->setScopes(['https://www.googleapis.com/auth/drive.readonly']); + $client->setCache($cache); + $token2 = null; + $client->setTokenCallback(function ($cacheKey, $accessToken) use (&$token2) { + $token2 = $accessToken; + }); + + /* Make another service call */ + $service = new Drive($client); + $files = $service->files->listFiles(); + $this->assertInstanceOf(Drive\FileList::class, $files); + + $this->assertNotEquals($token1, $token2); + } +} diff --git a/tests/Google/ClientTest.php b/tests/Google/ClientTest.php index 576daf432..4e53e34b4 100644 --- a/tests/Google/ClientTest.php +++ b/tests/Google/ClientTest.php @@ -18,529 +18,943 @@ * under the License. */ -use GuzzleHttp\Client; -use GuzzleHttp\Event\RequestEvents; -use Psr\Http\Message\Request; - -class Google_ClientTest extends BaseTest +namespace Google\Tests; + +use Google\Client; +use Google\Service\Drive; +use Google\AuthHandler\AuthHandlerFactory; +use Google\Auth\FetchAuthTokenCache; +use Google\Auth\CredentialsLoader; +use Google\Auth\GCECache; +use Google\Auth\Credentials\GCECredentials; +use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Exception\ClientException; +use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; +use ReflectionClass; +use ReflectionMethod; +use InvalidArgumentException; +use Exception; +use DomainException; + +class ClientTest extends BaseTest { - public function testClientConstructor() - { - $this->assertInstanceOf('Google_Client', $this->getClient()); - } - - public function testSignAppKey() - { - $client = $this->getClient(); - $client->setDeveloperKey('devKey'); - - $http = new Client(); - $client->authorize($http); - - $this->checkAuthHandler($http, 'Simple'); - } - - private function checkAuthHandler($http, $className) - { - if ($this->isGuzzle6()) { - $stack = $http->getConfig('handler'); - $class = new ReflectionClass(get_class($stack)); - $property = $class->getProperty('stack'); - $property->setAccessible(true); - $middlewares = $property->getValue($stack); - $middleware = array_pop($middlewares); - - if (is_null($className)) { - // only the default middlewares have been added - $this->assertEquals(3, count($middlewares)); - } else { - $authClass = sprintf('Google\Auth\Middleware\%sMiddleware', $className); - $this->assertInstanceOf($authClass, $middleware[0]); - } - } else { - $listeners = $http->getEmitter()->listeners('before'); - - if (is_null($className)) { - $this->assertEquals(0, count($listeners)); - } else { - $authClass = sprintf('Google\Auth\Subscriber\%sSubscriber', $className); - $this->assertEquals(1, count($listeners)); - $this->assertEquals(2, count($listeners[0])); - $this->assertInstanceOf($authClass, $listeners[0][0]); - } - } - } - - private function checkCredentials($http, $fetcherClass, $sub = null) - { - if ($this->isGuzzle6()) { - $stack = $http->getConfig('handler'); - $class = new ReflectionClass(get_class($stack)); - $property = $class->getProperty('stack'); - $property->setAccessible(true); - $middlewares = $property->getValue($stack); // Works - $middleware = array_pop($middlewares); - $auth = $middleware[0]; - } else { - // access the protected $fetcher property - $listeners = $http->getEmitter()->listeners('before'); - $auth = $listeners[0][0]; - } - - $class = new ReflectionClass(get_class($auth)); - $property = $class->getProperty('fetcher'); - $property->setAccessible(true); - $fetcher = $property->getValue($auth); - $this->assertInstanceOf($fetcherClass, $fetcher); - - if ($sub) { - // access the protected $auth property - $class = new ReflectionClass(get_class($fetcher)); - $property = $class->getProperty('auth'); - $property->setAccessible(true); - $auth = $property->getValue($fetcher); - - $this->assertEquals($sub, $auth->getSub()); - } - } - - public function testSignAccessToken() - { - $client = $this->getClient(); - - $http = new Client(); - $client->setAccessToken([ - 'access_token' => 'test_token', - 'expires_in' => 3600, - 'created' => time(), - ]); - $client->setScopes('test_scope'); - $client->authorize($http); - - $this->checkAuthHandler($http, 'ScopedAccessToken'); - } - - public function testCreateAuthUrl() - { - $client = $this->getClient(); - - $client->setClientId('clientId1'); - $client->setClientSecret('clientSecret1'); - $client->setRedirectUri('http://localhost/'); - $client->setDeveloperKey('devKey'); - $client->setState('xyz'); - $client->setAccessType('offline'); - $client->setApprovalPrompt('force'); - $client->setRequestVisibleActions('http://foo/'); - $client->setLoginHint('bob@example.org'); - - $authUrl = $client->createAuthUrl("http://googleapis.com/scope/foo"); - $expected = "https://accounts.google.com/o/oauth2/auth" - . "?response_type=code" - . "&access_type=offline" - . "&client_id=clientId1" - . "&redirect_uri=http%3A%2F%2Flocalhost" - . "&state=xyz" - . "&scope=http%3A%2F%2Fgoogleapis.com%2Fscope%2Ffoo" - . "&approval_prompt=force" - . "&login_hint=bob%40example.org"; - - $this->assertEquals($expected, $authUrl); - - // Again with a blank login hint (should remove all traces from authUrl) - $client->setLoginHint(''); - $client->setHostedDomain('example.com'); - $client->setOpenIdRealm('example.com'); - $client->setPrompt('select_account'); - $client->setIncludeGrantedScopes(true); - $authUrl = $client->createAuthUrl("http://googleapis.com/scope/foo"); - $expected = "https://accounts.google.com/o/oauth2/auth" - . "?response_type=code" - . "&access_type=offline" - . "&client_id=clientId1" - . "&redirect_uri=http%3A%2F%2Flocalhost" - . "&state=xyz" - . "&scope=http%3A%2F%2Fgoogleapis.com%2Fscope%2Ffoo" - . "&hd=example.com" - . "&include_granted_scopes=true" - . "&openid.realm=example.com" - . "&prompt=select_account"; - - $this->assertEquals($expected, $authUrl); - } - - public function testPrepareNoScopes() - { - $client = new Google_Client(); - - $scopes = $client->prepareScopes(); - $this->assertEquals(null, $scopes); - } - - public function testNoAuthIsNull() - { - $client = new Google_Client(); - - $this->assertNull($client->getAccessToken()); - } - - public function testPrepareService() - { - $client = new Google_Client(); - $client->setScopes(array("scope1", "scope2")); - $scopes = $client->prepareScopes(); - $this->assertEquals("scope1 scope2", $scopes); - - $client->setScopes(array("", "scope2")); - $scopes = $client->prepareScopes(); - $this->assertEquals(" scope2", $scopes); - - $client->setScopes("scope2"); - $client->addScope("scope3"); - $client->addScope(array("scope4", "scope5")); - $scopes = $client->prepareScopes(); - $this->assertEquals("scope2 scope3 scope4 scope5", $scopes); - - $client->setClientId('test1'); - $client->setRedirectUri('http://localhost/'); - $client->setState('xyz'); - $client->setScopes(array("http://test.com/", "scope2")); - $scopes = $client->prepareScopes(); - $this->assertEquals("http://test.com scope2", $scopes); - $this->assertEquals( - '' - . 'https://accounts.google.com/o/oauth2/auth' - . '?response_type=code' - . '&access_type=online' - . '&client_id=test1' - . '&redirect_uri=http%3A%2F%2Flocalhost%2F' - . '&state=xyz' - . '&scope=http%3A%2F%2Ftest.com%20scope2' - . '&approval_prompt=auto', - - $client->createAuthUrl() - ); - - $response = $this->getMock('Psr\Http\Message\ResponseInterface'); - $response->expects($this->once()) - ->method('getBody') - ->will($this->returnValue($this->getMock('Psr\Http\Message\StreamInterface'))); - $http = $this->getMock('GuzzleHttp\ClientInterface'); - $http->expects($this->once()) - ->method('send') - ->will($this->returnValue($response)); - - if ($this->isGuzzle5()) { - $guzzle5Request = new GuzzleHttp\Message\Request('POST', '/'); - $http->expects($this->once()) - ->method('createRequest') - ->will($this->returnValue($guzzle5Request)); - } - - - $client->setHttpClient($http); - $dr_service = new Google_Service_Drive($client); - $this->assertInstanceOf('Google_Model', $dr_service->files->listFiles()); - } - - public function testSettersGetters() - { - $client = new Google_Client(); - $client->setClientId("client1"); - $client->setClientSecret('client1secret'); - $client->setState('1'); - $client->setApprovalPrompt('force'); - $client->setAccessType('offline'); - - $client->setRedirectUri('localhost'); - $client->setConfig('application_name', 'me'); - $client->setCache($this->getMock('Psr\Cache\CacheItemPoolInterface')); - $this->assertEquals('object', gettype($client->getCache())); - - try { - $client->setAccessToken(null); - $this->fail('Should have thrown an Exception.'); - } catch (InvalidArgumentException $e) { - $this->assertEquals('invalid json token', $e->getMessage()); - } - - $token = array('access_token' => 'token'); - $client->setAccessToken($token); - $this->assertEquals($token, $client->getAccessToken()); - } - - public function testAppEngineStreamHandlerConfig() - { - $this->onlyGuzzle5(); - - $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; - $client = new Google_Client(); - - // check Stream Handler is used - $http = $client->getHttpClient(); - $class = new ReflectionClass(get_class($http)); - $property = $class->getProperty('fsm'); - $property->setAccessible(true); - $fsm = $property->getValue($http); - - $class = new ReflectionClass(get_class($fsm)); - $property = $class->getProperty('handler'); - $property->setAccessible(true); - $handler = $property->getValue($fsm); - - $this->assertInstanceOf('GuzzleHttp\Ring\Client\StreamHandler', $handler); - - unset($_SERVER['SERVER_SOFTWARE']); - } - - public function testAppEngineVerifyConfig() - { - $this->onlyGuzzle5(); - - $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; - $client = new Google_Client(); - - $this->assertEquals( - '/etc/ca-certificates.crt', - $client->getHttpClient()->getDefaultOption('verify') - ); - - unset($_SERVER['SERVER_SOFTWARE']); - } - - public function testJsonConfig() - { - // Device config - $client = new Google_Client(); - $device = - '{"installed":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","client_secret"'. - ':"N0aHCBT1qX1VAcF5J1pJAn6S","token_uri":"https://accounts.google.com/o/oauth2/token",'. - '"client_email":"","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","oob"],"client_x509_cert_url"'. - ':"","client_id":"123456789.apps.googleusercontent.com","auth_provider_x509_cert_url":'. - '"https://www.googleapis.com/oauth2/v1/certs"}}'; - $dObj = json_decode($device, true); - $client->setAuthConfig($dObj); - $this->assertEquals($client->getClientId(), $dObj['installed']['client_id']); - $this->assertEquals($client->getClientSecret(), $dObj['installed']['client_secret']); - $this->assertEquals($client->getRedirectUri(), $dObj['installed']['redirect_uris'][0]); - - // Web config - $client = new Google_Client(); - $web = '{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","client_secret"' . - ':"lpoubuib8bj-Fmke_YhhyHGgXc","token_uri":"https://accounts.google.com/o/oauth2/token"' . - ',"client_email":"123456789@developer.gserviceaccount.com","client_x509_cert_url":'. - '"https://www.googleapis.com/robot/v1/metadata/x509/123456789@developer.gserviceaccount.com"'. - ',"client_id":"123456789.apps.googleusercontent.com","auth_provider_x509_cert_url":'. - '"https://www.googleapis.com/oauth2/v1/certs"}}'; - $wObj = json_decode($web, true); - $client->setAuthConfig($wObj); - $this->assertEquals($client->getClientId(), $wObj['web']['client_id']); - $this->assertEquals($client->getClientSecret(), $wObj['web']['client_secret']); - $this->assertEquals($client->getRedirectUri(), ''); - } - - public function testIniConfig() - { - $config = parse_ini_file($this->testDir . "/config/test.ini"); - $client = new Google_Client($config); - - $this->assertEquals('My Test application', $client->getConfig('application_name')); - $this->assertEquals( - 'gjfiwnGinpena3', - $client->getClientSecret() - ); - } - - public function testNoAuth() - { - /** @var $noAuth Google_Auth_Simple */ - $client = new Google_Client(); - $client->setDeveloperKey(null); - - // unset application credentials - $GOOGLE_APPLICATION_CREDENTIALS = getenv('GOOGLE_APPLICATION_CREDENTIALS'); - $HOME = getenv('HOME'); - putenv('GOOGLE_APPLICATION_CREDENTIALS='); - putenv('HOME='.sys_get_temp_dir()); - $http = new Client(); - $client->authorize($http); - - putenv("GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS"); - putenv("HOME=$HOME"); - $this->checkAuthHandler($http, null); - } - - public function testApplicationDefaultCredentials() - { - $this->checkServiceAccountCredentials(); - $credentialsFile = getenv('GOOGLE_APPLICATION_CREDENTIALS'); - - $client = new Google_Client(); - $client->setAuthConfig($credentialsFile); - - $http = new Client(); - $client->authorize($http); - - $this->checkAuthHandler($http, 'AuthToken'); - $this->checkCredentials($http, 'Google\Auth\Credentials\ServiceAccountCredentials'); - } - - public function testApplicationDefaultCredentialsWithSubject() - { - $this->checkServiceAccountCredentials(); - $credentialsFile = getenv('GOOGLE_APPLICATION_CREDENTIALS'); - - $sub = 'sub123'; - $client = new Google_Client(); - $client->setAuthConfig($credentialsFile); - $client->setSubject($sub); - - $http = new Client(); - $client->authorize($http); - - $this->checkAuthHandler($http, 'AuthToken'); - $this->checkCredentials($http, 'Google\Auth\Credentials\ServiceAccountCredentials', $sub); - } - - /** - * Test that the ID token is properly refreshed. - */ - public function testRefreshTokenSetsValues() - { - $token = json_encode(array( - 'access_token' => 'xyz', - 'id_token' => 'ID_TOKEN', - )); - $postBody = $this->getMock('Psr\Http\Message\StreamInterface'); - $postBody->expects($this->once()) - ->method('__toString') - ->will($this->returnValue($token)); - $response = $this->getMock('Psr\Http\Message\ResponseInterface'); - $response->expects($this->once()) - ->method('getBody') - ->will($this->returnValue($postBody)); - $http = $this->getMock('GuzzleHttp\ClientInterface'); - $http->expects($this->once()) - ->method('send') - ->will($this->returnValue($response)); - - if ($this->isGuzzle5()) { - $guzzle5Request = new GuzzleHttp\Message\Request('POST', '/', ['body' => $token]); - $http->expects($this->once()) - ->method('createRequest') - ->will($this->returnValue($guzzle5Request)); - } - - $client = $this->getClient(); - $client->setHttpClient($http); - $client->fetchAccessTokenWithRefreshToken("REFRESH_TOKEN"); - $token = $client->getAccessToken(); - $this->assertEquals($token['id_token'], "ID_TOKEN"); - } - - /** - * Test fetching an access token with assertion credentials - * using "useApplicationDefaultCredentials" - */ - public function testFetchAccessTokenWithAssertionFromEnv() - { - $this->checkServiceAccountCredentials(); + public function testClientConstructor() + { + $this->assertInstanceOf(Client::class, $this->getClient()); + } - $client = $this->getClient(); - $client->useApplicationDefaultCredentials(); - $token = $client->fetchAccessTokenWithAssertion(); + public function testSignAppKey() + { + $client = $this->getClient(); + $client->setDeveloperKey('devKey'); - $this->assertNotNull($token); - $this->assertArrayHasKey('access_token', $token); - } + $http = new GuzzleClient(); + $client->authorize($http); - /** - * Test fetching an access token with assertion credentials - * using "setAuthConfig" - */ - public function testFetchAccessTokenWithAssertionFromFile() - { - $this->checkServiceAccountCredentials(); + $this->checkAuthHandler($http, 'Simple'); + } + + private function checkAuthHandler($http, $className) + { + $stack = $http->getConfig('handler'); + $class = new ReflectionClass(get_class($stack)); + $property = $class->getProperty('stack'); + $property->setAccessible(true); + $middlewares = $property->getValue($stack); + $middleware = array_pop($middlewares); + + if (null === $className) { + // only the default middlewares have been added + $this->assertCount(3, $middlewares); + } else { + $authClass = sprintf('Google\Auth\Middleware\%sMiddleware', $className); + $this->assertInstanceOf($authClass, $middleware[0]); + } + } + + private function checkCredentials($http, $fetcherClass, $sub = null) + { + $stack = $http->getConfig('handler'); + $class = new ReflectionClass(get_class($stack)); + $property = $class->getProperty('stack'); + $property->setAccessible(true); + $middlewares = $property->getValue($stack); // Works + $middleware = array_pop($middlewares); + $auth = $middleware[0]; + + $class = new ReflectionClass(get_class($auth)); + $property = $class->getProperty('fetcher'); + $property->setAccessible(true); + $cacheFetcher = $property->getValue($auth); + $this->assertInstanceOf(FetchAuthTokenCache::class, $cacheFetcher); + + $class = new ReflectionClass(get_class($cacheFetcher)); + $property = $class->getProperty('fetcher'); + $property->setAccessible(true); + $fetcher = $property->getValue($cacheFetcher); + $this->assertInstanceOf($fetcherClass, $fetcher); + + if ($sub) { + // access the protected $auth property + $class = new ReflectionClass(get_class($fetcher)); + $property = $class->getProperty('auth'); + $property->setAccessible(true); + $auth = $property->getValue($fetcher); + + $this->assertEquals($sub, $auth->getSub()); + } + } + + public function testSignAccessToken() + { + $client = $this->getClient(); + + $http = new GuzzleClient(); + $client->setAccessToken([ + 'access_token' => 'test_token', + 'expires_in' => 3600, + 'created' => time(), + ]); + $client->setScopes('test_scope'); + $client->authorize($http); + + $this->checkAuthHandler($http, 'ScopedAccessToken'); + } + + public function testCreateAuthUrl() + { + $client = $this->getClient(); + + $client->setClientId('clientId1'); + $client->setClientSecret('clientSecret1'); + $client->setRedirectUri('http://localhost/'); + $client->setDeveloperKey('devKey'); + $client->setState('xyz'); + $client->setAccessType('offline'); + $client->setApprovalPrompt('force'); + $client->setRequestVisibleActions('http://foo/'); + $client->setLoginHint('bob@example.org'); + + $authUrl = $client->createAuthUrl("http://googleapis.com/scope/foo"); + $expected = "https://accounts.google.com/o/oauth2/v2/auth" + . "?response_type=code" + . "&access_type=offline" + . "&client_id=clientId1" + . "&redirect_uri=http%3A%2F%2Flocalhost" + . "&state=xyz" + . "&scope=http%3A%2F%2Fgoogleapis.com%2Fscope%2Ffoo" + . "&approval_prompt=force" + . "&login_hint=bob%40example.org"; + + $this->assertEquals($expected, $authUrl); + + // Again with a blank login hint (should remove all traces from authUrl) + $client->setLoginHint(''); + $client->setHostedDomain('example.com'); + $client->setOpenIdRealm('example.com'); + $client->setPrompt('select_account'); + $client->setIncludeGrantedScopes(true); + $authUrl = $client->createAuthUrl("http://googleapis.com/scope/foo"); + $expected = "https://accounts.google.com/o/oauth2/v2/auth" + . "?response_type=code" + . "&access_type=offline" + . "&client_id=clientId1" + . "&redirect_uri=http%3A%2F%2Flocalhost" + . "&state=xyz" + . "&scope=http%3A%2F%2Fgoogleapis.com%2Fscope%2Ffoo" + . "&hd=example.com" + . "&include_granted_scopes=true" + . "&openid.realm=example.com" + . "&prompt=select_account"; + + $this->assertEquals($expected, $authUrl); + } + + public function testPrepareNoScopes() + { + $client = new Client(); + + $scopes = $client->prepareScopes(); + $this->assertNull($scopes); + } + + public function testNoAuthIsNull() + { + $client = new Client(); + + $this->assertNull($client->getAccessToken()); + } + + public function testPrepareService() + { + $client = new Client(); + $client->setScopes(["scope1", "scope2"]); + $scopes = $client->prepareScopes(); + $this->assertEquals("scope1 scope2", $scopes); + + $client->setScopes(["", "scope2"]); + $scopes = $client->prepareScopes(); + $this->assertEquals(" scope2", $scopes); + + $client->setScopes("scope2"); + $client->addScope("scope3"); + $client->addScope(["scope4", "scope5"]); + $scopes = $client->prepareScopes(); + $this->assertEquals("scope2 scope3 scope4 scope5", $scopes); + + $client->setClientId('test1'); + $client->setRedirectUri('http://localhost/'); + $client->setState('xyz'); + $client->setScopes(["http://test.com/", "scope2"]); + $scopes = $client->prepareScopes(); + $this->assertEquals("http://test.com scope2", $scopes); + $this->assertEquals( + '' + . 'https://accounts.google.com/o/oauth2/v2/auth' + . '?response_type=code' + . '&access_type=online' + . '&client_id=test1' + . '&redirect_uri=http%3A%2F%2Flocalhost%2F' + . '&state=xyz' + . '&scope=http%3A%2F%2Ftest.com%20scope2' + . '&approval_prompt=auto', + $client->createAuthUrl() + ); + + $stream = $this->prophesize('GuzzleHttp\Psr7\Stream'); + $stream->__toString()->willReturn(''); + + $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); + $response->getBody() + ->shouldBeCalledTimes(1) + ->willReturn($stream->reveal()); + + $response->getStatusCode()->willReturn(200); + + $http = $this->prophesize('GuzzleHttp\ClientInterface'); + + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willReturn($response->reveal()); + + $client->setHttpClient($http->reveal()); + $dr_service = new Drive($client); + $this->assertInstanceOf('Google\Model', $dr_service->files->listFiles()); + } + + public function testDefaultLogger() + { + $client = new Client(); + $logger = $client->getLogger(); + $this->assertInstanceOf('Monolog\Logger', $logger); + $handler = $logger->popHandler(); + $this->assertInstanceOf('Monolog\Handler\StreamHandler', $handler); + } + + public function testDefaultLoggerAppEngine() + { + $_SERVER['SERVER_SOFTWARE'] = 'Google App Engine'; + $client = new Client(); + $logger = $client->getLogger(); + $handler = $logger->popHandler(); + unset($_SERVER['SERVER_SOFTWARE']); + + $this->assertInstanceOf('Monolog\Logger', $logger); + $this->assertInstanceOf('Monolog\Handler\SyslogHandler', $handler); + } + + public function testLoggerFromConstructor() + { + $logger1 = new \Monolog\Logger('unit-test'); + $client = new Client(['logger' => $logger1]); + $logger2 = $client->getLogger(); + $this->assertInstanceOf('Monolog\Logger', $logger2); + $this->assertEquals('unit-test', $logger2->getName()); + $this->assertSame($logger1, $logger2); + } + + public function testSettersGetters() + { + $client = new Client(); + $client->setClientId("client1"); + $client->setClientSecret('client1secret'); + $client->setState('1'); + $client->setApprovalPrompt('force'); + $client->setAccessType('offline'); + + $client->setRedirectUri('localhost'); + $client->setConfig('application_name', 'me'); + $client->setLogger(new \Monolog\Logger('test')); + + $cache = $this->prophesize(CacheItemPoolInterface::class); + $client->setCache($cache->reveal()); + $this->assertInstanceOf(CacheItemPoolInterface::class, $client->getCache()); + + try { + $client->setAccessToken(null); + $this->fail('Should have thrown an Exception.'); + } catch (InvalidArgumentException $e) { + $this->assertEquals('invalid json token', $e->getMessage()); + } + + $token = ['access_token' => 'token']; + $client->setAccessToken($token); + $this->assertEquals($token, $client->getAccessToken()); + } + + public function testSetAccessTokenValidation() + { + $client = new Client(); + $client->setAccessToken([ + 'access_token' => 'token', + 'created' => time() + ]); + self::assertEquals(true, $client->isAccessTokenExpired()); + } + + public function testDefaultConfigOptions() + { + $client = new Client(); + $this->assertArrayHasKey('http_errors', $client->getHttpClient()->getConfig()); + $this->assertArrayNotHasKey('exceptions', $client->getHttpClient()->getConfig()); + $this->assertFalse($client->getHttpClient()->getConfig()['http_errors']); + } + + public function testJsonConfig() + { + // Device config + $client = new Client(); + $device = + '{"installed":{"auth_uri":"https://accounts.google.com/o/oauth2/v2/auth","client_secret"'. + ':"N0aHCBT1qX1VAcF5J1pJAn6S","token_uri":"https://oauth2.googleapis.com/token",'. + '"client_email":"","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","oob"],"client_x509_cert_url"'. + ':"","client_id":"123456789.apps.googleusercontent.com","auth_provider_x509_cert_url":'. + '"https://www.googleapis.com/oauth2/v1/certs"}}'; + $dObj = json_decode($device, true); + $client->setAuthConfig($dObj); + $this->assertEquals($client->getClientId(), $dObj['installed']['client_id']); + $this->assertEquals($client->getClientSecret(), $dObj['installed']['client_secret']); + $this->assertEquals($client->getRedirectUri(), $dObj['installed']['redirect_uris'][0]); + + // Web config + $client = new Client(); + $web = '{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/v2/auth","client_secret"' . + ':"lpoubuib8bj-Fmke_YhhyHGgXc","token_uri":"https://oauth2.googleapis.com/token"' . + ',"client_email":"123456789@developer.gserviceaccount.com","client_x509_cert_url":'. + '"https://www.googleapis.com/robot/v1/metadata/x509/123456789@developer.gserviceaccount.com"'. + ',"client_id":"123456789.apps.googleusercontent.com","auth_provider_x509_cert_url":'. + '"https://www.googleapis.com/oauth2/v1/certs"}}'; + $wObj = json_decode($web, true); + $client->setAuthConfig($wObj); + $this->assertEquals($client->getClientId(), $wObj['web']['client_id']); + $this->assertEquals($client->getClientSecret(), $wObj['web']['client_secret']); + $this->assertEquals($client->getRedirectUri(), ''); + } + + public function testIniConfig() + { + $config = parse_ini_file(__DIR__ . '/../config/test.ini'); + $client = new Client($config); + + $this->assertEquals('My Test application', $client->getConfig('application_name')); + $this->assertEquals( + 'gjfiwnGinpena3', + $client->getClientSecret() + ); + } - $client = $this->getClient(); - $client->setAuthConfig(getenv('GOOGLE_APPLICATION_CREDENTIALS')); - $token = $client->fetchAccessTokenWithAssertion(); + public function testNoAuth() + { + /** @var $noAuth Google_Auth_Simple */ + $client = new Client(); + $client->setDeveloperKey(null); + + // unset application credentials + $GOOGLE_APPLICATION_CREDENTIALS = getenv('GOOGLE_APPLICATION_CREDENTIALS'); + $HOME = getenv('HOME'); + putenv('GOOGLE_APPLICATION_CREDENTIALS='); + putenv('HOME='.sys_get_temp_dir()); + $http = new GuzzleClient(); + $client->authorize($http); + + putenv("GOOGLE_APPLICATION_CREDENTIALS=$GOOGLE_APPLICATION_CREDENTIALS"); + putenv("HOME=$HOME"); + $this->checkAuthHandler($http, null); + } + + public function testApplicationDefaultCredentials() + { + $this->checkServiceAccountCredentials(); + $credentialsFile = getenv('GOOGLE_APPLICATION_CREDENTIALS'); + + $client = new Client(); + $client->setAuthConfig($credentialsFile); + + $http = new GuzzleClient(); + $client->authorize($http); + + $this->checkAuthHandler($http, 'AuthToken'); + $this->checkCredentials($http, 'Google\Auth\Credentials\ServiceAccountCredentials'); + } + + public function testApplicationDefaultCredentialsWithSubject() + { + $this->checkServiceAccountCredentials(); + $credentialsFile = getenv('GOOGLE_APPLICATION_CREDENTIALS'); - $this->assertNotNull($token); - $this->assertArrayHasKey('access_token', $token); - } + $sub = 'sub123'; + $client = new Client(); + $client->setAuthConfig($credentialsFile); + $client->setSubject($sub); + + $http = new GuzzleClient(); + $client->authorize($http); + + $this->checkAuthHandler($http, 'AuthToken'); + $this->checkCredentials($http, 'Google\Auth\Credentials\ServiceAccountCredentials', $sub); + } + + /** + * Test that the ID token is properly refreshed. + */ + public function testRefreshTokenSetsValues() + { + $token = json_encode([ + 'access_token' => 'xyz', + 'id_token' => 'ID_TOKEN', + ]); + $postBody = $this->prophesize('GuzzleHttp\Psr7\Stream'); + $postBody->__toString() + ->shouldBeCalledTimes(1) + ->willReturn($token); + + $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); + + $response->getBody() + ->shouldBeCalledTimes(1) + ->willReturn($postBody->reveal()); + + $response->hasHeader('Content-Type')->willReturn(false); + + $http = $this->prophesize('GuzzleHttp\ClientInterface'); + + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willReturn($response->reveal()); + + $client = $this->getClient(); + $client->setHttpClient($http->reveal()); + $client->fetchAccessTokenWithRefreshToken("REFRESH_TOKEN"); + $token = $client->getAccessToken(); + $this->assertEquals("ID_TOKEN", $token['id_token']); + } + + /** + * Test that the Refresh Token is set when refreshed. + */ + public function testRefreshTokenIsSetOnRefresh() + { + $refreshToken = 'REFRESH_TOKEN'; + $token = json_encode([ + 'access_token' => 'xyz', + 'id_token' => 'ID_TOKEN', + ]); + $postBody = $this->prophesize('Psr\Http\Message\StreamInterface'); + $postBody->__toString() + ->shouldBeCalledTimes(1) + ->willReturn($token); + + $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); + + $response->getBody() + ->shouldBeCalledTimes(1) + ->willReturn($postBody->reveal()); + + $response->hasHeader('Content-Type')->willReturn(false); + + $http = $this->prophesize('GuzzleHttp\ClientInterface'); + + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willReturn($response->reveal()); + + $client = $this->getClient(); + $client->setHttpClient($http->reveal()); + $client->fetchAccessTokenWithRefreshToken($refreshToken); + $token = $client->getAccessToken(); + $this->assertEquals($refreshToken, $token['refresh_token']); + } - /** - * Test fetching an access token with assertion credentials - * using "setAuthConfig" and "setSubject" but with user credentials + /** + * Test that the Refresh Token is not set when a new refresh token is returned. + */ + public function testRefreshTokenIsNotSetWhenNewRefreshTokenIsReturned() + { + $refreshToken = 'REFRESH_TOKEN'; + $token = json_encode([ + 'access_token' => 'xyz', + 'id_token' => 'ID_TOKEN', + 'refresh_token' => 'NEW_REFRESH_TOKEN' + ]); + + $postBody = $this->prophesize('GuzzleHttp\Psr7\Stream'); + $postBody->__toString() + ->wilLReturn($token); + + $response = $this->prophesize('Psr\Http\Message\ResponseInterface'); + + $response->getBody() + ->willReturn($postBody->reveal()); + + $response->hasHeader('Content-Type')->willReturn(false); + + $http = $this->prophesize('GuzzleHttp\ClientInterface'); + + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willReturn($response->reveal()); + + $client = $this->getClient(); + $client->setHttpClient($http->reveal()); + $client->fetchAccessTokenWithRefreshToken($refreshToken); + $token = $client->getAccessToken(); + $this->assertEquals('NEW_REFRESH_TOKEN', $token['refresh_token']); + } + + /** + * Test fetching an access token with assertion credentials + * using "useApplicationDefaultCredentials" + */ + public function testFetchAccessTokenWithAssertionFromEnv() + { + $this->checkServiceAccountCredentials(); + + $client = $this->getClient(); + $client->useApplicationDefaultCredentials(); + $token = $client->fetchAccessTokenWithAssertion(); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + /** + * Test fetching an access token with assertion credentials + * using "setAuthConfig" + */ + public function testFetchAccessTokenWithAssertionFromFile() + { + $this->checkServiceAccountCredentials(); + + $client = $this->getClient(); + $client->setAuthConfig(getenv('GOOGLE_APPLICATION_CREDENTIALS')); + $token = $client->fetchAccessTokenWithAssertion(); + + $this->assertNotNull($token); + $this->assertArrayHasKey('access_token', $token); + } + + /** + * Test fetching an access token with assertion credentials + * populates the "created" field + */ + public function testFetchAccessTokenWithAssertionAddsCreated() + { + $this->checkServiceAccountCredentials(); + + $client = $this->getClient(); + $client->useApplicationDefaultCredentials(); + $token = $client->fetchAccessTokenWithAssertion(); + + $this->assertNotNull($token); + $this->assertArrayHasKey('created', $token); + } + + /** + * Test fetching an access token with assertion credentials + * using "setAuthConfig" and "setSubject" but with user credentials + */ + public function testBadSubjectThrowsException() + { + $this->checkServiceAccountCredentials(); + + $client = $this->getClient(); + $client->useApplicationDefaultCredentials(); + $client->setSubject('bad-subject'); + + $authHandler = AuthHandlerFactory::build(); + + // make this method public for testing purposes + $method = new ReflectionMethod($authHandler, 'createAuthHttp'); + $method->setAccessible(true); + $authHttp = $method->invoke($authHandler, $client->getHttpClient()); + + try { + $token = $client->fetchAccessTokenWithAssertion($authHttp); + $this->fail('no exception thrown'); + } catch (ClientException $e) { + $response = $e->getResponse(); + $this->assertContains('Invalid impersonation', (string) $response->getBody()); + } + } + + public function testTokenCallback() + { + $this->checkToken(); + + $client = $this->getClient(); + $accessToken = $client->getAccessToken(); + + if (!isset($accessToken['refresh_token'])) { + $this->markTestSkipped('Refresh Token required'); + } + + // make the auth library think the token is expired + $accessToken['expires_in'] = 0; + $cache = $client->getCache(); + $path = sys_get_temp_dir().'/google-api-php-client-tests-'.time(); + $client->setCache($this->getCache($path)); + $client->setAccessToken($accessToken); + + // create the callback function + $phpunit = $this; + $called = false; + $callback = function ($key, $value) use ($client, $cache, $phpunit, &$called) { + // assert the expected keys and values + $phpunit->assertNotNull($key); + $phpunit->assertNotNull($value); + $called = true; + + // go back to the previous cache + $client->setCache($cache); + }; + + // set the token callback to the client + $client->setTokenCallback($callback); + + // make a silly request to obtain a new token (it's ok if it fails) + $http = $client->authorize(); + try { + $http->get('https://www.googleapis.com/books/v1/volumes?q=Voltaire'); + } catch (Exception $e) { + + } + $newToken = $client->getAccessToken(); + + // go back to the previous cache + // (in case callback wasn't called) + $client->setCache($cache); + + $this->assertTrue($called); + } + + public function testDefaultTokenCallback() + { + $this->checkToken(); + + $client = $this->getClient(); + $accessToken = $client->getAccessToken(); + + if (!isset($accessToken['refresh_token'])) { + $this->markTestSkipped('Refresh Token required'); + } + + // make the auth library think the token is expired + $accessToken['expires_in'] = 0; + $client->setAccessToken($accessToken); + + // make a silly request to obtain a new token (it's ok if it fails) + $http = $client->authorize(); + try { + $http->get('https://www.googleapis.com/books/v1/volumes?q=Voltaire'); + } catch (Exception $e) { + + } + + // Assert the in-memory token has been updated + $newToken = $client->getAccessToken(); + $this->assertNotEquals( + $accessToken['access_token'], + $newToken['access_token'] + ); + + $this->assertFalse($client->isAccessTokenExpired()); + } + + /** @runInSeparateProcess */ + public function testOnGceCacheAndCacheOptions() + { + if (!class_exists(GCECache::class)) { + $this->markTestSkipped('Requires google/auth >= 1.12'); + } + + putenv('HOME='); + putenv('GOOGLE_APPLICATION_CREDENTIALS='); + $prefix = 'test_prefix_'; + $cacheConfig = ['gce_prefix' => $prefix]; + + $mockCacheItem = $this->prophesize(CacheItemInterface::class); + $mockCacheItem->isHit() + ->willReturn(true); + $mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $mockUniverseDomainCacheItem = $this->prophesize(CacheItemInterface::class); + $mockUniverseDomainCacheItem->isHit() + ->willReturn(true); + $mockUniverseDomainCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn('googleapis.com'); + + $mockCache = $this->prophesize(CacheItemPoolInterface::class); + $mockCache->getItem($prefix . GCECache::GCE_CACHE_KEY) + ->shouldBeCalledTimes(1) + ->willReturn($mockCacheItem->reveal()); + // cache key from GCECredentials::getTokenUri() . 'universe_domain' + $mockCache->getItem('cc685e3a0717258b6a4cefcb020e96de6bcf904e76fd9fc1647669f42deff9bf') // google/auth < 1.41.0 + ->willReturn($mockUniverseDomainCacheItem->reveal()); + $mockCache->getItem(GCECredentials::cacheKey . 'universe_domain') // google/auth >= 1.41.0 + ->willReturn($mockUniverseDomainCacheItem->reveal()); + + $client = new Client(['cache_config' => $cacheConfig]); + $client->setCache($mockCache->reveal()); + $client->useApplicationDefaultCredentials(); + $client->authorize(); + } + + /** @runInSeparateProcess */ + public function testFetchAccessTokenWithAssertionCache() + { + $this->checkServiceAccountCredentials(); + $cachedValue = ['access_token' => '2/abcdef1234567890']; + $mockCacheItem = $this->prophesize(CacheItemInterface::class); + $mockCacheItem->isHit() + ->shouldBeCalledTimes(1) + ->willReturn(true); + $mockCacheItem->get() + ->shouldBeCalledTimes(1) + ->willReturn($cachedValue); + + $mockCache = $this->prophesize(CacheItemPoolInterface::class); + $mockCache->getItem(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($mockCacheItem->reveal()); + + $client = new Client(); + $client->setCache($mockCache->reveal()); + $client->useApplicationDefaultCredentials(); + $token = $client->fetchAccessTokenWithAssertion(); + $this->assertArrayHasKey('access_token', $token); + $this->assertEquals($cachedValue['access_token'], $token['access_token']); + } + + public function testCacheClientOption() + { + $mockCache = $this->prophesize(CacheItemPoolInterface::class); + $client = new Client([ + 'cache' => $mockCache->reveal() + ]); + $this->assertEquals($mockCache->reveal(), $client->getCache()); + } + + public function testExecuteWithFormat() + { + $client = new Client([ + 'api_format_v2' => true + ]); + + $guzzle = $this->prophesize('GuzzleHttp\Client'); + $guzzle + ->send(Argument::allOf( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::that(function (RequestInterface $request) { + return $request->getHeaderLine('X-GOOG-API-FORMAT-VERSION') === '2'; + }) + ), []) + ->shouldBeCalled() + ->willReturn(new Response(200, [], null)); + + $client->setHttpClient($guzzle->reveal()); + + $request = new Request('POST', 'http://foo.bar/'); + $client->execute($request); + } + + public function testExecuteSetsCorrectHeaders() + { + $client = new Client(); + + $guzzle = $this->prophesize('GuzzleHttp\Client'); + $guzzle->send(Argument::that(function (RequestInterface $request) { + $userAgent = sprintf( + '%s%s', + Client::USER_AGENT_SUFFIX, + Client::LIBVER + ); + $xGoogApiClient = sprintf( + 'gl-php/%s gdcl/%s', + phpversion(), + Client::LIBVER + ); + + if ($request->getHeaderLine('User-Agent') !== $userAgent) { + return false; + } + + if ($request->getHeaderLine('x-goog-api-client') !== $xGoogApiClient) { + return false; + } + + return true; + }), [])->shouldBeCalledTimes(1)->willReturn(new Response(200, [], null)); + + $client->setHttpClient($guzzle->reveal()); + + $request = new Request('POST', 'http://foo.bar/'); + $client->execute($request); + } + + /** + * @runInSeparateProcess */ - public function testBadSubjectThrowsException() - { - $this->checkServiceAccountCredentials(); - - $client = $this->getClient(); - $client->useApplicationDefaultCredentials(); - $client->setSubject('bad-subject'); - - $authHandler = Google_AuthHandler_AuthHandlerFactory::build(); - - // make this method public for testing purposes - $method = new ReflectionMethod($authHandler, 'createAuthHttp'); - $method->setAccessible(true); - $authHttp = $method->invoke($authHandler, $client->getHttpClient()); - - try { - $token = $client->fetchAccessTokenWithAssertion($authHttp); - $this->fail('no exception thrown'); - } catch (GuzzleHttp\Exception\ClientException $e) { - $response = $e->getResponse(); - $this->assertContains('Invalid impersonation prn email address', (string) $response->getBody()); - } - } - - public function testTokenCallback() - { - $this->checkToken(); - - $client = $this->getClient(); - $accessToken = $client->getAccessToken(); - - if (!isset($accessToken['refresh_token'])) { - $this->markTestSkipped('Refresh Token required'); - } - - // make the auth library think the token is expired - $accessToken['expires_in'] = 0; - $cache = $client->getCache(); - $path = sys_get_temp_dir().'/google-api-php-client-tests-'.time(); - $client->setCache($this->getCache($path)); - $client->setAccessToken($accessToken); - - // create the callback function - $phpunit = $this; - $called = false; - $callback = function ($key, $value) use ($client, $cache, $phpunit, &$called) { - // go back to the previous cache - $client->setCache($cache); - - // assert the expected keys and values - $phpunit->assertContains('https---www.googleapis.com-auth-', $key); - $phpunit->assertNotNull($value); - $called = true; - }; - - // set the token callback to the client - $client->setTokenCallback($callback); - - // make a silly request to obtain a new token - $http = $client->authorize(); - $http->get('https://www.googleapis.com/books/v1/volumes?q=Voltaire'); - $newToken = $client->getAccessToken(); - - // go back to the previous cache - // (in case callback wasn't called) - $client->setCache($cache); - - $this->assertTrue($called); - } + public function testClientOptions() + { + // Test credential file + $tmpCreds = [ + 'type' => 'service_account', + 'client_id' => 'foo', + 'client_email' => '', + 'private_key' => '' + ]; + $tmpCredFile = tempnam(sys_get_temp_dir(), 'creds') . '.json'; + file_put_contents($tmpCredFile, json_encode($tmpCreds)); + $client = new Client([ + 'credentials' => $tmpCredFile + ]); + $this->assertEquals('foo', $client->getClientId()); + + // Test credentials array + $client = new Client([ + 'credentials' => $tmpCredFile + ]); + $this->assertEquals('foo', $client->getClientId()); + + // Test singular scope + $client = new Client([ + 'scopes' => 'a-scope' + ]); + $this->assertEquals(['a-scope'], $client->getScopes()); + + // Test multiple scopes + $client = new Client([ + 'scopes' => ['one-scope', 'two-scope'] + ]); + $this->assertEquals(['one-scope', 'two-scope'], $client->getScopes()); + + // Test quota project + $client = new Client([ + 'quota_project' => 'some-quota-project' + ]); + $this->assertEquals('some-quota-project', $client->getConfig('quota_project')); + // Test quota project in google/auth dependency + putenv('GOOGLE_APPLICATION_CREDENTIALS='.$tmpCredFile); + $method = new ReflectionMethod($client, 'createApplicationDefaultCredentials'); + $method->setAccessible(true); + $credentials = $method->invoke($client); + $this->assertEquals('some-quota-project', $credentials->getQuotaProject()); + } + + public function testCredentialsOptionWithCredentialsLoader() + { + $request = null; + $credentials = $this->prophesize('Google\Auth\CredentialsLoader'); + $credentials->getCacheKey() + ->willReturn('cache-key'); + $credentials->getUniverseDomain() + ->willReturn('googleapis.com'); + + // Ensure the access token provided by our credentials loader is used + $credentials->updateMetadata([], null, Argument::any()) + ->shouldBeCalledOnce() + ->willReturn(['authorization' => 'Bearer abc']); + $credentials->getLastReceivedToken() + ->shouldBeCalledTimes(2) + ->willReturn(null); + + $client = new Client(['credentials' => $credentials->reveal()]); + + $handler = $this->prophesize('GuzzleHttp\HandlerStack'); + $handler->remove('google_auth') + ->shouldBeCalledOnce(); + $handler->push(Argument::any(), 'google_auth') + ->shouldBeCalledOnce() + ->will(function ($args) use (&$request) { + $middleware = $args[0]; + $callable = $middleware(function ($req, $res) use (&$request) { + $request = $req; // test later + }); + $callable(new Request('GET', '/fake-uri'), ['auth' => 'google_auth']); + }); + + $httpClient = $this->prophesize('GuzzleHttp\ClientInterface'); + $httpClient->getConfig() + ->shouldBeCalled() + ->willReturn(['handler' => $handler->reveal()]); + $httpClient->send(Argument::any(), Argument::any()) + ->shouldNotBeCalled(); + + $http = $client->authorize($httpClient->reveal()); + + $this->assertNotNull($request); + $authHeader = $request->getHeaderLine('authorization'); + $this->assertNotNull($authHeader); + $this->assertEquals('Bearer abc', $authHeader); + } + + public function testSetNewRedirectUri() + { + $client = new Client(); + $redirectUri1 = 'https://foo.com/test1'; + $client->setRedirectUri($redirectUri1); + + $authUrl1 = $client->createAuthUrl(); + $this->assertStringContainsString(urlencode($redirectUri1), $authUrl1); + + $redirectUri2 = 'https://foo.com/test2'; + $client->setRedirectUri($redirectUri2); + + $authUrl2 = $client->createAuthUrl(); + $this->assertStringContainsString(urlencode($redirectUri2), $authUrl2); + } + + public function testQueryParamsForAuthUrl() + { + $client = new Client(); + $client->setRedirectUri('https://example.com/'); + $authUrl1 = $client->createAuthUrl(null, [ + 'enable_serial_consent' => 'true' + ]); + $this->assertStringContainsString('&enable_serial_consent=true', $authUrl1); + } + public function testUniverseDomainMismatch() + { + $this->expectException(DomainException::class); + $this->expectExceptionMessage( + 'The configured universe domain (example.com) does not match the credential universe domain (foo.com)' + ); + + $credentials = $this->prophesize(CredentialsLoader::class); + $credentials->getUniverseDomain() + ->shouldBeCalledOnce() + ->willReturn('foo.com'); + $client = new Client([ + 'universe_domain' => 'example.com', + 'credentials' => $credentials->reveal(), + ]); + $client->authorize(); + } } diff --git a/tests/Google/Http/BatchTest.php b/tests/Google/Http/BatchTest.php index 78a947eed..e60421a06 100644 --- a/tests/Google/Http/BatchTest.php +++ b/tests/Google/Http/BatchTest.php @@ -18,112 +18,76 @@ * under the License. */ +namespace Google\Tests\Http; + +use Google\Tests\BaseTest; +use Google\Http\Batch; +use Google\Service\Books; +use Google\Service\Storage; +use Google\Service\Exception as ServiceException; use GuzzleHttp\Psr7; -class Google_Http_BatchTest extends BaseTest +class BatchTest extends BaseTest { - public function testBatchRequestWithAuth() - { - $this->checkToken(); - - $client = $this->getClient(); - $batch = new Google_Http_Batch($client); - $plus = new Google_Service_Plus($client); - - $client->setUseBatch(true); - $batch->add($plus->people->get('me'), 'key1'); - $batch->add($plus->people->get('me'), 'key2'); - $batch->add($plus->people->get('me'), 'key3'); - - $result = $batch->execute(); - $this->assertTrue(isset($result['response-key1'])); - $this->assertTrue(isset($result['response-key2'])); - $this->assertTrue(isset($result['response-key3'])); - } - - public function testBatchRequest() - { - $client = $this->getClient(); - $batch = new Google_Http_Batch($client); - $plus = new Google_Service_Plus($client); - - $client->setUseBatch(true); - $batch->add($plus->people->get('+LarryPage'), 'key1'); - $batch->add($plus->people->get('+LarryPage'), 'key2'); - $batch->add($plus->people->get('+LarryPage'), 'key3'); - - $result = $batch->execute(); - $this->assertTrue(isset($result['response-key1'])); - $this->assertTrue(isset($result['response-key2'])); - $this->assertTrue(isset($result['response-key3'])); - } - - public function testBatchRequestWithPostBody() - { - $this->checkToken(); - - $client = $this->getClient(); - $batch = new Google_Http_Batch($client); - $shortener = new Google_Service_Urlshortener($client); - $url1 = new Google_Service_Urlshortener_Url; - $url2 = new Google_Service_Urlshortener_Url; - $url3 = new Google_Service_Urlshortener_Url; - $url1->setLongUrl('http://brentertainment.com/'); - $url2->setLongUrl('http://morehazards.com/'); - $url3->setLongUrl('http://github.com/bshaffer'); - - $client->setUseBatch(true); - $batch->add($shortener->url->insert($url1), 'key1'); - $batch->add($shortener->url->insert($url2), 'key2'); - $batch->add($shortener->url->insert($url3), 'key3'); - - $result = $batch->execute(); - $this->assertTrue(isset($result['response-key1'])); - $this->assertTrue(isset($result['response-key2'])); - $this->assertTrue(isset($result['response-key3'])); - } - - public function testInvalidBatchRequest() - { - $client = $this->getClient(); - $batch = new Google_Http_Batch($client); - $plus = new Google_Service_Plus($client); - - $client->setUseBatch(true); - $batch->add($plus->people->get('123456789987654321'), 'key1'); - $batch->add($plus->people->get('+LarryPage'), 'key2'); - - $result = $batch->execute(); - $this->assertTrue(isset($result['response-key2'])); - $this->assertInstanceOf( - 'Google_Service_Exception', - $result['response-key1'] - ); - } - - public function testMediaFileBatch() - { - $client = $this->getClient(); - $storage = new Google_Service_Storage($client); - $bucket = 'testbucket'; - $stream = Psr7\stream_for("testbucket-text"); - $params = [ - 'data' => $stream, - 'mimeType' => 'text/plain', - ]; - - // Metadata object for new Google Cloud Storage object - $obj = new Google_Service_Storage_StorageObject(); - $obj->contentType = "text/plain"; - - // Batch Upload - $client->setUseBatch(true); - $obj->name = "batch"; - /** @var \GuzzleHttp\Psr7\Request $request */ - $request = $storage->objects->insert($bucket, $obj, $params); - - $this->assertContains('multipart/related', $request->getHeaderLine('content-type')); - $this->assertContains('/upload/', $request->getUri()->getPath()); - $this->assertContains('uploadType=multipart', $request->getUri()->getQuery()); - } + public function testBatchRequest() + { + $this->checkKey(); + $client = $this->getClient(); + $client->setUseBatch(true); + $books = new Books($client); + $batch = $books->createBatch(); + + $batch->add($books->volumes->listVolumes('Henry David Thoreau'), 'key1'); + $batch->add($books->volumes->listVolumes('Edgar Allen Poe'), 'key2'); + + $result = $batch->execute(); + $this->assertArrayHasKey('response-key1', $result); + $this->assertArrayHasKey('response-key2', $result); + } + + public function testInvalidBatchRequest() + { + $this->checkKey(); + $client = $this->getClient(); + $client->setUseBatch(true); + $books = new Books($client); + $batch = $books->createBatch(); + + $batch->add($books->volumes->listVolumes(false), 'key1'); + $batch->add($books->volumes->listVolumes('Edgar Allen Poe'), 'key2'); + + $result = $batch->execute(); + $this->assertArrayHasKey('response-key1', $result); + $this->assertArrayHasKey('response-key2', $result); + $this->assertInstanceOf( + ServiceException::class, + $result['response-key1'] + ); + } + + public function testMediaFileBatch() + { + $client = $this->getClient(); + $storage = new Storage($client); + $bucket = 'testbucket'; + $stream = Psr7\Utils::streamFor("testbucket-text"); + $params = [ + 'data' => $stream, + 'mimeType' => 'text/plain', + ]; + + // Metadata object for new Google Cloud Storage object + $obj = new Storage\StorageObject(); + $obj->contentType = "text/plain"; + + // Batch Upload + $client->setUseBatch(true); + $obj->name = "batch"; + /** @var \GuzzleHttp\Psr7\Request $request */ + $request = $storage->objects->insert($bucket, $obj, $params); + + $this->assertStringContainsString('multipart/related', $request->getHeaderLine('content-type')); + $this->assertStringContainsString('/upload/', $request->getUri()->getPath()); + $this->assertStringContainsString('uploadType=multipart', $request->getUri()->getQuery()); + } } diff --git a/tests/Google/Http/MediaFileUploadTest.php b/tests/Google/Http/MediaFileUploadTest.php index a53663326..ae7bffedf 100644 --- a/tests/Google/Http/MediaFileUploadTest.php +++ b/tests/Google/Http/MediaFileUploadTest.php @@ -1,8 +1,5 @@ getClient(); - $request = new Request('POST', 'http://www.example.com/'); - $media = new Google_Http_MediaFileUpload( - $client, - $request, - 'image/png', - base64_decode('data:image/png;base64,a') - ); - $request = $media->getRequest(); - - $this->assertEquals(0, $media->getProgress()); - $this->assertGreaterThan(0, strlen($request->getBody())); - } - - public function testGetUploadType() - { - $client = $this->getClient(); - $request = new Request('POST', 'http://www.example.com/'); - - // Test resumable upload - $media = new Google_Http_MediaFileUpload($client, $request, 'image/png', 'a', true); - $this->assertEquals('resumable', $media->getUploadType(null)); - - // Test data *only* uploads - $media = new Google_Http_MediaFileUpload($client, $request, 'image/png', 'a', false); - $this->assertEquals('media', $media->getUploadType(null)); - - // Test multipart uploads - $media = new Google_Http_MediaFileUpload($client, $request, 'image/png', 'a', false); - $this->assertEquals('multipart', $media->getUploadType(array('a' => 'b'))); - } - - public function testProcess() - { - $client = $this->getClient(); - $data = 'foo'; - - // Test data *only* uploads. - $request = new Request('POST', 'http://www.example.com/'); - $media = new Google_Http_MediaFileUpload($client, $request, 'image/png', $data, false); - $request = $media->getRequest(); - $this->assertEquals($data, (string) $request->getBody()); - - // Test resumable (meta data) - we want to send the metadata, not the app data. - $request = new Request('POST', 'http://www.example.com/'); - $reqData = json_encode("hello"); - $request = $request->withBody(Psr7\stream_for($reqData)); - $media = new Google_Http_MediaFileUpload($client, $request, 'image/png', $data, true); - $request = $media->getRequest(); - $this->assertEquals(json_decode($reqData), (string) $request->getBody()); - - // Test multipart - we are sending encoded meta data and post data - $request = new Request('POST', 'http://www.example.com/'); - $reqData = json_encode("hello"); - $request = $request->withBody(Psr7\stream_for($reqData)); - $media = new Google_Http_MediaFileUpload($client, $request, 'image/png', $data, false); - $request = $media->getRequest(); - $this->assertContains($reqData, (string) $request->getBody()); - $this->assertContains(base64_encode($data), (string) $request->getBody()); - } - - public function testGetResumeUri() - { - $this->checkToken(); - - $client = $this->getClient(); - $client->addScope("https://www.googleapis.com/auth/drive"); - $service = new Google_Service_Drive($client); - $file = new Google_Service_Drive_DriveFile(); - $file->name = 'TESTFILE-testGetResumeUri'; - $chunkSizeBytes = 1 * 1024 * 1024; - - // Call the API with the media upload, defer so it doesn't immediately return. - $client->setDefer(true); - $request = $service->files->create($file); - - // Create a media file upload to represent our upload process. - $media = new Google_Http_MediaFileUpload( - $client, - $request, - 'text/plain', - null, - true, - $chunkSizeBytes - ); - - // request the resumable url - $uri = $media->getResumeUri(); - $this->assertTrue(is_string($uri)); - - // parse the URL - $parts = parse_url(http://github.com/$uri); - $this->assertArrayHasKey('query', $parts); - - // parse the querystring - parse_str($parts['query'], $query); - $this->assertArrayHasKey('uploadType', $query); - $this->assertArrayHasKey('upload_id', $query); - $this->assertEquals('resumable', $query['uploadType']); - } - - public function testNextChunk() - { - $this->checkToken(); - - $client = $this->getClient(); - $client->addScope("https://www.googleapis.com/auth/drive"); - $service = new Google_Service_Drive($client); - - $data = 'foo'; - $file = new Google_Service_Drive_DriveFile(); - $file->name = $name = 'TESTFILE-testNextChunk'; - - // Call the API with the media upload, defer so it doesn't immediately return. - $client->setDefer(true); - $request = $service->files->create($file); - - // Create a media file upload to represent our upload process. - $media = new Google_Http_MediaFileUpload( - $client, - $request, - 'text/plain', - null, - true - ); - $media->setFileSize(strlen($data)); - - // upload the file - $file = $media->nextChunk($data); - $this->assertInstanceOf('Google_Service_Drive_DriveFile', $file); - $this->assertEquals($name, $file->name); - - // remove the file - $client->setDefer(false); - $response = $service->files->delete($file->id); - $this->assertEquals(204, $response->getStatusCode()); - } - - public function testNextChunkWithMoreRemaining() - { - $this->checkToken(); - - $client = $this->getClient(); - $client->addScope("https://www.googleapis.com/auth/drive"); - $service = new Google_Service_Drive($client); - - $chunkSizeBytes = 262144; // smallest chunk size allowed by APIs - $data = str_repeat('.', $chunkSizeBytes+1); - $file = new Google_Service_Drive_DriveFile(); - $file->name = $name = 'TESTFILE-testNextChunkWithMoreRemaining'; - - // Call the API with the media upload, defer so it doesn't immediately return. - $client->setDefer(true); - $request = $service->files->create($file); - - // Create a media file upload to represent our upload process. - $media = new Google_Http_MediaFileUpload( - $client, - $request, - 'text/plain', - $data, - true, - $chunkSizeBytes - ); - $media->setFileSize(strlen($data)); - - // upload the file - $file = $media->nextChunk(); - // false means we aren't done uploading, which is exactly what we expect! - $this->assertFalse($file); - } + public function testMediaFile() + { + $client = $this->getClient(); + $request = new Request('POST', 'http://www.example.com/'); + $media = new MediaFileUpload( + $client, + $request, + 'image/png', + base64_decode('data:image/png;base64,a') + ); + $request = $media->getRequest(); + + $this->assertEquals(0, $media->getProgress()); + $this->assertGreaterThan(0, strlen($request->getBody())); + } + + public function testGetUploadType() + { + $client = $this->getClient(); + $request = new Request('POST', 'http://www.example.com/'); + + // Test resumable upload + $media = new MediaFileUpload($client, $request, 'image/png', 'a', true); + $this->assertEquals('resumable', $media->getUploadType(null)); + + // Test data *only* uploads + $media = new MediaFileUpload($client, $request, 'image/png', 'a', false); + $this->assertEquals('media', $media->getUploadType(null)); + + // Test multipart uploads + $media = new MediaFileUpload($client, $request, 'image/png', 'a', false); + $this->assertEquals('multipart', $media->getUploadType(['a' => 'b'])); + } + + public function testProcess() + { + $client = $this->getClient(); + $data = 'foo'; + + // Test data *only* uploads. + $request = new Request('POST', 'http://www.example.com/'); + $media = new MediaFileUpload($client, $request, 'image/png', $data, false); + $request = $media->getRequest(); + $this->assertEquals($data, (string) $request->getBody()); + + // Test resumable (meta data) - we want to send the metadata, not the app data. + $request = new Request('POST', 'http://www.example.com/'); + $reqData = json_encode("hello"); + $request = $request->withBody(Psr7\Utils::streamFor($reqData)); + $media = new MediaFileUpload($client, $request, 'image/png', $data, true); + $request = $media->getRequest(); + $this->assertEquals(json_decode($reqData), (string) $request->getBody()); + + // Test multipart - we are sending encoded meta data and post data + $request = new Request('POST', 'http://www.example.com/'); + $reqData = json_encode("hello"); + $request = $request->withBody(Psr7\Utils::streamFor($reqData)); + $media = new MediaFileUpload($client, $request, 'image/png', $data, false); + $request = $media->getRequest(); + $this->assertStringContainsString($reqData, (string) $request->getBody()); + $this->assertStringContainsString(base64_encode($data), (string) $request->getBody()); + } + + public function testGetResumeUri() + { + $this->checkToken(); + + $client = $this->getClient(); + $client->addScope("https://www.googleapis.com/auth/drive"); + $service = new Drive($client); + $file = new Drive\DriveFile(); + $file->name = 'TESTFILE-testGetResumeUri'; + $chunkSizeBytes = 1 * 1024 * 1024; + + // Call the API with the media upload, defer so it doesn't immediately return. + $client->setDefer(true); + $request = $service->files->create($file); + + // Create a media file upload to represent our upload process. + $media = new MediaFileUpload( + $client, + $request, + 'text/plain', + null, + true, + $chunkSizeBytes + ); + + // request the resumable url + $uri = $media->getResumeUri(); + $this->assertIsString($uri); + + // parse the URL + $parts = parse_url(http://github.com/$uri); + $this->assertArrayHasKey('query', $parts); + + // parse the querystring + parse_str($parts['query'], $query); + $this->assertArrayHasKey('uploadType', $query); + $this->assertArrayHasKey('upload_id', $query); + $this->assertEquals('resumable', $query['uploadType']); + } + + public function testNextChunk() + { + $this->checkToken(); + + $client = $this->getClient(); + $client->addScope("https://www.googleapis.com/auth/drive"); + $service = new Drive($client); + + $data = 'foo'; + $file = new Drive\DriveFile(); + $file->name = $name = 'TESTFILE-testNextChunk'; + + // Call the API with the media upload, defer so it doesn't immediately return. + $client->setDefer(true); + $request = $service->files->create($file); + + // Create a media file upload to represent our upload process. + $media = new MediaFileUpload( + $client, + $request, + 'text/plain', + null, + true + ); + $media->setFileSize(strlen($data)); + + // upload the file + $file = $media->nextChunk($data); + $this->assertInstanceOf(Drive\DriveFile::class, $file); + $this->assertEquals($name, $file->name); + + // remove the file + $client->setDefer(false); + $response = $service->files->delete($file->id); + $this->assertEquals(204, $response->getStatusCode()); + } + + public function testNextChunkWithMoreRemaining() + { + $this->checkToken(); + + $client = $this->getClient(); + $client->addScope("https://www.googleapis.com/auth/drive"); + $service = new Drive($client); + + $chunkSizeBytes = 262144; // smallest chunk size allowed by APIs + $data = str_repeat('.', $chunkSizeBytes+1); + $file = new Drive\DriveFile(); + $file->name = $name = 'TESTFILE-testNextChunkWithMoreRemaining'; + + // Call the API with the media upload, defer so it doesn't immediately return. + $client->setDefer(true); + $request = $service->files->create($file); + + // Create a media file upload to represent our upload process. + $media = new MediaFileUpload( + $client, + $request, + 'text/plain', + $data, + true, + $chunkSizeBytes + ); + $media->setFileSize(strlen($data)); + + // upload the file + $file = $media->nextChunk(); + // false means we aren't done uploading, which is exactly what we expect! + $this->assertFalse($file); + } } diff --git a/tests/Google/Http/RESTTest.php b/tests/Google/Http/RESTTest.php index b090fdd77..ef44ed0a2 100644 --- a/tests/Google/Http/RESTTest.php +++ b/tests/Google/Http/RESTTest.php @@ -15,127 +15,128 @@ * limitations under the License. */ +namespace Google\Tests\Http; + +use Google\Http\REST; +use Google\Service\Exception as ServiceException; +use Google\Tests\BaseTest; +use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -class Google_HTTP_RESTTest extends BaseTest +class RESTTest extends BaseTest { - /** - * @var Google_Http_REST $rest - */ - private $rest; - - public function setUp() - { - $this->rest = new Google_Http_REST(); - $this->request = new Request('GET', '/'); - } - - public function testDecodeResponse() - { - $client = $this->getClient(); - $response = new Response(204); - $decoded = $this->rest->decodeHttpResponse($response, $this->request); - $this->assertEquals($response, $decoded); - - foreach (array(200, 201) as $code) { - $headers = array('foo', 'bar'); - $stream = Psr7\stream_for('{"a": 1}'); - $response = new Response($code, $headers, $stream); - - $decoded = $this->rest->decodeHttpResponse($response, $this->request); - $this->assertEquals('{"a": 1}', (string) $decoded->getBody()); + /** + * @var REST $rest + */ + private $rest; + + public function setUp(): void + { + $this->rest = new REST(); + $this->request = new Request('GET', '/'); + } + + public function testDecodeResponse() + { + $client = $this->getClient(); + $response = new Response(204); + $decoded = $this->rest->decodeHttpResponse($response, $this->request); + $this->assertEquals($response, $decoded); + + foreach ([200, 201] as $code) { + $headers = ['foo', 'bar']; + $stream = Psr7\Utils::streamFor('{"a": 1}'); + $response = new Response($code, $headers, $stream); + + $decoded = $this->rest->decodeHttpResponse($response, $this->request); + $this->assertEquals('{"a": 1}', (string) $decoded->getBody()); + } + } + + public function testDecodeMediaResponse() + { + $client = $this->getClient(); + + $request = new Request('GET', 'http://www.example.com/?alt=media'); + $headers = []; + $stream = Psr7\Utils::streamFor('thisisnotvalidjson'); + $response = new Response(200, $headers, $stream); + + $decoded = $this->rest->decodeHttpResponse($response, $request); + $this->assertEquals('thisisnotvalidjson', (string) $decoded->getBody()); + } + + + public function testDecode500ResponseThrowsException() + { + $this->expectException(ServiceException::class); + $response = new Response(500); + $this->rest->decodeHttpResponse($response, $this->request); + } + + public function testExceptionResponse() + { + $this->expectException(ServiceException::class); + $http = new GuzzleClient(); + + $request = new Request('GET', 'http://httpbin.org/status/500'); + $response = $this->rest->doExecute($http, $request); + } + + public function testDecodeEmptyResponse() + { + $stream = Psr7\Utils::streamFor('{}'); + $response = new Response(200, [], $stream); + $decoded = $this->rest->decodeHttpResponse($response, $this->request); + $this->assertEquals('{}', (string) $decoded->getBody()); + } + + public function testBadErrorFormatting() + { + $this->expectException(ServiceException::class); + $stream = Psr7\Utils::streamFor( + '{ + "error": { + "code": 500, + "message": null + } + }' + ); + $response = new Response(500, [], $stream); + $this->rest->decodeHttpResponse($response, $this->request); + } + + public function tesProperErrorFormatting() + { + $this->expectException(ServiceException::class); + $stream = Psr7\Utils::streamFor( + '{ + error: { + errors: [ + { + "domain": "global", + "reason": "authError", + "message": "Invalid Credentials", + "locationType": "header", + "location": "Authorization", + } + ], + "code": 401, + "message": "Invalid Credentials" + } + }' + ); + $response = new Response(401, [], $stream); + $this->rest->decodeHttpResponse($response, $this->request); + } + + public function testNotJson404Error() + { + $this->expectException(ServiceException::class); + $stream = Psr7\Utils::streamFor('Not Found'); + $response = new Response(404, [], $stream); + $this->rest->decodeHttpResponse($response, $this->request); } - } - - public function testDecodeMediaResponse() - { - $client = $this->getClient(); - - $request = new Request('GET', 'http://www.example.com/?alt=media'); - $headers = array(); - $stream = Psr7\stream_for('thisisnotvalidjson'); - $response = new Response(200, $headers, $stream); - - $decoded = $this->rest->decodeHttpResponse($response, $request); - $this->assertEquals('thisisnotvalidjson', (string) $decoded->getBody()); - } - - - /** @expectedException Google_Service_Exception */ - public function testDecode500ResponseThrowsException() - { - $response = new Response(500); - $this->rest->decodeHttpResponse($response, $this->request); - } - - /** @expectedException Google_Service_Exception */ - public function testExceptionResponse() - { - $http = new GuzzleHttp\Client(); - - $request = new Request('GET', 'http://httpbin.org/status/500'); - $response = $this->rest->doExecute($http, $request); - } - - public function testDecodeEmptyResponse() - { - $stream = Psr7\stream_for('{}'); - $response = new Response(200, array(), $stream); - $decoded = $this->rest->decodeHttpResponse($response, $this->request); - $this->assertEquals('{}', (string) $decoded->getBody()); - } - - /** - * @expectedException Google_Service_Exception - */ - public function testBadErrorFormatting() - { - $stream = Psr7\stream_for( - '{ - "error": { - "code": 500, - "message": null - } - }' - ); - $response = new Response(500, array(), $stream); - $this->rest->decodeHttpResponse($response, $this->request); - } - - /** - * @expectedException Google_Service_Exception - */ - public function tesProperErrorFormatting() - { - $stream = Psr7\stream_for( - '{ - error: { - errors: [ - { - "domain": "global", - "reason": "authError", - "message": "Invalid Credentials", - "locationType": "header", - "location": "Authorization", - } - ], - "code": 401, - "message": "Invalid Credentials" - }' - ); - $response = new Response(401, array(), $stream); - $this->rest->decodeHttpResponse($response, $this->request); - } - - /** - * @expectedException Google_Service_Exception - */ - public function testNotJson404Error() - { - $stream = Psr7\stream_for('Not Found'); - $response = new Response(404, array(), $stream); - $this->rest->decodeHttpResponse($response, $this->request); - } } diff --git a/tests/Google/ModelTest.php b/tests/Google/ModelTest.php index ec8465773..77d35e09e 100644 --- a/tests/Google/ModelTest.php +++ b/tests/Google/ModelTest.php @@ -18,9 +18,17 @@ * under the License. */ -class Google_ModelTest extends BaseTest +namespace Google\Tests; + +use Google\Model; +use Google\Service\AdExchangeBuyer; +use Google\Service\Calendar; +use Google\Service\Dfareporting; +use Google\Service\Drive; + +class ModelTest extends BaseTest { - private $calendarData = '{ + private $calendarData = '{ "kind": "calendar#event", "etag": "\"-kteSF26GsdKQ5bfmcd4H3_-u3g/MTE0NTUyNTAxOTk0MjAwMA\"", "id": "1234566", @@ -53,136 +61,229 @@ class Google_ModelTest extends BaseTest } }'; - public function testIntentionalNulls() - { - $data = json_decode($this->calendarData, true); - $event = new Google_Service_Calendar_Event($data); - $obj = json_decode(json_encode($event->toSimpleObject()), true); - $this->assertArrayHasKey('date', $obj['start']); - $this->assertArrayNotHasKey('dateTime', $obj['start']); - $date = new Google_Service_Calendar_EventDateTime(); - $date->setDate(Google_Model::NULL_VALUE); - $event->setStart($date); - $obj = json_decode(json_encode($event->toSimpleObject()), true); - $this->assertNull($obj['start']['date']); - $this->assertArrayHasKey('date', $obj['start']); - $this->assertArrayNotHasKey('dateTime', $obj['start']); - } - public function testModelMutation() - { - $data = json_decode($this->calendarData, true); - $event = new Google_Service_Calendar_Event($data); - $date = new Google_Service_Calendar_EventDateTime(); - date_default_timezone_set('UTC'); - $dateString = Date("c"); - $summary = "hello"; - $date->setDate($dateString); - $event->setStart($date); - $event->setEnd($date); - $event->setSummary($summary); - $simpleEvent = $event->toSimpleObject(); - $this->assertEquals($dateString, $simpleEvent->start->date); - $this->assertEquals($dateString, $simpleEvent->end->date); - $this->assertEquals($summary, $simpleEvent->summary); - - $event2 = new Google_Service_Calendar_Event(); - $this->assertNull($event2->getStart()); - } - - public function testVariantTypes() - { - $file = new Google_Service_Drive_DriveFile(); - $metadata = new Google_Service_Drive_DriveFileImageMediaMetadata(); - $metadata->setCameraMake('Pokémon Snap'); - $file->setImageMediaMetadata($metadata); - $data = json_decode(json_encode($file->toSimpleObject()), true); - $this->assertEquals('Pokémon Snap', $data['imageMediaMetadata']['cameraMake']); - } - - public function testOddMappingNames() - { - $creative = new Google_Service_AdExchangeBuyer_Creative(); - $creative->setAccountId('12345'); - $creative->setBuyerCreativeId('12345'); - $creative->setAdvertiserName('Hi'); - $creative->setHTMLSnippet("

Foo!

"); - $creative->setClickThroughUrl(array('http://somedomain.com/')); - $creative->setWidth(100); - $creative->setHeight(100); - $data = json_decode(json_encode($creative->toSimpleObject()), true); - $this->assertEquals($data['HTMLSnippet'], "

Foo!

"); - $this->assertEquals($data['width'], 100); - $this->assertEquals($data['height'], 100); - $this->assertEquals($data['accountId'], "12345"); - } - - public function testJsonStructure() - { - $model = new Google_Model(); - $model->publicA = "This is a string"; - $model2 = new Google_Model(); - $model2->publicC = 12345; - $model2->publicD = null; - $model->publicB = $model2; - $model3 = new Google_Model(); - $model3->publicE = 54321; - $model3->publicF = null; - $model->publicG = array($model3, "hello", false); - $model->publicH = false; - $model->publicI = 0; - $string = json_encode($model->toSimpleObject()); - $data = json_decode($string, true); - $this->assertEquals(12345, $data['publicB']['publicC']); - $this->assertEquals("This is a string", $data['publicA']); - $this->assertArrayNotHasKey("publicD", $data['publicB']); - $this->assertArrayHasKey("publicE", $data['publicG'][0]); - $this->assertArrayNotHasKey("publicF", $data['publicG'][0]); - $this->assertEquals("hello", $data['publicG'][1]); - $this->assertEquals(false, $data['publicG'][2]); - $this->assertArrayNotHasKey("data", $data); - $this->assertEquals(false, $data['publicH']); - $this->assertEquals(0, $data['publicI']); - } - - public function testIssetPropertyOnModel() - { - $model = new Google_Model(); - $model['foo'] = 'bar'; - $this->assertTrue(isset($model->foo)); - } - - public function testUnsetPropertyOnModel() - { - $model = new Google_Model(); - $model['foo'] = 'bar'; - unset($model->foo); - $this->assertFalse(isset($model->foo)); - } - - public function testCollection() - { - $data = json_decode( - '{ - "kind": "calendar#events", - "id": "1234566", - "etag": "abcdef", - "totalItems": 4, - "items": [ - {"id": 1}, - {"id": 2}, - {"id": 3}, - {"id": 4} - ] - }', - true - ); - $collection = new Google_Service_Calendar_Events($data); - $this->assertEquals(4, count($collection)); - $count = 0; - foreach ($collection as $col) { - $count++; + public function testIntentionalNulls() + { + $data = json_decode($this->calendarData, true); + $event = new Calendar\Event($data); + $obj = json_decode(json_encode($event->toSimpleObject()), true); + $this->assertArrayHasKey('date', $obj['start']); + $this->assertArrayNotHasKey('dateTime', $obj['start']); + $date = new Calendar\EventDateTime(); + $date->setDate(Model::NULL_VALUE); + $event->setStart($date); + $obj = json_decode(json_encode($event->toSimpleObject()), true); + $this->assertNull($obj['start']['date']); + $this->assertArrayHasKey('date', $obj['start']); + $this->assertArrayNotHasKey('dateTime', $obj['start']); + } + public function testModelMutation() + { + $data = json_decode($this->calendarData, true); + $event = new Calendar\Event($data); + $date = new Calendar\EventDateTime(); + date_default_timezone_set('UTC'); + $dateString = Date("c"); + $summary = "hello"; + $date->setDate($dateString); + $event->setStart($date); + $event->setEnd($date); + $event->setSummary($summary); + $simpleEvent = $event->toSimpleObject(); + $this->assertEquals($dateString, $simpleEvent->start->date); + $this->assertEquals($dateString, $simpleEvent->end->date); + $this->assertEquals($summary, $simpleEvent->summary); + + $event2 = new Calendar\Event(); + $this->assertNull($event2->getStart()); + } + + public function testVariantTypes() + { + $file = new Drive\DriveFile(); + $metadata = new Drive\DriveFileImageMediaMetadata(); + $metadata->setCameraMake('Pokémon Snap'); + $file->setImageMediaMetadata($metadata); + $data = json_decode(json_encode($file->toSimpleObject()), true); + $this->assertEquals('Pokémon Snap', $data['imageMediaMetadata']['cameraMake']); + } + + public function testOddMappingNames() + { + $creative = new AdExchangeBuyer\Creative(); + $creative->setAccountId('12345'); + $creative->setBuyerCreativeId('12345'); + $creative->setAdvertiserName('Hi'); + $creative->setHTMLSnippet("

Foo!

"); + $creative->setClickThroughUrl(['http://somedomain.com/']); + $creative->setWidth(100); + $creative->setHeight(100); + $data = json_decode(json_encode($creative->toSimpleObject()), true); + $this->assertEquals($data['HTMLSnippet'], "

Foo!

"); + $this->assertEquals($data['width'], 100); + $this->assertEquals($data['height'], 100); + $this->assertEquals($data['accountId'], "12345"); + } + + public function testJsonStructure() + { + $model = new Model(); + $model->publicA = "This is a string"; + $model2 = new Model(); + $model2->publicC = 12345; + $model2->publicD = null; + $model->publicB = $model2; + $model3 = new Model(); + $model3->publicE = 54321; + $model3->publicF = null; + $model->publicG = [$model3, "hello", false]; + $model->publicH = false; + $model->publicI = 0; + $string = json_encode($model->toSimpleObject()); + $data = json_decode($string, true); + $this->assertEquals(12345, $data['publicB']['publicC']); + $this->assertEquals("This is a string", $data['publicA']); + $this->assertArrayNotHasKey("publicD", $data['publicB']); + $this->assertArrayHasKey("publicE", $data['publicG'][0]); + $this->assertArrayNotHasKey("publicF", $data['publicG'][0]); + $this->assertEquals("hello", $data['publicG'][1]); + $this->assertFalse($data['publicG'][2]); + $this->assertArrayNotHasKey("data", $data); + $this->assertFalse($data['publicH']); + $this->assertEquals(0, $data['publicI']); + } + + public function testIssetPropertyOnModel() + { + $model = new Model(); + $model['foo'] = 'bar'; + $this->assertTrue(isset($model->foo)); + } + + public function testUnsetPropertyOnModel() + { + $model = new Model(); + $model['foo'] = 'bar'; + unset($model->foo); + $this->assertFalse(isset($model->foo)); + } + + public function testCollectionWithItemsFromConstructor() + { + $data = json_decode( + '{ + "kind": "calendar#events", + "id": "1234566", + "etag": "abcdef", + "totalItems": 4, + "items": [ + {"id": 1}, + {"id": 2}, + {"id": 3}, + {"id": 4} + ] + }', + true + ); + $collection = new Calendar\Events($data); + $this->assertCount(4, $collection); + $count = 0; + foreach ($collection as $col) { + $count++; + } + $this->assertEquals(4, $count); + $this->assertEquals(1, $collection[0]->id); + } + + public function testCollectionWithItemsFromSetter() + { + $data = json_decode( + '{ + "kind": "calendar#events", + "id": "1234566", + "etag": "abcdef", + "totalItems": 4 + }', + true + ); + $collection = new Calendar\Events($data); + $collection->setItems([ + new Calendar\Event(['id' => 1]), + new Calendar\Event(['id' => 2]), + new Calendar\Event(['id' => 3]), + new Calendar\Event(['id' => 4]), + ]); + $this->assertCount(4, $collection); + $count = 0; + foreach ($collection as $col) { + $count++; + } + $this->assertEquals(4, $count); + $this->assertEquals(1, $collection[0]->id); + } + + public function testMapDataType() + { + $data = json_decode( + '{ + "calendar": { + "regular": { "background": "#FFF", "foreground": "#000" }, + "inverted": { "background": "#000", "foreground": "#FFF" } + } + }', + true + ); + $collection = new Calendar\Colors($data); + $this->assertCount(2, $collection->calendar); + $this->assertTrue(isset($collection->calendar['regular'])); + $this->assertTrue(isset($collection->calendar['inverted'])); + $this->assertInstanceOf(Calendar\ColorDefinition::class, $collection->calendar['regular']); + $this->assertEquals('#FFF', $collection->calendar['regular']->getBackground()); + $this->assertEquals('#FFF', $collection->calendar['inverted']->getForeground()); + } + + public function testPassingInstanceInConstructor() + { + $creator = new Calendar\EventCreator(); + $creator->setDisplayName('Brent Shaffer'); + $data = [ + "creator" => $creator + ]; + $event = new Calendar\Event($data); + $this->assertInstanceOf(Calendar\EventCreator::class, $event->getCreator()); + $this->assertEquals('Brent Shaffer', $event->creator->getDisplayName()); + } + + public function testPassingInstanceInConstructorForMap() + { + $regular = new Calendar\ColorDefinition(); + $regular->setBackground('#FFF'); + $regular->setForeground('#000'); + $data = [ + "calendar" => [ + "regular" => $regular, + "inverted" => [ "background" => "#000", "foreground" => "#FFF" ], + ] + ]; + $collection = new Calendar\Colors($data); + $this->assertCount(2, $collection->calendar); + $this->assertTrue(isset($collection->calendar['regular'])); + $this->assertTrue(isset($collection->calendar['inverted'])); + $this->assertInstanceOf(Calendar\ColorDefinition::class, $collection->calendar['regular']); + $this->assertEquals('#FFF', $collection->calendar['regular']->getBackground()); + $this->assertEquals('#FFF', $collection->calendar['inverted']->getForeground()); + } + + /** + * @see https://github.com/google/google-api-php-client/issues/1308 + */ + public function testKeyTypePropertyConflict() + { + $data = [ + "duration" => 0, + "durationType" => "unknown", + ]; + $creativeAsset = new Dfareporting\CreativeAsset($data); + $this->assertEquals(0, $creativeAsset->getDuration()); + $this->assertEquals('unknown', $creativeAsset->getDurationType()); } - $this->assertEquals(4, $count); - $this->assertEquals(1, $collection[0]->id); - } } diff --git a/tests/Google/Service/AdSenseTest.php b/tests/Google/Service/AdSenseTest.php index 14dbb3b1e..234075aea 100644 --- a/tests/Google/Service/AdSenseTest.php +++ b/tests/Google/Service/AdSenseTest.php @@ -15,473 +15,479 @@ * limitations under the License. */ -class Google_Service_AdSenseTest extends BaseTest +namespace Google\Tests\Service; + +use Google\Service\AdSense; +use Google\Tests\BaseTest; + +class AdSenseTest extends BaseTest { - public $adsense; - public function setUp() - { - $this->checkToken(); - $this->adsense = new Google_Service_AdSense($this->getClient()); - } - - public function testAccountsList() - { - $accounts = $this->adsense->accounts->listAccounts(); - $this->assertArrayHasKey('kind', $accounts); - $this->assertEquals($accounts['kind'], 'adsense#accounts'); - $account = $this->getRandomElementFromArray($accounts['items']); - $this->checkAccountElement($account); - } - - /** - * @depends testAccountsList - */ - public function testAccountsGet() - { - $accounts = $this->adsense->accounts->listAccounts(); - $account = $this->getRandomElementFromArray($accounts['items']); - $retrievedAccount = $this->adsense->accounts->get($account['id']); - $this->checkAccountElement($retrievedAccount); - } - - /** - * @depends testAccountsList - */ - public function testAccountsReportGenerate() - { - $startDate = '2011-01-01'; - $endDate = '2011-01-31'; - $optParams = $this->getReportOptParams(); - $accounts = $this->adsense->accounts->listAccounts(); - $accountId = $accounts['items'][0]['id']; - $report = $this->adsense->accounts_reports->generate( - $accountId, - $startDate, - $endDate, - $optParams - ); - $this->checkReport($report); - } - - /** - * @depends testAccountsList - */ - public function testAccountsAdClientsList() - { - $accounts = $this->adsense->accounts->listAccounts(); - $account = $this->getRandomElementFromArray($accounts['items']); - $adClients = - $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); - $this->checkAdClientsCollection($adClients); - } - - /** - * @depends testAccountsList - * @depends testAccountsAdClientsList - */ - public function testAccountsAdUnitsList() - { - $accounts = $this->adsense->accounts->listAccounts(); - foreach ($accounts['items'] as $account) { - $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); - foreach ($adClients['items'] as $adClient) { - $adUnits = $this->adsense->accounts_adunits->listAccountsAdunits( - $account['id'], - $adClient['id'] - ); - $this->checkAdUnitsCollection($adUnits); - break 2; - } + public $adsense; + public function setUp(): void + { + $this->markTestSkipped('Thesse tests need to be fixed'); + $this->checkToken(); + $this->adsense = new AdSense($this->getClient()); } - } - /** - * @depends testAccountsList - * @depends testAccountsAdClientsList - */ - public function testAccountsAdUnitsGet() - { - $accounts = $this->adsense->accounts->listAccounts(); - foreach ($accounts['items'] as $account) { - $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); - foreach ($adClients['items'] as $adClient) { - $adUnits = $this->adsense->accounts_adunits->listAccountsAdunits( - $account['id'], - $adClient['id'] - ); - if (array_key_exists('items', $adUnits)) { - $adUnit = $this->getRandomElementFromArray($adUnits['items']); - $this->checkAdUnitElement($adUnit); - break 2; - } - } + public function testAccountsList() + { + $accounts = $this->adsense->accounts->listAccounts(); + $this->assertArrayHasKey('kind', $accounts); + $this->assertEquals($accounts['kind'], 'adsense#accounts'); + $account = $this->getRandomElementFromArray($accounts['items']); + $this->checkAccountElement($account); } - } - /** - * @depends testAccountsList - * @depends testAccountsAdClientsList - */ - public function testAccountsCustomChannelsList() - { - $accounts = $this->adsense->accounts->listAccounts(); - foreach ($accounts['items'] as $account) { - $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); - foreach ($adClients['items'] as $adClient) { - $customChannels = $this->adsense->accounts_customchannels - ->listAccountsCustomchannels($account['id'], $adClient['id']); - $this->checkCustomChannelsCollection($customChannels); - break 2; - } + /** + * @depends testAccountsList + */ + public function testAccountsGet() + { + $accounts = $this->adsense->accounts->listAccounts(); + $account = $this->getRandomElementFromArray($accounts['items']); + $retrievedAccount = $this->adsense->accounts->get($account['id']); + $this->checkAccountElement($retrievedAccount); } - } - /** - * @depends testAccountsList - * @depends testAccountsAdClientsList - */ - public function testAccountsCustomChannelsGet() - { - $accounts = $this->adsense->accounts->listAccounts(); - foreach ($accounts['items'] as $account) { - $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); - foreach ($adClients['items'] as $adClient) { - $customChannels = - $this->adsense->accounts_customchannels->listAccountsCustomchannels( - $account['id'], - $adClient['id'] - ); - if (array_key_exists('items', $customChannels)) { - $customChannel = - $this->getRandomElementFromArray($customChannels['items']); - $this->checkCustomChannelElement($customChannel); - break 2; - } - } + /** + * @depends testAccountsList + */ + public function testAccountsReportGenerate() + { + $startDate = '2011-01-01'; + $endDate = '2011-01-31'; + $optParams = $this->getReportOptParams(); + $accounts = $this->adsense->accounts->listAccounts(); + $accountId = $accounts['items'][0]['id']; + $report = $this->adsense->accounts_reports->generate( + $accountId, + $startDate, + $endDate, + $optParams + ); + $this->checkReport($report); } - } - /** - * @depends testAccountsList - * @depends testAccountsAdClientsList - */ - public function testAccountsUrlChannelsList() - { - $accounts = $this->adsense->accounts->listAccounts(); - foreach ($accounts['items'] as $account) { - $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); - foreach ($adClients['items'] as $adClient) { - $urlChannels = - $this->adsense->accounts_urlchannels->listAccountsUrlchannels( - $account['id'], - $adClient['id'] - ); - $this->checkUrlChannelsCollection($urlChannels); - break 2; - } + /** + * @depends testAccountsList + */ + public function testAccountsAdClientsList() + { + $accounts = $this->adsense->accounts->listAccounts(); + $account = $this->getRandomElementFromArray($accounts['items']); + $adClients = + $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); + $this->checkAdClientsCollection($adClients); } - } - /** - * @depends testAccountsList - * @depends testAccountsAdClientsList - * @depends testAccountsAdUnitsList - */ - public function testAccountsAdUnitsCustomChannelsList() - { - $accounts = $this->adsense->accounts->listAccounts(); - foreach ($accounts['items'] as $account) { - $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); - foreach ($adClients['items'] as $adClient) { - $adUnits = - $this->adsense->accounts_adunits->listAccountsAdunits($account['id'], $adClient['id']); - if (array_key_exists('items', $adUnits)) { - foreach ($adUnits['items'] as $adUnit) { - $customChannels = - $this->adsense->accounts_adunits_customchannels->listAccountsAdunitsCustomchannels( + /** + * @depends testAccountsList + * @depends testAccountsAdClientsList + */ + public function testAccountsAdUnitsList() + { + $accounts = $this->adsense->accounts->listAccounts(); + foreach ($accounts['items'] as $account) { + $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); + foreach ($adClients['items'] as $adClient) { + $adUnits = $this->adsense->accounts_adunits->listAccountsAdunits( $account['id'], - $adClient['id'], - $adUnit['id'] + $adClient['id'] ); - $this->checkCustomChannelsCollection($customChannels); - // it's too expensive to go through each, if one is correct good - break 3; - } + $this->checkAdUnitsCollection($adUnits); + break 2; + } } - } } - } - /** - * @depends testAccountsList - * @depends testAccountsAdClientsList - * @depends testAccountsCustomChannelsList - */ - public function testAccountsCustomChannelsAdUnitsList() - { - $accounts = $this->adsense->accounts->listAccounts(); - foreach ($accounts['items'] as $account) { - $adClients = - $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); - foreach ($adClients['items'] as $adClient) { - $customChannels = - $this->adsense->accounts_customchannels->listAccountsCustomchannels( - $account['id'], - $adClient['id'] - ); - if (array_key_exists('items', $customChannels)) { - foreach ($customChannels['items'] as $customChannel) { - $adUnits = - $this->adsense->accounts_customchannels_adunits->listAccountsCustomchannelsAdunits( + /** + * @depends testAccountsList + * @depends testAccountsAdClientsList + */ + public function testAccountsAdUnitsGet() + { + $accounts = $this->adsense->accounts->listAccounts(); + foreach ($accounts['items'] as $account) { + $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); + foreach ($adClients['items'] as $adClient) { + $adUnits = $this->adsense->accounts_adunits->listAccountsAdunits( $account['id'], - $adClient['id'], - $customChannel['id'] + $adClient['id'] ); - $this->checkAdUnitsCollection($adUnits); - // it's too expensive to go through each, if one is correct good - break 3; - } + if (array_key_exists('items', $adUnits)) { + $adUnit = $this->getRandomElementFromArray($adUnits['items']); + $this->checkAdUnitElement($adUnit); + break 2; + } + } + } + } + + /** + * @depends testAccountsList + * @depends testAccountsAdClientsList + */ + public function testAccountsCustomChannelsList() + { + $accounts = $this->adsense->accounts->listAccounts(); + foreach ($accounts['items'] as $account) { + $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); + foreach ($adClients['items'] as $adClient) { + $customChannels = $this->adsense->accounts_customchannels + ->listAccountsCustomchannels($account['id'], $adClient['id']); + $this->checkCustomChannelsCollection($customChannels); + break 2; + } } - } } - } - public function testAdClientsList() - { - $adClients = $this->adsense->adclients->listAdclients(); - $this->checkAdClientsCollection($adClients); - } + /** + * @depends testAccountsList + * @depends testAccountsAdClientsList + */ + public function testAccountsCustomChannelsGet() + { + $accounts = $this->adsense->accounts->listAccounts(); + foreach ($accounts['items'] as $account) { + $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); + foreach ($adClients['items'] as $adClient) { + $customChannels = + $this->adsense->accounts_customchannels->listAccountsCustomchannels( + $account['id'], + $adClient['id'] + ); + if (array_key_exists('items', $customChannels)) { + $customChannel = + $this->getRandomElementFromArray($customChannels['items']); + $this->checkCustomChannelElement($customChannel); + break 2; + } + } + } + } + + /** + * @depends testAccountsList + * @depends testAccountsAdClientsList + */ + public function testAccountsUrlChannelsList() + { + $accounts = $this->adsense->accounts->listAccounts(); + foreach ($accounts['items'] as $account) { + $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); + foreach ($adClients['items'] as $adClient) { + $urlChannels = + $this->adsense->accounts_urlchannels->listAccountsUrlchannels( + $account['id'], + $adClient['id'] + ); + $this->checkUrlChannelsCollection($urlChannels); + break 2; + } + } + } - /** + /** + * @depends testAccountsList + * @depends testAccountsAdClientsList + * @depends testAccountsAdUnitsList + */ + public function testAccountsAdUnitsCustomChannelsList() + { + $accounts = $this->adsense->accounts->listAccounts(); + foreach ($accounts['items'] as $account) { + $adClients = $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); + foreach ($adClients['items'] as $adClient) { + $adUnits = + $this->adsense->accounts_adunits->listAccountsAdunits($account['id'], $adClient['id']); + if (array_key_exists('items', $adUnits)) { + foreach ($adUnits['items'] as $adUnit) { + $customChannels = + $this->adsense->accounts_adunits_customchannels->listAccountsAdunitsCustomchannels( + $account['id'], + $adClient['id'], + $adUnit['id'] + ); + $this->checkCustomChannelsCollection($customChannels); + // it's too expensive to go through each, if one is correct good + break 3; + } + } + } + } + } + + /** + * @depends testAccountsList + * @depends testAccountsAdClientsList + * @depends testAccountsCustomChannelsList + */ + public function testAccountsCustomChannelsAdUnitsList() + { + $accounts = $this->adsense->accounts->listAccounts(); + foreach ($accounts['items'] as $account) { + $adClients = + $this->adsense->accounts_adclients->listAccountsAdclients($account['id']); + foreach ($adClients['items'] as $adClient) { + $customChannels = + $this->adsense->accounts_customchannels->listAccountsCustomchannels( + $account['id'], + $adClient['id'] + ); + if (array_key_exists('items', $customChannels)) { + foreach ($customChannels['items'] as $customChannel) { + $adUnits = + $this->adsense->accounts_customchannels_adunits->listAccountsCustomchannelsAdunits( + $account['id'], + $adClient['id'], + $customChannel['id'] + ); + $this->checkAdUnitsCollection($adUnits); + // it's too expensive to go through each, if one is correct good + break 3; + } + } + } + } + } + + public function testAdClientsList() + { + $adClients = $this->adsense->adclients->listAdclients(); + $this->checkAdClientsCollection($adClients); + } + + /** * @depends testAdClientsList */ - public function testAdUnitsList() - { - $adClients = $this->adsense->adclients->listAdclients(); - foreach ($adClients['items'] as $adClient) { - $adUnits = $this->adsense->adunits->listAdunits($adClient['id']); - $this->checkAdUnitsCollection($adUnits); + public function testAdUnitsList() + { + $adClients = $this->adsense->adclients->listAdclients(); + foreach ($adClients['items'] as $adClient) { + $adUnits = $this->adsense->adunits->listAdunits($adClient['id']); + $this->checkAdUnitsCollection($adUnits); + } } - } - /** + /** * @depends testAdClientsList */ - public function testAdUnitsGet() - { - $adClients = $this->adsense->adclients->listAdclients(); - foreach ($adClients['items'] as $adClient) { - $adUnits = $this->adsense->adunits->listAdunits($adClient['id']); - if (array_key_exists('items', $adUnits)) { - $adUnit = $this->getRandomElementFromArray($adUnits['items']); - $this->checkAdUnitElement($adUnit); - break 1; - } + public function testAdUnitsGet() + { + $adClients = $this->adsense->adclients->listAdclients(); + foreach ($adClients['items'] as $adClient) { + $adUnits = $this->adsense->adunits->listAdunits($adClient['id']); + if (array_key_exists('items', $adUnits)) { + $adUnit = $this->getRandomElementFromArray($adUnits['items']); + $this->checkAdUnitElement($adUnit); + break 1; + } + } } - } - /** + /** * @depends testAdClientsList * @depends testAdUnitsList */ - public function testAdUnitsCustomChannelsList() - { - $adClients = $this->adsense->adclients->listAdclients(); - foreach ($adClients['items'] as $adClient) { - $adUnits = $this->adsense->adunits->listAdunits($adClient['id']); - if (array_key_exists('items', $adUnits)) { - foreach ($adUnits['items'] as $adUnit) { - $customChannels = - $this->adsense->adunits_customchannels->listAdunitsCustomchannels( - $adClient['id'], - $adUnit['id'] - ); - $this->checkCustomChannelsCollection($customChannels); - break 2; + public function testAdUnitsCustomChannelsList() + { + $adClients = $this->adsense->adclients->listAdclients(); + foreach ($adClients['items'] as $adClient) { + $adUnits = $this->adsense->adunits->listAdunits($adClient['id']); + if (array_key_exists('items', $adUnits)) { + foreach ($adUnits['items'] as $adUnit) { + $customChannels = + $this->adsense->adunits_customchannels->listAdunitsCustomchannels( + $adClient['id'], + $adUnit['id'] + ); + $this->checkCustomChannelsCollection($customChannels); + break 2; + } + } } - } } - } - /** + /** * @depends testAdClientsList */ - public function testCustomChannelsList() - { - $adClients = $this->adsense->adclients->listAdclients(); - foreach ($adClients['items'] as $adClient) { - $customChannels = - $this->adsense->customchannels->listCustomchannels($adClient['id']); - $this->checkCustomChannelsCollection($customChannels); + public function testCustomChannelsList() + { + $adClients = $this->adsense->adclients->listAdclients(); + foreach ($adClients['items'] as $adClient) { + $customChannels = + $this->adsense->customchannels->listCustomchannels($adClient['id']); + $this->checkCustomChannelsCollection($customChannels); + } } - } - /** + /** * @depends testAdClientsList */ - public function testCustomChannelsGet() - { - $adClients = $this->adsense->adclients->listAdclients(); - foreach ($adClients['items'] as $adClient) { - $customChannels = $this->adsense->customchannels->listCustomchannels($adClient['id']); - if (array_key_exists('items', $customChannels)) { - $customChannel = $this->getRandomElementFromArray($customChannels['items']); - $this->checkCustomChannelElement($customChannel); - break 1; - } + public function testCustomChannelsGet() + { + $adClients = $this->adsense->adclients->listAdclients(); + foreach ($adClients['items'] as $adClient) { + $customChannels = $this->adsense->customchannels->listCustomchannels($adClient['id']); + if (array_key_exists('items', $customChannels)) { + $customChannel = $this->getRandomElementFromArray($customChannels['items']); + $this->checkCustomChannelElement($customChannel); + break 1; + } + } } - } - /** + /** * @depends testAdClientsList * @depends testCustomChannelsList */ - public function testCustomChannelsAdUnitsList() - { - $adClients = $this->adsense->adclients->listAdclients(); - foreach ($adClients['items'] as $adClient) { - $customChannels = $this->adsense->customchannels->listCustomchannels($adClient['id']); - if (array_key_exists('items', $customChannels)) { - foreach ($customChannels['items'] as $customChannel) { - $adUnits = - $this->adsense->customchannels_adunits->listCustomchannelsAdunits( - $adClient['id'], - $customChannel['id'] - ); - $this->checkAdUnitsCollection($adUnits); - break 2; + public function testCustomChannelsAdUnitsList() + { + $adClients = $this->adsense->adclients->listAdclients(); + foreach ($adClients['items'] as $adClient) { + $customChannels = $this->adsense->customchannels->listCustomchannels($adClient['id']); + if (array_key_exists('items', $customChannels)) { + foreach ($customChannels['items'] as $customChannel) { + $adUnits = + $this->adsense->customchannels_adunits->listCustomchannelsAdunits( + $adClient['id'], + $customChannel['id'] + ); + $this->checkAdUnitsCollection($adUnits); + break 2; + } + } } - } } - } - /** + /** * @depends testAdClientsList */ - public function testUrlChannelsList() - { - $adClients = $this->adsense->adclients->listAdclients(); - foreach ($adClients['items'] as $adClient) { - $urlChannels = $this->adsense->urlchannels->listUrlchannels($adClient['id']); - $this->checkUrlChannelsCollection($urlChannels); + public function testUrlChannelsList() + { + $adClients = $this->adsense->adclients->listAdclients(); + foreach ($adClients['items'] as $adClient) { + $urlChannels = $this->adsense->urlchannels->listUrlchannels($adClient['id']); + $this->checkUrlChannelsCollection($urlChannels); + } } - } - public function testReportsGenerate() - { - if (!$this->checkToken()) { - return; + public function testReportsGenerate() + { + if (!$this->checkToken()) { + return; + } + $startDate = '2011-01-01'; + $endDate = '2011-01-31'; + $optParams = $this->getReportOptParams(); + $report = $this->adsense->reports->generate($startDate, $endDate, $optParams); + $this->checkReport($report); } - $startDate = '2011-01-01'; - $endDate = '2011-01-31'; - $optParams = $this->getReportOptParams(); - $report = $this->adsense->reports->generate($startDate, $endDate, $optParams); - $this->checkReport($report); - } - - private function checkAccountElement($account) - { - $this->assertArrayHasKey('kind', $account); - $this->assertArrayHasKey('id', $account); - $this->assertArrayHasKey('name', $account); - } - - private function checkAdClientsCollection($adClients) - { - $this->assertArrayHasKey('kind', $adClients); - $this->assertEquals($adClients['kind'], 'adsense#adClients'); - foreach ($adClients['items'] as $adClient) { - $this->assertArrayHasKey('id', $adClient); - $this->assertArrayHasKey('kind', $adClient); - $this->assertArrayHasKey('productCode', $adClient); - $this->assertArrayHasKey('supportsReporting', $adClient); + + private function checkAccountElement($account) + { + $this->assertArrayHasKey('kind', $account); + $this->assertArrayHasKey('id', $account); + $this->assertArrayHasKey('name', $account); } - } - - private function checkAdUnitsCollection($adUnits) - { - $this->assertArrayHasKey('kind', $adUnits); - $this->assertEquals($adUnits['kind'], 'adsense#adUnits'); - if (array_key_exists('items', $adUnits)) { - foreach ($adUnits['items'] as $adUnit) { - $this->checkAdUnitElement($adUnit); - } + + private function checkAdClientsCollection($adClients) + { + $this->assertArrayHasKey('kind', $adClients); + $this->assertEquals($adClients['kind'], 'adsense#adClients'); + foreach ($adClients['items'] as $adClient) { + $this->assertArrayHasKey('id', $adClient); + $this->assertArrayHasKey('kind', $adClient); + $this->assertArrayHasKey('productCode', $adClient); + $this->assertArrayHasKey('supportsReporting', $adClient); + } } - } - - private function checkAdUnitElement($adUnit) - { - $this->assertArrayHasKey('code', $adUnit); - $this->assertArrayHasKey('id', $adUnit); - $this->assertArrayHasKey('kind', $adUnit); - $this->assertArrayHasKey('name', $adUnit); - $this->assertArrayHasKey('status', $adUnit); - } - - private function checkCustomChannelsCollection($customChannels) - { - $this->assertArrayHasKey('kind', $customChannels); - $this->assertEquals($customChannels['kind'], 'adsense#customChannels'); - if (array_key_exists('items', $customChannels)) { - foreach ($customChannels['items'] as $customChannel) { - $this->checkCustomChannelElement($customChannel); - } + + private function checkAdUnitsCollection($adUnits) + { + $this->assertArrayHasKey('kind', $adUnits); + $this->assertEquals($adUnits['kind'], 'adsense#adUnits'); + if (array_key_exists('items', $adUnits)) { + foreach ($adUnits['items'] as $adUnit) { + $this->checkAdUnitElement($adUnit); + } + } } - } - - private function checkCustomChannelElement($customChannel) - { - $this->assertArrayHasKey('kind', $customChannel); - $this->assertArrayHasKey('id', $customChannel); - $this->assertArrayHasKey('code', $customChannel); - $this->assertArrayHasKey('name', $customChannel); - } - - private function checkUrlChannelsCollection($urlChannels) - { - $this->assertArrayHasKey('kind', $urlChannels); - $this->assertEquals($urlChannels['kind'], 'adsense#urlChannels'); - if (array_key_exists('items', $urlChannels)) { - foreach ($urlChannels['items'] as $urlChannel) { - $this->assertArrayHasKey('kind', $urlChannel); - $this->assertArrayHasKey('id', $urlChannel); - $this->assertArrayHasKey('urlPattern', $urlChannel); - } + + private function checkAdUnitElement($adUnit) + { + $this->assertArrayHasKey('code', $adUnit); + $this->assertArrayHasKey('id', $adUnit); + $this->assertArrayHasKey('kind', $adUnit); + $this->assertArrayHasKey('name', $adUnit); + $this->assertArrayHasKey('status', $adUnit); } - } - - private function getReportOptParams() - { - return array( - 'metric' => array('PAGE_VIEWS', 'AD_REQUESTS'), - 'dimension' => array ('DATE', 'AD_CLIENT_ID'), - 'sort' => array('DATE'), - 'filter' => array('COUNTRY_NAME==United States'), - ); - } - - private function checkReport($report) - { - $this->assertArrayHasKey('kind', $report); - $this->assertEquals($report['kind'], 'adsense#report'); - $this->assertArrayHasKey('totalMatchedRows', $report); - $this->assertGreaterThan(0, count($report->headers)); - foreach ($report['headers'] as $header) { - $this->assertArrayHasKey('name', $header); - $this->assertArrayHasKey('type', $header); + + private function checkCustomChannelsCollection($customChannels) + { + $this->assertArrayHasKey('kind', $customChannels); + $this->assertEquals($customChannels['kind'], 'adsense#customChannels'); + if (array_key_exists('items', $customChannels)) { + foreach ($customChannels['items'] as $customChannel) { + $this->checkCustomChannelElement($customChannel); + } + } } - if (array_key_exists('items', $report)) { - foreach ($report['items'] as $row) { - $this->assertCount(4, $row); - } + + private function checkCustomChannelElement($customChannel) + { + $this->assertArrayHasKey('kind', $customChannel); + $this->assertArrayHasKey('id', $customChannel); + $this->assertArrayHasKey('code', $customChannel); + $this->assertArrayHasKey('name', $customChannel); + } + + private function checkUrlChannelsCollection($urlChannels) + { + $this->assertArrayHasKey('kind', $urlChannels); + $this->assertEquals($urlChannels['kind'], 'adsense#urlChannels'); + if (array_key_exists('items', $urlChannels)) { + foreach ($urlChannels['items'] as $urlChannel) { + $this->assertArrayHasKey('kind', $urlChannel); + $this->assertArrayHasKey('id', $urlChannel); + $this->assertArrayHasKey('urlPattern', $urlChannel); + } + } + } + + private function getReportOptParams() + { + return [ + 'metric' => ['PAGE_VIEWS', 'AD_REQUESTS'], + 'dimension' => ['DATE', 'AD_CLIENT_ID'], + 'sort' => ['DATE'], + 'filter' => ['COUNTRY_NAME==United States'], + ]; + } + + private function checkReport($report) + { + $this->assertArrayHasKey('kind', $report); + $this->assertEquals($report['kind'], 'adsense#report'); + $this->assertArrayHasKey('totalMatchedRows', $report); + $this->assertGreaterThan(0, count($report->headers)); + foreach ($report['headers'] as $header) { + $this->assertArrayHasKey('name', $header); + $this->assertArrayHasKey('type', $header); + } + if (array_key_exists('items', $report)) { + foreach ($report['items'] as $row) { + $this->assertCount(4, $row); + } + } + $this->assertArrayHasKey('totals', $report); + $this->assertArrayHasKey('averages', $report); + } + + private function getRandomElementFromArray($array) + { + $elementKey = array_rand($array); + return $array[$elementKey]; } - $this->assertArrayHasKey('totals', $report); - $this->assertArrayHasKey('averages', $report); - } - - private function getRandomElementFromArray($array) - { - $elementKey = array_rand($array); - return $array[$elementKey]; - } } diff --git a/tests/Google/Service/PagespeedonlineTest.php b/tests/Google/Service/PagespeedonlineTest.php deleted file mode 100644 index e63a8780c..000000000 --- a/tests/Google/Service/PagespeedonlineTest.php +++ /dev/null @@ -1,34 +0,0 @@ -checkToken(); - $service = new Google_Service_Pagespeedonline($this->getClient()); - $psapi = $service->pagespeedapi; - $result = $psapi->runpagespeed('http://code.google.com/'); - $this->assertArrayHasKey('kind', $result); - $this->assertArrayHasKey('id', $result); - $this->assertArrayHasKey('responseCode', $result); - $this->assertArrayHasKey('title', $result); - $this->assertArrayHasKey('score', $result->ruleGroups['SPEED']); - $this->assertInstanceOf('Google_Service_Pagespeedonline_ResultPageStats', $result->pageStats); - $this->assertArrayHasKey('minor', $result['version']); - } -} diff --git a/tests/Google/Service/PlusTest.php b/tests/Google/Service/PlusTest.php deleted file mode 100644 index edd012f52..000000000 --- a/tests/Google/Service/PlusTest.php +++ /dev/null @@ -1,69 +0,0 @@ -checkToken(); - $this->plus = new Google_Service_Plus($this->getClient()); - } - - public function testGetPerson() - { - $person = $this->plus->people->get("118051310819094153327"); - $this->assertArrayHasKey('kind', $person); - $this->assertArrayHasKey('displayName', $person); - $this->assertArrayHasKey('gender', $person); - $this->assertArrayHasKey('id', $person); - } - - public function testListActivities() - { - $activities = $this->plus->activities - ->listActivities("118051310819094153327", "public"); - - $this->assertArrayHasKey('kind', $activities); - $this->assertGreaterThan(0, count($activities)); - - // Test a variety of access methods. - $this->assertItem($activities['items'][0]); - $this->assertItem($activities[0]); - foreach ($activities as $item) { - $this->assertItem($item); - break; - } - - // Test deeper type transformations - $this->assertGreaterThan(0, strlen($activities[0]->actor->displayName)); - } - - public function assertItem($item) - { - // assertArrayHasKey uses array_key_exists, which is not great: - // it doesn't understand SPL ArrayAccess - $this->assertTrue(isset($item['actor'])); - $this->assertInstanceOf('Google_Service_Plus_ActivityActor', $item->actor); - $this->assertTrue(isset($item['actor']['displayName'])); - $this->assertTrue(isset($item['actor']->url)); - $this->assertTrue(isset($item['object'])); - $this->assertTrue(isset($item['access'])); - $this->assertTrue(isset($item['provider'])); - } -} diff --git a/tests/Google/Service/ResourceTest.php b/tests/Google/Service/ResourceTest.php index 5bb7d9172..ccf29a7f7 100644 --- a/tests/Google/Service/ResourceTest.php +++ b/tests/Google/Service/ResourceTest.php @@ -18,422 +18,489 @@ * under the License. */ +namespace Google\Tests\Service; + +use Google\Client; +use Google\Tests\BaseTest; +use Google\Service as GoogleService; +use Google\Service\Exception as ServiceException; +use Google\Service\Resource as GoogleResource; +use Google\Exception as GoogleException; use GuzzleHttp\Psr7; +use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use GuzzleHttp\Psr7\Stream; +use Prophecy\Argument; -class Test_Google_Service extends Google_Service +class TestService extends \Google\Service { - public function __construct(Google_Client $client) - { - parent::__construct($client); - $this->rootUrl = "https://test.example.com/"; - $this->servicePath = ""; - $this->version = "v1beta1"; - $this->serviceName = "test"; - } + public function __construct(Client $client, $rootUrl = null) + { + parent::__construct($client); + $this->rootUrl = $rootUrl ?: "https://test.example.com/"; + $this->rootUrlTemplate = $rootUrl ?: "https://test.universe_domain/"; + $this->servicePath = ""; + $this->version = "v1beta1"; + $this->serviceName = "test"; + } } -class Test_MediaType_Stream extends Stream +class ResourceTest extends BaseTest { - public $toStringCalled = false; + private $client; + private $service; - public function __toString() - { - $this->toStringCalled = true; + public function setUp(): void + { + $this->client = $this->prophesize(Client::class); - return parent::__toString(); - } -} + $logger = $this->prophesize("Monolog\Logger"); -class Google_Service_ResourceTest extends BaseTest -{ - private $client; - private $service; - private $logger; - - public function setUp() - { - $this->client = $this->getMockBuilder("Google_Client") - ->disableOriginalConstructor() - ->getMock(); - $this->logger = $this->getMockBuilder("Monolog\Logger") - ->disableOriginalConstructor() - ->getMock(); - $this->client->expects($this->any()) - ->method("getLogger") - ->will($this->returnValue($this->logger)); - $this->client->expects($this->any()) - ->method("shouldDefer") - ->will($this->returnValue(true)); - $this->client->expects($this->any()) - ->method("getHttpClient") - ->will($this->returnValue(new GuzzleHttp\Client())); - $this->service = new Test_Google_Service($this->client); - } - - public function testCallFailure() - { - $resource = new Google_Service_Resource( - $this->service, - "test", - "testResource", - array("methods" => - array( - "testMethod" => array( - "parameters" => array(), - "path" => "method/path", - "httpMethod" => "POST", - ) - ) - ) - ); - $this->setExpectedException( - "Google_Exception", - "Unknown function: test->testResource->someothermethod()" - ); - $resource->call("someothermethod", array()); - } - - public function testCall() - { - $resource = new Google_Service_Resource( - $this->service, - "test", - "testResource", - array("methods" => - array( - "testMethod" => array( - "parameters" => array(), - "path" => "method/path", - "httpMethod" => "POST", - ) - ) - ) - ); - $request = $resource->call("testMethod", array(array())); - $this->assertEquals("https://test.example.com/method/path", (string) $request->getUri()); - $this->assertEquals("POST", $request->getMethod()); - } - - public function testCallServiceDefinedRoot() - { - $this->service->rootUrl = "https://sample.example.com/"; - $resource = new Google_Service_Resource( - $this->service, - "test", - "testResource", - array("methods" => - array( - "testMethod" => array( - "parameters" => array(), - "path" => "method/path", - "httpMethod" => "POST", - ) - ) - ) - ); - $request = $resource->call("testMethod", array(array())); - $this->assertEquals("https://sample.example.com/method/path", (string) $request->getUri()); - $this->assertEquals("POST", $request->getMethod()); - } - - public function testCreateRequestUri() - { - $restPath = "/plus/{u}"; - $service = new Google_Service($this->client); - $service->servicePath = "http://localhost/"; - $resource = new Google_Service_Resource($service, 'test', 'testResource', array()); - - // Test Path - $params = array(); - $params['u']['type'] = 'string'; - $params['u']['location'] = 'path'; - $params['u']['value'] = 'me'; - $value = $resource->createRequestUri($restPath, $params); - $this->assertEquals("http://localhost/plus/me", $value); - - // Test Query - $params = array(); - $params['u']['type'] = 'string'; - $params['u']['location'] = 'query'; - $params['u']['value'] = 'me'; - $value = $resource->createRequestUri('/plus', $params); - $this->assertEquals("http://localhost/plus?u=me", $value); - - // Test Booleans - $params = array(); - $params['u']['type'] = 'boolean'; - $params['u']['location'] = 'path'; - $params['u']['value'] = '1'; - $value = $resource->createRequestUri($restPath, $params); - $this->assertEquals("http://localhost/plus/true", $value); - - $params['u']['location'] = 'query'; - $value = $resource->createRequestUri('/plus', $params); - $this->assertEquals("http://localhost/plus?u=true", $value); - - // Test encoding - $params = array(); - $params['u']['type'] = 'string'; - $params['u']['location'] = 'query'; - $params['u']['value'] = '@me/'; - $value = $resource->createRequestUri('/plus', $params); - $this->assertEquals("http://localhost/plus?u=%40me%2F", $value); - } - - public function testNoExpectedClassForAltMediaWithHttpSuccess() - { - // set the "alt" parameter to "media" - $arguments = [['alt' => 'media']]; - $request = new Request('GET', '/?alt=media'); - $body = Psr7\stream_for('thisisnotvalidjson'); - $response = new Response(200, [], $body); - - $http = $this->getMockBuilder("GuzzleHttp\Client") - ->disableOriginalConstructor() - ->getMock(); - $http->expects($this->once()) - ->method('send') - ->will($this->returnValue($response)); - - if ($this->isGuzzle5()) { - $http->expects($this->once()) - ->method('createRequest') - ->will($this->returnValue(new GuzzleHttp\Message\Request('GET', '/?alt=media'))); + $this->client->getLogger()->willReturn($logger->reveal()); + $this->client->shouldDefer()->willReturn(true); + $this->client->getHttpClient()->willReturn(new GuzzleClient()); + $this->client->getUniverseDomain()->willReturn('example.com'); + + $this->service = new TestService($this->client->reveal()); } - $client = new Google_Client(); - $client->setHttpClient($http); - $service = new Test_Google_Service($client); - - // set up mock objects - $resource = new Google_Service_Resource( - $service, - "test", - "testResource", - array("methods" => - array( - "testMethod" => array( - "parameters" => array(), - "path" => "method/path", - "httpMethod" => "POST", - ) - ) - ) - ); - - $expectedClass = 'ThisShouldBeIgnored'; - $response = $resource->call('testMethod', $arguments, $expectedClass); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); - $this->assertEquals('thisisnotvalidjson', (string) $response->getBody()); - } - - public function testNoExpectedClassForAltMediaWithHttpFail() - { - // set the "alt" parameter to "media" - $arguments = [['alt' => 'media']]; - $request = new Request('GET', '/?alt=media'); - $body = Psr7\stream_for('thisisnotvalidjson'); - $response = new Response(400, [], $body); - - $http = $this->getMockBuilder("GuzzleHttp\Client") - ->disableOriginalConstructor() - ->getMock(); - $http->expects($this->once()) - ->method('send') - ->will($this->returnValue($response)); - - if ($this->isGuzzle5()) { - $http->expects($this->once()) - ->method('createRequest') - ->will($this->returnValue(new GuzzleHttp\Message\Request('GET', '/?alt=media'))); + public function testCallFailure() + { + $this->expectException(GoogleException::class); + $this->expectExceptionMessage('Unknown function: test->testResource->someothermethod()'); + $resource = new GoogleResource( + $this->service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + $resource->call("someothermethod", []); } - $client = new Google_Client(); - $client->setHttpClient($http); - $service = new Test_Google_Service($client); - - // set up mock objects - $resource = new Google_Service_Resource( - $service, - "test", - "testResource", - array("methods" => - array( - "testMethod" => array( - "parameters" => array(), - "path" => "method/path", - "httpMethod" => "POST", - ) - ) - ) - ); - - try { - $expectedClass = 'ThisShouldBeIgnored'; - $decoded = $resource->call('testMethod', $arguments, $expectedClass); - $this->fail('should have thrown exception'); - } catch (Google_Service_Exception $e) { - // Alt Media on error should return a safe error - $this->assertEquals('thisisnotvalidjson', $e->getMessage()); + public function testCall() + { + $resource = new GoogleResource( + $this->service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + $request = $resource->call("testMethod", [[]]); + $this->assertEquals("https://test.example.com/method/path", (string) $request->getUri()); + $this->assertEquals("POST", $request->getMethod()); + $this->assertFalse($request->hasHeader('Content-Type')); + $this->assertFalse($request->hasHeader('X-Goog-Api-Version')); } - } - - public function testErrorResponseWithVeryLongBody() - { - // set the "alt" parameter to "media" - $arguments = [['alt' => 'media']]; - $request = new Request('GET', '/?alt=media'); - $body = Psr7\stream_for('this will be pulled into memory'); - $response = new Response(400, [], $body); - - $http = $this->getMockBuilder("GuzzleHttp\Client") - ->disableOriginalConstructor() - ->getMock(); - $http->expects($this->once()) - ->method('send') - ->will($this->returnValue($response)); - - if ($this->isGuzzle5()) { - $http->expects($this->once()) - ->method('createRequest') - ->will($this->returnValue(new GuzzleHttp\Message\Request('GET', '/?alt=media'))); + + public function testCallWithUniverseDomainTemplate() + { + $client = $this->prophesize(Client::class); + $logger = $this->prophesize("Monolog\Logger"); + $this->client->getLogger()->willReturn($logger->reveal()); + $this->client->shouldDefer()->willReturn(true); + $this->client->getHttpClient()->willReturn(new GuzzleClient()); + $this->client->getUniverseDomain()->willReturn('example-universe-domain.com'); + + $this->service = new TestService($this->client->reveal()); + + $resource = new GoogleResource( + $this->service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + $request = $resource->call("testMethod", [[]]); + $this->assertEquals("https://test.example-universe-domain.com/method/path", (string) $request->getUri()); + $this->assertEquals("POST", $request->getMethod()); + $this->assertFalse($request->hasHeader('Content-Type')); } - $client = new Google_Client(); - $client->setHttpClient($http); - $service = new Test_Google_Service($client); - - // set up mock objects - $resource = new Google_Service_Resource( - $service, - "test", - "testResource", - array("methods" => - array( - "testMethod" => array( - "parameters" => array(), - "path" => "method/path", - "httpMethod" => "POST", - ) - ) - ) - ); - - try { - $expectedClass = 'ThisShouldBeIgnored'; - $decoded = $resource->call('testMethod', $arguments, $expectedClass); - $this->fail('should have thrown exception'); - } catch (Google_Service_Exception $e) { - // empty message - alt=media means no message - $this->assertEquals('this will be pulled into memory', $e->getMessage()); + public function testCallWithPostBody() + { + $resource = new GoogleResource( + $this->service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + $request = $resource->call("testMethod", [['postBody' => ['foo' => 'bar']]]); + $this->assertEquals("https://test.example.com/method/path", (string) $request->getUri()); + $this->assertEquals("POST", $request->getMethod()); + $this->assertTrue($request->hasHeader('Content-Type')); } - } - - public function testSuccessResponseWithVeryLongBody() - { - // set the "alt" parameter to "media" - $arguments = [['alt' => 'media']]; - $request = new Request('GET', '/?alt=media'); - $resource = fopen('php://temp', 'r+'); - $stream = new Test_MediaType_Stream($resource); - $response = new Response(200, [], $stream); - - $http = $this->getMockBuilder("GuzzleHttp\Client") - ->disableOriginalConstructor() - ->getMock(); - $http->expects($this->once()) - ->method('send') - ->will($this->returnValue($response)); - - if ($this->isGuzzle5()) { - $http->expects($this->once()) - ->method('createRequest') - ->will($this->returnValue(new GuzzleHttp\Message\Request('GET', '/?alt=media'))); + + public function testCallServiceDefinedRoot() + { + $service = new TestService($this->client->reveal(), "https://sample.example.com/"); + $resource = new GoogleResource( + $service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + $request = $resource->call("testMethod", [[]]); + $this->assertEquals("https://sample.example.com/method/path", (string) $request->getUri()); + $this->assertEquals("POST", $request->getMethod()); } - $client = new Google_Client(); - $client->setHttpClient($http); - $service = new Test_Google_Service($client); - - // set up mock objects - $resource = new Google_Service_Resource( - $service, - "test", - "testResource", - array("methods" => - array( - "testMethod" => array( - "parameters" => array(), - "path" => "method/path", - "httpMethod" => "POST", - ) - ) - ) - ); - - $expectedClass = 'ThisShouldBeIgnored'; - $response = $resource->call('testMethod', $arguments, $expectedClass); - - $this->assertEquals(200, $response->getStatusCode()); - $this->assertFalse($stream->toStringCalled); - } - - public function testExceptionMessage() - { - // set the "alt" parameter to "media" - $request = new Request('GET', '/'); - $errors = [ ["domain" => "foo"] ]; - - $body = Psr7\stream_for(json_encode([ - 'error' => [ - 'errors' => $errors - ] - ])); - - $response = new Response(400, [], $body); - - $http = $this->getMockBuilder("GuzzleHttp\Client") - ->disableOriginalConstructor() - ->getMock(); - $http->expects($this->once()) - ->method('send') - ->will($this->returnValue($response)); - - if ($this->isGuzzle5()) { - $http->expects($this->once()) - ->method('createRequest') - ->will($this->returnValue(new GuzzleHttp\Message\Request('GET', '/?alt=media'))); + /** + * Some Google Service (Google\Service\Directory\Resource\Channels and + * Google\Service\Reports\Resource\Channels) use a different servicePath value + * that should override the default servicePath value, it's represented by a / + * before the resource path. All other Services have no / before the path + */ + public function testCreateRequestUriForASelfDefinedServicePath() + { + $this->service->servicePath = '/admin/directory/v1/'; + $resource = new GoogleResource( + $this->service, + 'test', + 'testResource', + [ + "methods" => [ + 'testMethod' => [ + 'parameters' => [], + 'path' => '/admin/directory_v1/watch/stop', + 'httpMethod' => 'POST', + ] + ] + ] + ); + $request = $resource->call('testMethod', [[]]); + $this->assertEquals('https://test.example.com/admin/directory_v1/watch/stop', (string) $request->getUri()); } - $client = new Google_Client(); - $client->setHttpClient($http); - $service = new Test_Google_Service($client); - - // set up mock objects - $resource = new Google_Service_Resource( - $service, - "test", - "testResource", - array("methods" => - array( - "testMethod" => array( - "parameters" => array(), - "path" => "method/path", - "httpMethod" => "POST", - ) - ) - ) - ); - - try { - - $decoded = $resource->call('testMethod', array(array())); - $this->fail('should have thrown exception'); - } catch (Google_Service_Exception $e) { - $this->assertEquals($errors, $e->getErrors()); + public function testCreateRequestUri() + { + $restPath = "plus/{u}"; + $service = new GoogleService($this->client->reveal()); + $service->servicePath = "http://localhost/"; + $resource = new GoogleResource($service, 'test', 'testResource', []); + + // Test Path + $params = []; + $params['u']['type'] = 'string'; + $params['u']['location'] = 'path'; + $params['u']['value'] = 'me'; + $value = $resource->createRequestUri($restPath, $params); + $this->assertEquals("http://localhost/plus/me", $value); + + // Test Query + $params = []; + $params['u']['type'] = 'string'; + $params['u']['location'] = 'query'; + $params['u']['value'] = 'me'; + $value = $resource->createRequestUri('plus', $params); + $this->assertEquals("http://localhost/plus?u=me", $value); + + // Test Booleans + $params = []; + $params['u']['type'] = 'boolean'; + $params['u']['location'] = 'path'; + $params['u']['value'] = '1'; + $value = $resource->createRequestUri($restPath, $params); + $this->assertEquals("http://localhost/plus/true", $value); + + $params['u']['location'] = 'query'; + $value = $resource->createRequestUri('plus', $params); + $this->assertEquals("http://localhost/plus?u=true", $value); + + // Test encoding + $params = []; + $params['u']['type'] = 'string'; + $params['u']['location'] = 'query'; + $params['u']['value'] = '@me/'; + $value = $resource->createRequestUri('plus', $params); + $this->assertEquals("http://localhost/plus?u=%40me%2F", $value); } - } + + public function testNoExpectedClassForAltMediaWithHttpSuccess() + { + // set the "alt" parameter to "media" + $arguments = [['alt' => 'media']]; + $request = new Request('GET', '/?alt=media'); + + $http = $this->prophesize("GuzzleHttp\Client"); + + $body = Psr7\Utils::streamFor('thisisnotvalidjson'); + $response = new Response(200, [], $body); + + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willReturn($response); + + $client = new Client(); + $client->setHttpClient($http->reveal()); + $service = new TestService($client); + + // set up mock objects + $resource = new GoogleResource( + $service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + + $expectedClass = 'ThisShouldBeIgnored'; + $response = $resource->call('testMethod', $arguments, $expectedClass); + $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); + $this->assertEquals('thisisnotvalidjson', (string) $response->getBody()); + } + + public function testNoExpectedClassForAltMediaWithHttpFail() + { + // set the "alt" parameter to "media" + $arguments = [['alt' => 'media']]; + $request = new Request('GET', '/?alt=media'); + + $http = $this->prophesize("GuzzleHttp\Client"); + + $body = Psr7\Utils::streamFor('thisisnotvalidjson'); + $response = new Response(400, [], $body); + + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willReturn($response); + + $client = new Client(); + $client->setHttpClient($http->reveal()); + $service = new TestService($client); + + // set up mock objects + $resource = new GoogleResource( + $service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + + try { + $expectedClass = 'ThisShouldBeIgnored'; + $decoded = $resource->call('testMethod', $arguments, $expectedClass); + $this->fail('should have thrown exception'); + } catch (ServiceException $e) { + // Alt Media on error should return a safe error + $this->assertEquals('thisisnotvalidjson', $e->getMessage()); + } + } + + public function testErrorResponseWithVeryLongBody() + { + // set the "alt" parameter to "media" + $arguments = [['alt' => 'media']]; + $request = new Request('GET', '/?alt=media'); + + $http = $this->prophesize("GuzzleHttp\Client"); + + $body = Psr7\Utils::streamFor('this will be pulled into memory'); + $response = new Response(400, [], $body); + + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willReturn($response); + + $client = new Client(); + $client->setHttpClient($http->reveal()); + $service = new TestService($client); + + // set up mock objects + $resource = new GoogleResource( + $service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + + try { + $expectedClass = 'ThisShouldBeIgnored'; + $decoded = $resource->call('testMethod', $arguments, $expectedClass); + $this->fail('should have thrown exception'); + } catch (ServiceException $e) { + // empty message - alt=media means no message + $this->assertEquals('this will be pulled into memory', $e->getMessage()); + } + } + + public function testSuccessResponseWithVeryLongBody() + { + // set the "alt" parameter to "media" + $arguments = [['alt' => 'media']]; + $stream = $this->prophesize(Stream::class); + $stream->__toString() + ->shouldNotBeCalled(); + $response = new Response(200, [], $stream->reveal()); + + $http = $this->prophesize("GuzzleHttp\Client"); + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willReturn($response); + + $client = new Client(); + $client->setHttpClient($http->reveal()); + $service = new TestService($client); + + // set up mock objects + $resource = new GoogleResource( + $service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + + $expectedClass = 'ThisShouldBeIgnored'; + $response = $resource->call('testMethod', $arguments, $expectedClass); + + $this->assertEquals(200, $response->getStatusCode()); + // $this->assertFalse($stream->toStringCalled); + } + + public function testExceptionMessage() + { + // set the "alt" parameter to "media" + $request = new Request('GET', '/'); + $errors = [ ["domain" => "foo"] ]; + $content = json_encode([ + 'error' => [ + 'errors' => $errors + ] + ]); + + $http = $this->prophesize("GuzzleHttp\Client"); + + $body = Psr7\Utils::streamFor($content); + $response = new Response(400, [], $body); + + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes(1) + ->willReturn($response); + + $client = new Client(); + $client->setHttpClient($http->reveal()); + $service = new TestService($client); + + // set up mock objects + $resource = new GoogleResource( + $service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + + try { + + $decoded = $resource->call('testMethod', [[]]); + $this->fail('should have thrown exception'); + } catch (ServiceException $e) { + $this->assertEquals($errors, $e->getErrors()); + } + } + + public function testVersionedResource() + { + $resource = new VersionedResource( + $this->service, + "test", + "testResource", + [ + "methods" => [ + "testMethod" => [ + "parameters" => [], + "path" => "method/path", + "httpMethod" => "POST", + ] + ] + ] + ); + $request = $resource->call("testMethod", [['postBody' => ['foo' => 'bar']]]); + $this->assertEquals("https://test.example.com/method/path", (string) $request->getUri()); + $this->assertEquals("POST", $request->getMethod()); + $this->assertTrue($request->hasHeader('X-Goog-Api-Version')); + $this->assertEquals('v1_20240101', $request->getHeaderLine('X-Goog-Api-Version')); + } +} + +class VersionedResource extends GoogleResource +{ + protected $apiVersion = 'v1_20240101'; } diff --git a/tests/Google/Service/TasksTest.php b/tests/Google/Service/TasksTest.php index 26e8a8fb6..7e035fd57 100644 --- a/tests/Google/Service/TasksTest.php +++ b/tests/Google/Service/TasksTest.php @@ -15,76 +15,81 @@ * limitations under the License. */ -class Google_Service_TasksTest extends BaseTest -{ - /** @var Google_TasksService */ - public $taskService; +namespace Google\Tests\Service; - public function setUp() - { - $this->checkToken(); - $this->taskService = new Google_Service_Tasks($this->getClient()); - } +use Google\Service\Tasks; +use Google\Tests\BaseTest; - public function testInsertTask() - { - $list = $this->createTaskList('List: ' . __METHOD__); - $task = $this->createTask('Task: '.__METHOD__, $list->id); - $this->assertIsTask($task); - } +class TasksTest extends BaseTest +{ + /** @var Tasks */ + public $taskService; - /** - * @depends testInsertTask - */ - public function testGetTask() - { - $tasks = $this->taskService->tasks; - $list = $this->createTaskList('List: ' . __METHOD__); - $task = $this->createTask('Task: '. __METHOD__, $list['id']); + public function setUp(): void + { + $this->checkToken(); + $this->taskService = new Tasks($this->getClient()); + } - $task = $tasks->get($list['id'], $task['id']); - $this->assertIsTask($task); - } + public function testInsertTask() + { + $list = $this->createTaskList('List: ' . __METHOD__); + $task = $this->createTask('Task: '.__METHOD__, $list->id); + $this->assertIsTask($task); + } - /** - * @depends testInsertTask - */ - public function testListTask() - { - $tasks = $this->taskService->tasks; - $list = $this->createTaskList('List: ' . __METHOD__); + /** + * @depends testInsertTask + */ + public function testGetTask() + { + $tasks = $this->taskService->tasks; + $list = $this->createTaskList('List: ' . __METHOD__); + $task = $this->createTask('Task: '. __METHOD__, $list['id']); - for ($i=0; $i<4; $i++) { - $this->createTask("Task: $i ".__METHOD__, $list['id']); + $task = $tasks->get($list['id'], $task['id']); + $this->assertIsTask($task); } - $tasksArray = $tasks->listTasks($list['id']); - $this->assertTrue(sizeof($tasksArray) > 1); - foreach ($tasksArray['items'] as $task) { - $this->assertIsTask($task); + /** + * @depends testInsertTask + */ + public function testListTask() + { + $tasks = $this->taskService->tasks; + $list = $this->createTaskList('List: ' . __METHOD__); + + for ($i=0; $i<4; $i++) { + $this->createTask("Task: $i ".__METHOD__, $list['id']); + } + + $tasksArray = $tasks->listTasks($list['id']); + $this->assertGreaterThan(1, count($tasksArray)); + foreach ($tasksArray['items'] as $task) { + $this->assertIsTask($task); + } } - } - private function createTaskList($name) - { - $list = new Google_Service_Tasks_TaskList(); - $list->title = $name; - return $this->taskService->tasklists->insert($list); - } + private function createTaskList($name) + { + $list = new Tasks\TaskList(); + $list->title = $name; + return $this->taskService->tasklists->insert($list); + } - private function createTask($title, $listId) - { - $tasks = $this->taskService->tasks; - $task = new Google_Service_Tasks_Task(); - $task->title = $title; - return $tasks->insert($listId, $task); - } + private function createTask($title, $listId) + { + $tasks = $this->taskService->tasks; + $task = new Tasks\Task(); + $task->title = $title; + return $tasks->insert($listId, $task); + } - private function assertIsTask($task) - { - $this->assertArrayHasKey('title', $task); - $this->assertArrayHasKey('kind', $task); - $this->assertArrayHasKey('id', $task); - $this->assertArrayHasKey('position', $task); - } + private function assertIsTask($task) + { + $this->assertArrayHasKey('title', $task); + $this->assertArrayHasKey('kind', $task); + $this->assertArrayHasKey('id', $task); + $this->assertArrayHasKey('position', $task); + } } diff --git a/tests/Google/Service/UrlshortenerTest.php b/tests/Google/Service/UrlshortenerTest.php deleted file mode 100644 index 9eacd0e35..000000000 --- a/tests/Google/Service/UrlshortenerTest.php +++ /dev/null @@ -1,44 +0,0 @@ -checkKey(); - - $service = new Google_Service_Urlshortener($this->getClient()); - $url = new Google_Service_Urlshortener_Url(); - $url->longUrl = "http://google.com/"; - - $shortUrl = $service->url->insert($url); - $this->assertEquals('urlshortener#url', $shortUrl['kind']); - $this->assertEquals('http://google.com/', $shortUrl['longUrl']); - } - - public function testEmptyJsonResponse() - { - $this->checkKey(); - - $service = new Google_Service_Urlshortener($this->getClient()); - - $optParams = array('fields' => ''); - $resp = $service->url->get('http://goo.gl/KkHq8', $optParams); - - $this->assertEquals("", $resp->longUrl); - } -} diff --git a/tests/Google/Service/YouTubeTest.php b/tests/Google/Service/YouTubeTest.php index b7745f8ae..2b08035f1 100644 --- a/tests/Google/Service/YouTubeTest.php +++ b/tests/Google/Service/YouTubeTest.php @@ -15,64 +15,69 @@ * limitations under the License. */ -class Google_Service_YouTubeTest extends BaseTest +namespace Google\Tests\Service; + +use Google\Service\YouTube; +use Google\Tests\BaseTest; + +class YouTubeTest extends BaseTest { - /** @var Google_Service_YouTube */ - public $youtube; - public function setUp() - { - $this->checkToken(); - $this->youtube = new Google_Service_YouTube($this->getClient()); - } + /** @var YouTube */ + public $youtube; + public function setUp(): void + { + $this->checkToken(); + $this->youtube = new YouTube($this->getClient()); + } - public function testMissingFieldsAreNull() - { - $parts = "id,brandingSettings"; - $opts = array("mine" => true); - $channels = $this->youtube->channels->listChannels($parts, $opts); + public function testMissingFieldsAreNull() + { + $parts = "id,brandingSettings"; + $opts = ["mine" => true]; + $channels = $this->youtube->channels->listChannels($parts, $opts); - $newChannel = new Google_Service_YouTube_Channel(); - $newChannel->setId( $channels[0]->getId()); - $newChannel->setBrandingSettings($channels[0]->getBrandingSettings()); + $newChannel = new YouTube\Channel(); + $newChannel->setId($channels[0]->getId()); + $newChannel->setBrandingSettings($channels[0]->getBrandingSettings()); - $simpleOriginal = $channels[0]->toSimpleObject(); - $simpleNew = $newChannel->toSimpleObject(); + $simpleOriginal = $channels[0]->toSimpleObject(); + $simpleNew = $newChannel->toSimpleObject(); - $this->assertObjectHasAttribute('etag', $simpleOriginal); - $this->assertObjectNotHasAttribute('etag', $simpleNew); + $this->assertObjectHasAttribute('etag', $simpleOriginal); + $this->assertObjectNotHasAttribute('etag', $simpleNew); - $owner_details = new Google_Service_YouTube_ChannelContentOwnerDetails(); - $owner_details->setTimeLinked("123456789"); - $o_channel = new Google_Service_YouTube_Channel(); - $o_channel->setContentOwnerDetails($owner_details); - $simpleManual = $o_channel->toSimpleObject(); - $this->assertObjectHasAttribute('timeLinked', $simpleManual->contentOwnerDetails); - $this->assertObjectNotHasAttribute('contentOwner', $simpleManual->contentOwnerDetails); + $owner_details = new YouTube\ChannelContentOwnerDetails(); + $owner_details->setTimeLinked("123456789"); + $o_channel = new YouTube\Channel(); + $o_channel->setContentOwnerDetails($owner_details); + $simpleManual = $o_channel->toSimpleObject(); + $this->assertObjectHasAttribute('timeLinked', $simpleManual->contentOwnerDetails); + $this->assertObjectNotHasAttribute('contentOwner', $simpleManual->contentOwnerDetails); - $owner_details = new Google_Service_YouTube_ChannelContentOwnerDetails(); - $owner_details->timeLinked = "123456789"; - $o_channel = new Google_Service_YouTube_Channel(); - $o_channel->setContentOwnerDetails($owner_details); - $simpleManual = $o_channel->toSimpleObject(); + $owner_details = new YouTube\ChannelContentOwnerDetails(); + $owner_details->timeLinked = "123456789"; + $o_channel = new YouTube\Channel(); + $o_channel->setContentOwnerDetails($owner_details); + $simpleManual = $o_channel->toSimpleObject(); - $this->assertObjectHasAttribute('timeLinked', $simpleManual->contentOwnerDetails); - $this->assertObjectNotHasAttribute('contentOwner', $simpleManual->contentOwnerDetails); + $this->assertObjectHasAttribute('timeLinked', $simpleManual->contentOwnerDetails); + $this->assertObjectNotHasAttribute('contentOwner', $simpleManual->contentOwnerDetails); - $owner_details = new Google_Service_YouTube_ChannelContentOwnerDetails(); - $owner_details['timeLinked'] = "123456789"; - $o_channel = new Google_Service_YouTube_Channel(); - $o_channel->setContentOwnerDetails($owner_details); - $simpleManual = $o_channel->toSimpleObject(); + $owner_details = new YouTube\ChannelContentOwnerDetails(); + $owner_details['timeLinked'] = "123456789"; + $o_channel = new YouTube\Channel(); + $o_channel->setContentOwnerDetails($owner_details); + $simpleManual = $o_channel->toSimpleObject(); - $this->assertObjectHasAttribute('timeLinked', $simpleManual->contentOwnerDetails); - $this->assertObjectNotHasAttribute('contentOwner', $simpleManual->contentOwnerDetails); + $this->assertObjectHasAttribute('timeLinked', $simpleManual->contentOwnerDetails); + $this->assertObjectNotHasAttribute('contentOwner', $simpleManual->contentOwnerDetails); - $ping = new Google_Service_YouTube_ChannelConversionPing(); - $ping->setContext("hello"); - $pings = new Google_Service_YouTube_ChannelConversionPings(); - $pings->setPings(array($ping)); - $simplePings = $pings->toSimpleObject(); - $this->assertObjectHasAttribute('context', $simplePings->pings[0]); - $this->assertObjectNotHasAttribute('conversionUrl', $simplePings->pings[0]); - } + $ping = new YouTube\ChannelConversionPing(); + $ping->setContext("hello"); + $pings = new YouTube\ChannelConversionPings(); + $pings->setPings([$ping]); + $simplePings = $pings->toSimpleObject(); + $this->assertObjectHasAttribute('context', $simplePings->pings[0]); + $this->assertObjectNotHasAttribute('conversionUrl', $simplePings->pings[0]); + } } diff --git a/tests/Google/ServiceTest.php b/tests/Google/ServiceTest.php index f119a5c42..10bb44c7d 100644 --- a/tests/Google/ServiceTest.php +++ b/tests/Google/ServiceTest.php @@ -18,79 +18,162 @@ * under the License. */ -class TestModel extends Google_Model +namespace Google\Tests; + +use Google\Client; +use Google\Model; +use Google\Service; +use Google\Http\Batch; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; +use Prophecy\PhpUnit\ProphecyTrait; + +class TestModel extends Model +{ + public function mapTypes($array) + { + return parent::mapTypes($array); + } + + public function isAssociativeArray($array) + { + return parent::isAssociativeArray($array); + } +} + +class TestService extends Service { - public function mapTypes($array) - { - return parent::mapTypes($array); - } - - public function isAssociativeArray($array) - { - return parent::isAssociativeArray($array); - } + public $batchPath = 'batch/test'; } -class Google_ServiceTest extends PHPUnit_Framework_TestCase +class ServiceTest extends TestCase { + private static $errorMessage; - public function testModel() - { - $model = new TestModel(); - - $model->mapTypes( - array( - 'name' => 'asdf', - 'gender' => 'z', - ) - ); - $this->assertEquals('asdf', $model->name); - $this->assertEquals('z', $model->gender); - $model->mapTypes( - array( - '__infoType' => 'Google_Model', - '__infoDataType' => 'map', - 'info' => array ( - 'location' => 'mars', - 'timezone' => 'mst', - ), - 'name' => 'asdf', - 'gender' => 'z', - ) - ); - $this->assertEquals('asdf', $model->name); - $this->assertEquals('z', $model->gender); - - $this->assertEquals(false, $model->isAssociativeArray("")); - $this->assertEquals(false, $model->isAssociativeArray(false)); - $this->assertEquals(false, $model->isAssociativeArray(null)); - $this->assertEquals(false, $model->isAssociativeArray(array())); - $this->assertEquals(false, $model->isAssociativeArray(array(1, 2))); - $this->assertEquals(false, $model->isAssociativeArray(array(1 => 2))); - - $this->assertEquals(true, $model->isAssociativeArray(array('test' => 'a'))); - $this->assertEquals(true, $model->isAssociativeArray(array("a", "b" => 2))); - } - - /** - * @dataProvider serviceProvider - */ - public function testIncludes($class) - { - $this->assertTrue( - class_exists($class), - sprintf('Failed asserting class %s exists.', $class) - ); - } - - public function serviceProvider() - { - $classes = array(); - $path = dirname(dirname(dirname(__FILE__))) . '/src/Google/Service'; - foreach (glob($path . "/*.php") as $file) { - $classes[] = array('Google_Service_' . basename($file, '.php')); + use ProphecyTrait; + + public function testCreateBatch() + { + $body = $this->prophesize(StreamInterface::class); + $body->__toString()->willReturn(''); + $response = $this->prophesize(ResponseInterface::class); + $response->getHeaderLine('content-type') + ->willReturn(''); + $response->getBody() + ->willReturn($body->reveal()); + $client = $this->prophesize(Client::class); + + $client->execute( + Argument::allOf( + Argument::type(RequestInterface::class), + Argument::that( + function ($request) { + $this->assertEquals('/batch/test', $request->getRequestTarget()); + return $request; + } + ) + ), + Argument::any() + )->willReturn($response->reveal()); + + $client->getConfig('base_path')->willReturn(''); + $client->getUniverseDomain()->willReturn(''); + + $model = new TestService($client->reveal()); + $batch = $model->createBatch(); + $this->assertInstanceOf(Batch::class, $batch); + $batch->execute(); } - return $classes; - } + public function testModel() + { + $model = new TestModel(); + + $model->mapTypes([ + 'name' => 'asdf', + 'gender' => 'z', + ]); + $this->assertEquals('asdf', $model->name); + $this->assertEquals('z', $model->gender); + $model->mapTypes([ + '__infoType' => 'Google_Model', + '__infoDataType' => 'map', + 'info' => [ + 'location' => 'mars', + 'timezone' => 'mst', + ], + 'name' => 'asdf', + 'gender' => 'z', + ]); + $this->assertEquals('asdf', $model->name); + $this->assertEquals('z', $model->gender); + + $this->assertFalse($model->isAssociativeArray("")); + $this->assertFalse($model->isAssociativeArray(false)); + $this->assertFalse($model->isAssociativeArray(null)); + $this->assertFalse($model->isAssociativeArray([])); + $this->assertFalse($model->isAssociativeArray([1, 2])); + $this->assertFalse($model->isAssociativeArray([1 => 2])); + + $this->assertTrue($model->isAssociativeArray(['test' => 'a'])); + $this->assertTrue($model->isAssociativeArray(["a", "b" => 2])); + } + + public function testConfigConstructor() + { + $clientId = 'test-client-id'; + $service = new TestService(['client_id' => $clientId]); + $this->assertEquals($clientId, $service->getClient()->getClientId()); + } + + public function testNoConstructor() + { + $service = new TestService(); + $this->assertInstanceOf(Client::class, $service->getClient()); + } + + public function testInvalidConstructorPhp7Plus() + { + if (!class_exists('TypeError')) { + $this->markTestSkipped('PHP 7+ only'); + } + + try { + $service = new TestService('foo'); + } catch (\TypeError $e) { + } + + $this->assertInstanceOf('TypeError', $e); + $this->assertEquals( + 'constructor must be array or instance of Google\Client', + $e->getMessage() + ); + } + + /** @runInSeparateProcess */ + public function testInvalidConstructorPhp5() + { + if (class_exists('TypeError')) { + $this->markTestSkipped('PHP 5 only'); + } + + set_error_handler('Google\Tests\ServiceTest::handlePhp5Error'); + + $service = new TestService('foo'); + + $this->assertEquals( + 'constructor must be array or instance of Google\Client', + self::$errorMessage + ); + } + + public static function handlePhp5Error($errno, $errstr, $errfile, $errline) + { + self::assertEquals(E_USER_ERROR, $errno); + self::$errorMessage = $errstr; + return true; + } } diff --git a/tests/Google/Task/ComposerTest.php b/tests/Google/Task/ComposerTest.php new file mode 100644 index 000000000..2976eabd8 --- /dev/null +++ b/tests/Google/Task/ComposerTest.php @@ -0,0 +1,268 @@ + [ + [ + 'type' => 'path', + 'url' => __DIR__ . '/../../..', + 'options' => [ + 'symlink' => false + ] + ] + ], + 'require' => [ + 'google/apiclient' => '*' + ], + 'scripts' => [ + 'pre-autoload-dump' => 'Google\Task\Composer::cleanup' + ], + 'minimum-stability' => 'dev', + ]; + + public function testInvalidServiceName() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Google service "Foo" does not exist'); + + Composer::cleanup($this->createMockEvent(['Foo'])); + } + + public function testRelatePathServiceName() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid Google service name "../YouTube"'); + + Composer::cleanup($this->createMockEvent(['../YouTube'])); + } + + public function testEmptyServiceName() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Google service "" does not exist'); + + Composer::cleanup($this->createMockEvent([''])); + } + + public function testWildcardServiceName() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid Google service name "YouTube*"'); + + Composer::cleanup($this->createMockEvent(['YouTube*'])); + } + + public function testRemoveServices() + { + $vendorDir = sys_get_temp_dir() . '/rand-' . rand(); + $serviceDir = sprintf( + '%s/google/apiclient-services/src/', + $vendorDir + ); + $dirs = [ + 'ServiceToKeep', + 'ServiceToDelete1', + 'ServiceToDelete2', + ]; + $files = [ + 'ServiceToKeep/ServiceFoo.php', + 'ServiceToKeep.php', + 'SomeRandomFile.txt', + 'ServiceToDelete1/ServiceFoo.php', + 'ServiceToDelete1.php', + 'ServiceToDelete2/ServiceFoo.php', + 'ServiceToDelete2.php', + ]; + foreach ($dirs as $dir) { + @mkdir($serviceDir . $dir, 0777, true); + } + foreach ($files as $file) { + touch($serviceDir . $file); + } + $print = 'Removing 2 google services'; + Composer::cleanup( + $this->createMockEvent(['ServiceToKeep'], $vendorDir, $print), + $this->createMockFilesystem([ + 'ServiceToDelete2', + 'ServiceToDelete2.php', + 'ServiceToDelete1', + 'ServiceToDelete1.php', + ], $serviceDir) + ); + } + + private function createMockFilesystem(array $files, $serviceDir) + { + $mockFilesystem = $this->prophesize(Filesystem::class); + foreach ($files as $filename) { + $file = new \SplFileInfo($serviceDir . $filename); + $mockFilesystem->remove($file->getRealPath()) + ->shouldBeCalledTimes(1); + } + + return $mockFilesystem->reveal(); + } + + private function createMockEvent( + array $servicesToKeep, + $vendorDir = '', + $print = null + ) { + $mockPackage = $this->prophesize('Composer\Package\RootPackage'); + $mockPackage->getExtra() + ->shouldBeCalledTimes(1) + ->willReturn(['google/apiclient-services' => $servicesToKeep]); + + $mockConfig = $this->prophesize('Composer\Config'); + $mockConfig->get('vendor-dir') + ->shouldBeCalledTimes(1) + ->willReturn($vendorDir); + + $mockComposer = $this->prophesize('Composer\Composer'); + $mockComposer->getPackage() + ->shouldBeCalledTimes(1) + ->willReturn($mockPackage->reveal()); + $mockComposer->getConfig() + ->shouldBeCalledTimes(1) + ->willReturn($mockConfig->reveal()); + + $mockEvent = $this->prophesize('Composer\Script\Event'); + $mockEvent->getComposer() + ->shouldBeCalledTimes(1) + ->willReturn($mockComposer); + + if ($print) { + $mockIO = $this->prophesize('Composer\IO\ConsoleIO'); + $mockIO->write($print) + ->shouldBeCalledTimes(1); + + $mockEvent->getIO() + ->shouldBeCalledTimes(1) + ->willReturn($mockIO->reveal()); + } + + return $mockEvent->reveal(); + } + + public function testE2E() + { + $dir = $this->runComposerInstall(self::$composerBaseConfig + [ + 'extra' => [ + 'google/apiclient-services' => [ + 'Drive', + 'YouTube' + ] + ] + ]); + + $serviceDir = $dir . '/vendor/google/apiclient-services/src'; + $this->assertFileExists($serviceDir . '/Drive.php'); + $this->assertFileExists($serviceDir . '/Drive'); + $this->assertFileExists($serviceDir . '/YouTube.php'); + $this->assertFileExists($serviceDir . '/YouTube'); + $this->assertFileDoesNotExist($serviceDir . '/YouTubeReporting.php'); + $this->assertFileDoesNotExist($serviceDir . '/YouTubeReporting'); + + // Remove the "apiclient-services" directory, which is required to + // update the cleanup command. + passthru('rm -r ' . $dir . '/vendor/google/apiclient-services'); + + $this->runComposerInstall(self::$composerBaseConfig + [ + 'extra' => [ + 'google/apiclient-services' => [ + 'Drive', + 'YouTube', + 'YouTubeReporting', + ] + ] + ], $dir); + + $this->assertFileExists($serviceDir . '/Drive.php'); + $this->assertFileExists($serviceDir . '/Drive'); + $this->assertFileExists($serviceDir . '/YouTube.php'); + $this->assertFileExists($serviceDir . '/YouTube'); + $this->assertFileExists($serviceDir . '/YouTubeReporting.php'); + $this->assertFileExists($serviceDir . '/YouTubeReporting'); + } + + public function testE2EBCTaskName() + { + $composerConfig = self::$composerBaseConfig + [ + 'extra' => [ + 'google/apiclient-services' => [ + 'Drive', + ] + ] + ]; + // Test BC Task name + $composerConfig['scripts']['pre-autoload-dump'] = 'Google_Task_Composer::cleanup'; + + $dir = $this->runComposerInstall($composerConfig); + $serviceDir = $dir . '/vendor/google/apiclient-services/src'; + + $this->assertFileExists($serviceDir . '/Drive.php'); + $this->assertFileExists($serviceDir . '/Drive'); + $this->assertFileDoesNotExist($serviceDir . '/YouTube.php'); + $this->assertFileDoesNotExist($serviceDir . '/YouTube'); + $this->assertFileDoesNotExist($serviceDir . '/YouTubeReporting.php'); + $this->assertFileDoesNotExist($serviceDir . '/YouTubeReporting'); + } + + public function testE2EOptimized() + { + $dir = $this->runComposerInstall(self::$composerBaseConfig + [ + 'config' => [ + 'optimize-autoloader' => true, + ], + 'extra' => [ + 'google/apiclient-services' => [ + 'Drive' + ] + ] + ]); + + $classmap = require_once $dir . '/vendor/composer/autoload_classmap.php'; + + // Verify removed services do not show up in the classmap + $this->assertArrayHasKey('Google\Service\Drive', $classmap); + $this->assertArrayNotHasKey('Google\Service\YouTube', $classmap); + } + + private function runComposerInstall(array $composerConfig, $dir = null) + { + $composerJson = json_encode($composerConfig); + + if (is_null($dir)) { + $dir = sys_get_temp_dir() . '/test-' . rand(); + mkdir($dir); + } + + file_put_contents($dir . '/composer.json', $composerJson); + passthru('composer install -d ' . $dir); + + return $dir; + } +} diff --git a/tests/Google/Task/RunnerTest.php b/tests/Google/Task/RunnerTest.php index 9f03d1180..650ef5c75 100644 --- a/tests/Google/Task/RunnerTest.php +++ b/tests/Google/Task/RunnerTest.php @@ -15,746 +15,755 @@ * limitations under the License. */ +namespace Google\Tests\Task; + +use Google\Client; +use Google\Task\Runner; +use Google\Tests\BaseTest; +use Google\Http\Request as GoogleRequest; +use Google\Http\REST; +use Google\Service\Exception as ServiceException; +use Google\Task\Exception as TaskException; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; +use Prophecy\Argument; +use Exception; -class Google_Task_RunnerTest extends BaseTest +class RunnerTest extends BaseTest { - private $client; - - private $mockedCallsCount = 0; - private $currentMockedCall = 0; - private $mockedCalls = array(); - private $retryMap; - private $retryConfig; - - protected function setUp() - { - $this->client = new Google_Client(); - } - - /** - * @dataProvider defaultRestErrorProvider - * @expectedException Google_Service_Exception - */ - public function testRestRetryOffByDefault($errorCode, $errorBody = '{}') - { - $this->setNextResponse($errorCode, $errorBody)->makeRequest(); - } - - /** - * @dataProvider defaultRestErrorProvider - * @expectedException Google_Service_Exception - */ - public function testOneRestRetryWithError($errorCode, $errorBody = '{}') - { - $this->setRetryConfig(array('retries' => 1)); - $this->setNextResponses(2, $errorCode, $errorBody)->makeRequest(); - } - - /** - * @dataProvider defaultRestErrorProvider - * @expectedException Google_Service_Exception - */ - public function testMultipleRestRetriesWithErrors( - $errorCode, - $errorBody = '{}' - ) { - $this->setRetryConfig(array('retries' => 5)); - $this->setNextResponses(6, $errorCode, $errorBody)->makeRequest(); - } - - /** - * @dataProvider defaultRestErrorProvider - */ - public function testOneRestRetryWithSuccess($errorCode, $errorBody = '{}') - { - $this->setRetryConfig(array('retries' => 1)); - $result = $this->setNextResponse($errorCode, $errorBody) + private $client; + + private $mockedCallsCount = 0; + private $currentMockedCall = 0; + private $mockedCalls = []; + private $retryMap; + private $retryConfig; + + public function setUp(): void + { + $this->client = new Client(); + } + + /** + * @dataProvider defaultRestErrorProvider + */ + public function testRestRetryOffByDefault($errorCode, $errorBody = '{}') + { + $this->expectException(ServiceException::class); + $this->setNextResponse($errorCode, $errorBody)->makeRequest(); + } + + /** + * @dataProvider defaultRestErrorProvider + */ + public function testOneRestRetryWithError($errorCode, $errorBody = '{}') + { + $this->expectException(ServiceException::class); + $this->setRetryConfig(['retries' => 1]); + $this->setNextResponses(2, $errorCode, $errorBody)->makeRequest(); + } + + /** + * @dataProvider defaultRestErrorProvider + */ + public function testMultipleRestRetriesWithErrors( + $errorCode, + $errorBody = '{}' + ) { + $this->expectException(ServiceException::class); + + $this->setRetryConfig(['retries' => 5]); + $this->setNextResponses(6, $errorCode, $errorBody)->makeRequest(); + } + + /** + * @dataProvider defaultRestErrorProvider + */ + public function testOneRestRetryWithSuccess($errorCode, $errorBody = '{}') + { + $this->setRetryConfig(['retries' => 1]); + $result = $this->setNextResponse($errorCode, $errorBody) ->setNextResponse(200, '{"success": true}') ->makeRequest(); - $this->assertEquals('{"success": true}', (string) $result->getBody()); - } - - /** - * @dataProvider defaultRestErrorProvider - */ - public function testMultipleRestRetriesWithSuccess( - $errorCode, - $errorBody = '{}' - ) { - $this->setRetryConfig(array('retries' => 5)); - $result = $this->setNextResponses(2, $errorCode, $errorBody) + $this->assertEquals('{"success": true}', (string) $result->getBody()); + } + + /** + * @dataProvider defaultRestErrorProvider + */ + public function testMultipleRestRetriesWithSuccess( + $errorCode, + $errorBody = '{}' + ) { + $this->setRetryConfig(['retries' => 5]); + $result = $this->setNextResponses(2, $errorCode, $errorBody) ->setNextResponse(200, '{"success": true}') ->makeRequest(); - $this->assertEquals('{"success": true}', (string) $result->getBody()); - } - - /** - * @dataProvider defaultRestErrorProvider - * @expectedException Google_Service_Exception - */ - public function testCustomRestRetryMapReplacesDefaults( - $errorCode, - $errorBody = '{}' - ) { - $this->setRetryMap(array()); - - $this->setRetryConfig(array('retries' => 5)); - $this->setNextResponse($errorCode, $errorBody)->makeRequest(); - } - - public function testCustomRestRetryMapAddsNewHandlers() - { - $this->setRetryMap( - array('403' => Google_Task_Runner::TASK_RETRY_ALWAYS) - ); - - $this->setRetryConfig(array('retries' => 5)); - $result = $this->setNextResponses(2, 403) + $this->assertEquals('{"success": true}', (string) $result->getBody()); + } + + /** + * @dataProvider defaultRestErrorProvider + */ + public function testCustomRestRetryMapReplacesDefaults( + $errorCode, + $errorBody = '{}' + ) { + $this->expectException(ServiceException::class); + + $this->setRetryMap([]); + + $this->setRetryConfig(['retries' => 5]); + $this->setNextResponse($errorCode, $errorBody)->makeRequest(); + } + + public function testCustomRestRetryMapAddsNewHandlers() + { + $this->setRetryMap( + ['403' => Runner::TASK_RETRY_ALWAYS] + ); + + $this->setRetryConfig(['retries' => 5]); + $result = $this->setNextResponses(2, 403) ->setNextResponse(200, '{"success": true}') ->makeRequest(); - $this->assertEquals('{"success": true}', (string) $result->getBody()); - } - - /** - * @expectedException Google_Service_Exception - * @dataProvider customLimitsProvider - */ - public function testCustomRestRetryMapWithCustomLimits($limit) - { - $this->setRetryMap( - array('403' => $limit) - ); - - $this->setRetryConfig(array('retries' => 5)); - $this->setNextResponses($limit + 1, 403)->makeRequest(); - } - - /** - * @dataProvider timeoutProvider - */ - public function testRestTimeouts($config, $minTime) - { - $this->setRetryConfig($config); - $this->setNextResponses($config['retries'], 500) + $this->assertEquals('{"success": true}', (string) $result->getBody()); + } + + /** + * @dataProvider customLimitsProvider + */ + public function testCustomRestRetryMapWithCustomLimits($limit) + { + $this->expectException(ServiceException::class); + + $this->setRetryMap( + ['403' => $limit] + ); + + $this->setRetryConfig(['retries' => 5]); + $this->setNextResponses($limit + 1, 403)->makeRequest(); + } + + /** + * @dataProvider timeoutProvider + */ + public function testRestTimeouts($config, $minTime) + { + $this->setRetryConfig($config); + $this->setNextResponses($config['retries'], 500) ->setNextResponse(200, '{"success": true}'); - $this->assertTaskTimeGreaterThanOrEqual( - $minTime, - array($this, 'makeRequest'), - $config['initial_delay'] / 10 - ); - } - - /** - * @requires extension curl - * @dataProvider defaultCurlErrorProvider - * @expectedException Google_Service_Exception - */ - public function testCurlRetryOffByDefault($errorCode, $errorMessage = '') - { - $this->setNextResponseThrows($errorMessage, $errorCode)->makeRequest(); - } - - /** - * @requires extension curl - * @dataProvider defaultCurlErrorProvider - * @expectedException Google_Service_Exception - */ - public function testOneCurlRetryWithError($errorCode, $errorMessage = '') - { - $this->setRetryConfig(array('retries' => 1)); - $this->setNextResponsesThrow(2, $errorMessage, $errorCode)->makeRequest(); - } - - /** - * @requires extension curl - * @dataProvider defaultCurlErrorProvider - * @expectedException Google_Service_Exception - */ - public function testMultipleCurlRetriesWithErrors( - $errorCode, - $errorMessage = '' - ) { - $this->setRetryConfig(array('retries' => 5)); - $this->setNextResponsesThrow(6, $errorMessage, $errorCode)->makeRequest(); - } - - /** - * @requires extension curl - * @dataProvider defaultCurlErrorProvider - */ - public function testOneCurlRetryWithSuccess($errorCode, $errorMessage = '') - { - $this->setRetryConfig(array('retries' => 1)); - $result = $this->setNextResponseThrows($errorMessage, $errorCode) + $this->assertTaskTimeGreaterThanOrEqual( + $minTime, + [$this, 'makeRequest'], + $config['initial_delay'] / 10 + ); + } + + /** + * @requires extension curl + * @dataProvider defaultCurlErrorProvider + */ + public function testCurlRetryOffByDefault($errorCode, $errorMessage = '') + { + $this->expectException(ServiceException::class); + + $this->setNextResponseThrows($errorMessage, $errorCode)->makeRequest(); + } + + /** + * @requires extension curl + * @dataProvider defaultCurlErrorProvider + */ + public function testOneCurlRetryWithError($errorCode, $errorMessage = '') + { + $this->expectException(ServiceException::class); + + $this->setRetryConfig(['retries' => 1]); + $this->setNextResponsesThrow(2, $errorMessage, $errorCode)->makeRequest(); + } + + /** + * @requires extension curl + * @dataProvider defaultCurlErrorProvider + */ + public function testMultipleCurlRetriesWithErrors( + $errorCode, + $errorMessage = '' + ) { + $this->expectException(ServiceException::class); + + $this->setRetryConfig(['retries' => 5]); + $this->setNextResponsesThrow(6, $errorMessage, $errorCode)->makeRequest(); + } + + /** + * @requires extension curl + * @dataProvider defaultCurlErrorProvider + */ + public function testOneCurlRetryWithSuccess($errorCode, $errorMessage = '') + { + $this->setRetryConfig(['retries' => 1]); + $result = $this->setNextResponseThrows($errorMessage, $errorCode) ->setNextResponse(200, '{"success": true}') ->makeRequest(); - $this->assertEquals('{"success": true}', (string) $result->getBody()); - } - - /** - * @requires extension curl - * @dataProvider defaultCurlErrorProvider - */ - public function testMultipleCurlRetriesWithSuccess( - $errorCode, - $errorMessage = '' - ) { - $this->setRetryConfig(array('retries' => 5)); - $result = $this->setNextResponsesThrow(2, $errorMessage, $errorCode) + $this->assertEquals('{"success": true}', (string) $result->getBody()); + } + + /** + * @requires extension curl + * @dataProvider defaultCurlErrorProvider + */ + public function testMultipleCurlRetriesWithSuccess( + $errorCode, + $errorMessage = '' + ) { + $this->setRetryConfig(['retries' => 5]); + $result = $this->setNextResponsesThrow(2, $errorMessage, $errorCode) ->setNextResponse(200, '{"success": true}') ->makeRequest(); - $this->assertEquals('{"success": true}', (string) $result->getBody()); - } - - /** - * @requires extension curl - * @dataProvider defaultCurlErrorProvider - * @expectedException Google_Service_Exception - */ - public function testCustomCurlRetryMapReplacesDefaults( - $errorCode, - $errorMessage = '' - ) { - $this->setRetryMap(array()); - - $this->setRetryConfig(array('retries' => 5)); - $this->setNextResponseThrows($errorMessage, $errorCode)->makeRequest(); - } - - /** - * @requires extension curl - */ - public function testCustomCurlRetryMapAddsNewHandlers() - { - $this->setRetryMap( - array(CURLE_COULDNT_RESOLVE_PROXY => Google_Task_Runner::TASK_RETRY_ALWAYS) - ); - - $this->setRetryConfig(array('retries' => 5)); - $result = $this->setNextResponsesThrow(2, '', CURLE_COULDNT_RESOLVE_PROXY) + $this->assertEquals('{"success": true}', (string) $result->getBody()); + } + + /** + * @requires extension curl + * @dataProvider defaultCurlErrorProvider + */ + public function testCustomCurlRetryMapReplacesDefaults( + $errorCode, + $errorMessage = '' + ) { + $this->expectException(ServiceException::class); + + $this->setRetryMap([]); + + $this->setRetryConfig(['retries' => 5]); + $this->setNextResponseThrows($errorMessage, $errorCode)->makeRequest(); + } + + /** + * @requires extension curl + */ + public function testCustomCurlRetryMapAddsNewHandlers() + { + $this->setRetryMap( + [CURLE_COULDNT_RESOLVE_PROXY => Runner::TASK_RETRY_ALWAYS] + ); + + $this->setRetryConfig(['retries' => 5]); + $result = $this->setNextResponsesThrow(2, '', CURLE_COULDNT_RESOLVE_PROXY) ->setNextResponse(200, '{"success": true}') ->makeRequest(); - $this->assertEquals('{"success": true}', (string) $result->getBody()); - } - - /** - * @requires extension curl - * @expectedException Google_Service_Exception - * @dataProvider customLimitsProvider - */ - public function testCustomCurlRetryMapWithCustomLimits($limit) - { - $this->setRetryMap( - array(CURLE_COULDNT_RESOLVE_PROXY => $limit) - ); - - $this->setRetryConfig(array('retries' => 5)); - $this->setNextResponsesThrow($limit + 1, '', CURLE_COULDNT_RESOLVE_PROXY) + $this->assertEquals('{"success": true}', (string) $result->getBody()); + } + + /** + * @requires extension curl + * @dataProvider customLimitsProvider + */ + public function testCustomCurlRetryMapWithCustomLimits($limit) + { + $this->expectException(ServiceException::class); + + $this->setRetryMap( + [CURLE_COULDNT_RESOLVE_PROXY => $limit] + ); + + $this->setRetryConfig(['retries' => 5]); + $this->setNextResponsesThrow($limit + 1, '', CURLE_COULDNT_RESOLVE_PROXY) ->makeRequest(); - } - - /** - * @requires extension curl - * @dataProvider timeoutProvider - */ - public function testCurlTimeouts($config, $minTime) - { - $this->setRetryConfig($config); - $this->setNextResponsesThrow($config['retries'], '', CURLE_GOT_NOTHING) + } + + /** + * @requires extension curl + * @dataProvider timeoutProvider + */ + public function testCurlTimeouts($config, $minTime) + { + $this->setRetryConfig($config); + $this->setNextResponsesThrow($config['retries'], '', CURLE_GOT_NOTHING) ->setNextResponse(200, '{"success": true}'); - $this->assertTaskTimeGreaterThanOrEqual( - $minTime, - array($this, 'makeRequest'), - $config['initial_delay'] / 10 - ); - } - - /** - * @dataProvider badTaskConfigProvider - */ - public function testBadTaskConfig($config, $message) - { - $this->setExpectedException('Google_Task_Exception', $message); - $this->setRetryConfig($config); - - new Google_Task_Runner( - $this->retryConfig, - '', - array($this, 'testBadTaskConfig') - ); - } - - /** - * @expectedException Google_Task_Exception - * @expectedExceptionMessage must be a valid callable - */ - public function testBadTaskCallback() - { - $config = []; - new Google_Task_Runner($config, '', 5); - } - - /** - * @expectedException Google_Service_Exception - */ - public function testTaskRetryOffByDefault() - { - $this->setNextTaskAllowedRetries(Google_Task_Runner::TASK_RETRY_ALWAYS) + $this->assertTaskTimeGreaterThanOrEqual( + $minTime, + [$this, 'makeRequest'], + $config['initial_delay'] / 10 + ); + } + + /** + * @dataProvider badTaskConfigProvider + */ + public function testBadTaskConfig($config, $message) + { + $this->expectException(TaskException::class); + $this->expectExceptionMessage($message); + $this->setRetryConfig($config); + + new Runner( + $this->retryConfig, + '', + [$this, 'testBadTaskConfig'] + ); + } + + /** + * @expectedExceptionMessage must be a valid callable + */ + public function testBadTaskCallback() + { + $this->expectException(TaskException::class); + $config = []; + new Runner($config, '', 5); + } + + public function testTaskRetryOffByDefault() + { + $this->expectException(ServiceException::class); + + $this->setNextTaskAllowedRetries(Runner::TASK_RETRY_ALWAYS) ->runTask(); - } - - /** - * @expectedException Google_Service_Exception - */ - public function testOneTaskRetryWithError() - { - $this->setRetryConfig(array('retries' => 1)); - $this->setNextTasksAllowedRetries(2, Google_Task_Runner::TASK_RETRY_ALWAYS) + } + + public function testOneTaskRetryWithError() + { + $this->expectException(ServiceException::class); + + $this->setRetryConfig(['retries' => 1]); + $this->setNextTasksAllowedRetries(2, Runner::TASK_RETRY_ALWAYS) ->runTask(); - } - - /** - * @expectedException Google_Service_Exception - */ - public function testMultipleTaskRetriesWithErrors() - { - $this->setRetryConfig(array('retries' => 5)); - $this->setNextTasksAllowedRetries(6, Google_Task_Runner::TASK_RETRY_ALWAYS) + } + + public function testMultipleTaskRetriesWithErrors() + { + $this->expectException(ServiceException::class); + + $this->setRetryConfig(['retries' => 5]); + $this->setNextTasksAllowedRetries(6, Runner::TASK_RETRY_ALWAYS) ->runTask(); - } + } - public function testOneTaskRetryWithSuccess() - { - $this->setRetryConfig(array('retries' => 1)); - $result = $this->setNextTaskAllowedRetries(Google_Task_Runner::TASK_RETRY_ALWAYS) + public function testOneTaskRetryWithSuccess() + { + $this->setRetryConfig(['retries' => 1]); + $result = $this->setNextTaskAllowedRetries(Runner::TASK_RETRY_ALWAYS) ->setNextTaskReturnValue('success') ->runTask(); - $this->assertEquals('success', $result); - } + $this->assertEquals('success', $result); + } - public function testMultipleTaskRetriesWithSuccess() - { - $this->setRetryConfig(array('retries' => 5)); - $result = $this->setNextTasksAllowedRetries(2, Google_Task_Runner::TASK_RETRY_ALWAYS) + public function testMultipleTaskRetriesWithSuccess() + { + $this->setRetryConfig(['retries' => 5]); + $result = $this->setNextTasksAllowedRetries(2, Runner::TASK_RETRY_ALWAYS) ->setNextTaskReturnValue('success') ->runTask(); - $this->assertEquals('success', $result); - } - - /** - * @expectedException Google_Service_Exception - * @dataProvider customLimitsProvider - */ - public function testTaskRetryWithCustomLimits($limit) - { - $this->setRetryConfig(array('retries' => 5)); - $this->setNextTasksAllowedRetries($limit + 1, $limit) + $this->assertEquals('success', $result); + } + + /** + * @dataProvider customLimitsProvider + */ + public function testTaskRetryWithCustomLimits($limit) + { + $this->expectException(ServiceException::class); + + $this->setRetryConfig(['retries' => 5]); + $this->setNextTasksAllowedRetries($limit + 1, $limit) ->runTask(); - } - - /** - * @dataProvider timeoutProvider - */ - public function testTaskTimeouts($config, $minTime) - { - $this->setRetryConfig($config); - $this->setNextTasksAllowedRetries($config['retries'], $config['retries'] + 1) + } + + /** + * @dataProvider timeoutProvider + */ + public function testTaskTimeouts($config, $minTime) + { + $this->setRetryConfig($config); + $this->setNextTasksAllowedRetries($config['retries'], $config['retries'] + 1) ->setNextTaskReturnValue('success'); - $this->assertTaskTimeGreaterThanOrEqual( - $minTime, - array($this, 'runTask'), - $config['initial_delay'] / 10 - ); - } - - public function testTaskWithManualRetries() - { - $this->setRetryConfig(array('retries' => 2)); - $this->setNextTasksAllowedRetries(2, Google_Task_Runner::TASK_RETRY_ALWAYS); - - $task = new Google_Task_Runner( - $this->retryConfig, - '', - array($this, 'runNextTask') - ); - - $this->assertTrue($task->canAttempt()); - $this->assertTrue($task->attempt()); - - $this->assertTrue($task->canAttempt()); - $this->assertTrue($task->attempt()); - - $this->assertTrue($task->canAttempt()); - $this->assertTrue($task->attempt()); - - $this->assertFalse($task->canAttempt()); - $this->assertFalse($task->attempt()); - } - - /** - * Provider for backoff configurations and expected minimum runtimes. - * - * @return array - */ - public function timeoutProvider() - { - $config = array('initial_delay' => .001, 'max_delay' => .01); - - return array( - array(array_merge($config, array('retries' => 1)), .001), - array(array_merge($config, array('retries' => 2)), .0015), - array(array_merge($config, array('retries' => 3)), .00225), - array(array_merge($config, array('retries' => 4)), .00375), - array(array_merge($config, array('retries' => 5)), .005625) - ); - } - - /** - * Provider for custom retry limits. - * - * @return array - */ - public function customLimitsProvider() - { - return array( - array(Google_Task_Runner::TASK_RETRY_NEVER), - array(Google_Task_Runner::TASK_RETRY_ONCE), - ); - } - - /** - * Provider for invalid task configurations. - * - * @return array - */ - public function badTaskConfigProvider() - { - return array( - array(array('initial_delay' => -1), 'must not be negative'), - array(array('max_delay' => 0), 'must be greater than 0'), - array(array('factor' => 0), 'must be greater than 0'), - array(array('jitter' => 0), 'must be greater than 0'), - array(array('retries' => -1), 'must not be negative') - ); - } - - /** - * Provider for the default REST errors. - * - * @return array - */ - public function defaultRestErrorProvider() - { - return array( - array(500), - array(503), - array(403, '{"error":{"errors":[{"reason":"rateLimitExceeded"}]}}'), - array(403, '{"error":{"errors":[{"reason":"userRateLimitExceeded"}]}}'), - ); - } - - /** - * Provider for the default cURL errors. - * - * @return array - */ - public function defaultCurlErrorProvider() - { - return array( - array(6), // CURLE_COULDNT_RESOLVE_HOST - array(7), // CURLE_COULDNT_CONNECT - array(28), // CURLE_OPERATION_TIMEOUTED - array(35), // CURLE_SSL_CONNECT_ERROR - array(52), // CURLE_GOT_NOTHING - ); - } - - /** - * Assert the minimum amount of time required to run a task. - * - * NOTE: Intentionally crude for brevity. - * - * @param float $expected The expected minimum execution time - * @param callable $callback The task to time - * @param float $delta Allowable relative error - * - * @throws PHPUnit_Framework_ExpectationFailedException - */ - public static function assertTaskTimeGreaterThanOrEqual( - $expected, - $callback, - $delta = 0.0 - ) { - $time = microtime(true); - - call_user_func($callback); - - self::assertThat( - microtime(true) - $time, - self::logicalOr( - self::greaterThan($expected), - self::equalTo($expected, $delta) - ) - ); - } - - /** - * Sets the task runner configurations. - * - * @param array $config The task runner configurations - */ - private function setRetryConfig(array $config) - { - $config += array( - 'initial_delay' => .0001, - 'max_delay' => .001, - 'factor' => 2, - 'jitter' => .5, - 'retries' => 1 - ); - $this->retryConfig = $config; - } - - private function setRetryMap(array $retryMap) - { - $this->retryMap = $retryMap; - } - - /** - * Sets the next responses. - * - * @param integer $count The number of responses - * @param string $code The response code - * @param string $body The response body - * @param array $headers The response headers - * - * @return TaskTest - */ - private function setNextResponses( - $count, - $code = '200', - $body = '{}', - array $headers = array() - ) { - while ($count-- > 0) { - $this->setNextResponse($code, $body, $headers); - } - - return $this; - } - - /** - * Sets the next response. - * - * @param string $code The response code - * @param string $body The response body - * @param array $headers The response headers - * - * @return TaskTest - */ - private function setNextResponse( - $code = '200', - $body = '{}', - array $headers = array() - ) { - $this->mockedCalls[$this->mockedCallsCount++] = array( - 'code' => (string) $code, - 'headers' => $headers, - 'body' => is_string($body) ? $body : json_encode($body) - ); - - return $this; - } - - /** - * Forces the next responses to throw an IO exception. - * - * @param integer $count The number of responses - * @param string $message The exception messages - * @param string $code The exception code - * - * @return TaskTest - */ - private function setNextResponsesThrow($count, $message, $code) - { - while ($count-- > 0) { - $this->setNextResponseThrows($message, $code); - } - - return $this; - } - - /** - * Forces the next response to throw an IO exception. - * - * @param string $message The exception messages - * @param string $code The exception code - * - * @return TaskTest - */ - private function setNextResponseThrows($message, $code) - { - $this->mockedCalls[$this->mockedCallsCount++] = new Google_Service_Exception( - $message, - $code, - null, - array() - ); - - return $this; - } - - /** - * Runs the defined request. - * - * @return array - */ - private function makeRequest() - { - $request = new Request('GET', '/test'); - $http = $this->getMock('GuzzleHttp\ClientInterface'); - $http->expects($this->exactly($this->mockedCallsCount)) - ->method('send') - ->will($this->returnCallback(array($this, 'getNextMockedCall'))); - - if ($this->isGuzzle5()) { - $http->expects($this->exactly($this->mockedCallsCount)) - ->method('createRequest') - ->will($this->returnValue(new GuzzleHttp\Message\Request('GET', '/test'))); - } - - return Google_Http_REST::execute($http, $request, '', $this->retryConfig, $this->retryMap); - } - - /** - * Gets the next mocked response. - * - * @param Google_Http_Request $request The mocked request - * - * @return Google_Http_Request - */ - public function getNextMockedCall($request) - { - $current = $this->mockedCalls[$this->currentMockedCall++]; - - if ($current instanceof Exception) { - throw $current; - } - - $stream = Psr7\stream_for($current['body']); - $response = new Response($current['code'], $current['headers'], $stream); - - return $response; - } - - /** - * Sets the next task return value. - * - * @param mixed $value The next return value - * - * @return TaskTest - */ - private function setNextTaskReturnValue($value) - { - $this->mockedCalls[$this->mockedCallsCount++] = $value; - return $this; - } - - /** - * Sets the next exception `allowedRetries()` return value. - * - * @param boolean $allowedRetries The next `allowedRetries()` return value. - * - * @return TaskTest - */ - private function setNextTaskAllowedRetries($allowedRetries) - { - $this->mockedCalls[$this->mockedCallsCount++] = $allowedRetries; - return $this; - } - - /** - * Sets multiple exception `allowedRetries()` return value. - * - * @param integer $count The number of `allowedRetries()` return values. - * @param boolean $allowedRetries The `allowedRetries()` return value. - * - * @return TaskTest - */ - private function setNextTasksAllowedRetries($count, $allowedRetries) - { - while ($count-- > 0) { - $this->setNextTaskAllowedRetries($allowedRetries); - } - - return $this; - } - - /** - * Runs the defined task. - * - * @return mixed - */ - private function runTask() - { - $task = new Google_Task_Runner( - $this->retryConfig, - '', - array($this, 'runNextTask') - ); - - if (null !== $this->retryMap) { - $task->setRetryMap($this->retryMap); - } - - $exception = $this->getMockBuilder('Google_Service_Exception') - // HHVM blows up unless this is set - // @see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/207 - ->setMethods(array('setTraceOptions')) - ->disableOriginalConstructor() - ->getMock(); - $exceptionCount = 0; - $exceptionCalls = array(); - - for ($i = 0; $i < $this->mockedCallsCount; $i++) { - if (is_int($this->mockedCalls[$i])) { - $exceptionCalls[$exceptionCount++] = $this->mockedCalls[$i]; - $this->mockedCalls[$i] = $exception; - } - } - - $task->setRetryMap($exceptionCalls); - - return $task->run(); - } - - /** - * Gets the next task return value. - * - * @return mixed - */ - public function runNextTask() - { - $current = $this->mockedCalls[$this->currentMockedCall++]; - - if ($current instanceof Exception) { - throw $current; - } - - return $current; - } + $this->assertTaskTimeGreaterThanOrEqual( + $minTime, + [$this, 'runTask'], + $config['initial_delay'] / 10 + ); + } + + public function testTaskWithManualRetries() + { + $this->setRetryConfig(['retries' => 2]); + $this->setNextTasksAllowedRetries(2, Runner::TASK_RETRY_ALWAYS); + + $task = new Runner( + $this->retryConfig, + '', + [$this, 'runNextTask'] + ); + + $this->assertTrue($task->canAttempt()); + $this->assertTrue($task->attempt()); + + $this->assertTrue($task->canAttempt()); + $this->assertTrue($task->attempt()); + + $this->assertTrue($task->canAttempt()); + $this->assertTrue($task->attempt()); + + $this->assertFalse($task->canAttempt()); + $this->assertFalse($task->attempt()); + } + + /** + * Provider for backoff configurations and expected minimum runtimes. + * + * @return array + */ + public function timeoutProvider() + { + $config = ['initial_delay' => .001, 'max_delay' => .01]; + + return [ + [array_merge($config, ['retries' => 1]), .001], + [array_merge($config, ['retries' => 2]), .0015], + [array_merge($config, ['retries' => 3]), .00225], + [array_merge($config, ['retries' => 4]), .00375], + [array_merge($config, ['retries' => 5]), .005625] + ]; + } + + /** + * Provider for custom retry limits. + * + * @return array + */ + public function customLimitsProvider() + { + return [ + [Runner::TASK_RETRY_NEVER], + [Runner::TASK_RETRY_ONCE], + ]; + } + + /** + * Provider for invalid task configurations. + * + * @return array + */ + public function badTaskConfigProvider() + { + return [ + [['initial_delay' => -1], 'must not be negative'], + [['max_delay' => 0], 'must be greater than 0'], + [['factor' => 0], 'must be greater than 0'], + [['jitter' => 0], 'must be greater than 0'], + [['retries' => -1], 'must not be negative'] + ]; + } + + /** + * Provider for the default REST errors. + * + * @return array + */ + public function defaultRestErrorProvider() + { + return [ + [500], + [503], + [403, '{"error":{"errors":[{"reason":"rateLimitExceeded"}]}}'], + [403, '{"error":{"errors":[{"reason":"userRateLimitExceeded"}]}}'], + ]; + } + + /** + * Provider for the default cURL errors. + * + * @return array + */ + public function defaultCurlErrorProvider() + { + return [ + [6], // CURLE_COULDNT_RESOLVE_HOST + [7], // CURLE_COULDNT_CONNECT + [28], // CURLE_OPERATION_TIMEOUTED + [35], // CURLE_SSL_CONNECT_ERROR + [52], // CURLE_GOT_NOTHING + ]; + } + + /** + * Assert the minimum amount of time required to run a task. + * + * NOTE: Intentionally crude for brevity. + * + * @param float $expected The expected minimum execution time + * @param callable $callback The task to time + * @param float $delta Allowable relative error + * + * @throws PHPUnit_Framework_ExpectationFailedException + */ + public static function assertTaskTimeGreaterThanOrEqual( + $expected, + $callback, + $delta = 0.0 + ) { + $time = microtime(true); + + call_user_func($callback); + + self::assertThat( + microtime(true) - $time, + self::logicalOr( + self::greaterThan($expected), + self::equalTo($expected, $delta) + ) + ); + } + + /** + * Sets the task runner configurations. + * + * @param array $config The task runner configurations + */ + private function setRetryConfig(array $config) + { + $config += [ + 'initial_delay' => .0001, + 'max_delay' => .001, + 'factor' => 2, + 'jitter' => .5, + 'retries' => 1 + ]; + $this->retryConfig = $config; + } + + private function setRetryMap(array $retryMap) + { + $this->retryMap = $retryMap; + } + + /** + * Sets the next responses. + * + * @param integer $count The number of responses + * @param string $code The response code + * @param string $body The response body + * @param array $headers The response headers + * + * @return TaskTest + */ + private function setNextResponses( + $count, + $code = '200', + $body = '{}', + array $headers = [] + ) { + while ($count-- > 0) { + $this->setNextResponse($code, $body, $headers); + } + + return $this; + } + + /** + * Sets the next response. + * + * @param string $code The response code + * @param string $body The response body + * @param array $headers The response headers + * + * @return TaskTest + */ + private function setNextResponse( + $code = '200', + $body = '{}', + array $headers = [] + ) { + $this->mockedCalls[$this->mockedCallsCount++] = [ + 'code' => (string) $code, + 'headers' => $headers, + 'body' => is_string($body) ? $body : json_encode($body) + ]; + + return $this; + } + + /** + * Forces the next responses to throw an IO exception. + * + * @param integer $count The number of responses + * @param string $message The exception messages + * @param string $code The exception code + * + * @return TaskTest + */ + private function setNextResponsesThrow($count, $message, $code) + { + while ($count-- > 0) { + $this->setNextResponseThrows($message, $code); + } + + return $this; + } + + /** + * Forces the next response to throw an IO exception. + * + * @param string $message The exception messages + * @param string $code The exception code + * + * @return TaskTest + */ + private function setNextResponseThrows($message, $code) + { + $this->mockedCalls[$this->mockedCallsCount++] = new ServiceException( + $message, + $code, + null, + [] + ); + + return $this; + } + + /** + * Runs the defined request. + * + * @return array + */ + private function makeRequest() + { + $request = new Request('GET', '/test'); + $http = $this->prophesize('GuzzleHttp\ClientInterface'); + + $http->send(Argument::type('Psr\Http\Message\RequestInterface'), []) + ->shouldBeCalledTimes($this->mockedCallsCount) + ->will([$this, 'getNextMockedCall']); + + return REST::execute($http->reveal(), $request, '', $this->retryConfig, $this->retryMap); + } + + /** + * Gets the next mocked response. + * + * @param GoogleRequest $request The mocked request + * + * @return GoogleRequest + */ + public function getNextMockedCall($request) + { + $current = $this->mockedCalls[$this->currentMockedCall++]; + + if ($current instanceof Exception) { + throw $current; + } + + $stream = Psr7\Utils::streamFor($current['body']); + $response = new Response($current['code'], $current['headers'], $stream); + + return $response; + } + + /** + * Sets the next task return value. + * + * @param mixed $value The next return value + * + * @return TaskTest + */ + private function setNextTaskReturnValue($value) + { + $this->mockedCalls[$this->mockedCallsCount++] = $value; + return $this; + } + + /** + * Sets the next exception `allowedRetries()` return value. + * + * @param boolean $allowedRetries The next `allowedRetries()` return value. + * + * @return TaskTest + */ + private function setNextTaskAllowedRetries($allowedRetries) + { + $this->mockedCalls[$this->mockedCallsCount++] = $allowedRetries; + return $this; + } + + /** + * Sets multiple exception `allowedRetries()` return value. + * + * @param integer $count The number of `allowedRetries()` return values. + * @param boolean $allowedRetries The `allowedRetries()` return value. + * + * @return TaskTest + */ + private function setNextTasksAllowedRetries($count, $allowedRetries) + { + while ($count-- > 0) { + $this->setNextTaskAllowedRetries($allowedRetries); + } + + return $this; + } + + /** + * Runs the defined task. + * + * @return mixed + */ + private function runTask() + { + $task = new Runner( + $this->retryConfig, + '', + [$this, 'runNextTask'] + ); + + if (null !== $this->retryMap) { + $task->setRetryMap($this->retryMap); + } + + $exception = $this->prophesize(ServiceException::class); + + $exceptionCount = 0; + $exceptionCalls = []; + + for ($i = 0; $i < $this->mockedCallsCount; $i++) { + if (is_int($this->mockedCalls[$i])) { + $exceptionCalls[$exceptionCount++] = $this->mockedCalls[$i]; + $this->mockedCalls[$i] = $exception->reveal(); + } + } + + $task->setRetryMap($exceptionCalls); + + return $task->run(); + } + + /** + * Gets the next task return value. + * + * @return mixed + */ + public function runNextTask() + { + $current = $this->mockedCalls[$this->currentMockedCall++]; + + if ($current instanceof Exception) { + throw $current; + } + + return $current; + } } diff --git a/tests/Google/Utils/UriTemplateTest.php b/tests/Google/Utils/UriTemplateTest.php index ba5dc3f3b..8c366435d 100644 --- a/tests/Google/Utils/UriTemplateTest.php +++ b/tests/Google/Utils/UriTemplateTest.php @@ -18,279 +18,288 @@ * under the License. */ -class Google_Utils_UriTemplateTest extends BaseTest +namespace Google\Tests\Utils; + +use Google\Tests\BaseTest; +use Google\Utils\UriTemplate; + +class UriTemplateTest extends BaseTest { - public function testLevelOne() - { - $var = "value"; - $hello = "Hello World!"; + public function testLevelOne() + { + $var = "value"; + $hello = "Hello World!"; - $urit = new Google_Utils_UriTemplate(); - $this->assertEquals( - "value", - $urit->parse("{var}", array("var" => $var)) - ); - $this->assertEquals( - "Hello%20World%21", - $urit->parse("{hello}", array("hello" => $hello)) - ); - } + $urit = new UriTemplate(); + $this->assertEquals( + "value", + $urit->parse("{var}", ["var" => $var]) + ); + $this->assertEquals( + "Hello%20World%21", + $urit->parse("{hello}", ["hello" => $hello]) + ); + } - public function testLevelTwo() - { - $var = "value"; - $hello = "Hello World!"; - $path = "/foo/bar"; + public function testLevelTwo() + { + $var = "value"; + $hello = "Hello World!"; + $path = "/foo/bar"; - $urit = new Google_Utils_UriTemplate(); - $this->assertEquals( - "value", - $urit->parse("{+var}", array("var" => $var)) - ); - $this->assertEquals( - "Hello%20World!", - $urit->parse("{+hello}", array("hello" => $hello)) - ); - $this->assertEquals( - "/foo/bar/here", - $urit->parse("{+path}/here", array("path" => $path)) - ); - $this->assertEquals( - "here?ref=/foo/bar", - $urit->parse("here?ref={+path}", array("path" => $path)) - ); - $this->assertEquals( - "X#value", - $urit->parse("X{#var}", array("var" => $var)) - ); - $this->assertEquals( - "X#Hello%20World!", - $urit->parse("X{#hello}", array("hello" => $hello)) - ); - } + $urit = new UriTemplate(); + $this->assertEquals( + "value", + $urit->parse("{+var}", ["var" => $var]) + ); + $this->assertEquals( + "Hello%20World!", + $urit->parse("{+hello}", ["hello" => $hello]) + ); + $this->assertEquals( + "/foo/bar/here", + $urit->parse("{+path}/here", ["path" => $path]) + ); + $this->assertEquals( + "here?ref=/foo/bar", + $urit->parse("here?ref={+path}", ["path" => $path]) + ); + $this->assertEquals( + "X#value", + $urit->parse("X{#var}", ["var" => $var]) + ); + $this->assertEquals( + "X#Hello%20World!", + $urit->parse("X{#hello}", ["hello" => $hello]) + ); + } - public function testLevelThree() - { - $var = "value"; - $hello = "Hello World!"; - $empty = ''; - $path = "/foo/bar"; - $x = "1024"; - $y = "768"; + public function testLevelThree() + { + $var = "value"; + $hello = "Hello World!"; + $empty = ''; + $path = "/foo/bar"; + $x = "1024"; + $y = "768"; - $urit = new Google_Utils_UriTemplate(); - $this->assertEquals( - "map?1024,768", - $urit->parse("map?{x,y}", array("x" => $x, "y" => $y)) - ); - $this->assertEquals( - "1024,Hello%20World%21,768", - $urit->parse("{x,hello,y}", array("x" => $x, "y" => $y, "hello" => $hello)) - ); + $urit = new UriTemplate(); + $this->assertEquals( + "map?1024,768", + $urit->parse("map?{x,y}", ["x" => $x, "y" => $y]) + ); + $this->assertEquals( + "1024,Hello%20World%21,768", + $urit->parse("{x,hello,y}", ["x" => $x, "y" => $y, "hello" => $hello]) + ); - $this->assertEquals( - "1024,Hello%20World!,768", - $urit->parse("{+x,hello,y}", array("x" => $x, "y" => $y, "hello" => $hello)) - ); - $this->assertEquals( - "/foo/bar,1024/here", - $urit->parse("{+path,x}/here", array("x" => $x, "path" => $path)) - ); + $this->assertEquals( + "1024,Hello%20World!,768", + $urit->parse("{+x,hello,y}", ["x" => $x, "y" => $y, "hello" => $hello]) + ); + $this->assertEquals( + "/foo/bar,1024/here", + $urit->parse("{+path,x}/here", ["x" => $x, "path" => $path]) + ); - $this->assertEquals( - "#1024,Hello%20World!,768", - $urit->parse("{#x,hello,y}", array("x" => $x, "y" => $y, "hello" => $hello)) - ); - $this->assertEquals( - "#/foo/bar,1024/here", - $urit->parse("{#path,x}/here", array("x" => $x, "path" => $path)) - ); + $this->assertEquals( + "#1024,Hello%20World!,768", + $urit->parse("{#x,hello,y}", ["x" => $x, "y" => $y, "hello" => $hello]) + ); + $this->assertEquals( + "#/foo/bar,1024/here", + $urit->parse("{#path,x}/here", ["x" => $x, "path" => $path]) + ); - $this->assertEquals( - "X.value", - $urit->parse("X{.var}", array("var" => $var)) - ); - $this->assertEquals( - "X.1024.768", - $urit->parse("X{.x,y}", array("x" => $x, "y" => $y)) - ); + $this->assertEquals( + "X.value", + $urit->parse("X{.var}", ["var" => $var]) + ); + $this->assertEquals( + "X.1024.768", + $urit->parse("X{.x,y}", ["x" => $x, "y" => $y]) + ); - $this->assertEquals( - "X.value", - $urit->parse("X{.var}", array("var" => $var)) - ); - $this->assertEquals( - "X.1024.768", - $urit->parse("X{.x,y}", array("x" => $x, "y" => $y)) - ); + $this->assertEquals( + "X.value", + $urit->parse("X{.var}", ["var" => $var]) + ); + $this->assertEquals( + "X.1024.768", + $urit->parse("X{.x,y}", ["x" => $x, "y" => $y]) + ); - $this->assertEquals( - "/value", - $urit->parse("{/var}", array("var" => $var)) - ); - $this->assertEquals( - "/value/1024/here", - $urit->parse("{/var,x}/here", array("x" => $x, "var" => $var)) - ); + $this->assertEquals( + "/value", + $urit->parse("{/var}", ["var" => $var]) + ); + $this->assertEquals( + "/value/1024/here", + $urit->parse("{/var,x}/here", ["x" => $x, "var" => $var]) + ); - $this->assertEquals( - ";x=1024;y=768", - $urit->parse("{;x,y}", array("x" => $x, "y" => $y)) - ); - $this->assertEquals( - ";x=1024;y=768;empty", - $urit->parse("{;x,y,empty}", array("x" => $x, "y" => $y, "empty" => $empty)) - ); + $this->assertEquals( + ";x=1024;y=768", + $urit->parse("{;x,y}", ["x" => $x, "y" => $y]) + ); + $this->assertEquals( + ";x=1024;y=768;empty", + $urit->parse("{;x,y,empty}", ["x" => $x, "y" => $y, "empty" => $empty]) + ); - $this->assertEquals( - "?x=1024&y=768", - $urit->parse("{?x,y}", array("x" => $x, "y" => $y)) - ); - $this->assertEquals( - "?x=1024&y=768&empty=", - $urit->parse("{?x,y,empty}", array("x" => $x, "y" => $y, "empty" => $empty)) - ); + $this->assertEquals( + "?x=1024&y=768", + $urit->parse("{?x,y}", ["x" => $x, "y" => $y]) + ); + $this->assertEquals( + "?x=1024&y=768&empty=", + $urit->parse("{?x,y,empty}", ["x" => $x, "y" => $y, "empty" => $empty]) + ); - $this->assertEquals( - "?fixed=yes&x=1024", - $urit->parse("?fixed=yes{&x}", array("x" => $x, "y" => $y)) - ); - $this->assertEquals( - "&x=1024&y=768&empty=", - $urit->parse("{&x,y,empty}", array("x" => $x, "y" => $y, "empty" => $empty)) - ); - } + $this->assertEquals( + "?fixed=yes&x=1024", + $urit->parse("?fixed=yes{&x}", ["x" => $x, "y" => $y]) + ); + $this->assertEquals( + "&x=1024&y=768&empty=", + $urit->parse("{&x,y,empty}", ["x" => $x, "y" => $y, "empty" => $empty]) + ); + } - public function testLevelFour() - { - $values = array( - 'var' => "value", - 'hello' => "Hello World!", - 'path' => "/foo/bar", - 'list' => array("red", "green", "blue"), - 'keys' => array("semi" => ";", "dot" => ".", "comma" => ","), - ); + public function testLevelFour() + { + $values = [ + 'var' => "value", + 'hello' => "Hello World!", + 'path' => "/foo/bar", + 'list' => ["red", "green", "blue"], + 'keys' => ["semi" => ";", "dot" => ".", "comma" => ","], + ]; - $tests = array( - "{var:3}" => "val", - "{var:30}" => "value", - "{list}" => "red,green,blue", - "{list*}" => "red,green,blue", - "{keys}" => "semi,%3B,dot,.,comma,%2C", - "{keys*}" => "semi=%3B,dot=.,comma=%2C", - "{+path:6}/here" => "/foo/b/here", - "{+list}" => "red,green,blue", - "{+list*}" => "red,green,blue", - "{+keys}" => "semi,;,dot,.,comma,,", - "{+keys*}" => "semi=;,dot=.,comma=,", - "{#path:6}/here" => "#/foo/b/here", - "{#list}" => "#red,green,blue", - "{#list*}" => "#red,green,blue", - "{#keys}" => "#semi,;,dot,.,comma,,", - "{#keys*}" => "#semi=;,dot=.,comma=,", - "X{.var:3}" => "X.val", - "X{.list}" => "X.red,green,blue", - "X{.list*}" => "X.red.green.blue", - "X{.keys}" => "X.semi,%3B,dot,.,comma,%2C", - "X{.keys*}" => "X.semi=%3B.dot=..comma=%2C", - "{/var:1,var}" => "/v/value", - "{/list}" => "/red,green,blue", - "{/list*}" => "/red/green/blue", - "{/list*,path:4}" => "/red/green/blue/%2Ffoo", - "{/keys}" => "/semi,%3B,dot,.,comma,%2C", - "{/keys*}" => "/semi=%3B/dot=./comma=%2C", - "{;hello:5}" => ";hello=Hello", - "{;list}" => ";list=red,green,blue", - "{;list*}" => ";list=red;list=green;list=blue", - "{;keys}" => ";keys=semi,%3B,dot,.,comma,%2C", - "{;keys*}" => ";semi=%3B;dot=.;comma=%2C", - "{?var:3}" => "?var=val", - "{?list}" => "?list=red,green,blue", - "{?list*}" => "?list=red&list=green&list=blue", - "{?keys}" => "?keys=semi,%3B,dot,.,comma,%2C", - "{?keys*}" => "?semi=%3B&dot=.&comma=%2C", - "{&var:3}" => "&var=val", - "{&list}" => "&list=red,green,blue", - "{&list*}" => "&list=red&list=green&list=blue", - "{&keys}" => "&keys=semi,%3B,dot,.,comma,%2C", - "{&keys*}" => "&semi=%3B&dot=.&comma=%2C", - "find{?list*}" => "find?list=red&list=green&list=blue", - "www{.list*}" => "www.red.green.blue" + $tests = [ + "{var:3}" => "val", + "{var:30}" => "value", + "{list}" => "red,green,blue", + "{list*}" => "red,green,blue", + "{keys}" => "semi,%3B,dot,.,comma,%2C", + "{keys*}" => "semi=%3B,dot=.,comma=%2C", + "{+path:6}/here" => "/foo/b/here", + "{+list}" => "red,green,blue", + "{+list*}" => "red,green,blue", + "{+keys}" => "semi,;,dot,.,comma,,", + "{+keys*}" => "semi=;,dot=.,comma=,", + "{#path:6}/here" => "#/foo/b/here", + "{#list}" => "#red,green,blue", + "{#list*}" => "#red,green,blue", + "{#keys}" => "#semi,;,dot,.,comma,,", + "{#keys*}" => "#semi=;,dot=.,comma=,", + "X{.var:3}" => "X.val", + "X{.list}" => "X.red,green,blue", + "X{.list*}" => "X.red.green.blue", + "X{.keys}" => "X.semi,%3B,dot,.,comma,%2C", + "X{.keys*}" => "X.semi=%3B.dot=..comma=%2C", + "{/var:1,var}" => "/v/value", + "{/list}" => "/red,green,blue", + "{/list*}" => "/red/green/blue", + "{/list*,path:4}" => "/red/green/blue/%2Ffoo", + "{/keys}" => "/semi,%3B,dot,.,comma,%2C", + "{/keys*}" => "/semi=%3B/dot=./comma=%2C", + "{;hello:5}" => ";hello=Hello", + "{;list}" => ";list=red,green,blue", + "{;list*}" => ";list=red;list=green;list=blue", + "{;keys}" => ";keys=semi,%3B,dot,.,comma,%2C", + "{;keys*}" => ";semi=%3B;dot=.;comma=%2C", + "{?var:3}" => "?var=val", + "{?list}" => "?list=red,green,blue", + "{?list*}" => "?list=red&list=green&list=blue", + "{?keys}" => "?keys=semi,%3B,dot,.,comma,%2C", + "{?keys*}" => "?semi=%3B&dot=.&comma=%2C", + "{&var:3}" => "&var=val", + "{&list}" => "&list=red,green,blue", + "{&list*}" => "&list=red&list=green&list=blue", + "{&keys}" => "&keys=semi,%3B,dot,.,comma,%2C", + "{&keys*}" => "&semi=%3B&dot=.&comma=%2C", + "find{?list*}" => "find?list=red&list=green&list=blue", + "www{.list*}" => "www.red.green.blue" + ]; - ); + $urit = new UriTemplate(); - $urit = new Google_Utils_UriTemplate(); + foreach ($tests as $input => $output) { + $this->assertEquals($output, $urit->parse($input, $values), $input . " failed"); + } + } - foreach ($tests as $input => $output) { - $this->assertEquals($output, $urit->parse($input, $values), $input . " failed"); + public function testMultipleAnnotations() + { + $var = "value"; + $hello = "Hello World!"; + $urit = new UriTemplate(); + $this->assertEquals( + "http://www.google.com/Hello%20World!?var=value", + $urit->parse( + "http://www.google.com/%7B+hello%7D%7B?var}", + ["var" => $var, "hello" => $hello] + ) + ); + $params = [ + "playerId" => "me", + "leaderboardId" => "CgkIhcG1jYEbEAIQAw", + "timeSpan" => "ALL_TIME", + "other" => "irrelevant" + ]; + $this->assertEquals( + "players/me/leaderboards/CgkIhcG1jYEbEAIQAw/scores/ALL_TIME", + $urit->parse( + "players/{playerId}/leaderboards/{leaderboardId}/scores/{timeSpan}", + $params + ) + ); } - } - public function testMultipleAnnotations() - { - $var = "value"; - $hello = "Hello World!"; - $urit = new Google_Utils_UriTemplate(); - $this->assertEquals( - "http://www.google.com/Hello%20World!?var=value", - $urit->parse( - "http://www.google.com/%7B+hello%7D%7B?var}", - array("var" => $var, "hello" => $hello) - ) - ); - $params = array( - "playerId" => "me", - "leaderboardId" => "CgkIhcG1jYEbEAIQAw", - "timeSpan" => "ALL_TIME", - "other" => "irrelevant" - ); - $this->assertEquals( - "players/me/leaderboards/CgkIhcG1jYEbEAIQAw/scores/ALL_TIME", - $urit->parse( - "players/{playerId}/leaderboards/{leaderboardId}/scores/{timeSpan}", - $params - ) - ); - } + /** + * This test test against the JSON files defined in + * https://github.com/uri-templates/uritemplate-test + * + * We don't ship these tests with it, so they'll just silently + * skip unless provided - this is mainly for use when + * making specific URI template changes and wanting + * to do a full regression check. + */ + public function testAgainstStandardTests() + { + $location = __DIR__ . "/../../uritemplate-test/*.json"; + $files = glob($location); - /** - * This test test against the JSON files defined in - * https://github.com/uri-templates/uritemplate-test - * - * We don't ship these tests with it, so they'll just silently - * skip unless provided - this is mainly for use when - * making specific URI template changes and wanting - * to do a full regression check. - */ - public function testAgainstStandardTests() - { - $location = "../../uritemplate-test/*.json"; + if (!$files) { + $this->markTestSkipped('No JSON files provided'); + } - $urit = new Google_Utils_UriTemplate(); - foreach (glob($location) as $file) { - $test = json_decode(file_get_contents($file), true); - foreach ($test as $title => $testsets) { - foreach ($testsets['testcases'] as $cases) { - $input = $cases[0]; - $output = $cases[1]; - if ($output == false) { - continue; // skipping negative tests for now - } else if (is_array($output)) { - $response = $urit->parse($input, $testsets['variables']); - $this->assertContains( - $response, - $output, - $input . " failed from " . $title - ); - } else { - $this->assertEquals( - $output, - $urit->parse($input, $testsets['variables']), - $input . " failed." - ); - } + $urit = new UriTemplate(); + foreach ($files as $file) { + $test = json_decode(file_get_contents($file), true); + foreach ($test as $title => $testsets) { + foreach ($testsets['testcases'] as $cases) { + $input = $cases[0]; + $output = $cases[1]; + if ($output == false) { + continue; // skipping negative tests for now + } else if (is_array($output)) { + $response = $urit->parse($input, $testsets['variables']); + $this->assertContains( + $response, + $output, + $input . " failed from " . $title + ); + } else { + $this->assertEquals( + $output, + $urit->parse($input, $testsets['variables']), + $input . " failed." + ); + } + } + } } - } } - } } diff --git a/tests/examples/batchTest.php b/tests/examples/batchTest.php index c16ef8bea..bf2e5a53b 100644 --- a/tests/examples/batchTest.php +++ b/tests/examples/batchTest.php @@ -19,17 +19,21 @@ * under the License. */ -class examples_batchTest extends BaseTest +namespace Google\Tests\Examples; + +use Google\Tests\BaseTest; + +class batchTest extends BaseTest { - public function testBatch() - { - $this->checkKey(); + public function testBatch() + { + $this->checkKey(); - $crawler = $this->loadExample('batch.php'); + $crawler = $this->loadExample('batch.php'); - $nodes = $crawler->filter('br'); - $this->assertEquals(20, count($nodes)); - $this->assertContains('Life of Henry David Thoreau', $crawler->text()); - $this->assertContains('George Bernard Shaw His Life and Works', $crawler->text()); - } -} \ No newline at end of file + $nodes = $crawler->filter('br'); + $this->assertCount(20, $nodes); + $this->assertContains('Walden', $crawler->text()); + $this->assertContains('George Bernard Shaw', $crawler->text()); + } +} diff --git a/tests/examples/idTokenTest.php b/tests/examples/idTokenTest.php index 4f44bc3a4..b40531e8c 100644 --- a/tests/examples/idTokenTest.php +++ b/tests/examples/idTokenTest.php @@ -19,20 +19,24 @@ * under the License. */ -class examples_idTokenTest extends BaseTest +namespace Google\Tests\Examples; + +use Google\Tests\BaseTest; + +class idTokenTest extends BaseTest { - public function testIdToken() - { - $this->checkServiceAccountCredentials(); + public function testIdToken() + { + $this->checkServiceAccountCredentials(); - $crawler = $this->loadExample('idtoken.php'); + $crawler = $this->loadExample('idtoken.php'); - $nodes = $crawler->filter('h1'); - $this->assertEquals(1, count($nodes)); - $this->assertEquals('Retrieving An Id Token', $nodes->first()->text()); + $nodes = $crawler->filter('h1'); + $this->assertCount(1, $nodes); + $this->assertEquals('Retrieving An Id Token', $nodes->first()->text()); - $nodes = $crawler->filter('a.login'); - $this->assertEquals(1, count($nodes)); - $this->assertEquals('Connect Me!', $nodes->first()->text()); - } -} \ No newline at end of file + $nodes = $crawler->filter('a.login'); + $this->assertCount(1, $nodes); + $this->assertEquals('Connect Me!', $nodes->first()->text()); + } +} diff --git a/tests/examples/indexTest.php b/tests/examples/indexTest.php index d869f9145..6b2e65a44 100644 --- a/tests/examples/indexTest.php +++ b/tests/examples/indexTest.php @@ -19,14 +19,18 @@ * under the License. */ -class examples_indexTest extends BaseTest +namespace Google\Tests\Examples; + +use Google\Tests\BaseTest; + +class indexTest extends BaseTest { - public function testIndex() - { - $crawler = $this->loadExample('index.php'); + public function testIndex() + { + $crawler = $this->loadExample('index.php'); - $nodes = $crawler->filter('li'); - $this->assertEquals(8, count($nodes)); - $this->assertEquals('A query using simple API access', $nodes->first()->text()); - } -} \ No newline at end of file + $nodes = $crawler->filter('li'); + $this->assertCount(8, $nodes); + $this->assertEquals('A query using simple API access', $nodes->first()->text()); + } +} diff --git a/tests/examples/largeFileDownloadTest.php b/tests/examples/largeFileDownloadTest.php new file mode 100644 index 000000000..49e3bba9a --- /dev/null +++ b/tests/examples/largeFileDownloadTest.php @@ -0,0 +1,59 @@ +checkServiceAccountCredentials(); + + $crawler = $this->loadExample('large-file-download.php'); + + $nodes = $crawler->filter('h1'); + $this->assertCount(1, $nodes); + $this->assertEquals('File Download - Downloading a large file', $nodes->first()->text()); + + $nodes = $crawler->filter('a.login'); + $this->assertCount(1, $nodes); + $this->assertEquals('Connect Me!', $nodes->first()->text()); + } + + public function testSimpleFileDownloadWithToken() + { + $this->checkToken(); + + global $_SESSION; + $_SESSION['upload_token'] = $this->getClient()->getAccessToken(); + + $crawler = $this->loadExample('large-file-download.php'); + + $buttonText = 'Click here to download a large (20MB) test file'; + $nodes = $crawler->filter('input'); + $this->assertCount(1, $nodes); + $this->assertEquals($buttonText, $nodes->first()->attr('value')); + } +} diff --git a/tests/examples/largeFileUploadTest.php b/tests/examples/largeFileUploadTest.php index edbc0a259..f1c8af4cb 100644 --- a/tests/examples/largeFileUploadTest.php +++ b/tests/examples/largeFileUploadTest.php @@ -19,20 +19,27 @@ * under the License. */ -class examples_largeFileUploadTest extends BaseTest +namespace Google\Tests\Examples; + +use Google\Tests\BaseTest; + +class largeFileUploadTest extends BaseTest { - public function testLargeFileUpload() - { - $this->checkServiceAccountCredentials(); + /** + * @runInSeparateProcess + */ + public function testLargeFileUpload() + { + $this->checkServiceAccountCredentials(); - $crawler = $this->loadExample('large-file-upload.php'); + $crawler = $this->loadExample('large-file-upload.php'); - $nodes = $crawler->filter('h1'); - $this->assertEquals(1, count($nodes)); - $this->assertEquals('File Upload - Uploading a large file', $nodes->first()->text()); + $nodes = $crawler->filter('h1'); + $this->assertCount(1, $nodes); + $this->assertEquals('File Upload - Uploading a large file', $nodes->first()->text()); - $nodes = $crawler->filter('a.login'); - $this->assertEquals(1, count($nodes)); - $this->assertEquals('Connect Me!', $nodes->first()->text()); - } -} \ No newline at end of file + $nodes = $crawler->filter('a.login'); + $this->assertCount(1, $nodes); + $this->assertEquals('Connect Me!', $nodes->first()->text()); + } +} diff --git a/tests/examples/multiApiTest.php b/tests/examples/multiApiTest.php index 47d2b178b..b572c31ee 100644 --- a/tests/examples/multiApiTest.php +++ b/tests/examples/multiApiTest.php @@ -19,16 +19,20 @@ * under the License. */ -class examples_multiApiTest extends BaseTest +namespace Google\Tests\Examples; + +use Google\Tests\BaseTest; + +class multiApiTest extends BaseTest { - public function testMultiApi() - { - $this->checkKey(); + public function testMultiApi() + { + $this->checkKey(); - $crawler = $this->loadExample('multi-api.php'); + $crawler = $this->loadExample('multi-api.php'); - $nodes = $crawler->filter('h1'); - $this->assertEquals(1, count($nodes)); - $this->assertEquals('User Query - Multiple APIs', $nodes->first()->text()); - } -} \ No newline at end of file + $nodes = $crawler->filter('h1'); + $this->assertCount(1, $nodes); + $this->assertEquals('User Query - Multiple APIs', $nodes->first()->text()); + } +} diff --git a/tests/examples/serviceAccountTest.php b/tests/examples/serviceAccountTest.php index b12f6557d..719057bf5 100644 --- a/tests/examples/serviceAccountTest.php +++ b/tests/examples/serviceAccountTest.php @@ -19,16 +19,20 @@ * under the License. */ -class examples_serviceAccountTest extends BaseTest +namespace Google\Tests\Examples; + +use Google\Tests\BaseTest; + +class serviceAccountTest extends BaseTest { - public function testServiceAccount() - { - $this->checkServiceAccountCredentials(); + public function testServiceAccount() + { + $this->checkServiceAccountCredentials(); - $crawler = $this->loadExample('service-account.php'); + $crawler = $this->loadExample('service-account.php'); - $nodes = $crawler->filter('br'); - $this->assertEquals(10, count($nodes)); - $this->assertContains('Life of Henry David Thoreau', $crawler->text()); - } -} \ No newline at end of file + $nodes = $crawler->filter('br'); + $this->assertCount(10, $nodes); + $this->assertContains('Walden', $crawler->text()); + } +} diff --git a/tests/examples/simpleFileUploadTest.php b/tests/examples/simpleFileUploadTest.php index d81ccef01..1942586bb 100644 --- a/tests/examples/simpleFileUploadTest.php +++ b/tests/examples/simpleFileUploadTest.php @@ -19,35 +19,42 @@ * under the License. */ -class examples_simpleFileUploadTest extends BaseTest -{ - public function testSimpleFileUploadNoToken() - { - $this->checkServiceAccountCredentials(); - - $crawler = $this->loadExample('simple-file-upload.php'); - - $nodes = $crawler->filter('h1'); - $this->assertEquals(1, count($nodes)); - $this->assertEquals('File Upload - Uploading a simple file', $nodes->first()->text()); - - $nodes = $crawler->filter('a.login'); - $this->assertEquals(1, count($nodes)); - $this->assertEquals('Connect Me!', $nodes->first()->text()); - } +namespace Google\Tests\Examples; - public function testSimpleFileUploadWithToken() - { - $this->checkToken(); +use Google\Tests\BaseTest; - global $_SESSION; - $_SESSION['upload_token'] = $this->getClient()->getAccessToken(); - - $crawler = $this->loadExample('simple-file-upload.php'); - - $buttonText = 'Click here to upload two small (1MB) test files'; - $nodes = $crawler->filter('input'); - $this->assertEquals(1, count($nodes)); - $this->assertEquals($buttonText, $nodes->first()->attr('value')); - } -} \ No newline at end of file +class simpleFileUploadTest extends BaseTest +{ + /** + * @runInSeparateProcess + */ + public function testSimpleFileUploadNoToken() + { + $this->checkServiceAccountCredentials(); + + $crawler = $this->loadExample('simple-file-upload.php'); + + $nodes = $crawler->filter('h1'); + $this->assertCount(1, $nodes); + $this->assertEquals('File Upload - Uploading a simple file', $nodes->first()->text()); + + $nodes = $crawler->filter('a.login'); + $this->assertCount(1, $nodes); + $this->assertEquals('Connect Me!', $nodes->first()->text()); + } + + public function testSimpleFileUploadWithToken() + { + $this->checkToken(); + + global $_SESSION; + $_SESSION['upload_token'] = $this->getClient()->getAccessToken(); + + $crawler = $this->loadExample('simple-file-upload.php'); + + $buttonText = 'Click here to upload two small (1MB) test files'; + $nodes = $crawler->filter('input'); + $this->assertCount(1, $nodes); + $this->assertEquals($buttonText, $nodes->first()->attr('value')); + } +} diff --git a/tests/examples/simpleQueryTest.php b/tests/examples/simpleQueryTest.php index ad0f3b34e..8bc7eac7d 100644 --- a/tests/examples/simpleQueryTest.php +++ b/tests/examples/simpleQueryTest.php @@ -19,19 +19,23 @@ * under the License. */ -class examples_simpleQueryTest extends BaseTest +namespace Google\Tests\Examples; + +use Google\Tests\BaseTest; + +class simpleQueryTest extends BaseTest { - public function testSimpleQuery() - { - $this->checkKey(); + public function testSimpleQuery() + { + $this->checkKey(); - $crawler = $this->loadExample('simple-query.php'); + $crawler = $this->loadExample('simple-query.php'); - $nodes = $crawler->filter('br'); - $this->assertEquals(20, count($nodes)); + $nodes = $crawler->filter('br'); + $this->assertCount(20, $nodes); - $nodes = $crawler->filter('h1'); - $this->assertEquals(1, count($nodes)); - $this->assertEquals('Simple API Access', $nodes->first()->text()); - } -} \ No newline at end of file + $nodes = $crawler->filter('h1'); + $this->assertCount(1, $nodes); + $this->assertEquals('Simple API Access', $nodes->first()->text()); + } +} diff --git a/tests/examples/urlShortenerTest.php b/tests/examples/urlShortenerTest.php deleted file mode 100644 index ea9d60c29..000000000 --- a/tests/examples/urlShortenerTest.php +++ /dev/null @@ -1,34 +0,0 @@ -checkKey(); - - $crawler = $this->loadExample('url-shortener.php'); - - $nodes = $crawler->filter('h1'); - $this->assertEquals(1, count($nodes)); - $this->assertEquals('User Query - URL Shortener', $nodes->first()->text()); - } -} \ No newline at end of file