├── 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 | 
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 |
--------------------------------------------------------------------------------