├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── composer.json └── src ├── AccessToken ├── Revoke.php └── Verify.php ├── AuthHandler ├── AuthHandlerFactory.php ├── Guzzle6AuthHandler.php └── Guzzle7AuthHandler.php ├── Client.php ├── Collection.php ├── Exception.php ├── Http ├── Batch.php ├── MediaFileUpload.php └── REST.php ├── Model.php ├── Service.php ├── Service ├── Exception.php ├── README.md └── Resource.php ├── Task ├── Composer.php ├── Exception.php ├── Retryable.php └── Runner.php ├── Utils └── UriTemplate.php └── aliases.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.18.3](https://github.com/googleapis/google-api-php-client/compare/v2.18.2...v2.18.3) (2025-04-08) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * 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)) 9 | 10 | ## [2.18.2](https://github.com/googleapis/google-api-php-client/compare/v2.18.1...v2.18.2) (2024-12-16) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * 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)) 16 | 17 | ## [2.18.1](https://github.com/googleapis/google-api-php-client/compare/v2.18.0...v2.18.1) (2024-11-24) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * 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)) 23 | 24 | ## [2.18.0](https://github.com/googleapis/google-api-php-client/compare/v2.17.0...v2.18.0) (2024-10-16) 25 | 26 | 27 | ### Features 28 | 29 | * **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)) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * 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)) 35 | 36 | ## [2.17.0](https://github.com/googleapis/google-api-php-client/compare/v2.16.0...v2.17.0) (2024-07-10) 37 | 38 | 39 | ### Features 40 | 41 | * 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)) 42 | * 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)) 43 | 44 | ## [2.16.0](https://github.com/googleapis/google-api-php-client/compare/v2.15.4...v2.16.0) (2024-04-24) 45 | 46 | 47 | ### Features 48 | 49 | * 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)) 50 | 51 | ## [2.15.4](https://github.com/googleapis/google-api-php-client/compare/v2.15.3...v2.15.4) (2024-03-06) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * 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)) 57 | 58 | ## [2.15.3](https://github.com/googleapis/google-api-php-client/compare/v2.15.2...v2.15.3) (2024-01-04) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * 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)) 64 | 65 | ## [2.15.2](https://github.com/googleapis/google-api-php-client/compare/v2.15.1...v2.15.2) (2024-01-03) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * 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)) 71 | * 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)) 72 | * 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)) 73 | 74 | ## [2.15.1](https://github.com/googleapis/google-api-php-client/compare/v2.15.0...v2.15.1) (2023-09-12) 75 | 76 | 77 | ### Bug Fixes 78 | 79 | * 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)) 80 | 81 | ## [2.15.0](https://github.com/googleapis/google-api-php-client/compare/v2.14.0...v2.15.0) (2023-05-18) 82 | 83 | 84 | ### Features 85 | 86 | * 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)) 87 | * 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)) 88 | 89 | ## [2.14.0](https://github.com/googleapis/google-api-php-client/compare/v2.13.2...v2.14.0) (2023-05-11) 90 | 91 | 92 | ### Features 93 | 94 | * 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)) 95 | 96 | ## [2.13.2](https://github.com/googleapis/google-api-php-client/compare/v2.13.1...v2.13.2) (2023-03-23) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * 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)) 102 | 103 | ## [2.13.1](https://github.com/googleapis/google-api-php-client/compare/v2.13.0...v2.13.1) (2023-03-13) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * 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)) 109 | 110 | ## [2.13.0](https://github.com/googleapis/google-api-php-client/compare/v2.12.6...v2.13.0) (2022-12-19) 111 | 112 | 113 | ### Features 114 | 115 | * 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)) 116 | 117 | 118 | ### Bug Fixes 119 | 120 | * 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)) 121 | * 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)) 122 | * Lint errors ([#2315](https://github.com/googleapis/google-api-php-client/issues/2315)) ([88cc63c](https://github.com/googleapis/google-api-php-client/commit/88cc63c38b0cf88629f66fdf8ba6006f6c6d5a2c)) 123 | * 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)) 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/googleapis/google-api-php-client/workflows/.github/workflows/tests.yml/badge.svg) 2 | 3 | # Google APIs Client Library for PHP # 4 | 5 | **NOTE**: please check to see if the package you'd like to install is available in our 6 | list of [Google cloud packages](https://cloud.google.com/php/docs/reference) first, as 7 | these are the recommended libraries. 8 | 9 |
10 |
Reference Docs
https://googleapis.github.io/google-api-php-client/
11 |
License
Apache 2.0
12 |
13 | 14 | The Google API Client Library enables you to work with Google APIs such as Gmail, Drive or YouTube on your server. 15 | 16 | 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. 17 | 18 | ## Google Cloud Platform 19 | 20 | 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). 21 | 22 | [cloud-datastore]: https://github.com/googleapis/google-cloud-php-datastore 23 | [cloud-pubsub]: https://github.com/googleapis/google-cloud-php-pubsub 24 | [cloud-storage]: https://github.com/googleapis/google-cloud-php-storage 25 | [cloud-compute]: https://github.com/googleapis/google-cloud-php-compute 26 | 27 | ## Requirements ## 28 | * [PHP 8.0 or higher](https://www.php.net/) 29 | 30 | ## Developer Documentation ## 31 | 32 | The [docs folder](docs/) provides detailed guides for using this library. 33 | 34 | ## Installation ## 35 | 36 | You can use **Composer** or simply **Download the Release** 37 | 38 | ### Composer 39 | 40 | The preferred method is via [composer](https://getcomposer.org/). Follow the 41 | [installation instructions](https://getcomposer.org/doc/00-intro.md) if you do not already have 42 | composer installed. 43 | 44 | Once composer is installed, execute the following command in your project root to install this library: 45 | 46 | ```sh 47 | composer require google/apiclient 48 | ``` 49 | 50 | 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: 51 | ``` 52 | { 53 | "config": { 54 | "process-timeout": 600 55 | } 56 | } 57 | ``` 58 | 59 | Finally, be sure to include the autoloader: 60 | 61 | ```php 62 | require_once '/path/to/your-project/vendor/autoload.php'; 63 | ``` 64 | 65 | 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. 66 | 67 | #### Cleaning up unused services 68 | 69 | There are over 200 Google API services. The chances are good that you will not 70 | want them all. In order to avoid shipping these dependencies with your code, 71 | you can run the `Google\Task\Composer::cleanup` task and specify the services 72 | you want to keep in `composer.json`: 73 | 74 | ```json 75 | { 76 | "require": { 77 | "google/apiclient": "^2.15.0" 78 | }, 79 | "scripts": { 80 | "pre-autoload-dump": "Google\\Task\\Composer::cleanup" 81 | }, 82 | "extra": { 83 | "google/apiclient-services": [ 84 | "Drive", 85 | "YouTube" 86 | ] 87 | } 88 | } 89 | ``` 90 | 91 | This example will remove all services other than "Drive" and "YouTube" when 92 | `composer update` or a fresh `composer install` is run. 93 | 94 | **IMPORTANT**: If you add any services back in `composer.json`, you will need to 95 | remove the `vendor/google/apiclient-services` directory explicitly for the 96 | change you made to have effect: 97 | 98 | ```sh 99 | rm -r vendor/google/apiclient-services 100 | composer update 101 | ``` 102 | 103 | **NOTE**: This command performs an exact match on the service name, so to keep 104 | `YouTubeReporting` and `YouTubeAnalytics` as well, you'd need to add each of 105 | them explicitly: 106 | 107 | ```json 108 | { 109 | "extra": { 110 | "google/apiclient-services": [ 111 | "Drive", 112 | "YouTube", 113 | "YouTubeAnalytics", 114 | "YouTubeReporting" 115 | ] 116 | } 117 | } 118 | ``` 119 | 120 | ### Download the Release 121 | 122 | 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 123 | with the name `google-api-php-client-[RELEASE_NAME].zip` for a package including this library and its dependencies. 124 | 125 | Uncompress the zip file you download, and include the autoloader in your project: 126 | 127 | ```php 128 | require_once '/path/to/google-api-php-client/vendor/autoload.php'; 129 | ``` 130 | 131 | For additional installation and setup instructions, see [the documentation](docs/). 132 | 133 | ## Examples ## 134 | See the [`examples/`](examples) directory for examples of the key client features. You can 135 | view them in your browser by running the php built-in web server. 136 | 137 | ``` 138 | $ php -S localhost:8000 -t examples/ 139 | ``` 140 | 141 | And then browsing to the host and port you specified 142 | (in the above example, `http://localhost:8000`). 143 | 144 | ### Basic Example ### 145 | 146 | ```php 147 | // include your composer dependencies 148 | require_once 'vendor/autoload.php'; 149 | 150 | $client = new Google\Client(); 151 | $client->setApplicationName("Client_Library_Examples"); 152 | $client->setDeveloperKey("YOUR_APP_KEY"); 153 | 154 | $service = new Google\Service\Books($client); 155 | $query = 'Henry David Thoreau'; 156 | $optParams = [ 157 | 'filter' => 'free-ebooks', 158 | ]; 159 | $results = $service->volumes->listVolumes($query, $optParams); 160 | 161 | foreach ($results->getItems() as $item) { 162 | echo $item['volumeInfo']['title'], "
\n"; 163 | } 164 | ``` 165 | 166 | ### Authentication with OAuth ### 167 | 168 | > An example of this can be seen in [`examples/simple-file-upload.php`](examples/simple-file-upload.php). 169 | 170 | 1. Follow the instructions to [Create Web Application Credentials](docs/oauth-web.md#create-authorization-credentials) 171 | 1. Download the JSON credentials 172 | 1. Set the path to these credentials using `Google\Client::setAuthConfig`: 173 | 174 | ```php 175 | $client = new Google\Client(); 176 | $client->setAuthConfig('/path/to/client_credentials.json'); 177 | ``` 178 | 179 | 1. Set the scopes required for the API you are going to call 180 | 181 | ```php 182 | $client->addScope(Google\Service\Drive::DRIVE); 183 | ``` 184 | 185 | 1. Set your application's redirect URI 186 | 187 | ```php 188 | // Your redirect URI can be any registered URI, but in this example 189 | // we redirect back to this same page 190 | $redirect_uri = 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF']; 191 | $client->setRedirectUri($redirect_uri); 192 | ``` 193 | 194 | 1. In the script handling the redirect URI, exchange the authorization code for an access token: 195 | 196 | ```php 197 | if (isset($_GET['code'])) { 198 | $token = $client->fetchAccessTokenWithAuthCode($_GET['code']); 199 | } 200 | ``` 201 | 202 | ### Authentication with Service Accounts ### 203 | 204 | > An example of this can be seen in [`examples/service-account.php`](examples/service-account.php). 205 | 206 | Some APIs 207 | (such as the [YouTube Data API](https://developers.google.com/youtube/v3/)) do 208 | not support service accounts. Check with the specific API documentation if API 209 | calls return unexpected 401 or 403 errors. 210 | 211 | 1. Follow the instructions to [Create a Service Account](docs/oauth-server.md#creating-a-service-account) 212 | 1. Download the JSON credentials 213 | 1. Set the path to these credentials using the `GOOGLE_APPLICATION_CREDENTIALS` environment variable: 214 | 215 | ```php 216 | putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json'); 217 | ``` 218 | 219 | 1. Tell the Google client to use your service account credentials to authenticate: 220 | 221 | ```php 222 | $client = new Google\Client(); 223 | $client->useApplicationDefaultCredentials(); 224 | ``` 225 | 226 | 1. Set the scopes required for the API you are going to call 227 | 228 | ```php 229 | $client->addScope(Google\Service\Drive::DRIVE); 230 | ``` 231 | 232 | 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: 233 | 234 | ```php 235 | $client->setSubject($user_to_impersonate); 236 | ``` 237 | 238 | #### How to use a specific JSON key 239 | 240 | If you want to a specific JSON key instead of using `GOOGLE_APPLICATION_CREDENTIALS` environment variable, you can do this: 241 | 242 | ```php 243 | $jsonKey = [ 244 | 'type' => 'service_account', 245 | // ... 246 | ]; 247 | $client = new Google\Client(); 248 | $client->setAuthConfig($jsonKey); 249 | ``` 250 | 251 | ### Making Requests ### 252 | 253 | 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/). 254 | 255 | A JSON request to the [Datastore API](https://developers.google.com/apis-explorer/#p/datastore/v1beta3/datastore.projects.runQuery) would look like this: 256 | 257 | ``` 258 | POST https://datastore.googleapis.com/v1beta3/projects/YOUR_PROJECT_ID:runQuery?key=YOUR_API_KEY 259 | ``` 260 | ```json 261 | { 262 | "query": { 263 | "kind": [{ 264 | "name": "Book" 265 | }], 266 | "order": [{ 267 | "property": { 268 | "name": "title" 269 | }, 270 | "direction": "descending" 271 | }], 272 | "limit": 10 273 | } 274 | } 275 | ``` 276 | 277 | Using this library, the same call would look something like this: 278 | 279 | ```php 280 | // create the datastore service class 281 | $datastore = new Google\Service\Datastore($client); 282 | 283 | // build the query - this maps directly to the JSON 284 | $query = new Google\Service\Datastore\Query([ 285 | 'kind' => [ 286 | [ 287 | 'name' => 'Book', 288 | ], 289 | ], 290 | 'order' => [ 291 | 'property' => [ 292 | 'name' => 'title', 293 | ], 294 | 'direction' => 'descending', 295 | ], 296 | 'limit' => 10, 297 | ]); 298 | 299 | // build the request and response 300 | $request = new Google\Service\Datastore\RunQueryRequest(['query' => $query]); 301 | $response = $datastore->projects->runQuery('YOUR_DATASET_ID', $request); 302 | ``` 303 | 304 | However, as each property of the JSON API has a corresponding generated class, the above code could also be written like this: 305 | 306 | ```php 307 | // create the datastore service class 308 | $datastore = new Google\Service\Datastore($client); 309 | 310 | // build the query 311 | $request = new Google\Service\Datastore_RunQueryRequest(); 312 | $query = new Google\Service\Datastore\Query(); 313 | // - set the order 314 | $order = new Google\Service\Datastore_PropertyOrder(); 315 | $order->setDirection('descending'); 316 | $property = new Google\Service\Datastore\PropertyReference(); 317 | $property->setName('title'); 318 | $order->setProperty($property); 319 | $query->setOrder([$order]); 320 | // - set the kinds 321 | $kind = new Google\Service\Datastore\KindExpression(); 322 | $kind->setName('Book'); 323 | $query->setKinds([$kind]); 324 | // - set the limit 325 | $query->setLimit(10); 326 | 327 | // add the query to the request and make the request 328 | $request->setQuery($query); 329 | $response = $datastore->projects->runQuery('YOUR_DATASET_ID', $request); 330 | ``` 331 | 332 | The method used is a matter of preference, but *it will be very difficult to use this library without first understanding the JSON syntax for the API*, so it is recommended to look at the [APIs Explorer](https://developers.google.com/apis-explorer/#p/) before using any of the services here. 333 | 334 | ### Making HTTP Requests Directly ### 335 | 336 | 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. 337 | 338 | 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. 339 | 340 | The `authorize` method returns an authorized [Guzzle Client](http://docs.guzzlephp.org/), so any request made using the client will contain the corresponding authorization. 341 | 342 | ```php 343 | // create the Google client 344 | $client = new Google\Client(); 345 | 346 | /** 347 | * Set your method for authentication. Depending on the API, This could be 348 | * directly with an access token, API key, or (recommended) using 349 | * Application Default Credentials. 350 | */ 351 | $client->useApplicationDefaultCredentials(); 352 | $client->addScope(Google\Service\Plus::PLUS_ME); 353 | 354 | // returns a Guzzle HTTP Client 355 | $httpClient = $client->authorize(); 356 | 357 | // make an HTTP request 358 | $response = $httpClient->get('https://www.googleapis.com/plus/v1/people/me'); 359 | ``` 360 | 361 | ### Caching ### 362 | 363 | 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: 364 | 365 | ```php 366 | use League\Flysystem\Adapter\Local; 367 | use League\Flysystem\Filesystem; 368 | use Cache\Adapter\Filesystem\FilesystemCachePool; 369 | 370 | $filesystemAdapter = new Local(__DIR__.'/'); 371 | $filesystem = new Filesystem($filesystemAdapter); 372 | 373 | $cache = new FilesystemCachePool($filesystem); 374 | $client->setCache($cache); 375 | ``` 376 | 377 | In this example we use [PHP Cache](http://www.php-cache.com/). Add this to your project with composer: 378 | 379 | ``` 380 | composer require cache/filesystem-adapter 381 | ``` 382 | 383 | ### Updating Tokens ### 384 | 385 | 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: 386 | 387 | ```php 388 | $logger = new Monolog\Logger(); 389 | $tokenCallback = function ($cacheKey, $accessToken) use ($logger) { 390 | $logger->debug(sprintf('new access token received at cache key %s', $cacheKey)); 391 | }; 392 | $client->setTokenCallback($tokenCallback); 393 | ``` 394 | 395 | ### Debugging Your HTTP Request using Charles ### 396 | 397 | It is often very useful to debug your API calls by viewing the raw HTTP request. This library supports the use of [Charles Web Proxy](https://www.charlesproxy.com/documentation/getting-started/). Download and run Charles, and then capture all HTTP traffic through Charles with the following code: 398 | 399 | ```php 400 | // FOR DEBUGGING ONLY 401 | $httpClient = new GuzzleHttp\Client([ 402 | 'proxy' => 'localhost:8888', // by default, Charles runs on localhost port 8888 403 | 'verify' => false, // otherwise HTTPS requests will fail. 404 | ]); 405 | 406 | $client = new Google\Client(); 407 | $client->setHttpClient($httpClient); 408 | ``` 409 | 410 | Now all calls made by this library will appear in the Charles UI. 411 | 412 | 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`. 413 | 414 | ### Controlling HTTP Client Configuration Directly 415 | 416 | 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. 417 | 418 | Let's say, for instance, we wished to apply a referrer to each request. 419 | 420 | ```php 421 | use GuzzleHttp\Client; 422 | 423 | $httpClient = new Client([ 424 | 'headers' => [ 425 | 'referer' => 'mysite.com' 426 | ] 427 | ]); 428 | 429 | $client = new Google\Client(); 430 | $client->setHttpClient($httpClient); 431 | ``` 432 | 433 | Other Guzzle features such as [Handlers and Middleware](http://docs.guzzlephp.org/en/stable/handlers-and-middleware.html) offer even more control. 434 | 435 | ### Partial Consent and Granted Scopes 436 | 437 | When using OAuth2 3LO (e.g. you're a client requesting credentials from a 3rd 438 | party, such as in the [simple file upload example](examples/simple-file-upload.php)), 439 | you may want to take advantage of Partial Consent. 440 | 441 | To allow clients to only grant certain scopes in the OAuth2 screen, pass the 442 | querystring parameter for `enable_serial_consent` when generating the 443 | authorization URL: 444 | 445 | ```php 446 | $authUrl = $client->createAuthUrl($scope, ['enable_serial_consent' => 'true']); 447 | ``` 448 | 449 | Once the flow is completed, you can see which scopes were granted by calling 450 | `getGrantedScope` on the OAuth2 object: 451 | 452 | ```php 453 | // Space-separated string of granted scopes if it exists, otherwise null. 454 | echo $client->getOAuth2Service()->getGrantedScope(); 455 | ``` 456 | 457 | ### Service Specific Examples ### 458 | 459 | YouTube: https://github.com/youtube/api-samples/tree/master/php 460 | 461 | ## How Do I Contribute? ## 462 | 463 | 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. 464 | 465 | ## Frequently Asked Questions ## 466 | 467 | ### What do I do if something isn't working? ### 468 | 469 | 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 470 | 471 | 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. 472 | 473 | ### I want an example of X! ### 474 | 475 | 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! 476 | 477 | ### Why do some Google\Service classes have weird names? ### 478 | 479 | 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. 480 | 481 | ### How do I deal with non-JSON response types? ### 482 | 483 | 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: 484 | 485 | ```php 486 | $opt_params = array( 487 | 'alt' => "json" 488 | ); 489 | ``` 490 | 491 | ### How do I set a field to null? ### 492 | 493 | 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. 494 | 495 | ## Code Quality ## 496 | 497 | Run the PHPUnit tests with PHPUnit. You can configure an API key and token in BaseTest.php to run all calls, but this will require some setup on the Google Developer Console. 498 | 499 | phpunit tests/ 500 | 501 | ### Coding Style 502 | 503 | To check for coding style violations, run 504 | 505 | ``` 506 | vendor/bin/phpcs src --standard=style/ruleset.xml -np 507 | ``` 508 | 509 | To automatically fix (fixable) coding style violations, run 510 | 511 | ``` 512 | vendor/bin/phpcbf src --standard=style/ruleset.xml 513 | ``` 514 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security issue, please use [g.co/vulnz](https://g.co/vulnz). 4 | 5 | The Google Security Team will respond within 5 working days of your report on g.co/vulnz. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "google/apiclient", 3 | "type": "library", 4 | "description": "Client library for Google APIs", 5 | "keywords": ["google"], 6 | "homepage": "http://developers.google.com/api-client-library/php", 7 | "license": "Apache-2.0", 8 | "require": { 9 | "php": "^8.0", 10 | "google/auth": "^1.37", 11 | "google/apiclient-services": "~0.350", 12 | "firebase/php-jwt": "^6.0", 13 | "monolog/monolog": "^2.9||^3.0", 14 | "phpseclib/phpseclib": "^3.0.36", 15 | "guzzlehttp/guzzle": "^7.4.5", 16 | "guzzlehttp/psr7": "^2.6" 17 | }, 18 | "require-dev": { 19 | "squizlabs/php_codesniffer": "^3.8", 20 | "symfony/dom-crawler": "~2.1", 21 | "symfony/css-selector": "~2.1", 22 | "cache/filesystem-adapter": "^1.1", 23 | "phpcompatibility/php-compatibility": "^9.2", 24 | "composer/composer": "^1.10.23", 25 | "phpspec/prophecy-phpunit": "^2.1", 26 | "phpunit/phpunit": "^9.6" 27 | }, 28 | "suggest": { 29 | "cache/filesystem-adapter": "For caching certs and tokens (using Google\\Client::setCache)" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Google\\": "src/" 34 | }, 35 | "files": [ 36 | "src/aliases.php" 37 | ], 38 | "classmap": [ 39 | "src/aliases.php" 40 | ] 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-main": "2.x-dev" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AccessToken/Revoke.php: -------------------------------------------------------------------------------- 1 | http = $http; 45 | } 46 | 47 | /** 48 | * Revoke an OAuth2 access token or refresh token. This method will revoke the current access 49 | * token, if a token isn't provided. 50 | * 51 | * @param string|array $token The token (access token or a refresh token) that should be revoked. 52 | * @return boolean Returns True if the revocation was successful, otherwise False. 53 | */ 54 | public function revokeToken($token) 55 | { 56 | if (is_array($token)) { 57 | if (isset($token['refresh_token'])) { 58 | $token = $token['refresh_token']; 59 | } else { 60 | $token = $token['access_token']; 61 | } 62 | } 63 | 64 | $body = Psr7\Utils::streamFor(http_build_query(['token' => $token])); 65 | $request = new Request( 66 | 'POST', 67 | Client::OAUTH2_REVOKE_URI, 68 | [ 69 | 'Cache-Control' => 'no-store', 70 | 'Content-Type' => 'application/x-www-form-urlencoded', 71 | ], 72 | $body 73 | ); 74 | 75 | $httpHandler = HttpHandlerFactory::build($this->http); 76 | 77 | $response = $httpHandler($request); 78 | 79 | return $response->getStatusCode() == 200; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/AccessToken/Verify.php: -------------------------------------------------------------------------------- 1 | http = $http; 83 | $this->cache = $cache; 84 | $this->jwt = $jwt ?: $this->getJwtService(); 85 | } 86 | 87 | /** 88 | * Verifies an id token and returns the authenticated apiLoginTicket. 89 | * Throws an exception if the id token is not valid. 90 | * The audience parameter can be used to control which id tokens are 91 | * accepted. By default, the id token must have been issued to this OAuth2 client. 92 | * 93 | * @param string $idToken the ID token in JWT format 94 | * @param string $audience Optional. The audience to verify against JWt "aud" 95 | * @return array|false the token payload, if successful 96 | */ 97 | public function verifyIdToken($idToken, $audience = null) 98 | { 99 | if (empty($idToken)) { 100 | throw new LogicException('id_token cannot be null'); 101 | } 102 | 103 | // set phpseclib constants if applicable 104 | $this->setPhpsecConstants(); 105 | 106 | // Check signature 107 | $certs = $this->getFederatedSignOnCerts(); 108 | foreach ($certs as $cert) { 109 | try { 110 | $args = [$idToken]; 111 | $publicKey = $this->getPublicKey($cert); 112 | if (class_exists(Key::class)) { 113 | $args[] = new Key($publicKey, 'RS256'); 114 | } else { 115 | $args[] = $publicKey; 116 | $args[] = ['RS256']; 117 | } 118 | $payload = \call_user_func_array([$this->jwt, 'decode'], $args); 119 | 120 | if (property_exists($payload, 'aud')) { 121 | if ($audience && $payload->aud != $audience) { 122 | return false; 123 | } 124 | } 125 | 126 | // support HTTP and HTTPS issuers 127 | // @see https://developers.google.com/identity/sign-in/web/backend-auth 128 | $issuers = [self::OAUTH2_ISSUER, self::OAUTH2_ISSUER_HTTPS]; 129 | if (!isset($payload->iss) || !in_array($payload->iss, $issuers)) { 130 | return false; 131 | } 132 | 133 | return (array)$payload; 134 | } catch (ExpiredException $e) { // @phpstan-ignore-line 135 | return false; 136 | } catch (ExpiredExceptionV3 $e) { 137 | return false; 138 | } catch (SignatureInvalidException $e) { 139 | // continue 140 | } catch (DomainException $e) { 141 | // continue 142 | } 143 | } 144 | 145 | return false; 146 | } 147 | 148 | private function getCache() 149 | { 150 | return $this->cache; 151 | } 152 | 153 | /** 154 | * Retrieve and cache a certificates file. 155 | * 156 | * @param string $url location 157 | * @return array certificates 158 | * @throws \Google\Exception 159 | */ 160 | private function retrieveCertsFromLocation($url) 161 | { 162 | // If we're retrieving a local file, just grab it. 163 | if (0 !== strpos($url, 'http')) { 164 | if (!$file = file_get_contents($url)) { 165 | throw new GoogleException( 166 | "Failed to retrieve verification certificates: '". 167 | $url."'." 168 | ); 169 | } 170 | 171 | return json_decode($file, true); 172 | } 173 | 174 | // @phpstan-ignore-next-line 175 | $response = $this->http->get($url); 176 | 177 | if ($response->getStatusCode() == 200) { 178 | return json_decode((string)$response->getBody(), true); 179 | } 180 | throw new GoogleException( 181 | sprintf( 182 | 'Failed to retrieve verification certificates: "%s".', 183 | $response->getBody()->getContents() 184 | ), 185 | $response->getStatusCode() 186 | ); 187 | } 188 | 189 | // Gets federated sign-on certificates to use for verifying identity tokens. 190 | // Returns certs as array structure, where keys are key ids, and values 191 | // are PEM encoded certificates. 192 | private function getFederatedSignOnCerts() 193 | { 194 | $certs = null; 195 | if ($cache = $this->getCache()) { 196 | $cacheItem = $cache->getItem('federated_signon_certs_v3'); 197 | $certs = $cacheItem->get(); 198 | } 199 | 200 | 201 | if (!$certs) { 202 | $certs = $this->retrieveCertsFromLocation( 203 | self::FEDERATED_SIGNON_CERT_URL 204 | ); 205 | 206 | if ($cache) { 207 | $cacheItem->expiresAt(new DateTime('+1 hour')); 208 | $cacheItem->set($certs); 209 | $cache->save($cacheItem); 210 | } 211 | } 212 | 213 | if (!isset($certs['keys'])) { 214 | throw new InvalidArgumentException( 215 | 'federated sign-on certs expects "keys" to be set' 216 | ); 217 | } 218 | 219 | return $certs['keys']; 220 | } 221 | 222 | private function getJwtService() 223 | { 224 | $jwt = new JWT(); 225 | if ($jwt::$leeway < 1) { 226 | // Ensures JWT leeway is at least 1 227 | // @see https://github.com/google/google-api-php-client/issues/827 228 | $jwt::$leeway = 1; 229 | } 230 | 231 | return $jwt; 232 | } 233 | 234 | private function getPublicKey($cert) 235 | { 236 | $modulus = new BigInteger($this->jwt->urlsafeB64Decode($cert['n']), 256); 237 | $exponent = new BigInteger($this->jwt->urlsafeB64Decode($cert['e']), 256); 238 | $component = ['n' => $modulus, 'e' => $exponent]; 239 | 240 | $loader = PublicKeyLoader::load($component); 241 | 242 | return $loader->toString('PKCS8'); 243 | } 244 | 245 | /** 246 | * phpseclib calls "phpinfo" by default, which requires special 247 | * whitelisting in the AppEngine VM environment. This function 248 | * sets constants to bypass the need for phpseclib to check phpinfo 249 | * 250 | * @see phpseclib/Math/BigInteger 251 | * @see https://github.com/GoogleCloudPlatform/getting-started-php/issues/85 252 | */ 253 | private function setPhpsecConstants() 254 | { 255 | if (filter_var(getenv('GAE_VM'), FILTER_VALIDATE_BOOLEAN)) { 256 | if (!defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) { 257 | define('MATH_BIGINTEGER_OPENSSL_ENABLED', true); 258 | } 259 | if (!defined('CRYPT_RSA_MODE')) { 260 | define('CRYPT_RSA_MODE', AES::ENGINE_OPENSSL); 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/AuthHandler/AuthHandlerFactory.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 26 | $this->cacheConfig = $cacheConfig; 27 | } 28 | 29 | public function attachCredentials( 30 | ClientInterface $http, 31 | CredentialsLoader $credentials, 32 | ?callable $tokenCallback = null 33 | ) { 34 | // use the provided cache 35 | if ($this->cache) { 36 | $credentials = new FetchAuthTokenCache( 37 | $credentials, 38 | $this->cacheConfig, 39 | $this->cache 40 | ); 41 | } 42 | 43 | return $this->attachCredentialsCache($http, $credentials, $tokenCallback); 44 | } 45 | 46 | public function attachCredentialsCache( 47 | ClientInterface $http, 48 | FetchAuthTokenCache $credentials, 49 | ?callable $tokenCallback = null 50 | ) { 51 | // if we end up needing to make an HTTP request to retrieve credentials, we 52 | // can use our existing one, but we need to throw exceptions so the error 53 | // bubbles up. 54 | $authHttp = $this->createAuthHttp($http); 55 | $authHttpHandler = HttpHandlerFactory::build($authHttp); 56 | $middleware = new AuthTokenMiddleware( 57 | $credentials, 58 | $authHttpHandler, 59 | $tokenCallback 60 | ); 61 | 62 | $config = $http->getConfig(); 63 | $config['handler']->remove('google_auth'); 64 | $config['handler']->push($middleware, 'google_auth'); 65 | $config['auth'] = 'google_auth'; 66 | $http = new Client($config); 67 | 68 | return $http; 69 | } 70 | 71 | public function attachToken(ClientInterface $http, array $token, array $scopes) 72 | { 73 | $tokenFunc = function ($scopes) use ($token) { 74 | return $token['access_token']; 75 | }; 76 | 77 | // Derive a cache prefix from the token, to ensure setting a new token 78 | // results in a cache-miss. 79 | // Note: Supplying a custom "prefix" will bust this behavior. 80 | $cacheConfig = $this->cacheConfig; 81 | if (!isset($cacheConfig['prefix']) && isset($token['access_token'])) { 82 | $cacheConfig['prefix'] = substr(sha1($token['access_token']), -10); 83 | } 84 | 85 | $middleware = new ScopedAccessTokenMiddleware( 86 | $tokenFunc, 87 | $scopes, 88 | $cacheConfig, 89 | $this->cache 90 | ); 91 | 92 | $config = $http->getConfig(); 93 | $config['handler']->remove('google_auth'); 94 | $config['handler']->push($middleware, 'google_auth'); 95 | $config['auth'] = 'scoped'; 96 | $http = new Client($config); 97 | 98 | return $http; 99 | } 100 | 101 | public function attachKey(ClientInterface $http, $key) 102 | { 103 | $middleware = new SimpleMiddleware(['key' => $key]); 104 | 105 | $config = $http->getConfig(); 106 | $config['handler']->remove('google_auth'); 107 | $config['handler']->push($middleware, 'google_auth'); 108 | $config['auth'] = 'simple'; 109 | $http = new Client($config); 110 | 111 | return $http; 112 | } 113 | 114 | private function createAuthHttp(ClientInterface $http) 115 | { 116 | return new Client(['http_errors' => true] + $http->getConfig()); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/AuthHandler/Guzzle7AuthHandler.php: -------------------------------------------------------------------------------- 1 | config = array_merge([ 181 | 'application_name' => '', 182 | 'base_path' => self::API_BASE_PATH, 183 | 'client_id' => '', 184 | 'client_secret' => '', 185 | 'credentials' => null, 186 | 'scopes' => null, 187 | 'quota_project' => null, 188 | 'redirect_uri' => null, 189 | 'state' => null, 190 | 'developer_key' => '', 191 | 'use_application_default_credentials' => false, 192 | 'signing_key' => null, 193 | 'signing_algorithm' => null, 194 | 'subject' => null, 195 | 'hd' => '', 196 | 'prompt' => '', 197 | 'openid.realm' => '', 198 | 'include_granted_scopes' => null, 199 | 'logger' => null, 200 | 'login_hint' => '', 201 | 'request_visible_actions' => '', 202 | 'access_type' => 'online', 203 | 'approval_prompt' => 'auto', 204 | 'retry' => [], 205 | 'retry_map' => null, 206 | 'cache' => null, 207 | 'cache_config' => [], 208 | 'token_callback' => null, 209 | 'jwt' => null, 210 | 'api_format_v2' => false, 211 | 'universe_domain' => getenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN') 212 | ?: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN, 213 | ], $config); 214 | 215 | if (!is_null($this->config['credentials'])) { 216 | if ($this->config['credentials'] instanceof CredentialsLoader) { 217 | $this->credentials = $this->config['credentials']; 218 | } else { 219 | $this->setAuthConfig($this->config['credentials']); 220 | } 221 | unset($this->config['credentials']); 222 | } 223 | 224 | if (!is_null($this->config['scopes'])) { 225 | $this->setScopes($this->config['scopes']); 226 | unset($this->config['scopes']); 227 | } 228 | 229 | // Set a default token callback to update the in-memory access token 230 | if (is_null($this->config['token_callback'])) { 231 | $this->config['token_callback'] = function ($cacheKey, $newAccessToken) { 232 | $this->setAccessToken( 233 | [ 234 | 'access_token' => $newAccessToken, 235 | 'expires_in' => 3600, // Google default 236 | 'created' => time(), 237 | ] 238 | ); 239 | }; 240 | } 241 | 242 | if (!is_null($this->config['cache'])) { 243 | $this->setCache($this->config['cache']); 244 | unset($this->config['cache']); 245 | } 246 | 247 | if (!is_null($this->config['logger'])) { 248 | $this->setLogger($this->config['logger']); 249 | unset($this->config['logger']); 250 | } 251 | } 252 | 253 | /** 254 | * Get a string containing the version of the library. 255 | * 256 | * @return string 257 | */ 258 | public function getLibraryVersion() 259 | { 260 | return self::LIBVER; 261 | } 262 | 263 | /** 264 | * For backwards compatibility 265 | * alias for fetchAccessTokenWithAuthCode 266 | * 267 | * @param string $code string code from accounts.google.com 268 | * @return array access token 269 | * @deprecated 270 | */ 271 | public function authenticate($code) 272 | { 273 | return $this->fetchAccessTokenWithAuthCode($code); 274 | } 275 | 276 | /** 277 | * Attempt to exchange a code for an valid authentication token. 278 | * Helper wrapped around the OAuth 2.0 implementation. 279 | * 280 | * @param string $code code from accounts.google.com 281 | * @param string $codeVerifier the code verifier used for PKCE (if applicable) 282 | * @return array access token 283 | */ 284 | public function fetchAccessTokenWithAuthCode($code, $codeVerifier = null) 285 | { 286 | if (strlen($code) == 0) { 287 | throw new InvalidArgumentException("Invalid code"); 288 | } 289 | 290 | $auth = $this->getOAuth2Service(); 291 | $auth->setCode($code); 292 | $auth->setRedirectUri($this->getRedirectUri()); 293 | if ($codeVerifier) { 294 | $auth->setCodeVerifier($codeVerifier); 295 | } 296 | 297 | $httpHandler = HttpHandlerFactory::build($this->getHttpClient()); 298 | $creds = $auth->fetchAuthToken($httpHandler); 299 | if ($creds && isset($creds['access_token'])) { 300 | $creds['created'] = time(); 301 | $this->setAccessToken($creds); 302 | } 303 | 304 | return $creds; 305 | } 306 | 307 | /** 308 | * For backwards compatibility 309 | * alias for fetchAccessTokenWithAssertion 310 | * 311 | * @return array access token 312 | * @deprecated 313 | */ 314 | public function refreshTokenWithAssertion() 315 | { 316 | return $this->fetchAccessTokenWithAssertion(); 317 | } 318 | 319 | /** 320 | * Fetches a fresh access token with a given assertion token. 321 | * @param ClientInterface $authHttp optional. 322 | * @return array access token 323 | */ 324 | public function fetchAccessTokenWithAssertion(?ClientInterface $authHttp = null) 325 | { 326 | if (!$this->isUsingApplicationDefaultCredentials()) { 327 | throw new DomainException( 328 | 'set the JSON service account credentials using' 329 | . ' Google\Client::setAuthConfig or set the path to your JSON file' 330 | . ' with the "GOOGLE_APPLICATION_CREDENTIALS" environment variable' 331 | . ' and call Google\Client::useApplicationDefaultCredentials to' 332 | . ' refresh a token with assertion.' 333 | ); 334 | } 335 | 336 | $this->getLogger()->log( 337 | 'info', 338 | 'OAuth2 access token refresh with Signed JWT assertion grants.' 339 | ); 340 | 341 | $credentials = $this->createApplicationDefaultCredentials(); 342 | 343 | $httpHandler = HttpHandlerFactory::build($authHttp); 344 | $creds = $credentials->fetchAuthToken($httpHandler); 345 | if ($creds && isset($creds['access_token'])) { 346 | $creds['created'] = time(); 347 | $this->setAccessToken($creds); 348 | } 349 | 350 | return $creds; 351 | } 352 | 353 | /** 354 | * For backwards compatibility 355 | * alias for fetchAccessTokenWithRefreshToken 356 | * 357 | * @param string $refreshToken 358 | * @return array access token 359 | */ 360 | public function refreshToken($refreshToken) 361 | { 362 | return $this->fetchAccessTokenWithRefreshToken($refreshToken); 363 | } 364 | 365 | /** 366 | * Fetches a fresh OAuth 2.0 access token with the given refresh token. 367 | * @param string $refreshToken 368 | * @return array access token 369 | */ 370 | public function fetchAccessTokenWithRefreshToken($refreshToken = null) 371 | { 372 | if (null === $refreshToken) { 373 | if (!isset($this->token['refresh_token'])) { 374 | throw new LogicException( 375 | 'refresh token must be passed in or set as part of setAccessToken' 376 | ); 377 | } 378 | $refreshToken = $this->token['refresh_token']; 379 | } 380 | $this->getLogger()->info('OAuth2 access token refresh'); 381 | $auth = $this->getOAuth2Service(); 382 | $auth->setRefreshToken($refreshToken); 383 | 384 | $httpHandler = HttpHandlerFactory::build($this->getHttpClient()); 385 | $creds = $auth->fetchAuthToken($httpHandler); 386 | if ($creds && isset($creds['access_token'])) { 387 | $creds['created'] = time(); 388 | if (!isset($creds['refresh_token'])) { 389 | $creds['refresh_token'] = $refreshToken; 390 | } 391 | $this->setAccessToken($creds); 392 | } 393 | 394 | return $creds; 395 | } 396 | 397 | /** 398 | * Create a URL to obtain user authorization. 399 | * The authorization endpoint allows the user to first 400 | * authenticate, and then grant/deny the access request. 401 | * @param string|array $scope The scope is expressed as an array or list of space-delimited strings. 402 | * @param array $queryParams Querystring params to add to the authorization URL. 403 | * @return string 404 | */ 405 | public function createAuthUrl($scope = null, array $queryParams = []) 406 | { 407 | if (empty($scope)) { 408 | $scope = $this->prepareScopes(); 409 | } 410 | if (is_array($scope)) { 411 | $scope = implode(' ', $scope); 412 | } 413 | 414 | // only accept one of prompt or approval_prompt 415 | $approvalPrompt = $this->config['prompt'] 416 | ? null 417 | : $this->config['approval_prompt']; 418 | 419 | // include_granted_scopes should be string "true", string "false", or null 420 | $includeGrantedScopes = $this->config['include_granted_scopes'] === null 421 | ? null 422 | : var_export($this->config['include_granted_scopes'], true); 423 | 424 | $params = array_filter([ 425 | 'access_type' => $this->config['access_type'], 426 | 'approval_prompt' => $approvalPrompt, 427 | 'hd' => $this->config['hd'], 428 | 'include_granted_scopes' => $includeGrantedScopes, 429 | 'login_hint' => $this->config['login_hint'], 430 | 'openid.realm' => $this->config['openid.realm'], 431 | 'prompt' => $this->config['prompt'], 432 | 'redirect_uri' => $this->config['redirect_uri'], 433 | 'response_type' => 'code', 434 | 'scope' => $scope, 435 | 'state' => $this->config['state'], 436 | ]) + $queryParams; 437 | 438 | // If the list of scopes contains plus.login, add request_visible_actions 439 | // to auth URL. 440 | $rva = $this->config['request_visible_actions']; 441 | if (strlen($rva) > 0 && false !== strpos($scope, 'plus.login')) { 442 | $params['request_visible_actions'] = $rva; 443 | } 444 | 445 | $auth = $this->getOAuth2Service(); 446 | 447 | return (string) $auth->buildFullAuthorizationUri($params); 448 | } 449 | 450 | /** 451 | * Adds auth listeners to the HTTP client based on the credentials 452 | * set in the Google API Client object 453 | * 454 | * @param ClientInterface $http the http client object. 455 | * @return ClientInterface the http client object 456 | */ 457 | public function authorize(?ClientInterface $http = null) 458 | { 459 | $http = $http ?: $this->getHttpClient(); 460 | $authHandler = $this->getAuthHandler(); 461 | 462 | // These conditionals represent the decision tree for authentication 463 | // 1. Check if a Google\Auth\CredentialsLoader instance has been supplied via the "credentials" option 464 | // 2. Check for Application Default Credentials 465 | // 3a. Check for an Access Token 466 | // 3b. If access token exists but is expired, try to refresh it 467 | // 4. Check for API Key 468 | if ($this->credentials) { 469 | $this->checkUniverseDomain($this->credentials); 470 | return $authHandler->attachCredentials( 471 | $http, 472 | $this->credentials, 473 | $this->config['token_callback'] 474 | ); 475 | } 476 | 477 | if ($this->isUsingApplicationDefaultCredentials()) { 478 | $credentials = $this->createApplicationDefaultCredentials(); 479 | $this->checkUniverseDomain($credentials); 480 | return $authHandler->attachCredentialsCache( 481 | $http, 482 | $credentials, 483 | $this->config['token_callback'] 484 | ); 485 | } 486 | 487 | if ($token = $this->getAccessToken()) { 488 | $scopes = $this->prepareScopes(); 489 | // add refresh subscriber to request a new token 490 | if (isset($token['refresh_token']) && $this->isAccessTokenExpired()) { 491 | $credentials = $this->createUserRefreshCredentials( 492 | $scopes, 493 | $token['refresh_token'] 494 | ); 495 | $this->checkUniverseDomain($credentials); 496 | return $authHandler->attachCredentials( 497 | $http, 498 | $credentials, 499 | $this->config['token_callback'] 500 | ); 501 | } 502 | 503 | return $authHandler->attachToken($http, $token, (array) $scopes); 504 | } 505 | 506 | if ($key = $this->config['developer_key']) { 507 | return $authHandler->attachKey($http, $key); 508 | } 509 | 510 | return $http; 511 | } 512 | 513 | /** 514 | * Set the configuration to use application default credentials for 515 | * authentication 516 | * 517 | * @see https://developers.google.com/identity/protocols/application-default-credentials 518 | * @param boolean $useAppCreds 519 | */ 520 | public function useApplicationDefaultCredentials($useAppCreds = true) 521 | { 522 | $this->config['use_application_default_credentials'] = $useAppCreds; 523 | } 524 | 525 | /** 526 | * To prevent useApplicationDefaultCredentials from inappropriately being 527 | * called in a conditional 528 | * 529 | * @see https://developers.google.com/identity/protocols/application-default-credentials 530 | */ 531 | public function isUsingApplicationDefaultCredentials() 532 | { 533 | return $this->config['use_application_default_credentials']; 534 | } 535 | 536 | /** 537 | * Set the access token used for requests. 538 | * 539 | * Note that at the time requests are sent, tokens are cached. A token will be 540 | * cached for each combination of service and authentication scopes. If a 541 | * cache pool is not provided, creating a new instance of the client will 542 | * allow modification of access tokens. If a persistent cache pool is 543 | * provided, in order to change the access token, you must clear the cached 544 | * token by calling `$client->getCache()->clear()`. (Use caution in this case, 545 | * as calling `clear()` will remove all cache items, including any items not 546 | * related to Google API PHP Client.) 547 | * 548 | * **NOTE:** The universe domain is assumed to be "googleapis.com" unless 549 | * explicitly set. When setting an access token directly via this method, there 550 | * is no way to verify the universe domain. Be sure to set the "universe_domain" 551 | * option if "googleapis.com" is not intended. 552 | * 553 | * @param string|array $token 554 | * @throws InvalidArgumentException 555 | */ 556 | public function setAccessToken($token) 557 | { 558 | if (is_string($token)) { 559 | if ($json = json_decode($token, true)) { 560 | $token = $json; 561 | } else { 562 | // assume $token is just the token string 563 | $token = [ 564 | 'access_token' => $token, 565 | ]; 566 | } 567 | } 568 | if ($token == null) { 569 | throw new InvalidArgumentException('invalid json token'); 570 | } 571 | if (!isset($token['access_token'])) { 572 | throw new InvalidArgumentException("Invalid token format"); 573 | } 574 | $this->token = $token; 575 | } 576 | 577 | public function getAccessToken() 578 | { 579 | return $this->token; 580 | } 581 | 582 | /** 583 | * @return string|null 584 | */ 585 | public function getRefreshToken() 586 | { 587 | if (isset($this->token['refresh_token'])) { 588 | return $this->token['refresh_token']; 589 | } 590 | 591 | return null; 592 | } 593 | 594 | /** 595 | * Returns if the access_token is expired. 596 | * @return bool Returns True if the access_token is expired. 597 | */ 598 | public function isAccessTokenExpired() 599 | { 600 | if (!$this->token) { 601 | return true; 602 | } 603 | 604 | $created = 0; 605 | if (isset($this->token['created'])) { 606 | $created = $this->token['created']; 607 | } elseif (isset($this->token['id_token'])) { 608 | // check the ID token for "iat" 609 | // signature verification is not required here, as we are just 610 | // using this for convenience to save a round trip request 611 | // to the Google API server 612 | $idToken = $this->token['id_token']; 613 | if (substr_count($idToken, '.') == 2) { 614 | $parts = explode('.', $idToken); 615 | $payload = json_decode(base64_decode($parts[1]), true); 616 | if ($payload && isset($payload['iat'])) { 617 | $created = $payload['iat']; 618 | } 619 | } 620 | } 621 | if (!isset($this->token['expires_in'])) { 622 | // if the token does not have an "expires_in", then it's considered expired 623 | return true; 624 | } 625 | 626 | // If the token is set to expire in the next 30 seconds. 627 | return ($created + ($this->token['expires_in'] - 30)) < time(); 628 | } 629 | 630 | /** 631 | * @deprecated See UPGRADING.md for more information 632 | */ 633 | public function getAuth() 634 | { 635 | throw new BadMethodCallException( 636 | 'This function no longer exists. See UPGRADING.md for more information' 637 | ); 638 | } 639 | 640 | /** 641 | * @deprecated See UPGRADING.md for more information 642 | */ 643 | public function setAuth($auth) 644 | { 645 | throw new BadMethodCallException( 646 | 'This function no longer exists. See UPGRADING.md for more information' 647 | ); 648 | } 649 | 650 | /** 651 | * Set the OAuth 2.0 Client ID. 652 | * @param string $clientId 653 | */ 654 | public function setClientId($clientId) 655 | { 656 | $this->config['client_id'] = $clientId; 657 | } 658 | 659 | public function getClientId() 660 | { 661 | return $this->config['client_id']; 662 | } 663 | 664 | /** 665 | * Set the OAuth 2.0 Client Secret. 666 | * @param string $clientSecret 667 | */ 668 | public function setClientSecret($clientSecret) 669 | { 670 | $this->config['client_secret'] = $clientSecret; 671 | } 672 | 673 | public function getClientSecret() 674 | { 675 | return $this->config['client_secret']; 676 | } 677 | 678 | /** 679 | * Set the OAuth 2.0 Redirect URI. 680 | * @param string $redirectUri 681 | */ 682 | public function setRedirectUri($redirectUri) 683 | { 684 | $this->config['redirect_uri'] = $redirectUri; 685 | } 686 | 687 | public function getRedirectUri() 688 | { 689 | return $this->config['redirect_uri']; 690 | } 691 | 692 | /** 693 | * Set OAuth 2.0 "state" parameter to achieve per-request customization. 694 | * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-22#section-3.1.2.2 695 | * @param string $state 696 | */ 697 | public function setState($state) 698 | { 699 | $this->config['state'] = $state; 700 | } 701 | 702 | /** 703 | * @param string $accessType Possible values for access_type include: 704 | * {@code "offline"} to request offline access from the user. 705 | * {@code "online"} to request online access from the user. 706 | */ 707 | public function setAccessType($accessType) 708 | { 709 | $this->config['access_type'] = $accessType; 710 | } 711 | 712 | /** 713 | * @param string $approvalPrompt Possible values for approval_prompt include: 714 | * {@code "force"} to force the approval UI to appear. 715 | * {@code "auto"} to request auto-approval when possible. (This is the default value) 716 | */ 717 | public function setApprovalPrompt($approvalPrompt) 718 | { 719 | $this->config['approval_prompt'] = $approvalPrompt; 720 | } 721 | 722 | /** 723 | * Set the login hint, email address or sub id. 724 | * @param string $loginHint 725 | */ 726 | public function setLoginHint($loginHint) 727 | { 728 | $this->config['login_hint'] = $loginHint; 729 | } 730 | 731 | /** 732 | * Set the application name, this is included in the User-Agent HTTP header. 733 | * @param string $applicationName 734 | */ 735 | public function setApplicationName($applicationName) 736 | { 737 | $this->config['application_name'] = $applicationName; 738 | } 739 | 740 | /** 741 | * If 'plus.login' is included in the list of requested scopes, you can use 742 | * this method to define types of app activities that your app will write. 743 | * You can find a list of available types here: 744 | * @link https://developers.google.com/+/api/moment-types 745 | * 746 | * @param array $requestVisibleActions Array of app activity types 747 | */ 748 | public function setRequestVisibleActions($requestVisibleActions) 749 | { 750 | if (is_array($requestVisibleActions)) { 751 | $requestVisibleActions = implode(" ", $requestVisibleActions); 752 | } 753 | $this->config['request_visible_actions'] = $requestVisibleActions; 754 | } 755 | 756 | /** 757 | * Set the developer key to use, these are obtained through the API Console. 758 | * @see http://code.google.com/apis/console-help/#generatingdevkeys 759 | * @param string $developerKey 760 | */ 761 | public function setDeveloperKey($developerKey) 762 | { 763 | $this->config['developer_key'] = $developerKey; 764 | } 765 | 766 | /** 767 | * Set the hd (hosted domain) parameter streamlines the login process for 768 | * Google Apps hosted accounts. By including the domain of the user, you 769 | * restrict sign-in to accounts at that domain. 770 | * @param string $hd the domain to use. 771 | */ 772 | public function setHostedDomain($hd) 773 | { 774 | $this->config['hd'] = $hd; 775 | } 776 | 777 | /** 778 | * Set the prompt hint. Valid values are none, consent and select_account. 779 | * If no value is specified and the user has not previously authorized 780 | * access, then the user is shown a consent screen. 781 | * @param string $prompt 782 | * {@code "none"} Do not display any authentication or consent screens. Must not be specified with other values. 783 | * {@code "consent"} Prompt the user for consent. 784 | * {@code "select_account"} Prompt the user to select an account. 785 | */ 786 | public function setPrompt($prompt) 787 | { 788 | $this->config['prompt'] = $prompt; 789 | } 790 | 791 | /** 792 | * openid.realm is a parameter from the OpenID 2.0 protocol, not from OAuth 793 | * 2.0. It is used in OpenID 2.0 requests to signify the URL-space for which 794 | * an authentication request is valid. 795 | * @param string $realm the URL-space to use. 796 | */ 797 | public function setOpenidRealm($realm) 798 | { 799 | $this->config['openid.realm'] = $realm; 800 | } 801 | 802 | /** 803 | * If this is provided with the value true, and the authorization request is 804 | * granted, the authorization will include any previous authorizations 805 | * granted to this user/application combination for other scopes. 806 | * @param bool $include the URL-space to use. 807 | */ 808 | public function setIncludeGrantedScopes($include) 809 | { 810 | $this->config['include_granted_scopes'] = $include; 811 | } 812 | 813 | /** 814 | * sets function to be called when an access token is fetched 815 | * @param callable $tokenCallback - function ($cacheKey, $accessToken) 816 | */ 817 | public function setTokenCallback(callable $tokenCallback) 818 | { 819 | $this->config['token_callback'] = $tokenCallback; 820 | } 821 | 822 | /** 823 | * Revoke an OAuth2 access token or refresh token. This method will revoke the current access 824 | * token, if a token isn't provided. 825 | * 826 | * @param string|array|null $token The token (access token or a refresh token) that should be revoked. 827 | * @return boolean Returns True if the revocation was successful, otherwise False. 828 | */ 829 | public function revokeToken($token = null) 830 | { 831 | $tokenRevoker = new Revoke($this->getHttpClient()); 832 | 833 | return $tokenRevoker->revokeToken($token ?: $this->getAccessToken()); 834 | } 835 | 836 | /** 837 | * Verify an id_token. This method will verify the current id_token, if one 838 | * isn't provided. 839 | * 840 | * @throws LogicException If no token was provided and no token was set using `setAccessToken`. 841 | * @throws UnexpectedValueException If the token is not a valid JWT. 842 | * @param string|null $idToken The token (id_token) that should be verified. 843 | * @return array|false Returns the token payload as an array if the verification was 844 | * successful, false otherwise. 845 | */ 846 | public function verifyIdToken($idToken = null) 847 | { 848 | $tokenVerifier = new Verify( 849 | $this->getHttpClient(), 850 | $this->getCache(), 851 | $this->config['jwt'] 852 | ); 853 | 854 | if (null === $idToken) { 855 | $token = $this->getAccessToken(); 856 | if (!isset($token['id_token'])) { 857 | throw new LogicException( 858 | 'id_token must be passed in or set as part of setAccessToken' 859 | ); 860 | } 861 | $idToken = $token['id_token']; 862 | } 863 | 864 | return $tokenVerifier->verifyIdToken( 865 | $idToken, 866 | $this->getClientId() 867 | ); 868 | } 869 | 870 | /** 871 | * Set the scopes to be requested. Must be called before createAuthUrl(). 872 | * Will remove any previously configured scopes. 873 | * @param string|array $scope_or_scopes, ie: 874 | * array( 875 | * 'https://www.googleapis.com/auth/plus.login', 876 | * 'https://www.googleapis.com/auth/moderator' 877 | * ); 878 | */ 879 | public function setScopes($scope_or_scopes) 880 | { 881 | $this->requestedScopes = []; 882 | $this->addScope($scope_or_scopes); 883 | } 884 | 885 | /** 886 | * This functions adds a scope to be requested as part of the OAuth2.0 flow. 887 | * Will append any scopes not previously requested to the scope parameter. 888 | * A single string will be treated as a scope to request. An array of strings 889 | * will each be appended. 890 | * @param string|string[] $scope_or_scopes e.g. "profile" 891 | */ 892 | public function addScope($scope_or_scopes) 893 | { 894 | if (is_string($scope_or_scopes) && !in_array($scope_or_scopes, $this->requestedScopes)) { 895 | $this->requestedScopes[] = $scope_or_scopes; 896 | } elseif (is_array($scope_or_scopes)) { 897 | foreach ($scope_or_scopes as $scope) { 898 | $this->addScope($scope); 899 | } 900 | } 901 | } 902 | 903 | /** 904 | * Returns the list of scopes requested by the client 905 | * @return array the list of scopes 906 | * 907 | */ 908 | public function getScopes() 909 | { 910 | return $this->requestedScopes; 911 | } 912 | 913 | /** 914 | * @return string|null 915 | * @visible For Testing 916 | */ 917 | public function prepareScopes() 918 | { 919 | if (empty($this->requestedScopes)) { 920 | return null; 921 | } 922 | 923 | return implode(' ', $this->requestedScopes); 924 | } 925 | 926 | /** 927 | * Helper method to execute deferred HTTP requests. 928 | * 929 | * @template T 930 | * @param RequestInterface $request 931 | * @param class-string|false|null $expectedClass 932 | * @throws \Google\Exception 933 | * @return mixed|T|ResponseInterface 934 | */ 935 | public function execute(RequestInterface $request, $expectedClass = null) 936 | { 937 | $request = $request 938 | ->withHeader( 939 | 'User-Agent', 940 | sprintf( 941 | '%s %s%s', 942 | $this->config['application_name'], 943 | self::USER_AGENT_SUFFIX, 944 | $this->getLibraryVersion() 945 | ) 946 | ) 947 | ->withHeader( 948 | 'x-goog-api-client', 949 | sprintf( 950 | 'gl-php/%s gdcl/%s', 951 | phpversion(), 952 | $this->getLibraryVersion() 953 | ) 954 | ); 955 | 956 | if ($this->config['api_format_v2']) { 957 | $request = $request->withHeader( 958 | 'X-GOOG-API-FORMAT-VERSION', 959 | '2' 960 | ); 961 | } 962 | 963 | // call the authorize method 964 | // this is where most of the grunt work is done 965 | $http = $this->authorize(); 966 | 967 | return REST::execute( 968 | $http, 969 | $request, 970 | $expectedClass, 971 | $this->config['retry'], 972 | $this->config['retry_map'] 973 | ); 974 | } 975 | 976 | /** 977 | * Declare whether batch calls should be used. This may increase throughput 978 | * by making multiple requests in one connection. 979 | * 980 | * @param boolean $useBatch True if the batch support should 981 | * be enabled. Defaults to False. 982 | */ 983 | public function setUseBatch($useBatch) 984 | { 985 | // This is actually an alias for setDefer. 986 | $this->setDefer($useBatch); 987 | } 988 | 989 | /** 990 | * Are we running in Google AppEngine? 991 | * return bool 992 | */ 993 | public function isAppEngine() 994 | { 995 | return (isset($_SERVER['SERVER_SOFTWARE']) && 996 | strpos($_SERVER['SERVER_SOFTWARE'], 'Google App Engine') !== false); 997 | } 998 | 999 | public function setConfig($name, $value) 1000 | { 1001 | $this->config[$name] = $value; 1002 | } 1003 | 1004 | public function getConfig($name, $default = null) 1005 | { 1006 | return isset($this->config[$name]) ? $this->config[$name] : $default; 1007 | } 1008 | 1009 | /** 1010 | * For backwards compatibility 1011 | * alias for setAuthConfig 1012 | * 1013 | * @param string $file the configuration file 1014 | * @throws \Google\Exception 1015 | * @deprecated 1016 | */ 1017 | public function setAuthConfigFile($file) 1018 | { 1019 | $this->setAuthConfig($file); 1020 | } 1021 | 1022 | /** 1023 | * Set the auth config from new or deprecated JSON config. 1024 | * This structure should match the file downloaded from 1025 | * the "Download JSON" button on in the Google Developer 1026 | * Console. 1027 | * @param string|array $config the configuration json 1028 | * @throws \Google\Exception 1029 | */ 1030 | public function setAuthConfig($config) 1031 | { 1032 | if (is_string($config)) { 1033 | if (!file_exists($config)) { 1034 | throw new InvalidArgumentException(sprintf('file "%s" does not exist', $config)); 1035 | } 1036 | 1037 | $json = file_get_contents($config); 1038 | 1039 | if (!$config = json_decode($json, true)) { 1040 | throw new LogicException('invalid json for auth config'); 1041 | } 1042 | } 1043 | 1044 | $key = isset($config['installed']) ? 'installed' : 'web'; 1045 | if (isset($config['type']) && $config['type'] == 'service_account') { 1046 | // @TODO(v3): Remove this, as it isn't accurate. ADC applies only to determining 1047 | // credentials based on the user's environment. 1048 | $this->useApplicationDefaultCredentials(); 1049 | 1050 | // set the information from the config 1051 | $this->setClientId($config['client_id']); 1052 | $this->config['client_email'] = $config['client_email']; 1053 | $this->config['signing_key'] = $config['private_key']; 1054 | $this->config['signing_algorithm'] = 'HS256'; 1055 | } elseif (isset($config[$key])) { 1056 | // old-style 1057 | $this->setClientId($config[$key]['client_id']); 1058 | $this->setClientSecret($config[$key]['client_secret']); 1059 | if (isset($config[$key]['redirect_uris'])) { 1060 | $this->setRedirectUri($config[$key]['redirect_uris'][0]); 1061 | } 1062 | } else { 1063 | // new-style 1064 | $this->setClientId($config['client_id']); 1065 | $this->setClientSecret($config['client_secret']); 1066 | if (isset($config['redirect_uris'])) { 1067 | $this->setRedirectUri($config['redirect_uris'][0]); 1068 | } 1069 | } 1070 | } 1071 | 1072 | /** 1073 | * Use when the service account has been delegated domain wide access. 1074 | * 1075 | * @param string $subject an email address account to impersonate 1076 | */ 1077 | public function setSubject($subject) 1078 | { 1079 | $this->config['subject'] = $subject; 1080 | } 1081 | 1082 | /** 1083 | * Declare whether making API calls should make the call immediately, or 1084 | * return a request which can be called with ->execute(); 1085 | * 1086 | * @param boolean $defer True if calls should not be executed right away. 1087 | */ 1088 | public function setDefer($defer) 1089 | { 1090 | $this->deferExecution = $defer; 1091 | } 1092 | 1093 | /** 1094 | * Whether or not to return raw requests 1095 | * @return boolean 1096 | */ 1097 | public function shouldDefer() 1098 | { 1099 | return $this->deferExecution; 1100 | } 1101 | 1102 | /** 1103 | * @return OAuth2 implementation 1104 | */ 1105 | public function getOAuth2Service() 1106 | { 1107 | if (!isset($this->auth)) { 1108 | $this->auth = $this->createOAuth2Service(); 1109 | } 1110 | 1111 | return $this->auth; 1112 | } 1113 | 1114 | /** 1115 | * create a default google auth object 1116 | */ 1117 | protected function createOAuth2Service() 1118 | { 1119 | $auth = new OAuth2([ 1120 | 'clientId' => $this->getClientId(), 1121 | 'clientSecret' => $this->getClientSecret(), 1122 | 'authorizationUri' => self::OAUTH2_AUTH_URL, 1123 | 'tokenCredentialUri' => self::OAUTH2_TOKEN_URI, 1124 | 'redirectUri' => $this->getRedirectUri(), 1125 | 'issuer' => $this->config['client_id'], 1126 | 'signingKey' => $this->config['signing_key'], 1127 | 'signingAlgorithm' => $this->config['signing_algorithm'], 1128 | ]); 1129 | 1130 | return $auth; 1131 | } 1132 | 1133 | /** 1134 | * Set the Cache object 1135 | * @param CacheItemPoolInterface $cache 1136 | */ 1137 | public function setCache(CacheItemPoolInterface $cache) 1138 | { 1139 | $this->cache = $cache; 1140 | } 1141 | 1142 | /** 1143 | * @return CacheItemPoolInterface 1144 | */ 1145 | public function getCache() 1146 | { 1147 | if (!$this->cache) { 1148 | $this->cache = $this->createDefaultCache(); 1149 | } 1150 | 1151 | return $this->cache; 1152 | } 1153 | 1154 | /** 1155 | * @param array $cacheConfig 1156 | */ 1157 | public function setCacheConfig(array $cacheConfig) 1158 | { 1159 | $this->config['cache_config'] = $cacheConfig; 1160 | } 1161 | 1162 | /** 1163 | * Set the Logger object 1164 | * @param LoggerInterface $logger 1165 | */ 1166 | public function setLogger(LoggerInterface $logger) 1167 | { 1168 | $this->logger = $logger; 1169 | } 1170 | 1171 | /** 1172 | * @return LoggerInterface 1173 | */ 1174 | public function getLogger() 1175 | { 1176 | if (!isset($this->logger)) { 1177 | $this->logger = $this->createDefaultLogger(); 1178 | } 1179 | 1180 | return $this->logger; 1181 | } 1182 | 1183 | protected function createDefaultLogger() 1184 | { 1185 | $logger = new Logger('google-api-php-client'); 1186 | if ($this->isAppEngine()) { 1187 | $handler = new MonologSyslogHandler('app', LOG_USER, Logger::NOTICE); 1188 | } else { 1189 | $handler = new MonologStreamHandler('php://stderr', Logger::NOTICE); 1190 | } 1191 | $logger->pushHandler($handler); 1192 | 1193 | return $logger; 1194 | } 1195 | 1196 | protected function createDefaultCache() 1197 | { 1198 | return new MemoryCacheItemPool(); 1199 | } 1200 | 1201 | /** 1202 | * Set the Http Client object 1203 | * @param ClientInterface $http 1204 | */ 1205 | public function setHttpClient(ClientInterface $http) 1206 | { 1207 | $this->http = $http; 1208 | } 1209 | 1210 | /** 1211 | * @return ClientInterface 1212 | */ 1213 | public function getHttpClient() 1214 | { 1215 | if (null === $this->http) { 1216 | $this->http = $this->createDefaultHttpClient(); 1217 | } 1218 | 1219 | return $this->http; 1220 | } 1221 | 1222 | /** 1223 | * Set the API format version. 1224 | * 1225 | * `true` will use V2, which may return more useful error messages. 1226 | * 1227 | * @param bool $value 1228 | */ 1229 | public function setApiFormatV2($value) 1230 | { 1231 | $this->config['api_format_v2'] = (bool) $value; 1232 | } 1233 | 1234 | protected function createDefaultHttpClient() 1235 | { 1236 | $guzzleVersion = null; 1237 | if (defined('\GuzzleHttp\ClientInterface::MAJOR_VERSION')) { 1238 | $guzzleVersion = ClientInterface::MAJOR_VERSION; 1239 | } elseif (defined('\GuzzleHttp\ClientInterface::VERSION')) { 1240 | $guzzleVersion = (int)substr(ClientInterface::VERSION, 0, 1); 1241 | } 1242 | 1243 | if (5 === $guzzleVersion) { 1244 | $options = [ 1245 | 'base_url' => $this->config['base_path'], 1246 | 'defaults' => ['exceptions' => false], 1247 | ]; 1248 | if ($this->isAppEngine()) { 1249 | if (class_exists(StreamHandler::class)) { 1250 | // set StreamHandler on AppEngine by default 1251 | $options['handler'] = new StreamHandler(); 1252 | $options['defaults']['verify'] = '/etc/ca-certificates.crt'; 1253 | } 1254 | } 1255 | } elseif (6 === $guzzleVersion || 7 === $guzzleVersion) { 1256 | // guzzle 6 or 7 1257 | $options = [ 1258 | 'base_uri' => $this->config['base_path'], 1259 | 'http_errors' => false, 1260 | ]; 1261 | } else { 1262 | throw new LogicException('Could not find supported version of Guzzle.'); 1263 | } 1264 | 1265 | return new GuzzleClient($options); 1266 | } 1267 | 1268 | /** 1269 | * @return FetchAuthTokenCache 1270 | */ 1271 | private function createApplicationDefaultCredentials() 1272 | { 1273 | $scopes = $this->prepareScopes(); 1274 | $sub = $this->config['subject']; 1275 | $signingKey = $this->config['signing_key']; 1276 | 1277 | // create credentials using values supplied in setAuthConfig 1278 | if ($signingKey) { 1279 | $serviceAccountCredentials = [ 1280 | 'client_id' => $this->config['client_id'], 1281 | 'client_email' => $this->config['client_email'], 1282 | 'private_key' => $signingKey, 1283 | 'type' => 'service_account', 1284 | 'quota_project_id' => $this->config['quota_project'], 1285 | ]; 1286 | $credentials = CredentialsLoader::makeCredentials( 1287 | $scopes, 1288 | $serviceAccountCredentials 1289 | ); 1290 | } else { 1291 | // When $sub is provided, we cannot pass cache classes to ::getCredentials 1292 | // because FetchAuthTokenCache::setSub does not exist. 1293 | // The result is when $sub is provided, calls to ::onGce are not cached. 1294 | $credentials = ApplicationDefaultCredentials::getCredentials( 1295 | $scopes, 1296 | null, 1297 | $sub ? null : $this->config['cache_config'], 1298 | $sub ? null : $this->getCache(), 1299 | $this->config['quota_project'] 1300 | ); 1301 | } 1302 | 1303 | // for service account domain-wide authority (impersonating a user) 1304 | // @see https://developers.google.com/identity/protocols/OAuth2ServiceAccount 1305 | if ($sub) { 1306 | if (!$credentials instanceof ServiceAccountCredentials) { 1307 | throw new DomainException('domain-wide authority requires service account credentials'); 1308 | } 1309 | 1310 | $credentials->setSub($sub); 1311 | } 1312 | 1313 | // If we are not using FetchAuthTokenCache yet, create it now 1314 | if (!$credentials instanceof FetchAuthTokenCache) { 1315 | $credentials = new FetchAuthTokenCache( 1316 | $credentials, 1317 | $this->config['cache_config'], 1318 | $this->getCache() 1319 | ); 1320 | } 1321 | return $credentials; 1322 | } 1323 | 1324 | protected function getAuthHandler() 1325 | { 1326 | // Be very careful using the cache, as the underlying auth library's cache 1327 | // implementation is naive, and the cache keys do not account for user 1328 | // sessions. 1329 | // 1330 | // @see https://github.com/google/google-api-php-client/issues/821 1331 | return AuthHandlerFactory::build( 1332 | $this->getCache(), 1333 | $this->config['cache_config'] 1334 | ); 1335 | } 1336 | 1337 | private function createUserRefreshCredentials($scope, $refreshToken) 1338 | { 1339 | $creds = array_filter([ 1340 | 'client_id' => $this->getClientId(), 1341 | 'client_secret' => $this->getClientSecret(), 1342 | 'refresh_token' => $refreshToken, 1343 | ]); 1344 | 1345 | return new UserRefreshCredentials($scope, $creds); 1346 | } 1347 | 1348 | private function checkUniverseDomain($credentials) 1349 | { 1350 | $credentialsUniverse = $credentials instanceof GetUniverseDomainInterface 1351 | ? $credentials->getUniverseDomain() 1352 | : GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN; 1353 | if ($credentialsUniverse !== $this->getUniverseDomain()) { 1354 | throw new DomainException(sprintf( 1355 | 'The configured universe domain (%s) does not match the credential universe domain (%s)', 1356 | $this->getUniverseDomain(), 1357 | $credentialsUniverse 1358 | )); 1359 | } 1360 | } 1361 | 1362 | public function getUniverseDomain() 1363 | { 1364 | return $this->config['universe_domain']; 1365 | } 1366 | } 1367 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | {$this->collection_key}) 20 | && is_array($this->{$this->collection_key}) 21 | ) { 22 | reset($this->{$this->collection_key}); 23 | } 24 | } 25 | 26 | /** @return mixed */ 27 | #[\ReturnTypeWillChange] 28 | public function current() 29 | { 30 | $this->coerceType($this->key()); 31 | if (is_array($this->{$this->collection_key})) { 32 | return current($this->{$this->collection_key}); 33 | } 34 | } 35 | 36 | /** @return mixed */ 37 | #[\ReturnTypeWillChange] 38 | public function key() 39 | { 40 | if ( 41 | isset($this->{$this->collection_key}) 42 | && is_array($this->{$this->collection_key}) 43 | ) { 44 | return key($this->{$this->collection_key}); 45 | } 46 | } 47 | 48 | /** @return mixed */ 49 | #[\ReturnTypeWillChange] 50 | public function next() 51 | { 52 | return next($this->{$this->collection_key}); 53 | } 54 | 55 | /** @return bool */ 56 | #[\ReturnTypeWillChange] 57 | public function valid() 58 | { 59 | $key = $this->key(); 60 | return $key !== null && $key !== false; 61 | } 62 | 63 | /** @return int */ 64 | #[\ReturnTypeWillChange] 65 | public function count() 66 | { 67 | if (!isset($this->{$this->collection_key})) { 68 | return 0; 69 | } 70 | return count($this->{$this->collection_key}); 71 | } 72 | 73 | /** @return bool */ 74 | #[\ReturnTypeWillChange] 75 | public function offsetExists($offset) 76 | { 77 | if (!is_numeric($offset)) { 78 | return parent::offsetExists($offset); 79 | } 80 | return isset($this->{$this->collection_key}[$offset]); 81 | } 82 | 83 | /** @return mixed */ 84 | #[\ReturnTypeWillChange] 85 | public function offsetGet($offset) 86 | { 87 | if (!is_numeric($offset)) { 88 | return parent::offsetGet($offset); 89 | } 90 | $this->coerceType($offset); 91 | return $this->{$this->collection_key}[$offset]; 92 | } 93 | 94 | /** @return void */ 95 | #[\ReturnTypeWillChange] 96 | public function offsetSet($offset, $value) 97 | { 98 | if (!is_numeric($offset)) { 99 | parent::offsetSet($offset, $value); 100 | } 101 | $this->{$this->collection_key}[$offset] = $value; 102 | } 103 | 104 | /** @return void */ 105 | #[\ReturnTypeWillChange] 106 | public function offsetUnset($offset) 107 | { 108 | if (!is_numeric($offset)) { 109 | parent::offsetUnset($offset); 110 | } 111 | unset($this->{$this->collection_key}[$offset]); 112 | } 113 | 114 | private function coerceType($offset) 115 | { 116 | $keyType = $this->keyType($this->collection_key); 117 | if ($keyType && !is_object($this->{$this->collection_key}[$offset])) { 118 | $this->{$this->collection_key}[$offset] = 119 | new $keyType($this->{$this->collection_key}[$offset]); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Exception.php: -------------------------------------------------------------------------------- 1 | client = $client; 64 | $this->boundary = $boundary ?: mt_rand(); 65 | $rootUrl = rtrim($rootUrl ?: $this->client->getConfig('base_path'), '/'); 66 | $this->rootUrl = str_replace( 67 | 'UNIVERSE_DOMAIN', 68 | $this->client->getUniverseDomain(), 69 | $rootUrl 70 | ); 71 | $this->batchPath = $batchPath ?: self::BATCH_PATH; 72 | } 73 | 74 | public function add(RequestInterface $request, $key = false) 75 | { 76 | if (false == $key) { 77 | $key = mt_rand(); 78 | } 79 | 80 | $this->requests[$key] = $request; 81 | } 82 | 83 | public function execute() 84 | { 85 | $body = ''; 86 | $classes = []; 87 | $batchHttpTemplate = <<requests as $key => $request) { 102 | $firstLine = sprintf( 103 | '%s %s HTTP/%s', 104 | $request->getMethod(), 105 | $request->getRequestTarget(), 106 | $request->getProtocolVersion() 107 | ); 108 | 109 | $content = (string) $request->getBody(); 110 | 111 | $headers = ''; 112 | foreach ($request->getHeaders() as $name => $values) { 113 | $headers .= sprintf("%s:%s\r\n", $name, implode(', ', $values)); 114 | } 115 | 116 | $body .= sprintf( 117 | $batchHttpTemplate, 118 | $this->boundary, 119 | $key, 120 | $firstLine, 121 | $headers, 122 | $content ? "\n" . $content : '' 123 | ); 124 | 125 | $classes['response-' . $key] = $request->getHeaderLine('X-Php-Expected-Class'); 126 | } 127 | 128 | $body .= "--{$this->boundary}--"; 129 | $body = trim($body); 130 | $url = $this->rootUrl . '/' . $this->batchPath; 131 | $headers = [ 132 | 'Content-Type' => sprintf('multipart/mixed; boundary=%s', $this->boundary), 133 | 'Content-Length' => (string) strlen($body), 134 | ]; 135 | 136 | $request = new Request( 137 | 'POST', 138 | $url, 139 | $headers, 140 | $body 141 | ); 142 | 143 | $response = $this->client->execute($request); 144 | 145 | return $this->parseResponse($response, $classes); 146 | } 147 | 148 | public function parseResponse(ResponseInterface $response, $classes = []) 149 | { 150 | $contentType = $response->getHeaderLine('content-type'); 151 | $contentType = explode(';', $contentType); 152 | $boundary = false; 153 | foreach ($contentType as $part) { 154 | $part = explode('=', $part, 2); 155 | if (isset($part[0]) && 'boundary' == trim($part[0])) { 156 | $boundary = $part[1]; 157 | } 158 | } 159 | 160 | $body = (string) $response->getBody(); 161 | if (!empty($body)) { 162 | $body = str_replace("--$boundary--", "--$boundary", $body); 163 | $parts = explode("--$boundary", $body); 164 | $responses = []; 165 | $requests = array_values($this->requests); 166 | 167 | foreach ($parts as $i => $part) { 168 | $part = trim($part); 169 | if (!empty($part)) { 170 | list($rawHeaders, $part) = explode("\r\n\r\n", $part, 2); 171 | $headers = $this->parseRawHeaders($rawHeaders); 172 | 173 | $status = substr($part, 0, strpos($part, "\n")); 174 | $status = explode(" ", $status); 175 | $status = $status[1]; 176 | 177 | list($partHeaders, $partBody) = $this->parseHttpResponse($part, 0); 178 | $response = new Response( 179 | (int) $status, 180 | $partHeaders, 181 | Psr7\Utils::streamFor($partBody) 182 | ); 183 | 184 | // Need content id. 185 | $key = $headers['content-id']; 186 | 187 | try { 188 | $response = REST::decodeHttpResponse($response, $requests[$i-1]); 189 | } catch (GoogleServiceException $e) { 190 | // Store the exception as the response, so successful responses 191 | // can be processed. 192 | $response = $e; 193 | } 194 | 195 | $responses[$key] = $response; 196 | } 197 | } 198 | 199 | return $responses; 200 | } 201 | 202 | return null; 203 | } 204 | 205 | private function parseRawHeaders($rawHeaders) 206 | { 207 | $headers = []; 208 | $responseHeaderLines = explode("\r\n", $rawHeaders); 209 | foreach ($responseHeaderLines as $headerLine) { 210 | if ($headerLine && strpos($headerLine, ':') !== false) { 211 | list($header, $value) = explode(': ', $headerLine, 2); 212 | $header = strtolower($header); 213 | if (isset($headers[$header])) { 214 | $headers[$header] = array_merge((array)$headers[$header], (array)$value); 215 | } else { 216 | $headers[$header] = $value; 217 | } 218 | } 219 | } 220 | return $headers; 221 | } 222 | 223 | /** 224 | * Used by the IO lib and also the batch processing. 225 | * 226 | * @param string $respData 227 | * @param int $headerSize 228 | * @return array 229 | */ 230 | private function parseHttpResponse($respData, $headerSize) 231 | { 232 | // check proxy header 233 | foreach (self::$CONNECTION_ESTABLISHED_HEADERS as $established_header) { 234 | if (stripos($respData, $established_header) !== false) { 235 | // existed, remove it 236 | $respData = str_ireplace($established_header, '', $respData); 237 | // Subtract the proxy header size unless the cURL bug prior to 7.30.0 238 | // is present which prevented the proxy header size from being taken into 239 | // account. 240 | // @TODO look into this 241 | // if (!$this->needsQuirk()) { 242 | // $headerSize -= strlen($established_header); 243 | // } 244 | break; 245 | } 246 | } 247 | 248 | if ($headerSize) { 249 | $responseBody = substr($respData, $headerSize); 250 | $responseHeaders = substr($respData, 0, $headerSize); 251 | } else { 252 | $responseSegments = explode("\r\n\r\n", $respData, 2); 253 | $responseHeaders = $responseSegments[0]; 254 | $responseBody = isset($responseSegments[1]) ? $responseSegments[1] : null; 255 | } 256 | 257 | $responseHeaders = $this->parseRawHeaders($responseHeaders); 258 | 259 | return [$responseHeaders, $responseBody]; 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/Http/MediaFileUpload.php: -------------------------------------------------------------------------------- 1 | client = $client; 91 | $this->request = $request; 92 | $this->mimeType = $mimeType; 93 | $this->data = $data; 94 | $this->resumable = $resumable; 95 | $this->chunkSize = $chunkSize; 96 | $this->progress = 0; 97 | 98 | $this->process(); 99 | } 100 | 101 | /** 102 | * Set the size of the file that is being uploaded. 103 | * @param int $size - int file size in bytes 104 | */ 105 | public function setFileSize($size) 106 | { 107 | $this->size = $size; 108 | } 109 | 110 | /** 111 | * Return the progress on the upload 112 | * @return int progress in bytes uploaded. 113 | */ 114 | public function getProgress() 115 | { 116 | return $this->progress; 117 | } 118 | 119 | /** 120 | * Send the next part of the file to upload. 121 | * @param string|bool $chunk Optional. The next set of bytes to send. If false will 122 | * use $data passed at construct time. 123 | */ 124 | public function nextChunk($chunk = false) 125 | { 126 | $resumeUri = $this->getResumeUri(); 127 | 128 | if (false == $chunk) { 129 | $chunk = substr($this->data, $this->progress, $this->chunkSize); 130 | } 131 | 132 | $lastBytePos = $this->progress + strlen($chunk) - 1; 133 | $headers = [ 134 | 'content-range' => "bytes $this->progress-$lastBytePos/$this->size", 135 | 'content-length' => (string) strlen($chunk), 136 | 'expect' => '', 137 | ]; 138 | 139 | $request = new Request( 140 | 'PUT', 141 | $resumeUri, 142 | $headers, 143 | Psr7\Utils::streamFor($chunk) 144 | ); 145 | 146 | return $this->makePutRequest($request); 147 | } 148 | 149 | /** 150 | * Return the HTTP result code from the last call made. 151 | * @return int code 152 | */ 153 | public function getHttpResultCode() 154 | { 155 | return $this->httpResultCode; 156 | } 157 | 158 | /** 159 | * Sends a PUT-Request to google drive and parses the response, 160 | * setting the appropiate variables from the response() 161 | * 162 | * @param RequestInterface $request the Request which will be send 163 | * 164 | * @return false|mixed false when the upload is unfinished or the decoded http response 165 | * 166 | */ 167 | private function makePutRequest(RequestInterface $request) 168 | { 169 | $response = $this->client->execute($request); 170 | $this->httpResultCode = $response->getStatusCode(); 171 | 172 | if (308 == $this->httpResultCode) { 173 | // Track the amount uploaded. 174 | $range = $response->getHeaderLine('range'); 175 | if ($range) { 176 | $range_array = explode('-', $range); 177 | $this->progress = ((int) $range_array[1]) + 1; 178 | } 179 | 180 | // Allow for changing upload URLs. 181 | $location = $response->getHeaderLine('location'); 182 | if ($location) { 183 | $this->resumeUri = $location; 184 | } 185 | 186 | // No problems, but upload not complete. 187 | return false; 188 | } 189 | 190 | return REST::decodeHttpResponse($response, $this->request); 191 | } 192 | 193 | /** 194 | * Resume a previously unfinished upload 195 | * @param string $resumeUri the resume-URI of the unfinished, resumable upload. 196 | */ 197 | public function resume($resumeUri) 198 | { 199 | $this->resumeUri = $resumeUri; 200 | $headers = [ 201 | 'content-range' => "bytes */$this->size", 202 | 'content-length' => '0', 203 | ]; 204 | $httpRequest = new Request( 205 | 'PUT', 206 | $this->resumeUri, 207 | $headers 208 | ); 209 | return $this->makePutRequest($httpRequest); 210 | } 211 | 212 | /** 213 | * @return RequestInterface 214 | * @visible for testing 215 | */ 216 | private function process() 217 | { 218 | $this->transformToUploadUrl(); 219 | $request = $this->request; 220 | 221 | $postBody = ''; 222 | $contentType = false; 223 | 224 | $meta = json_decode((string) $request->getBody(), true); 225 | 226 | $uploadType = $this->getUploadType($meta); 227 | $request = $request->withUri( 228 | Uri::withQueryValue($request->getUri(), 'uploadType', $uploadType) 229 | ); 230 | 231 | $mimeType = $this->mimeType ?: $request->getHeaderLine('content-type'); 232 | 233 | if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) { 234 | $contentType = $mimeType; 235 | $postBody = is_string($meta) ? $meta : json_encode($meta); 236 | } elseif (self::UPLOAD_MEDIA_TYPE == $uploadType) { 237 | $contentType = $mimeType; 238 | $postBody = $this->data; 239 | } elseif (self::UPLOAD_MULTIPART_TYPE == $uploadType) { 240 | // This is a multipart/related upload. 241 | $boundary = $this->boundary ?: mt_rand(); 242 | $boundary = str_replace('"', '', $boundary); 243 | $contentType = 'multipart/related; boundary=' . $boundary; 244 | $related = "--$boundary\r\n"; 245 | $related .= "Content-Type: application/json; charset=UTF-8\r\n"; 246 | $related .= "\r\n" . json_encode($meta) . "\r\n"; 247 | $related .= "--$boundary\r\n"; 248 | $related .= "Content-Type: $mimeType\r\n"; 249 | $related .= "Content-Transfer-Encoding: base64\r\n"; 250 | $related .= "\r\n" . base64_encode($this->data) . "\r\n"; 251 | $related .= "--$boundary--"; 252 | $postBody = $related; 253 | } 254 | 255 | $request = $request->withBody(Psr7\Utils::streamFor($postBody)); 256 | 257 | if ($contentType) { 258 | $request = $request->withHeader('content-type', $contentType); 259 | } 260 | 261 | return $this->request = $request; 262 | } 263 | 264 | /** 265 | * Valid upload types: 266 | * - resumable (UPLOAD_RESUMABLE_TYPE) 267 | * - media (UPLOAD_MEDIA_TYPE) 268 | * - multipart (UPLOAD_MULTIPART_TYPE) 269 | * @param string|false $meta 270 | * @return string 271 | * @visible for testing 272 | */ 273 | public function getUploadType($meta) 274 | { 275 | if ($this->resumable) { 276 | return self::UPLOAD_RESUMABLE_TYPE; 277 | } 278 | 279 | if (false == $meta && $this->data) { 280 | return self::UPLOAD_MEDIA_TYPE; 281 | } 282 | 283 | return self::UPLOAD_MULTIPART_TYPE; 284 | } 285 | 286 | public function getResumeUri() 287 | { 288 | if (null === $this->resumeUri) { 289 | $this->resumeUri = $this->fetchResumeUri(); 290 | } 291 | 292 | return $this->resumeUri; 293 | } 294 | 295 | private function fetchResumeUri() 296 | { 297 | $body = $this->request->getBody(); 298 | $headers = [ 299 | 'content-type' => 'application/json; charset=UTF-8', 300 | 'content-length' => $body->getSize(), 301 | 'x-upload-content-type' => $this->mimeType, 302 | 'x-upload-content-length' => $this->size, 303 | 'expect' => '', 304 | ]; 305 | foreach ($headers as $key => $value) { 306 | $this->request = $this->request->withHeader($key, $value); 307 | } 308 | 309 | $response = $this->client->execute($this->request, false); 310 | $location = $response->getHeaderLine('location'); 311 | $code = $response->getStatusCode(); 312 | 313 | if (200 == $code && true == $location) { 314 | return $location; 315 | } 316 | 317 | $message = $code; 318 | $body = json_decode((string) $this->request->getBody(), true); 319 | if (isset($body['error']['errors'])) { 320 | $message .= ': '; 321 | foreach ($body['error']['errors'] as $error) { 322 | $message .= "{$error['domain']}, {$error['message']};"; 323 | } 324 | $message = rtrim($message, ';'); 325 | } 326 | 327 | $error = "Failed to start the resumable upload (HTTP {$message})"; 328 | $this->client->getLogger()->error($error); 329 | 330 | throw new GoogleException($error); 331 | } 332 | 333 | private function transformToUploadUrl() 334 | { 335 | $parts = parse_url((string) $this->request->getUri()); 336 | if (!isset($parts['path'])) { 337 | $parts['path'] = ''; 338 | } 339 | $parts['path'] = '/upload' . $parts['path']; 340 | $uri = Uri::fromParts($parts); 341 | $this->request = $this->request->withUri($uri); 342 | } 343 | 344 | public function setChunkSize($chunkSize) 345 | { 346 | $this->chunkSize = $chunkSize; 347 | } 348 | 349 | public function getRequest() 350 | { 351 | return $this->request; 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/Http/REST.php: -------------------------------------------------------------------------------- 1 | |false|null $expectedClass 42 | * @param array $config 43 | * @param array $retryMap 44 | * @return mixed|T|null 45 | * @throws \Google\Service\Exception on server side error (ie: not authenticated, 46 | * invalid or malformed post body, invalid url) 47 | */ 48 | public static function execute( 49 | ClientInterface $client, 50 | RequestInterface $request, 51 | $expectedClass = null, 52 | $config = [], 53 | $retryMap = null 54 | ) { 55 | $runner = new Runner( 56 | $config, 57 | sprintf('%s %s', $request->getMethod(), (string)$request->getUri()), 58 | [self::class, 'doExecute'], 59 | [$client, $request, $expectedClass] 60 | ); 61 | 62 | if (null !== $retryMap) { 63 | $runner->setRetryMap($retryMap); 64 | } 65 | 66 | return $runner->run(); 67 | } 68 | 69 | /** 70 | * Executes a Psr\Http\Message\RequestInterface 71 | * 72 | * @template T 73 | * @param ClientInterface $client 74 | * @param RequestInterface $request 75 | * @param class-string|false|null $expectedClass 76 | * @return mixed|T|null 77 | * @throws \Google\Service\Exception on server side error (ie: not authenticated, 78 | * invalid or malformed post body, invalid url) 79 | */ 80 | public static function doExecute(ClientInterface $client, RequestInterface $request, $expectedClass = null) 81 | { 82 | try { 83 | $httpHandler = HttpHandlerFactory::build($client); 84 | $response = $httpHandler($request); 85 | } catch (RequestException $e) { 86 | // if Guzzle throws an exception, catch it and handle the response 87 | if (!$e->hasResponse()) { 88 | throw $e; 89 | } 90 | 91 | $response = $e->getResponse(); 92 | // specific checking for Guzzle 5: convert to PSR7 response 93 | if ( 94 | interface_exists('\GuzzleHttp\Message\ResponseInterface') 95 | && $response instanceof \GuzzleHttp\Message\ResponseInterface 96 | ) { 97 | $response = new Response( 98 | $response->getStatusCode(), 99 | $response->getHeaders() ?: [], 100 | $response->getBody(), 101 | $response->getProtocolVersion(), 102 | $response->getReasonPhrase() 103 | ); 104 | } 105 | } 106 | 107 | return self::decodeHttpResponse($response, $request, $expectedClass); 108 | } 109 | 110 | /** 111 | * Decode an HTTP Response. 112 | * @static 113 | * 114 | * @template T 115 | * @param RequestInterface $response The http response to be decoded. 116 | * @param ResponseInterface $response 117 | * @param class-string|false|null $expectedClass 118 | * @return mixed|T|null 119 | * @throws \Google\Service\Exception 120 | */ 121 | public static function decodeHttpResponse( 122 | ResponseInterface $response, 123 | ?RequestInterface $request = null, 124 | $expectedClass = null 125 | ) { 126 | $code = $response->getStatusCode(); 127 | 128 | // retry strategy 129 | if (intVal($code) >= 400) { 130 | // if we errored out, it should be safe to grab the response body 131 | $body = (string)$response->getBody(); 132 | 133 | // Check if we received errors, and add those to the Exception for convenience 134 | throw new GoogleServiceException($body, $code, null, self::getResponseErrors($body)); 135 | } 136 | 137 | // Ensure we only pull the entire body into memory if the request is not 138 | // of media type 139 | $body = self::decodeBody($response, $request); 140 | 141 | if ($expectedClass = self::determineExpectedClass($expectedClass, $request)) { 142 | $json = json_decode($body, true); 143 | 144 | return new $expectedClass($json); 145 | } 146 | 147 | return $response; 148 | } 149 | 150 | private static function decodeBody(ResponseInterface $response, ?RequestInterface $request = null) 151 | { 152 | if (self::isAltMedia($request)) { 153 | // don't decode the body, it's probably a really long string 154 | return ''; 155 | } 156 | 157 | return (string)$response->getBody(); 158 | } 159 | 160 | private static function determineExpectedClass($expectedClass, ?RequestInterface $request = null) 161 | { 162 | // "false" is used to explicitly prevent an expected class from being returned 163 | if (false === $expectedClass) { 164 | return null; 165 | } 166 | 167 | // if we don't have a request, we just use what's passed in 168 | if (null === $request) { 169 | return $expectedClass; 170 | } 171 | 172 | // return what we have in the request header if one was not supplied 173 | return $expectedClass ?: $request->getHeaderLine('X-Php-Expected-Class'); 174 | } 175 | 176 | private static function getResponseErrors($body) 177 | { 178 | $json = json_decode($body, true); 179 | 180 | if (isset($json['error']['errors'])) { 181 | return $json['error']['errors']; 182 | } 183 | 184 | return null; 185 | } 186 | 187 | private static function isAltMedia(?RequestInterface $request = null) 188 | { 189 | if ($request && $qs = $request->getUri()->getQuery()) { 190 | parse_str($qs, $query); 191 | if (isset($query['alt']) && $query['alt'] == 'media') { 192 | return true; 193 | } 194 | } 195 | 196 | return false; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/Model.php: -------------------------------------------------------------------------------- 1 | mapTypes($array); 53 | } 54 | $this->gapiInit(); 55 | } 56 | 57 | /** 58 | * Getter that handles passthrough access to the data array, and lazy object creation. 59 | * @param string $key Property name. 60 | * @return mixed The value if any, or null. 61 | */ 62 | public function __get($key) 63 | { 64 | $keyType = $this->keyType($key); 65 | $keyDataType = $this->dataType($key); 66 | if ($keyType && !isset($this->processed[$key])) { 67 | if (isset($this->modelData[$key])) { 68 | $val = $this->modelData[$key]; 69 | } elseif ($keyDataType == 'array' || $keyDataType == 'map') { 70 | $val = []; 71 | } else { 72 | $val = null; 73 | } 74 | 75 | if ($this->isAssociativeArray($val)) { 76 | if ($keyDataType && 'map' == $keyDataType) { 77 | foreach ($val as $arrayKey => $arrayItem) { 78 | $this->modelData[$key][$arrayKey] = 79 | new $keyType($arrayItem); 80 | } 81 | } else { 82 | $this->modelData[$key] = new $keyType($val); 83 | } 84 | } elseif (is_array($val)) { 85 | $arrayObject = []; 86 | foreach ($val as $arrayIndex => $arrayItem) { 87 | $arrayObject[$arrayIndex] = new $keyType($arrayItem); 88 | } 89 | $this->modelData[$key] = $arrayObject; 90 | } 91 | $this->processed[$key] = true; 92 | } 93 | 94 | return isset($this->modelData[$key]) ? $this->modelData[$key] : null; 95 | } 96 | 97 | /** 98 | * Initialize this object's properties from an array. 99 | * 100 | * @param array $array Used to seed this object's properties. 101 | * @return void 102 | */ 103 | protected function mapTypes($array) 104 | { 105 | // Hard initialise simple types, lazy load more complex ones. 106 | foreach ($array as $key => $val) { 107 | if ($keyType = $this->keyType($key)) { 108 | $dataType = $this->dataType($key); 109 | if ($dataType == 'array' || $dataType == 'map') { 110 | $this->$key = []; 111 | foreach ($val as $itemKey => $itemVal) { 112 | if ($itemVal instanceof $keyType) { 113 | $this->{$key}[$itemKey] = $itemVal; 114 | } else { 115 | $this->{$key}[$itemKey] = new $keyType($itemVal); 116 | } 117 | } 118 | } elseif ($val instanceof $keyType) { 119 | $this->$key = $val; 120 | } else { 121 | $this->$key = new $keyType($val); 122 | } 123 | unset($array[$key]); 124 | } elseif (property_exists($this, $key)) { 125 | $this->$key = $val; 126 | unset($array[$key]); 127 | } elseif (property_exists($this, $camelKey = $this->camelCase($key))) { 128 | // This checks if property exists as camelCase, leaving it in array as snake_case 129 | // in case of backwards compatibility issues. 130 | $this->$camelKey = $val; 131 | } 132 | } 133 | $this->modelData = $array; 134 | } 135 | 136 | /** 137 | * Blank initialiser to be used in subclasses to do post-construction initialisation - this 138 | * avoids the need for subclasses to have to implement the variadics handling in their 139 | * constructors. 140 | */ 141 | protected function gapiInit() 142 | { 143 | return; 144 | } 145 | 146 | /** 147 | * Create a simplified object suitable for straightforward 148 | * conversion to JSON. This is relatively expensive 149 | * due to the usage of reflection, but shouldn't be called 150 | * a whole lot, and is the most straightforward way to filter. 151 | */ 152 | public function toSimpleObject() 153 | { 154 | $object = new stdClass(); 155 | 156 | // Process all other data. 157 | foreach ($this->modelData as $key => $val) { 158 | $result = $this->getSimpleValue($val); 159 | if ($result !== null) { 160 | $object->$key = $this->nullPlaceholderCheck($result); 161 | } 162 | } 163 | 164 | // Process all public properties. 165 | $reflect = new ReflectionObject($this); 166 | $props = $reflect->getProperties(ReflectionProperty::IS_PUBLIC); 167 | foreach ($props as $member) { 168 | $name = $member->getName(); 169 | $result = $this->getSimpleValue($this->$name); 170 | if ($result !== null) { 171 | $name = $this->getMappedName($name); 172 | $object->$name = $this->nullPlaceholderCheck($result); 173 | } 174 | } 175 | 176 | return $object; 177 | } 178 | 179 | /** 180 | * Handle different types of values, primarily 181 | * other objects and map and array data types. 182 | */ 183 | private function getSimpleValue($value) 184 | { 185 | if ($value instanceof Model) { 186 | return $value->toSimpleObject(); 187 | } elseif (is_array($value)) { 188 | $return = []; 189 | foreach ($value as $key => $a_value) { 190 | $a_value = $this->getSimpleValue($a_value); 191 | if ($a_value !== null) { 192 | $key = $this->getMappedName($key); 193 | $return[$key] = $this->nullPlaceholderCheck($a_value); 194 | } 195 | } 196 | return $return; 197 | } 198 | return $value; 199 | } 200 | 201 | /** 202 | * Check whether the value is the null placeholder and return true null. 203 | */ 204 | private function nullPlaceholderCheck($value) 205 | { 206 | if ($value === self::NULL_VALUE) { 207 | return null; 208 | } 209 | return $value; 210 | } 211 | 212 | /** 213 | * If there is an internal name mapping, use that. 214 | */ 215 | private function getMappedName($key) 216 | { 217 | if (isset($this->internal_gapi_mappings, $this->internal_gapi_mappings[$key])) { 218 | $key = $this->internal_gapi_mappings[$key]; 219 | } 220 | return $key; 221 | } 222 | 223 | /** 224 | * Returns true only if the array is associative. 225 | * @param array $array 226 | * @return bool True if the array is associative. 227 | */ 228 | protected function isAssociativeArray($array) 229 | { 230 | if (!is_array($array)) { 231 | return false; 232 | } 233 | $keys = array_keys($array); 234 | foreach ($keys as $key) { 235 | if (is_string($key)) { 236 | return true; 237 | } 238 | } 239 | return false; 240 | } 241 | 242 | /** 243 | * Verify if $obj is an array. 244 | * @throws \Google\Exception Thrown if $obj isn't an array. 245 | * @param array $obj Items that should be validated. 246 | * @param string $method Method expecting an array as an argument. 247 | */ 248 | public function assertIsArray($obj, $method) 249 | { 250 | if ($obj && !is_array($obj)) { 251 | throw new GoogleException( 252 | "Incorrect parameter type passed to $method(). Expected an array." 253 | ); 254 | } 255 | } 256 | 257 | /** @return bool */ 258 | #[\ReturnTypeWillChange] 259 | public function offsetExists($offset) 260 | { 261 | return isset($this->$offset) || isset($this->modelData[$offset]); 262 | } 263 | 264 | /** @return mixed */ 265 | #[\ReturnTypeWillChange] 266 | public function offsetGet($offset) 267 | { 268 | return isset($this->$offset) ? 269 | $this->$offset : 270 | $this->__get($offset); 271 | } 272 | 273 | /** @return void */ 274 | #[\ReturnTypeWillChange] 275 | public function offsetSet($offset, $value) 276 | { 277 | if (property_exists($this, $offset)) { 278 | $this->$offset = $value; 279 | } else { 280 | $this->modelData[$offset] = $value; 281 | $this->processed[$offset] = true; 282 | } 283 | } 284 | 285 | /** @return void */ 286 | #[\ReturnTypeWillChange] 287 | public function offsetUnset($offset) 288 | { 289 | unset($this->modelData[$offset]); 290 | } 291 | 292 | protected function keyType($key) 293 | { 294 | $keyType = $key . "Type"; 295 | 296 | // ensure keyType is a valid class 297 | if (property_exists($this, $keyType) && $this->$keyType !== null && class_exists($this->$keyType)) { 298 | return $this->$keyType; 299 | } 300 | } 301 | 302 | protected function dataType($key) 303 | { 304 | $dataType = $key . "DataType"; 305 | 306 | if (property_exists($this, $dataType)) { 307 | return $this->$dataType; 308 | } 309 | } 310 | 311 | public function __isset($key) 312 | { 313 | return isset($this->modelData[$key]); 314 | } 315 | 316 | public function __unset($key) 317 | { 318 | unset($this->modelData[$key]); 319 | } 320 | 321 | /** 322 | * Convert a string to camelCase 323 | * @param string $value 324 | * @return string 325 | */ 326 | private function camelCase($value) 327 | { 328 | $value = ucwords(str_replace(['-', '_'], ' ', $value)); 329 | $value = str_replace(' ', '', $value); 330 | $value[0] = strtolower($value[0]); 331 | return $value; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/Service.php: -------------------------------------------------------------------------------- 1 | client = $clientOrConfig; 42 | } elseif (is_array($clientOrConfig)) { 43 | $this->client = new Client($clientOrConfig ?: []); 44 | } else { 45 | $errorMessage = 'constructor must be array or instance of Google\Client'; 46 | if (class_exists('TypeError')) { 47 | throw new TypeError($errorMessage); 48 | } 49 | trigger_error($errorMessage, E_USER_ERROR); 50 | } 51 | } 52 | 53 | /** 54 | * Return the associated Google\Client class. 55 | * @return \Google\Client 56 | */ 57 | public function getClient() 58 | { 59 | return $this->client; 60 | } 61 | 62 | /** 63 | * Create a new HTTP Batch handler for this service 64 | * 65 | * @return Batch 66 | */ 67 | public function createBatch() 68 | { 69 | return new Batch( 70 | $this->client, 71 | false, 72 | $this->rootUrlTemplate ?? $this->rootUrl, 73 | $this->batchPath 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Service/Exception.php: -------------------------------------------------------------------------------- 1 | >|null $errors List of errors returned in an HTTP 37 | * response or null. Defaults to []. 38 | */ 39 | public function __construct( 40 | $message, 41 | $code = 0, 42 | ?Exception $previous = null, 43 | $errors = [] 44 | ) { 45 | if (version_compare(PHP_VERSION, '5.3.0') >= 0) { 46 | parent::__construct($message, $code, $previous); 47 | } else { 48 | parent::__construct($message, $code); 49 | } 50 | 51 | $this->errors = $errors; 52 | } 53 | 54 | /** 55 | * An example of the possible errors returned. 56 | * 57 | * [ 58 | * { 59 | * "domain": "global", 60 | * "reason": "authError", 61 | * "message": "Invalid Credentials", 62 | * "locationType": "header", 63 | * "location": "Authorization", 64 | * } 65 | * ] 66 | * 67 | * @return array>|null List of errors returned in an HTTP response or null. 68 | */ 69 | public function getErrors() 70 | { 71 | return $this->errors; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Service/README.md: -------------------------------------------------------------------------------- 1 | # Google API Client Services 2 | 3 | Google API Client Service classes have been moved to the 4 | [google-api-php-client-services](https://github.com/google/google-api-php-client-services) 5 | repository. 6 | -------------------------------------------------------------------------------- /src/Service/Resource.php: -------------------------------------------------------------------------------- 1 | ['type' => 'string', 'location' => 'query'], 37 | 'fields' => ['type' => 'string', 'location' => 'query'], 38 | 'trace' => ['type' => 'string', 'location' => 'query'], 39 | 'userIp' => ['type' => 'string', 'location' => 'query'], 40 | 'quotaUser' => ['type' => 'string', 'location' => 'query'], 41 | 'data' => ['type' => 'string', 'location' => 'body'], 42 | 'mimeType' => ['type' => 'string', 'location' => 'header'], 43 | 'uploadType' => ['type' => 'string', 'location' => 'query'], 44 | 'mediaUpload' => ['type' => 'complex', 'location' => 'query'], 45 | 'prettyPrint' => ['type' => 'string', 'location' => 'query'], 46 | ]; 47 | 48 | /** @var string $rootUrlTemplate */ 49 | private $rootUrlTemplate; 50 | 51 | /** @var string $apiVersion */ 52 | protected $apiVersion; 53 | 54 | /** @var \Google\Client $client */ 55 | private $client; 56 | 57 | /** @var string $serviceName */ 58 | private $serviceName; 59 | 60 | /** @var string $servicePath */ 61 | private $servicePath; 62 | 63 | /** @var string $resourceName */ 64 | private $resourceName; 65 | 66 | /** @var array $methods */ 67 | private $methods; 68 | 69 | public function __construct($service, $serviceName, $resourceName, $resource) 70 | { 71 | $this->rootUrlTemplate = $service->rootUrlTemplate ?? $service->rootUrl; 72 | $this->client = $service->getClient(); 73 | $this->servicePath = $service->servicePath; 74 | $this->serviceName = $serviceName; 75 | $this->resourceName = $resourceName; 76 | $this->methods = is_array($resource) && isset($resource['methods']) ? 77 | $resource['methods'] : 78 | [$resourceName => $resource]; 79 | } 80 | 81 | /** 82 | * TODO: This function needs simplifying. 83 | * 84 | * @template T 85 | * @param string $name 86 | * @param array $arguments 87 | * @param class-string $expectedClass - optional, the expected class name 88 | * @return mixed|T|ResponseInterface|RequestInterface 89 | * @throws \Google\Exception 90 | */ 91 | public function call($name, $arguments, $expectedClass = null) 92 | { 93 | if (! isset($this->methods[$name])) { 94 | $this->client->getLogger()->error( 95 | 'Service method unknown', 96 | [ 97 | 'service' => $this->serviceName, 98 | 'resource' => $this->resourceName, 99 | 'method' => $name 100 | ] 101 | ); 102 | 103 | throw new GoogleException( 104 | "Unknown function: " . 105 | "{$this->serviceName}->{$this->resourceName}->{$name}()" 106 | ); 107 | } 108 | $method = $this->methods[$name]; 109 | $parameters = $arguments[0]; 110 | 111 | // postBody is a special case since it's not defined in the discovery 112 | // document as parameter, but we abuse the param entry for storing it. 113 | $postBody = null; 114 | if (isset($parameters['postBody'])) { 115 | if ($parameters['postBody'] instanceof Model) { 116 | // In the cases the post body is an existing object, we want 117 | // to use the smart method to create a simple object for 118 | // for JSONification. 119 | $parameters['postBody'] = $parameters['postBody']->toSimpleObject(); 120 | } elseif (is_object($parameters['postBody'])) { 121 | // If the post body is another kind of object, we will try and 122 | // wrangle it into a sensible format. 123 | $parameters['postBody'] = 124 | $this->convertToArrayAndStripNulls($parameters['postBody']); 125 | } 126 | $postBody = (array) $parameters['postBody']; 127 | unset($parameters['postBody']); 128 | } 129 | 130 | // TODO: optParams here probably should have been 131 | // handled already - this may well be redundant code. 132 | if (isset($parameters['optParams'])) { 133 | $optParams = $parameters['optParams']; 134 | unset($parameters['optParams']); 135 | $parameters = array_merge($parameters, $optParams); 136 | } 137 | 138 | if (!isset($method['parameters'])) { 139 | $method['parameters'] = []; 140 | } 141 | 142 | $method['parameters'] = array_merge( 143 | $this->stackParameters, 144 | $method['parameters'] 145 | ); 146 | 147 | foreach ($parameters as $key => $val) { 148 | if ($key != 'postBody' && !isset($method['parameters'][$key])) { 149 | $this->client->getLogger()->error( 150 | 'Service parameter unknown', 151 | [ 152 | 'service' => $this->serviceName, 153 | 'resource' => $this->resourceName, 154 | 'method' => $name, 155 | 'parameter' => $key 156 | ] 157 | ); 158 | throw new GoogleException("($name) unknown parameter: '$key'"); 159 | } 160 | } 161 | 162 | foreach ($method['parameters'] as $paramName => $paramSpec) { 163 | if ( 164 | isset($paramSpec['required']) && 165 | $paramSpec['required'] && 166 | ! isset($parameters[$paramName]) 167 | ) { 168 | $this->client->getLogger()->error( 169 | 'Service parameter missing', 170 | [ 171 | 'service' => $this->serviceName, 172 | 'resource' => $this->resourceName, 173 | 'method' => $name, 174 | 'parameter' => $paramName 175 | ] 176 | ); 177 | throw new GoogleException("($name) missing required param: '$paramName'"); 178 | } 179 | if (isset($parameters[$paramName])) { 180 | $value = $parameters[$paramName]; 181 | $parameters[$paramName] = $paramSpec; 182 | $parameters[$paramName]['value'] = $value; 183 | unset($parameters[$paramName]['required']); 184 | } else { 185 | // Ensure we don't pass nulls. 186 | unset($parameters[$paramName]); 187 | } 188 | } 189 | 190 | $this->client->getLogger()->info( 191 | 'Service Call', 192 | [ 193 | 'service' => $this->serviceName, 194 | 'resource' => $this->resourceName, 195 | 'method' => $name, 196 | 'arguments' => $parameters, 197 | ] 198 | ); 199 | 200 | // build the service uri 201 | $url = $this->createRequestUri($method['path'], $parameters); 202 | 203 | // NOTE: because we're creating the request by hand, 204 | // and because the service has a rootUrl property 205 | // the "base_uri" of the Http Client is not accounted for 206 | $request = new Request( 207 | $method['httpMethod'], 208 | $url, 209 | $postBody ? ['content-type' => 'application/json'] : [], 210 | $postBody ? json_encode($postBody) : '' 211 | ); 212 | 213 | // support uploads 214 | if (isset($parameters['data'])) { 215 | $mimeType = isset($parameters['mimeType']) 216 | ? $parameters['mimeType']['value'] 217 | : 'application/octet-stream'; 218 | $data = $parameters['data']['value']; 219 | $upload = new MediaFileUpload($this->client, $request, $mimeType, $data); 220 | 221 | // pull down the modified request 222 | $request = $upload->getRequest(); 223 | } 224 | 225 | // if this is a media type, we will return the raw response 226 | // rather than using an expected class 227 | if (isset($parameters['alt']) && $parameters['alt']['value'] == 'media') { 228 | $expectedClass = null; 229 | } 230 | 231 | // If the class which is extending from this one contains 232 | // an Api Version, add it to the header 233 | if ($this->apiVersion) { 234 | $request = $request 235 | ->withHeader('X-Goog-Api-Version', $this->apiVersion); 236 | } 237 | 238 | // if the client is marked for deferring, rather than 239 | // execute the request, return the response 240 | if ($this->client->shouldDefer()) { 241 | // @TODO find a better way to do this 242 | $request = $request 243 | ->withHeader('X-Php-Expected-Class', $expectedClass); 244 | 245 | return $request; 246 | } 247 | 248 | return $this->client->execute($request, $expectedClass); 249 | } 250 | 251 | protected function convertToArrayAndStripNulls($o) 252 | { 253 | $o = (array) $o; 254 | foreach ($o as $k => $v) { 255 | if ($v === null) { 256 | unset($o[$k]); 257 | } elseif (is_object($v) || is_array($v)) { 258 | $o[$k] = $this->convertToArrayAndStripNulls($o[$k]); 259 | } 260 | } 261 | return $o; 262 | } 263 | 264 | /** 265 | * Parse/expand request parameters and create a fully qualified 266 | * request uri. 267 | * @static 268 | * @param string $restPath 269 | * @param array $params 270 | * @return string $requestUrl 271 | */ 272 | public function createRequestUri($restPath, $params) 273 | { 274 | // Override the default servicePath address if the $restPath use a / 275 | if ('/' == substr($restPath, 0, 1)) { 276 | $requestUrl = substr($restPath, 1); 277 | } else { 278 | $requestUrl = $this->servicePath . $restPath; 279 | } 280 | 281 | if ($this->rootUrlTemplate) { 282 | // code for universe domain 283 | $rootUrl = str_replace('UNIVERSE_DOMAIN', $this->client->getUniverseDomain(), $this->rootUrlTemplate); 284 | // code for leading slash 285 | if ('/' !== substr($rootUrl, -1) && '/' !== substr($requestUrl, 0, 1)) { 286 | $requestUrl = '/' . $requestUrl; 287 | } 288 | $requestUrl = $rootUrl . $requestUrl; 289 | } 290 | $uriTemplateVars = []; 291 | $queryVars = []; 292 | foreach ($params as $paramName => $paramSpec) { 293 | if ($paramSpec['type'] == 'boolean') { 294 | $paramSpec['value'] = $paramSpec['value'] ? 'true' : 'false'; 295 | } 296 | if ($paramSpec['location'] == 'path') { 297 | $uriTemplateVars[$paramName] = $paramSpec['value']; 298 | } elseif ($paramSpec['location'] == 'query') { 299 | if (is_array($paramSpec['value'])) { 300 | foreach ($paramSpec['value'] as $value) { 301 | $queryVars[] = $paramName . '=' . rawurlencode(rawurldecode($value)); 302 | } 303 | } else { 304 | $queryVars[] = $paramName . '=' . rawurlencode(rawurldecode($paramSpec['value'])); 305 | } 306 | } 307 | } 308 | 309 | if (count($uriTemplateVars)) { 310 | $uriTemplateParser = new UriTemplate(); 311 | $requestUrl = $uriTemplateParser->parse($requestUrl, $uriTemplateVars); 312 | } 313 | 314 | if (count($queryVars)) { 315 | $requestUrl .= '?' . implode('&', $queryVars); 316 | } 317 | 318 | return $requestUrl; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/Task/Composer.php: -------------------------------------------------------------------------------- 1 | getComposer(); 36 | $extra = $composer->getPackage()->getExtra(); 37 | $servicesToKeep = $extra['google/apiclient-services'] ?? []; 38 | if (empty($servicesToKeep)) { 39 | return; 40 | } 41 | $vendorDir = $composer->getConfig()->get('vendor-dir'); 42 | $serviceDir = sprintf( 43 | '%s/google/apiclient-services/src/Google/Service', 44 | $vendorDir 45 | ); 46 | if (!is_dir($serviceDir)) { 47 | // path for google/apiclient-services >= 0.200.0 48 | $serviceDir = sprintf( 49 | '%s/google/apiclient-services/src', 50 | $vendorDir 51 | ); 52 | } 53 | self::verifyServicesToKeep($serviceDir, $servicesToKeep); 54 | $finder = self::getServicesToRemove($serviceDir, $servicesToKeep); 55 | $filesystem = $filesystem ?: new Filesystem(); 56 | $servicesToRemoveCount = $finder->count(); 57 | if (0 === $servicesToRemoveCount) { 58 | return; 59 | } 60 | $event->getIO()->write( 61 | sprintf('Removing %d google services', $servicesToRemoveCount) 62 | ); 63 | $pathsToRemove = iterator_to_array($finder); 64 | foreach ($pathsToRemove as $pathToRemove) { 65 | $realpath = $pathToRemove->getRealPath(); 66 | $filesystem->remove($realpath); 67 | $filesystem->remove($realpath . '.php'); 68 | } 69 | } 70 | 71 | /** 72 | * @throws InvalidArgumentException when the service doesn't exist 73 | */ 74 | private static function verifyServicesToKeep( 75 | $serviceDir, 76 | array $servicesToKeep 77 | ) { 78 | $finder = (new Finder()) 79 | ->directories() 80 | ->depth('== 0'); 81 | 82 | foreach ($servicesToKeep as $service) { 83 | if (!preg_match('/^[a-zA-Z0-9]*$/', $service)) { 84 | throw new InvalidArgumentException( 85 | sprintf( 86 | 'Invalid Google service name "%s"', 87 | $service 88 | ) 89 | ); 90 | } 91 | try { 92 | $finder->in($serviceDir . '/' . $service); 93 | } catch (InvalidArgumentException $e) { 94 | throw new InvalidArgumentException( 95 | sprintf( 96 | 'Google service "%s" does not exist or was removed previously', 97 | $service 98 | ) 99 | ); 100 | } 101 | } 102 | } 103 | 104 | private static function getServicesToRemove( 105 | $serviceDir, 106 | array $servicesToKeep 107 | ) { 108 | // find all files in the current directory 109 | return (new Finder()) 110 | ->directories() 111 | ->depth('== 0') 112 | ->in($serviceDir) 113 | ->exclude($servicesToKeep); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Task/Exception.php: -------------------------------------------------------------------------------- 1 | self::TASK_RETRY_ALWAYS, 81 | '503' => self::TASK_RETRY_ALWAYS, 82 | 'rateLimitExceeded' => self::TASK_RETRY_ALWAYS, 83 | 'userRateLimitExceeded' => self::TASK_RETRY_ALWAYS, 84 | 6 => self::TASK_RETRY_ALWAYS, // CURLE_COULDNT_RESOLVE_HOST 85 | 7 => self::TASK_RETRY_ALWAYS, // CURLE_COULDNT_CONNECT 86 | 28 => self::TASK_RETRY_ALWAYS, // CURLE_OPERATION_TIMEOUTED 87 | 35 => self::TASK_RETRY_ALWAYS, // CURLE_SSL_CONNECT_ERROR 88 | 52 => self::TASK_RETRY_ALWAYS, // CURLE_GOT_NOTHING 89 | 'lighthouseError' => self::TASK_RETRY_NEVER 90 | ]; 91 | 92 | /** 93 | * Creates a new task runner with exponential backoff support. 94 | * 95 | * @param array $config The task runner config 96 | * @param string $name The name of the current task (used for logging) 97 | * @param callable $action The task to run and possibly retry 98 | * @param array $arguments The task arguments 99 | * @throws \Google\Task\Exception when misconfigured 100 | */ 101 | // @phpstan-ignore-next-line 102 | public function __construct( 103 | $config, 104 | $name, 105 | $action, 106 | array $arguments = [] 107 | ) { 108 | if (isset($config['initial_delay'])) { 109 | if ($config['initial_delay'] < 0) { 110 | throw new GoogleTaskException( 111 | 'Task configuration `initial_delay` must not be negative.' 112 | ); 113 | } 114 | 115 | $this->delay = $config['initial_delay']; 116 | } 117 | 118 | if (isset($config['max_delay'])) { 119 | if ($config['max_delay'] <= 0) { 120 | throw new GoogleTaskException( 121 | 'Task configuration `max_delay` must be greater than 0.' 122 | ); 123 | } 124 | 125 | $this->maxDelay = $config['max_delay']; 126 | } 127 | 128 | if (isset($config['factor'])) { 129 | if ($config['factor'] <= 0) { 130 | throw new GoogleTaskException( 131 | 'Task configuration `factor` must be greater than 0.' 132 | ); 133 | } 134 | 135 | $this->factor = $config['factor']; 136 | } 137 | 138 | if (isset($config['jitter'])) { 139 | if ($config['jitter'] <= 0) { 140 | throw new GoogleTaskException( 141 | 'Task configuration `jitter` must be greater than 0.' 142 | ); 143 | } 144 | 145 | $this->jitter = $config['jitter']; 146 | } 147 | 148 | if (isset($config['retries'])) { 149 | if ($config['retries'] < 0) { 150 | throw new GoogleTaskException( 151 | 'Task configuration `retries` must not be negative.' 152 | ); 153 | } 154 | $this->maxAttempts += $config['retries']; 155 | } 156 | 157 | if (!is_callable($action)) { 158 | throw new GoogleTaskException( 159 | 'Task argument `$action` must be a valid callable.' 160 | ); 161 | } 162 | 163 | $this->action = $action; 164 | $this->arguments = $arguments; 165 | } 166 | 167 | /** 168 | * Checks if a retry can be attempted. 169 | * 170 | * @return boolean 171 | */ 172 | public function canAttempt() 173 | { 174 | return $this->attempts < $this->maxAttempts; 175 | } 176 | 177 | /** 178 | * Runs the task and (if applicable) automatically retries when errors occur. 179 | * 180 | * @return mixed 181 | * @throws \Google\Service\Exception on failure when no retries are available. 182 | */ 183 | public function run() 184 | { 185 | while ($this->attempt()) { 186 | try { 187 | return call_user_func_array($this->action, $this->arguments); 188 | } catch (GoogleServiceException $exception) { 189 | $allowedRetries = $this->allowedRetries( 190 | $exception->getCode(), 191 | $exception->getErrors() 192 | ); 193 | 194 | if (!$this->canAttempt() || !$allowedRetries) { 195 | throw $exception; 196 | } 197 | 198 | if ($allowedRetries > 0) { 199 | $this->maxAttempts = min( 200 | $this->maxAttempts, 201 | $this->attempts + $allowedRetries 202 | ); 203 | } 204 | } 205 | } 206 | } 207 | 208 | /** 209 | * Runs a task once, if possible. This is useful for bypassing the `run()` 210 | * loop. 211 | * 212 | * NOTE: If this is not the first attempt, this function will sleep in 213 | * accordance to the backoff configurations before running the task. 214 | * 215 | * @return boolean 216 | */ 217 | public function attempt() 218 | { 219 | if (!$this->canAttempt()) { 220 | return false; 221 | } 222 | 223 | if ($this->attempts > 0) { 224 | $this->backOff(); 225 | } 226 | 227 | $this->attempts++; 228 | 229 | return true; 230 | } 231 | 232 | /** 233 | * Sleeps in accordance to the backoff configurations. 234 | */ 235 | private function backOff() 236 | { 237 | $delay = $this->getDelay(); 238 | 239 | usleep((int) ($delay * 1000000)); 240 | } 241 | 242 | /** 243 | * Gets the delay (in seconds) for the current backoff period. 244 | * 245 | * @return int 246 | */ 247 | private function getDelay() 248 | { 249 | $jitter = $this->getJitter(); 250 | $factor = $this->attempts > 1 ? $this->factor + $jitter : 1 + abs($jitter); 251 | 252 | return $this->delay = min($this->maxDelay, $this->delay * $factor); 253 | } 254 | 255 | /** 256 | * Gets the current jitter (random number between -$this->jitter and 257 | * $this->jitter). 258 | * 259 | * @return float 260 | */ 261 | private function getJitter() 262 | { 263 | return $this->jitter * 2 * mt_rand() / mt_getrandmax() - $this->jitter; 264 | } 265 | 266 | /** 267 | * Gets the number of times the associated task can be retried. 268 | * 269 | * NOTE: -1 is returned if the task can be retried indefinitely 270 | * 271 | * @return integer 272 | */ 273 | public function allowedRetries($code, $errors = []) 274 | { 275 | if (isset($this->retryMap[$code])) { 276 | return $this->retryMap[$code]; 277 | } 278 | 279 | if ( 280 | !empty($errors) && 281 | isset($errors[0]['reason'], $this->retryMap[$errors[0]['reason']]) 282 | ) { 283 | return $this->retryMap[$errors[0]['reason']]; 284 | } 285 | 286 | return 0; 287 | } 288 | 289 | public function setRetryMap($retryMap) 290 | { 291 | $this->retryMap = $retryMap; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/Utils/UriTemplate.php: -------------------------------------------------------------------------------- 1 | "reserved", 38 | "/" => "segments", 39 | "." => "dotprefix", 40 | "#" => "fragment", 41 | ";" => "semicolon", 42 | "?" => "form", 43 | "&" => "continuation" 44 | ]; 45 | 46 | /** 47 | * @var array 48 | * These are the characters which should not be URL encoded in reserved 49 | * strings. 50 | */ 51 | private $reserved = [ 52 | "=", ",", "!", "@", "|", ":", "/", "?", "#", 53 | "[", "]", '$', "&", "'", "(", ")", "*", "+", ";" 54 | ]; 55 | private $reservedEncoded = [ 56 | "%3D", "%2C", "%21", "%40", "%7C", "%3A", "%2F", "%3F", 57 | "%23", "%5B", "%5D", "%24", "%26", "%27", "%28", "%29", 58 | "%2A", "%2B", "%3B" 59 | ]; 60 | 61 | public function parse($string, array $parameters) 62 | { 63 | return $this->resolveNextSection($string, $parameters); 64 | } 65 | 66 | /** 67 | * This function finds the first matching {...} block and 68 | * executes the replacement. It then calls itself to find 69 | * subsequent blocks, if any. 70 | */ 71 | private function resolveNextSection($string, $parameters) 72 | { 73 | $start = strpos($string, "{"); 74 | if ($start === false) { 75 | return $string; 76 | } 77 | $end = strpos($string, "}"); 78 | if ($end === false) { 79 | return $string; 80 | } 81 | $string = $this->replace($string, $start, $end, $parameters); 82 | return $this->resolveNextSection($string, $parameters); 83 | } 84 | 85 | private function replace($string, $start, $end, $parameters) 86 | { 87 | // We know a data block will have {} round it, so we can strip that. 88 | $data = substr($string, $start + 1, $end - $start - 1); 89 | 90 | // If the first character is one of the reserved operators, it effects 91 | // the processing of the stream. 92 | if (isset($this->operators[$data[0]])) { 93 | $op = $this->operators[$data[0]]; 94 | $data = substr($data, 1); 95 | $prefix = ""; 96 | $prefix_on_missing = false; 97 | 98 | switch ($op) { 99 | case "reserved": 100 | // Reserved means certain characters should not be URL encoded 101 | $data = $this->replaceVars($data, $parameters, ",", null, true); 102 | break; 103 | case "fragment": 104 | // Comma separated with fragment prefix. Bare values only. 105 | $prefix = "#"; 106 | $prefix_on_missing = true; 107 | $data = $this->replaceVars($data, $parameters, ",", null, true); 108 | break; 109 | case "segments": 110 | // Slash separated data. Bare values only. 111 | $prefix = "/"; 112 | $data =$this->replaceVars($data, $parameters, "/"); 113 | break; 114 | case "dotprefix": 115 | // Dot separated data. Bare values only. 116 | $prefix = "."; 117 | $prefix_on_missing = true; 118 | $data = $this->replaceVars($data, $parameters, "."); 119 | break; 120 | case "semicolon": 121 | // Semicolon prefixed and separated. Uses the key name 122 | $prefix = ";"; 123 | $data = $this->replaceVars($data, $parameters, ";", "=", false, true, false); 124 | break; 125 | case "form": 126 | // Standard URL format. Uses the key name 127 | $prefix = "?"; 128 | $data = $this->replaceVars($data, $parameters, "&", "="); 129 | break; 130 | case "continuation": 131 | // Standard URL, but with leading ampersand. Uses key name. 132 | $prefix = "&"; 133 | $data = $this->replaceVars($data, $parameters, "&", "="); 134 | break; 135 | } 136 | 137 | // Add the initial prefix character if data is valid. 138 | if ($data || ($data !== false && $prefix_on_missing)) { 139 | $data = $prefix . $data; 140 | } 141 | } else { 142 | // If no operator we replace with the defaults. 143 | $data = $this->replaceVars($data, $parameters); 144 | } 145 | // This is chops out the {...} and replaces with the new section. 146 | return substr($string, 0, $start) . $data . substr($string, $end + 1); 147 | } 148 | 149 | private function replaceVars( 150 | $section, 151 | $parameters, 152 | $sep = ",", 153 | $combine = null, 154 | $reserved = false, 155 | $tag_empty = false, 156 | $combine_on_empty = true 157 | ) { 158 | if (strpos($section, ",") === false) { 159 | // If we only have a single value, we can immediately process. 160 | return $this->combine( 161 | $section, 162 | $parameters, 163 | $sep, 164 | $combine, 165 | $reserved, 166 | $tag_empty, 167 | $combine_on_empty 168 | ); 169 | } else { 170 | // If we have multiple values, we need to split and loop over them. 171 | // Each is treated individually, then glued together with the 172 | // separator character. 173 | $vars = explode(",", $section); 174 | return $this->combineList( 175 | $vars, 176 | $sep, 177 | $parameters, 178 | $combine, 179 | $reserved, 180 | false, // Never emit empty strings in multi-param replacements 181 | $combine_on_empty 182 | ); 183 | } 184 | } 185 | 186 | public function combine( 187 | $key, 188 | $parameters, 189 | $sep, 190 | $combine, 191 | $reserved, 192 | $tag_empty, 193 | $combine_on_empty 194 | ) { 195 | $length = false; 196 | $explode = false; 197 | $skip_final_combine = false; 198 | $value = false; 199 | 200 | // Check for length restriction. 201 | if (strpos($key, ":") !== false) { 202 | list($key, $length) = explode(":", $key); 203 | } 204 | 205 | // Check for explode parameter. 206 | if ($key[strlen($key) - 1] == "*") { 207 | $explode = true; 208 | $key = substr($key, 0, -1); 209 | $skip_final_combine = true; 210 | } 211 | 212 | // Define the list separator. 213 | $list_sep = $explode ? $sep : ","; 214 | 215 | if (isset($parameters[$key])) { 216 | $data_type = $this->getDataType($parameters[$key]); 217 | switch ($data_type) { 218 | case self::TYPE_SCALAR: 219 | $value = $this->getValue($parameters[$key], $length); 220 | break; 221 | case self::TYPE_LIST: 222 | $values = []; 223 | foreach ($parameters[$key] as $pkey => $pvalue) { 224 | $pvalue = $this->getValue($pvalue, $length); 225 | if ($combine && $explode) { 226 | $values[$pkey] = $key . $combine . $pvalue; 227 | } else { 228 | $values[$pkey] = $pvalue; 229 | } 230 | } 231 | $value = implode($list_sep, $values); 232 | if ($value == '') { 233 | return ''; 234 | } 235 | break; 236 | case self::TYPE_MAP: 237 | $values = []; 238 | foreach ($parameters[$key] as $pkey => $pvalue) { 239 | $pvalue = $this->getValue($pvalue, $length); 240 | if ($explode) { 241 | $pkey = $this->getValue($pkey, $length); 242 | $values[] = $pkey . "=" . $pvalue; // Explode triggers = combine. 243 | } else { 244 | $values[] = $pkey; 245 | $values[] = $pvalue; 246 | } 247 | } 248 | $value = implode($list_sep, $values); 249 | if ($value == '') { 250 | return false; 251 | } 252 | break; 253 | } 254 | } elseif ($tag_empty) { 255 | // If we are just indicating empty values with their key name, return that. 256 | return $key; 257 | } else { 258 | // Otherwise we can skip this variable due to not being defined. 259 | return false; 260 | } 261 | 262 | if ($reserved) { 263 | $value = str_replace($this->reservedEncoded, $this->reserved, $value); 264 | } 265 | 266 | // If we do not need to include the key name, we just return the raw 267 | // value. 268 | if (!$combine || $skip_final_combine) { 269 | return $value; 270 | } 271 | 272 | // Else we combine the key name: foo=bar, if value is not the empty string. 273 | return $key . ($value != '' || $combine_on_empty ? $combine . $value : ''); 274 | } 275 | 276 | /** 277 | * Return the type of a passed in value 278 | */ 279 | private function getDataType($data) 280 | { 281 | if (is_array($data)) { 282 | reset($data); 283 | if (key($data) !== 0) { 284 | return self::TYPE_MAP; 285 | } 286 | return self::TYPE_LIST; 287 | } 288 | return self::TYPE_SCALAR; 289 | } 290 | 291 | /** 292 | * Utility function that merges multiple combine calls 293 | * for multi-key templates. 294 | */ 295 | private function combineList( 296 | $vars, 297 | $sep, 298 | $parameters, 299 | $combine, 300 | $reserved, 301 | $tag_empty, 302 | $combine_on_empty 303 | ) { 304 | $ret = []; 305 | foreach ($vars as $var) { 306 | $response = $this->combine( 307 | $var, 308 | $parameters, 309 | $sep, 310 | $combine, 311 | $reserved, 312 | $tag_empty, 313 | $combine_on_empty 314 | ); 315 | if ($response === false) { 316 | continue; 317 | } 318 | $ret[] = $response; 319 | } 320 | return implode($sep, $ret); 321 | } 322 | 323 | /** 324 | * Utility function to encode and trim values 325 | */ 326 | private function getValue($value, $length) 327 | { 328 | if ($length) { 329 | $value = substr($value, 0, $length); 330 | } 331 | $value = rawurlencode($value); 332 | return $value; 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/aliases.php: -------------------------------------------------------------------------------- 1 | 'Google_Client', 11 | 'Google\\Service' => 'Google_Service', 12 | 'Google\\AccessToken\\Revoke' => 'Google_AccessToken_Revoke', 13 | 'Google\\AccessToken\\Verify' => 'Google_AccessToken_Verify', 14 | 'Google\\Model' => 'Google_Model', 15 | 'Google\\Utils\\UriTemplate' => 'Google_Utils_UriTemplate', 16 | 'Google\\AuthHandler\\Guzzle6AuthHandler' => 'Google_AuthHandler_Guzzle6AuthHandler', 17 | 'Google\\AuthHandler\\Guzzle7AuthHandler' => 'Google_AuthHandler_Guzzle7AuthHandler', 18 | 'Google\\AuthHandler\\AuthHandlerFactory' => 'Google_AuthHandler_AuthHandlerFactory', 19 | 'Google\\Http\\Batch' => 'Google_Http_Batch', 20 | 'Google\\Http\\MediaFileUpload' => 'Google_Http_MediaFileUpload', 21 | 'Google\\Http\\REST' => 'Google_Http_REST', 22 | 'Google\\Task\\Retryable' => 'Google_Task_Retryable', 23 | 'Google\\Task\\Exception' => 'Google_Task_Exception', 24 | 'Google\\Task\\Runner' => 'Google_Task_Runner', 25 | 'Google\\Collection' => 'Google_Collection', 26 | 'Google\\Service\\Exception' => 'Google_Service_Exception', 27 | 'Google\\Service\\Resource' => 'Google_Service_Resource', 28 | 'Google\\Exception' => 'Google_Exception', 29 | ]; 30 | 31 | foreach ($classMap as $class => $alias) { 32 | class_alias($class, $alias); 33 | } 34 | 35 | /** 36 | * This class needs to be defined explicitly as scripts must be recognized by 37 | * the autoloader. 38 | */ 39 | class Google_Task_Composer extends \Google\Task\Composer 40 | { 41 | } 42 | 43 | /** @phpstan-ignore-next-line */ 44 | if (\false) { 45 | class Google_AccessToken_Revoke extends \Google\AccessToken\Revoke 46 | { 47 | } 48 | class Google_AccessToken_Verify extends \Google\AccessToken\Verify 49 | { 50 | } 51 | class Google_AuthHandler_AuthHandlerFactory extends \Google\AuthHandler\AuthHandlerFactory 52 | { 53 | } 54 | class Google_AuthHandler_Guzzle6AuthHandler extends \Google\AuthHandler\Guzzle6AuthHandler 55 | { 56 | } 57 | class Google_AuthHandler_Guzzle7AuthHandler extends \Google\AuthHandler\Guzzle7AuthHandler 58 | { 59 | } 60 | class Google_Client extends \Google\Client 61 | { 62 | } 63 | class Google_Collection extends \Google\Collection 64 | { 65 | } 66 | class Google_Exception extends \Google\Exception 67 | { 68 | } 69 | class Google_Http_Batch extends \Google\Http\Batch 70 | { 71 | } 72 | class Google_Http_MediaFileUpload extends \Google\Http\MediaFileUpload 73 | { 74 | } 75 | class Google_Http_REST extends \Google\Http\REST 76 | { 77 | } 78 | class Google_Model extends \Google\Model 79 | { 80 | } 81 | class Google_Service extends \Google\Service 82 | { 83 | } 84 | class Google_Service_Exception extends \Google\Service\Exception 85 | { 86 | } 87 | class Google_Service_Resource extends \Google\Service\Resource 88 | { 89 | } 90 | class Google_Task_Exception extends \Google\Task\Exception 91 | { 92 | } 93 | interface Google_Task_Retryable extends \Google\Task\Retryable 94 | { 95 | } 96 | class Google_Task_Runner extends \Google\Task\Runner 97 | { 98 | } 99 | class Google_Utils_UriTemplate extends \Google\Utils\UriTemplate 100 | { 101 | } 102 | } 103 | --------------------------------------------------------------------------------