├── .github
└── workflows
│ └── ci.yaml
├── .gitignore
├── CODEOWNERS
├── CONTRIBUTING.rst
├── LICENSE
├── MIGRATION.rst
├── README.md
├── codecov.yml
├── composer.json
├── examples
├── README.md
├── create-Redirection
│ ├── api_create_redirection.md
│ └── apiv6.php
├── hosting-attachedDomain
│ ├── api_attach_domain_to_web_hosting.md
│ ├── createAttachedDomain.php
│ ├── deleteAttachedDomain.php
│ └── listAttachedDomains.php
└── hosting-getCapabilities
│ ├── api_get_hosting_capacities.md
│ └── apiv6.php
├── img
└── logo.png
├── phpcs.xml
├── phpdoc.dist.xml
├── phpunit.xml.dist
├── scripts
├── bump-version.sh
├── release_binary.sh
└── update-copyright.sh
├── src
├── Api.php
├── Exceptions
│ ├── ApiException.php
│ ├── InvalidParameterException.php
│ ├── NotLoggedException.php
│ └── OAuth2FailureException.php
└── OAuth2.php
└── tests
├── ApiFunctionalTest.php
├── ApiTest.php
└── bootstrap.php
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/categories/automating-your-workflow-with-github-actions
2 |
3 | name: "CI"
4 |
5 | on:
6 | pull_request:
7 | push:
8 | branches:
9 | - "main"
10 | - "master"
11 |
12 | env:
13 | COMPOSER_ROOT_VERSION: "1.99.99"
14 | APP_KEY: ${{ secrets.OVH_TESTS_APP_KEY }}
15 | APP_SECRET: ${{ secrets.OVH_TESTS_APP_SECRET }}
16 | CONSUMER: ${{ secrets.OVH_TESTS_CONSUMER_KEY }}
17 | ENDPOINT: ${{ secrets.OVH_TESTS_ENDPOINT }}
18 |
19 | jobs:
20 |
21 | lint:
22 | name: "Lint"
23 | runs-on: "ubuntu-latest"
24 | strategy:
25 | fail-fast: false
26 | matrix:
27 | php-version: [ '8.1', '8.2', '8.3', '8.4' ]
28 | steps:
29 | - uses: "actions/checkout@v2"
30 | - uses: "shivammathur/setup-php@v2"
31 | with:
32 | php-version: "${{ matrix.php-version }}"
33 | coverage: "none"
34 | ini-values: "memory_limit=-1, zend.assertions=1, error_reporting=-1, display_errors=On"
35 | tools: "composer:v2"
36 | - uses: "ramsey/composer-install@v2"
37 | - name: "Lint the PHP source code"
38 | run: "composer parallel-lint"
39 |
40 | coding-standards:
41 | name: "Coding Standards"
42 | runs-on: "ubuntu-latest"
43 | steps:
44 | - uses: "actions/checkout@v2"
45 | - uses: "shivammathur/setup-php@v2"
46 | with:
47 | php-version: "latest"
48 | coverage: "none"
49 | ini-values: "memory_limit=-1"
50 | tools: "composer:v2"
51 | - uses: "ramsey/composer-install@v2"
52 | - name: "Check coding standards"
53 | run: "composer phpcs"
54 |
55 | coverage:
56 | name: "Coverage"
57 | runs-on: "ubuntu-latest"
58 | steps:
59 | - uses: "actions/checkout@v2"
60 | - uses: "shivammathur/setup-php@v2"
61 | with:
62 | php-version: "latest"
63 | coverage: "pcov"
64 | ini-values: "memory_limit=-1, zend.assertions=1, error_reporting=-1, display_errors=On"
65 | tools: "composer"
66 | - name: "Prepare for tests"
67 | run: "mkdir -p build/logs"
68 | - uses: "ramsey/composer-install@v2"
69 | - name: "Run unit tests"
70 | run: "composer phpunit"
71 | - name: "Publish coverage report to Codecov"
72 | uses: "codecov/codecov-action@v2"
73 | with:
74 | files: ./build/logs/clover.xml
75 |
76 | unit-tests:
77 | needs: coverage
78 | name: "Unit Tests"
79 | runs-on: "ubuntu-latest"
80 | strategy:
81 | max-parallel: 1
82 | fail-fast: false
83 | matrix:
84 | php-version: [ '8.1', '8.2', '8.3', '8.4' ]
85 | steps:
86 | - uses: "actions/checkout@v2"
87 | - uses: "shivammathur/setup-php@v2"
88 | with:
89 | php-version: "${{ matrix.php-version }}"
90 | coverage: "none"
91 | ini-values: "memory_limit=-1, zend.assertions=1, error_reporting=-1, display_errors=On"
92 | tools: "composer"
93 | - name: "Prepare for tests"
94 | run: "mkdir -p build/logs"
95 | - uses: "ramsey/composer-install@v2"
96 | - name: "Run unit tests"
97 | run: "composer phpunit -- --no-coverage"
98 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /build
3 | /vendor
4 | composer.lock
5 | composer.phar
6 | /phpunit.xml
7 | /build.properties
8 | /docs
9 | .phpunit.*.cache
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @rbeuque74 @deathiop @a-beudin @amstuta @mxpetit
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.rst:
--------------------------------------------------------------------------------
1 | Contributing to PHP-OVH
2 | ==========================
3 |
4 | This project accepts contributions. In order to contribute, you should
5 | pay attention to a few things:
6 |
7 | 1. your code must follow the coding style rules
8 | 2. your code must be fully (100% coverage) unit-tested
9 | 3. your code must be fully documented
10 | 4. your work must be signed
11 | 5. the format of the submission must be email patches or GitHub Pull Requests
12 |
13 |
14 | Coding and documentation Style:
15 | -------------------------------
16 |
17 | - The coding style follows PSR-2 `Coding Style Guide `_
18 | - The documentation uses `phpDocumentor `_
19 | - Unit test uses `phpUnit 9+ `_
20 | - Code syntax must follows `phpLint rules `_
21 |
22 | Submitting Modifications:
23 | -------------------------
24 |
25 | The contributions should be email patches. The guidelines are the same
26 | as the patch submission for the Linux kernel except for the DCO which
27 | is defined below. The guidelines are defined in the
28 | 'SubmittingPatches' file, available in the directory 'Documentation'
29 | of the Linux kernel source tree.
30 |
31 | It can be accessed online too:
32 |
33 | https://www.kernel.org/doc/html/latest/process/submitting-patches.html#sign-your-work-the-developer-s-certificate-of-origin
34 |
35 | You can submit your patches via GitHub.
36 |
37 | Licensing for new files:
38 | ------------------------
39 |
40 | PHP-OVH is licensed under a (modified) BSD license. Anything contributed to
41 | PHP-OVH must be released under this license.
42 |
43 | When introducing a new file into the project, please make sure it has a
44 | copyright header making clear under which license it's being released.
45 |
46 | Developer Certificate of Origin:
47 | --------------------------------
48 |
49 | To improve tracking of contributions to this project we will use a
50 | process modeled on the modified DCO 1.1 and use a "sign-off" procedure
51 | on patches that are being emailed around or contributed in any other
52 | way.
53 |
54 | The sign-off is a simple line at the end of the explanation for the
55 | patch, which certifies that you wrote it or otherwise have the right
56 | to pass it on as an open-source patch. The rules are pretty simple:
57 | if you can certify the below:
58 |
59 | By making a contribution to this project, I certify that:
60 |
61 | (a) The contribution was created in whole or in part by me and I have
62 | the right to submit it under the open source license indicated in
63 | the file; or
64 |
65 | (b) The contribution is based upon previous work that, to the best of
66 | my knowledge, is covered under an appropriate open source License
67 | and I have the right under that license to submit that work with
68 | modifications, whether created in whole or in part by me, under
69 | the same open source license (unless I am permitted to submit
70 | under a different license), as indicated in the file; or
71 |
72 | (c) The contribution was provided directly to me by some other person
73 | who certified (a), (b) or (c) and I have not modified it.
74 |
75 | (d) The contribution is made free of any other party's intellectual
76 | property claims or rights.
77 |
78 | (e) I understand and agree that this project and the contribution are
79 | public and that a record of the contribution (including all
80 | personal information I submit with it, including my sign-off) is
81 | maintained indefinitely and may be redistributed consistent with
82 | this project or the open source license(s) involved.
83 |
84 |
85 | then you just add a line saying
86 |
87 | Signed-off-by: Random J Developer
88 |
89 | using your real name (sorry, no pseudonyms or anonymous contributions.)
90 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013-2025, OVH SAS.
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 | * Neither the name of OVH SAS nor the
13 | names of its contributors may be used to endorse or promote products
14 | derived from this software without specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY OVH SAS AND CONTRIBUTORS ``AS IS'' AND ANY
17 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL OVH SAS AND CONTRIBUTORS BE LIABLE FOR ANY
20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
27 |
--------------------------------------------------------------------------------
/MIGRATION.rst:
--------------------------------------------------------------------------------
1 | ############################
2 | Migrate from legacy wrappers
3 | ############################
4 |
5 | This guide specifically targets developers comming from the legacy wrappers
6 | previously distributed on https://api.ovh.com/g934.first_step_with_api. It
7 | highligts the main evolutions between these 2 major version as well as some
8 | tips to help with the migration. If you have any further questions, feel free
9 | to drop a mail on api@ml.ovh.net (api-subscribe@ml.ovh.net to subscribe).
10 |
11 | Installation
12 | ============
13 |
14 | Legacy wrappers were distributed as zip files for direct integration into
15 | final projects. This new version is fully integrated with Composer standard
16 | distribution channels.
17 |
18 | Recommended way to add ``php-ovh`` to a project: add ``ovh`` to a
19 | ``composer.json`` file at the root of the project.
20 |
21 | .. code::
22 |
23 | # file: composer.json
24 | "require": {
25 | "ovh/ovh": "dev-master"
26 | }
27 |
28 |
29 | To refresh the dependencies, just run:
30 |
31 | .. code:: bash
32 |
33 | composer install
34 |
35 | Usage
36 | =====
37 |
38 | Import and the client class
39 | ---------------------------
40 |
41 | The new PHP wrapper use composer to manage project dependencies. If you
42 | want to use the client class, usage of namespace is more confortable
43 | with PSR-4 autoloading system. You can find more informations about
44 | `autoloading `_
45 |
46 | Legacy method:
47 | **************
48 |
49 | .. code:: php
50 |
51 | require('OvhApi/OvhApi.php');
52 |
53 | New method:
54 | ***********
55 |
56 | .. code:: php
57 |
58 | use \Ovh\Api;
59 |
60 | Instanciate a new client
61 | ------------------------
62 |
63 | Legacy method:
64 | **************
65 |
66 | .. code:: php
67 |
68 | $client = OvhApi(OVH_API_EU, 'app key', 'app secret', 'consumer key');
69 |
70 | New method:
71 | ***********
72 |
73 | .. code:: php
74 |
75 | $client = Client('app key', 'app secret', 'ovh-eu', 'consumer key');
76 |
77 | Similarly, ``OVH_API_CA`` has been replaced by ``'ovh-ca'``.
78 |
79 |
80 | Use the client
81 | --------------
82 |
83 | Legacy method:
84 | **************
85 |
86 | .. code:: php
87 |
88 | # API helpers
89 | $content = (object) array("param_1" => "value_1", "param_2" => "value_2");
90 | $data = $client->get('/my/method?filter_1=value_1&filter_2=value_2');
91 | $data = $client->post('/my/method', $content);
92 | $data = $client->put('/my/method', $content);
93 | $data = $client->delete('/my/method');
94 |
95 | New method:
96 | ***********
97 |
98 | .. code:: php
99 |
100 | # API helpers
101 | $content = (object) array("param_1" => "value_1", "param_2" => "value_2");
102 | $data = $client->get('/my/method?filter_1=value_1&filter_2=value_2');
103 | $data = $client->post('/my/method', $content);
104 | $data = $client->put('/my/method', $content);
105 | $data = $client->delete('/my/method');
106 |
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OVHcloud APIs lightweight PHP wrapper
2 |
3 | [](https://packagist.org/packages/ovh/ovh)
4 |
5 | [](https://github.com/ovh/php-ovh)
6 | [](https://github.com/ovh/php-ovh/actions?query=workflow%3ACI)
7 | [](https://codecov.io/gh/ovh/php-ovh)
8 | [](https://packagist.org/packages/ovh/ovh)
9 |
10 | This PHP package is a lightweight wrapper for OVHcloud APIs.
11 |
12 | The easiest way to use OVHcloud APIs in your PHP applications.
13 |
14 | Compatible with PHP 7.4, 8.0, 8.1, 8.2.
15 |
16 | ## Installation
17 |
18 | Install this wrapper and integrate it inside your PHP application with [Composer](https://getcomposer.org):
19 |
20 | composer require ovh/ovh
21 |
22 | ## Basic usage
23 |
24 | ```php
25 | get('/me')['firstname'];
35 | ```
36 |
37 | ## Advanced usage
38 |
39 | ### Handle exceptions
40 |
41 | Under the hood, `php-ovh` uses [Guzzle](http://docs.guzzlephp.org/en/latest/quickstart.html) by default to issue API requests.
42 |
43 | If everything goes well, it will return the response directly as shown in the examples above.
44 |
45 | If there is an error like a missing endpoint or object (404), an authentication or authorization error (401 or 403) or a parameter error, the Guzzle will raise a ``GuzzleHttp\Exception\ClientException`` exception. For server-side errors (5xx), it will raise a ``GuzzleHttp\Exception\ServerException`` exception.
46 |
47 | You can get the error details with a code like:
48 |
49 | ```php
50 | try {
51 | echo "Welcome " . $ovh->get('/me')['firstname'];
52 | } catch (GuzzleHttp\Exception\ClientException $e) {
53 | $response = $e->getResponse();
54 | $responseBodyAsString = $response->getBody()->getContents();
55 | echo $responseBodyAsString;
56 | }
57 | ```
58 |
59 | ### Customize HTTP client configuration
60 |
61 | You can inject your own HTTP client with your specific configuration. For instance, you can edit user-agent and timeout for all your requests
62 |
63 | ```php
64 | setDefaultOption('timeout', 1);
72 | $client->setDefaultOption('headers', ['User-Agent' => 'api_client']);
73 |
74 | // Api credentials can be retrieved from the urls specified in the "Supported endpoints" section below.
75 | // Inject the custom HTTP client as the 5th argument of the constructor
76 | $ovh = new Api($applicationKey,
77 | $applicationSecret,
78 | $endpoint,
79 | $consumerKey,
80 | $client);
81 |
82 | echo 'Welcome '.$ovh->get('/me')['firstname'];
83 | ```
84 |
85 | ### Authorization flow
86 |
87 | This flow will allow you to request consumerKey from an OVHcloud account owner.
88 | After allowing access to his account, he will be redirected to your application.
89 |
90 | See "OVHcloud API authentication" section below for more information about the authorization flow.
91 |
92 | ```php
93 | use \Ovh\Api;
94 | session_start();
95 |
96 | // Api credentials can be retrieved from the urls specified in the "Supported endpoints" section below.
97 | $ovh = new Api($applicationKey,
98 | $applicationSecret,
99 | $endpoint);
100 |
101 | // Specify the list of API routes you want to request
102 | $rights = [
103 | [ 'method' => 'GET', 'path' => '/me*' ],
104 | ];
105 |
106 | // After allowing your application access, the customer will be redirected to this URL.
107 | $redirectUrl = 'https://your_application_redirect_url'
108 |
109 | $credentials = $ovh->requestCredentials($rights, $redirectUrl);
110 |
111 | // Save consumer key and redirect to authentication page
112 | $_SESSION['consumerKey'] = $credentials['consumerKey'];
113 | header('location: '. $credentials['validationUrl']);
114 | // After successful redirect, the consumerKey in the session will be activated and you will be able to use it to make API requests like in the "Basic usage" section above.
115 | ```
116 |
117 | ### Code sample: Enable network burst on GRA1 dedicated servers
118 |
119 | Here is a more complex example of how to use the wrapper to enable network burst on GRA1 dedicated servers.
120 |
121 | ```php
122 | get('/dedicated/server/');
134 | foreach ($servers as $server) {
135 | // Load the server details
136 | $details = $ovh->get('/dedicated/server/'.$server);
137 | // Filter servers only inside GRA1
138 | if ($details['datacenter'] == 'gra1') {
139 | // Activate burst on server
140 | $content = ['status' => 'active'];
141 | $ovh->put('/dedicated/server/'.$server.'/burst', $content);
142 | echo 'Burst enabled on '.$server;
143 | }
144 | }
145 | ```
146 |
147 | ### More code samples
148 |
149 | Do you want to use OVHcloud APIs? Maybe the script you want is already written in the [example part](examples/README.md) of this repository!
150 |
151 | ## OAuth2 authentification
152 |
153 | `php-ovh` supports two forms of authentication:
154 |
155 | * OAuth2, using scopped service accounts, and compatible with OVHcloud IAM
156 | * application key & application secret & consumer key (covered in the next chapter)
157 |
158 | For OAuth2, first, you need to generate a pair of valid `client_id` and `client_secret`: you can proceed by
159 | [following this documentation](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343).
160 |
161 | Once you have retrieved your `client_id` and `client_secret`, you can instantiate an API client using:
162 |
163 | ```php
164 | use \Ovh\Api;
165 |
166 | $ovh = Api::withOauth2($clientId, $clientSecret, $endpoint);
167 | ```
168 |
169 | Supported endpoints are only `ovh-eu`, `ovh-ca` and `ovh-us`.
170 |
171 | ## Custom OVHcloud API authentication
172 |
173 | To use the OVHcloud APIs you need three credentials:
174 |
175 | * An application key
176 | * An application secret
177 | * A consumer key
178 |
179 | The application key and secret are not granting access to a specific account and are unique to identify your application.
180 | The consumer key is used to grant access to a specific OVHcloud account to a specified application.
181 |
182 | They can be created separately if your application is intended to be used by multiple accounts (your app will need to implement an authorization flow).
183 | In the authorization flow, the customer will be prompted to allow access to his account to your application, then he will be redirected to your application.
184 |
185 | They can also be created together if your application is intended to use only your own OVHcloud account.
186 |
187 | ## Supported endpoints
188 |
189 | ### OVHcloud Europe
190 |
191 | * `$endpoint = 'ovh-eu';`
192 | * Documentation:
193 | * Console:
194 | * Create application credentials (generate only application credentials, your app will need to implement an authorization flow):
195 | * Create account credentials (all keys at once for your own account only):
196 | * Community support: api-subscribe@ml.ovh.net
197 |
198 | ### OVHcloud US
199 |
200 | * `$endpoint = 'ovh-us';`
201 | * Documentation:
202 | * Console:
203 | * Create application credentials (generate only application credentials, your app will need to implement an authorization flow):
204 | * Create account credentials (all keys at once for your own account only):
205 |
206 | ### OVHcloud North America / Canada
207 |
208 | * `$endpoint = 'ovh-ca';`
209 | * Documentation:
210 | * Console:
211 | * Create application credentials (generate only application credentials, your app will need to implement an authorization flow):
212 | * Create account credentials (all keys at once for your own account only):
213 | * Community support: api-subscribe@ml.ovh.net
214 |
215 | ### So you Start Europe
216 |
217 | * `$endpoint = 'soyoustart-eu';`
218 | * Documentation:
219 | * Console:
220 | * Create application credentials (generate only application credentials, your app will need to implement an authorization flow):
221 | * Create account credentials (all keys at once for your own account only):
222 | * Community support: api-subscribe@ml.ovh.net
223 |
224 | ### So you Start North America
225 |
226 | * `$endpoint = 'soyoustart-ca';`
227 | * Documentation:
228 | * Console:
229 | * Create application credentials (generate only application credentials, your app will need to implement an authorization flow):
230 | * Create account credentials (all keys at once for your own account only):
231 | * Community support: api-subscribe@ml.ovh.net
232 |
233 | ### Kimsufi Europe
234 |
235 | * `$endpoint = 'kimsufi-eu';`
236 | * Documentation:
237 | * Console:
238 | * Create application credentials (generate only application credentials, your app will need to implement an authorization flow):
239 | * Create account credentials (all keys at once for your own account only):
240 | * Community support: api-subscribe@ml.ovh.net
241 |
242 | ### Kimsufi North America
243 |
244 | * `$endpoint = 'kimsufi-ca';`
245 | * Documentation:
246 | * Console:
247 | * Create application credentials (generate only application credentials, your app will need to implement an authorization flow):
248 | * Create account credentials (all keys at once for your own account only):
249 | * Community support: api-subscribe@ml.ovh.net
250 |
251 | ## Building documentation
252 |
253 | Documentation is based on phpdocumentor and inclued in the project.
254 | To generate documentation, it's possible to use directly:
255 |
256 | composer phpdoc
257 |
258 | Documentation is available in docs/ directory.
259 |
260 | ## Code check / Linting
261 |
262 | Code check is based on PHP CodeSniffer and inclued in the project.
263 | To check code, it's possible to use directly:
264 |
265 | composer phpcs
266 |
267 | Code linting is based on PHP Code Beautifier and Fixer and inclued in the project.
268 | To lint code, it's possible to use directly:
269 |
270 | composer phpcbf
271 |
272 | ## Testing
273 |
274 | Tests are based on phpunit and inclued in the project.
275 | To run functionals tests, you need to provide valid API credentials, that you can provide them via environment:
276 |
277 | APP_KEY=xxx APP_SECRET=xxx CONSUMER=xxx ENDPOINT=xxx composer phpunit
278 |
279 | ## Contributing
280 |
281 | Please see [CONTRIBUTING](https://github.com/ovh/php-ovh/blob/master/CONTRIBUTING.rst) for details.
282 |
283 | ## Credits
284 |
285 | [All Contributors from this repo](https://github.com/ovh/php-ovh/contributors)
286 |
287 | ## License
288 |
289 | (Modified) BSD license. Please see [LICENSE](https://github.com/ovh/php-ovh/blob/master/LICENSE) for more information.
290 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | codecov:
2 | require_ci_to_pass: yes
3 |
4 | coverage:
5 | precision: 2
6 | round: down
7 | range: "70...100"
8 | status:
9 | project:
10 | default:
11 | target: auto
12 | threshold: 0%
13 | patch:
14 | default:
15 | target: auto
16 | threshold: 0%
17 |
18 | parsers:
19 | gcov:
20 | branch_detection:
21 | conditional: yes
22 | loop: yes
23 | method: no
24 | macro: no
25 |
26 | comment:
27 | layout: "reach,diff,flags,tree"
28 | behavior: default
29 | require_changes: false
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ovh/ovh",
3 | "description": "Wrapper for OVHcloud APIs",
4 | "license": "BSD-3-Clause",
5 | "config": {
6 | "sort-packages": true,
7 | "allow-plugins": {
8 | "phpdocumentor/shim": true
9 | }
10 | },
11 | "keywords": [
12 | "api",
13 | "client",
14 | "authorization",
15 | "authorisation",
16 | "ovh",
17 | "ovhcloud"
18 | ],
19 | "require": {
20 | "php": ">=7.4",
21 | "ext-json": "*",
22 | "guzzlehttp/guzzle": "^6.0||^7.0",
23 | "league/oauth2-client": "^2.7"
24 | },
25 | "require-dev": {
26 | "php-parallel-lint/php-parallel-lint": "^1.3.1",
27 | "phpdocumentor/shim": "^3",
28 | "phpunit/phpunit": "^10.5",
29 | "squizlabs/php_codesniffer": "^3.6"
30 | },
31 | "autoload": {
32 | "psr-4": {"Ovh\\": "src/"}
33 | },
34 | "autoload-dev": {
35 | "psr-4": {
36 | "Ovh\\tests\\": "test/"
37 | }
38 | },
39 | "scripts": {
40 | "phpdoc": "vendor/bin/phpdoc",
41 | "phpcs": "vendor/bin/phpcs -sp --colors",
42 | "phpcbf": "vendor/bin/phpcbf -sp",
43 | "parallel-lint": "vendor/bin/parallel-lint src tests",
44 | "phpunit": "vendor/bin/phpunit --colors=always"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/examples/README.md:
--------------------------------------------------------------------------------
1 | PHP wrapper examples
2 | --------------------
3 |
4 | In this part, you can find real use cases for the OVH php wrapper
5 |
6 | ## Domains
7 |
8 | Following examples are related to [domains offers](https://www.ovh.ie/domains/) proposed by OVH.
9 |
10 | - [How to create HTTP redirection using php wrapper?](create-Redirection/api_create_redirection.md)
11 |
12 | ## Web hosting
13 |
14 | Following examples are related to [web hosting offers](https://www.ovh.ie/web-hosting/) proposed by OVH.
15 |
16 | - [How to get web hosting capabilities using php wrapper?](hosting-getCapabilities/api_get_hosting_capacities.md)
17 | - [How to attach domains to a web hosting offer using the php wrapper?](hosting-attachedDomain/api_attach_domain_to_web_hosting.md)
18 |
--------------------------------------------------------------------------------
/examples/create-Redirection/api_create_redirection.md:
--------------------------------------------------------------------------------
1 | How to create HTTP redirection using php wrapper?
2 | -------------------------------------------------
3 |
4 | This documentation will help you to create an HTTP redirection from a subdomain to another domain. Following script include DNS record check and delete record if their will conflict with your redirection!
5 |
6 | ## Requirements
7 |
8 | - Having PHP 5.2+
9 | - Having a DNS zone at OVH
10 |
11 | ## Download PHP wrapper
12 |
13 | - Download the latest release **with dependencies** on github: https://github.com/ovh/php-ovh/releases
14 |
15 | ```bash
16 | # When this article is written, latest version is 2.0.0
17 | wget https://github.com/ovh/php-ovh/releases/download/v2.0.0/php-ovh-2.0.0-with-dependencies.tar.gz
18 | ```
19 |
20 | - Extract it into a folder
21 |
22 | ```bash
23 | tar xzvf php-ovh-2.0.0-with-dependencies.tar.gz
24 | ```
25 |
26 | ## Create a new token
27 |
28 | You can create a new token using this url: [https://api.ovh.com/createToken/?GET=/domain/zone/*&POST=/domain/zone/*&DELETE=/domain/zone/*](https://api.ovh.com/createToken/?GET=/domain/zone/*&POST=/domain/zone/*&DELETE=/domain/zone/*).
29 | Keep application key, application secret and consumer key to complete the script.
30 |
31 | Be warned, this token is only valid for this script on **/domain/zone/\*** APIs.
32 | If you need a more generic token, you may adjust the **Rights** fields at your needs.
33 |
34 | ## Download the script
35 |
36 | - Download and edit the php php file to create your new HTTP redirection. You can download [this file](https://github.com/ovh/php-ovh/blob/master/examples/create-Redirection/apiv6.php). **You had to replace some variables in the beginning of the file**.
37 |
38 | ## Run script
39 |
40 | ```bash
41 | php apiv6.php
42 | ```
43 |
44 | For instance, using the example values in this script, the answer would look like:
45 | ```
46 | (
47 | [zone] => yourdomain.ovh
48 | [description] =>
49 | [keywords] =>
50 | [target] => my_target.ovh
51 | [id] => 1342424242
52 | [subDomain] => www
53 | [type] => visible
54 | [title] =>
55 | )
56 | ```
57 |
58 | ## What's more?
59 |
60 | You can discover all domain possibilities by using API console to show all available endpoints: [https://api.ovh.com/console](https://api.ovh.com/console)
61 |
62 |
--------------------------------------------------------------------------------
/examples/create-Redirection/apiv6.php:
--------------------------------------------------------------------------------
1 | get('/domain/zone/' . $domain . '/record?subDomain='. $subDomain );
34 |
35 | // If subdomain is not defined, we don't want to delete all A, AAAA and CNAME records
36 | if ( isset($subDomain) ) {
37 | foreach ($recordIds as $recordId) {
38 | $record = $conn->get('/domain/zone/' . $domain . '/record/' . $recordId);
39 |
40 | // If record include A, AAAA or CNAME for subdomain asked, we delete it
41 | if ( in_array( $record['fieldType'], array( 'A', 'AAAA', 'CNAME' ) ) ) {
42 |
43 | echo "We will delete field " . $record['fieldType'] . " for " . $record['subDomain'] . "." . $record['zone'] . PHP_EOL;
44 | $conn->delete('/domain/zone/' . $domain . '/record/' . $recordId);
45 | }
46 | }
47 | }
48 |
49 | // Now, we are ready to create our new redirection
50 | $redirection = $conn->post('/domain/zone/' . $domain . '/redirection', array(
51 | 'subDomain' => $subDomain,
52 | 'target' => $targetDomain,
53 | 'type' => $type,
54 | 'title' => $title,
55 | 'description' => $description,
56 | 'keywords' => $keywords,
57 | ));
58 |
59 | // We apply zone changes
60 | $conn->post('/domain/zone/' . $domain . '/refresh');
61 |
62 | print_r( $redirection );
63 |
64 | } catch ( Exception $ex ) {
65 | print_r( $ex->getMessage() );
66 | }
67 | ?>
68 |
--------------------------------------------------------------------------------
/examples/hosting-attachedDomain/api_attach_domain_to_web_hosting.md:
--------------------------------------------------------------------------------
1 | How to attach domains to a web hosting offer using the php wrapper?
2 | -------------------------------------------------------------------
3 |
4 | This documentation will help you to create, edit and delete domains attached to your webhosting offer. This documentation is the equivalent of [MultiDomain SoAPI](http://www.ovh.com/soapi/en/?method=multiDomainAdd) and [SubDomain SoAPI](http://www.ovh.com/soapi/en/?method=subDomainAdd)
5 |
6 | ## Compatibility note
7 |
8 | MultiDomains and SubDomains features are merged into one feature called **Attached Domains**. You can manage both with this new feature in OVH APIs.
9 |
10 | ## Requirements
11 |
12 | - Having PHP 5.2+
13 | - Having a domain at OVH
14 | - Having an hosting account
15 |
16 | ## Download PHP wrapper
17 |
18 | - Download the latest release **with dependencies** on github: https://github.com/ovh/php-ovh/releases
19 |
20 | ```bash
21 | # When this article was written, latest version was 2.0.0
22 | wget https://github.com/ovh/php-ovh/releases/download/v2.0.0/php-ovh-2.0.0-with-dependencies.tar.gz
23 | ```
24 |
25 | - Extract it
26 |
27 | ```bash
28 | tar xzvf php-ovh-2.0.0-with-dependencies.tar.gz
29 | ```
30 |
31 | ## Create a new token
32 | You can create a new token using [this url](https://api.ovh.com/createToken/?GET=/hosting/web/my_domain/attachedDomain&POST=/hosting/web/my_domain/attachedDomain&GET=/hosting/web/my_domain/attachedDomain/*&PUT=/hosting/web/my_domain/attachedDomain/*&DELETE=/hosting/web/my_domain/attachedDomain/*). Keep the application key, the application secret and the consumer key to complete the script.
33 |
34 | Be warned, this token is only valid for this script and for hosting called **my_domain**. Please replace **my_domain** by your web hosting reference!
35 | If you need a more generic token, make sure to set the rights field to your needs
36 |
37 | ## Attach a domain to your web hosting
38 |
39 | When you call the API to attach a domain to your web hosting, the api call returns a **task**. The task is the current state of an operation to attach this domain to your hosting. The [example script](createAttachedDomain.php) explains how to attach a domain an wait the end of the operation.
40 |
41 | If this script works, you should see someting like:
42 | ```bash
43 | Task #42 is created
44 | Status of task #42 is 'todo'
45 | Status of task #42 is 'todo'
46 | Status of task #42 is 'todo'
47 | Status of task #42 is 'todo'
48 | Status of task #42 is 'todo'
49 | Status of task #42 is 'doing'
50 | Domain attached to the web hosting
51 | ```
52 |
53 | ## List all your attached domains
54 |
55 | The [example script](listAttachedDomain.php) explains how to show all your domains attached to a web hosting
56 | For instance, using the example values in this script, the answer could be look like:
57 |
58 | ```bash
59 | Array
60 | (
61 | [domain] => myotherdomaintoattach.ovh
62 | [cdn] => none
63 | [ipLocation] =>
64 | [ownLog] => myotherdomaintoattach.ovh
65 | [firewall] => none
66 | [path] => otherFolder
67 | )
68 | ```
69 |
70 | ## Detach a domain from your web hosting
71 |
72 | The [example script](deleteAttachedDomain.php) explains how to detach a domain to your web hosting.
73 |
74 | If this script works, you should see someting like:
75 | ```bash
76 | Task #42 is created
77 | Status of task #42 is 'todo'
78 | Status of task #42 is 'todo'
79 | Status of task #42 is 'todo'
80 | Status of task #42 is 'todo'
81 | Status of task #42 is 'todo'
82 | Status of task #42 is 'doing'
83 | Domain detached from the web hosting
84 | ```
85 |
86 | ## What's next?
87 |
88 | You can discover all hosting possibilities by using API console to show all available endpoints: [https://api.ovh.com/console](https://api.ovh.com/console)
89 |
90 |
--------------------------------------------------------------------------------
/examples/hosting-attachedDomain/createAttachedDomain.php:
--------------------------------------------------------------------------------
1 | 30,
26 | 'connect_timeout' => 5,
27 | ]);
28 |
29 | // Create a new attached domain
30 | $conn = new Api( $applicationKey,
31 | $applicationSecret,
32 | $endpoint,
33 | $consumer_key,
34 | $http_client);
35 |
36 | try {
37 |
38 | // This call will create a "task". The task is the status of the attached domain creation.
39 | // You can follow the task on /hosting/web/{serviceName}/tasks/{id}
40 | $task = $conn->post('/hosting/web/' . $domain . '/attachedDomain', array(
41 | 'domain' => $domainToAttach,
42 | 'path' => $path,
43 | 'cdn' => $cdn,
44 | 'firewall' => $firewall,
45 | 'ownLog' => $ownLog,
46 | ));
47 |
48 | echo "Task #" . $task['id'] . " is created" . PHP_EOL;
49 |
50 | // we check every 5 seconds if the task is done
51 | // When the task disappears, the task is done
52 | while ( 1 ) {
53 | try {
54 | $wait = $conn->get('/hosting/web/' . $domain . '/tasks/' . $task['id']);
55 |
56 | if ( strcmp( $wait['status'], 'error' ) === 0 ) {
57 | // The task is in error state. Please check your parameters, retry or contact support.
58 | echo "An error has occured during the task" . PHP_EOL;
59 | break;
60 | } elseif ( strcmp( $wait['status'], 'cancelled' ) === 0 ) {
61 | // The task is in cancelled state. Please check your parameters, retry or contact support.
62 | echo "Task has been cancelled during the task" . PHP_EOL;
63 | break;
64 | }
65 |
66 | echo "Status of task #". $wait['id'] . " is '". $wait['status'] ."'" . PHP_EOL;
67 | } catch ( \GuzzleHttp\Exception\ClientException $ex) {
68 | $response = $ex->getResponse();
69 | if ( $response && $response->getStatusCode() === 404 ) {
70 | echo "Domain attached to the web hosting" . PHP_EOL;
71 | break;
72 | }
73 | throw $ex;
74 | }
75 |
76 | sleep(5);
77 | }
78 |
79 | } catch ( Exception $ex ) {
80 | print_r( $ex->getMessage() );
81 | }
82 | ?>
83 |
--------------------------------------------------------------------------------
/examples/hosting-attachedDomain/deleteAttachedDomain.php:
--------------------------------------------------------------------------------
1 | 30,
20 | 'connect_timeout' => 5,
21 | ]);
22 |
23 | // Create a new attached domain
24 | $conn = new Api( $applicationKey,
25 | $applicationSecret,
26 | $endpoint,
27 | $consumer_key,
28 | $http_client);
29 |
30 | try {
31 |
32 | // This call will create a "task". The task is the status of the attached domain deletion.
33 | // You can follow the task on /hosting/web/{serviceName}/tasks/{id}
34 | $task = $conn->delete('/hosting/web/' . $domain . '/attachedDomain/' . $domainToDetach);
35 |
36 | echo "Task #" . $task['id'] . " is created" . PHP_EOL;
37 |
38 | // we check every 5 seconds if task is done
39 | // When the task disappears, the task is done
40 | while ( 1 ) {
41 | try {
42 | $wait = $conn->get('/hosting/web/' . $domain . '/tasks/' . $task['id']);
43 |
44 | if ( strcmp( $wait['status'], 'error' ) === 0 ) {
45 | // The task is in error state. Please check your parameters, retry or contact support.
46 | echo "An error has occured during the task" . PHP_EOL;
47 | break;
48 | } elseif ( strcmp( $wait['status'], 'cancelled' ) === 0 ) {
49 | // The task is in cancelled state. Please check your parameters, retry or contact support.
50 | echo "Task has been cancelled during the task" . PHP_EOL;
51 | break;
52 | }
53 |
54 | echo "Status of task #". $wait['id'] . " is '". $wait['status'] ."'" . PHP_EOL;
55 | } catch ( \GuzzleHttp\Exception\ClientException $ex) {
56 | $response = $ex->getResponse();
57 | if ( $response && $response->getStatusCode() === 404 ) {
58 | echo "Domain detached from the web hosting" . PHP_EOL;
59 | break;
60 | }
61 | throw $ex;
62 | }
63 |
64 | sleep(5);
65 | }
66 |
67 | } catch ( Exception $ex ) {
68 | print_r( $ex->getMessage() );
69 | }
70 | ?>
71 |
--------------------------------------------------------------------------------
/examples/hosting-attachedDomain/listAttachedDomains.php:
--------------------------------------------------------------------------------
1 | 30,
19 | 'connect_timeout' => 5,
20 | ]);
21 |
22 | // Create a new attached domain
23 | $conn = new Api( $applicationKey,
24 | $applicationSecret,
25 | $endpoint,
26 | $consumer_key,
27 | $http_client);
28 |
29 | try {
30 |
31 | $attachedDomainsIds = $conn->get('/hosting/web/' . $domain . '/attachedDomain');
32 |
33 | foreach( $attachedDomainsIds as $attachedDomainsId) {
34 | $attachedDomain = $conn->get('/hosting/web/' . $domain . '/attachedDomain/' . $attachedDomainsId );
35 | print_r( $attachedDomain );
36 | }
37 |
38 | } catch ( Exception $ex ) {
39 | print_r( $ex->getMessage() );
40 | }
41 | ?>
42 |
--------------------------------------------------------------------------------
/examples/hosting-getCapabilities/api_get_hosting_capacities.md:
--------------------------------------------------------------------------------
1 | How to get web hosting capabilities using php wrapper?
2 | ----------------------------------------------------
3 |
4 | This documentation will help you to get informations about your web hosting offer: limits, features availables... This documentation is the equivalent of [hostingGetCapabilities SoAPI](https://www.ovh.com/soapi/fr/?method=hostingGetCapabilities)
5 |
6 | ## Requirements
7 |
8 | - Having PHP 5.2+
9 | - Having an hosting account
10 |
11 | ## Download PHP wrapper
12 |
13 | - Download the latest release **with dependencies** on github: https://github.com/ovh/php-ovh/releases
14 |
15 | ```bash
16 | # When this article is written, latest version is 2.0.0
17 | wget https://github.com/ovh/php-ovh/releases/download/v2.0.0/php-ovh-2.0.0-with-dependencies.tar.gz
18 | ```
19 |
20 | - Extract it into a folder
21 |
22 | ```bash
23 | tar xzvf php-ovh-2.0.0-with-dependencies.tar.gz
24 | ```
25 |
26 | - Create a new token
27 | You can create a new token using this url: [https://api.ovh.com/createToken/?GET=/hosting/web/my_domain&GET=/hosting/web/offerCapabilities](https://api.ovh.com/createToken/?GET=/hosting/web/my_domain&GET=/hosting/web/offerCapabilities). Keep application key, application secret and consumer key to complete the script.
28 |
29 | Be warn, this token is only validated for this script and for hosting called **my_domain**. Please replace **my_domain** by your web hosting reference!
30 | If you need a more generic token, you had to change right field.
31 |
32 | - Create php file to get capabilities in the folder. You can download [this file](https://github.com/ovh/php-ovh/blob/master/examples/hosting-getCapabilities/apiv6.php)
33 |
34 | ```php
35 | get('/hosting/web/' . $web_hosting );
56 |
57 | print_r( $conn->get('/hosting/web/offerCapabilities', array( 'offer' => $hosting['offer'] ) ) );
58 |
59 | ?>
60 | ```
61 |
62 | ## Run php file
63 |
64 | ```bash
65 | php getCapabilities.php
66 | ```
67 |
68 | For instance, for pro2014 account, the answer is
69 | ```
70 | Array
71 | (
72 | [traffic] =>
73 | [moduleOneClick] => 1
74 | [privateDatabases] => Array
75 | (
76 | )
77 |
78 | [extraUsers] => 1000
79 | [databases] => Array
80 | (
81 | [0] => Array
82 | (
83 | [quota] => Array
84 | (
85 | [unit] => MB
86 | [value] => 400
87 | )
88 |
89 | [type] => sqlPerso
90 | [available] => 3
91 | )
92 |
93 | [1] => Array
94 | (
95 | [quota] => Array
96 | (
97 | [unit] => MB
98 | [value] => 2000
99 | )
100 |
101 | [type] => sqlPro
102 | [available] => 1
103 | )
104 |
105 | )
106 |
107 | [ssh] => 1
108 | [sitesRecommended] => 10
109 | [attachedDomains] => 2000
110 | [crontab] => 1
111 | )
112 | ```
113 |
114 | ## What's more?
115 |
116 | You can discover all hosting possibilities by using API console to show all available endpoints: [https://api.ovh.com/console](https://api.ovh.com/console)
117 |
118 |
--------------------------------------------------------------------------------
/examples/hosting-getCapabilities/apiv6.php:
--------------------------------------------------------------------------------
1 | get('/hosting/web/' . $web_hosting );
22 |
23 | print_r( $conn->get('/hosting/web/offerCapabilities', array( 'offer' => $hosting['offer'] ) ) );
24 |
25 | ?>
26 |
--------------------------------------------------------------------------------
/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ovh/php-ovh/917ae0332cb6e2559d3b5045ef9107b32e07cd2d/img/logo.png
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | */tests/*
11 |
12 | src
13 | tests
14 |
15 |
16 |
--------------------------------------------------------------------------------
/phpdoc.dist.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | build/phpdoc-cache
11 |
12 |
13 |
14 |
15 | src
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 | ./tests
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | src/
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/scripts/bump-version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Usage: ./scripts/bump-version.sh
4 | #
5 |
6 | PCRE_MATCH_VERSION="[0-9]+\.[0-9]+\.[0-9]+"
7 | PCRE_MATCH_VERSION_BOUNDS="(^|[- v/'\"])${PCRE_MATCH_VERSION}([- /'\"]|$)"
8 | VERSION="$1"
9 |
10 | if ! echo "$VERSION" | grep -Pq "${PCRE_MATCH_VERSION}"; then
11 | echo "Usage: ./scripts/bump-version.sh "
12 | echo " must be a valid 3 digit version number"
13 | echo " Make sure to double check 'git diff' before commiting anything you'll regret on master"
14 | exit 1
15 | fi
16 |
17 | # Edit text files matching the PCRE, do *not* patch .git folder
18 | grep -PIrl "${PCRE_MATCH_VERSION_BOUNDS}" $(ls) | xargs sed -ir "s/${PCRE_MATCH_VERSION_BOUNDS}/"'\1'"${VERSION}"'\2/g'
19 |
20 |
--------------------------------------------------------------------------------
/scripts/release_binary.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # This script is used to build and deploy the binary from the current version on github
4 | # Usage ./scripts/release_binary.sh
5 | #
6 |
7 | set -e
8 |
9 | VERSION="$1"
10 | USER="$2"
11 | TOKEN="$3"
12 |
13 | function usage() {
14 | echo "Usage: $0 "
15 | echo "Hint: "
16 | echo " - Make sure there is outstanding changes in the current directory"
17 | echo " - Make sure the requested version does not exist yet"
18 | echo " - You may visit https://help.github.com/articles/creating-an-access-token-for-command-line-use/ to generate your Github Token"
19 | }
20 |
21 | #
22 | # Validate input
23 | #
24 |
25 | if [ -z "$VERSION" ];
26 | then
27 | echo "Missing version" >&2
28 | usage
29 | exit 1
30 | fi
31 |
32 | if [ -z "$USER" ];
33 | then
34 | echo "Missing github user" >&2
35 | usage
36 | exit 1
37 | fi
38 |
39 | if [ -z "$TOKEN" ];
40 | then
41 | echo "Missing github token" >&2
42 | usage
43 | exit 1
44 | fi
45 |
46 | #
47 | # Validate repository
48 | #
49 |
50 | if [ -n "$(git status --porcelain)" ]
51 | then
52 | echo "Working repository is not clean. Please commit or stage any pending changes." >&2
53 | usage
54 | exit 1
55 | fi
56 |
57 | CURRENTTAG=$(git describe --tag --exact-match 2>/dev/null || echo "")
58 | if [ "${CURRENTTAG}" != "${VERSION}" ]
59 | then
60 | if [ -n "${CURRENTTAG}" ]
61 | then
62 | echo "The current commit is already tagged with ${CURRENTTAG} which is not the requested release ${VERSION}" >&2
63 | usage
64 | exit 1
65 | fi
66 |
67 | if git rev-parse refs/tags/${VERSION} &>/dev/null
68 | then
69 | echo "The requested version ${VERSION} already exists" >&2
70 | usage
71 | exit 1
72 | fi
73 | fi
74 |
75 | #
76 | # Release
77 | #
78 |
79 | PROJECT_NAME=$(git remote -v | grep 'github.*push' | awk '{split($2, a, "/"); split(a[2], b, "."); print b[1]}')
80 | echo "Releasing ${PROJECT_NAME} version ${VERSION}..."
81 |
82 | git tag -m "Releasing ${PROJECT_NAME} version ${VERSION}" "${VERSION}"
83 | git push --tags
84 | git push
85 |
86 | cd /tmp
87 | mkdir -p ${PROJECT_NAME}-bin
88 | cd ${PROJECT_NAME}-bin
89 |
90 | curl -sS https://getcomposer.org/installer | php
91 |
92 | # FIXME: this will require the release to already be uploaded on packagist.org
93 | cat > composer.json << EOF
94 | {
95 | "name": "Example Application",
96 | "description": "This is an example of OVH APIs wrapper usage",
97 | "require": {
98 | "ovh/ovh": "${VERSION}"
99 | }
100 | }
101 | EOF
102 |
103 | php composer.phar install
104 |
105 | cat > script.php << EOF
106 | get("/me");
126 | print_r( $me );
127 |
128 | } catch ( Exception $ex ) {
129 | print_r( $ex->getMessage() );
130 | }
131 | EOF
132 |
133 |
134 | ID=$(curl https://api.github.com/repos/ovh/${PROJECT_NAME}/releases/tags/v${VERSION} -u "${USER}:${TOKEN}" | jq -r '.id')
135 |
136 | zip -r ${PROJECT_NAME}-${VERSION}-with-dependencies.zip .
137 | curl -X POST -d @${PROJECT_NAME}-${VERSION}-with-dependencies.zip -H "Content-Type: application/zip" "https://uploads.github.com/repos/ovh/${PROJECT_NAME}/releases/${ID}/assets?name=${PROJECT_NAME}-${VERSION}-with-dependencies.zip" -i -u "${USER}:${TOKEN}"
138 | rm ${PROJECT_NAME}-${VERSION}-with-dependencies.zip
139 |
140 | tar -czf ${PROJECT_NAME}-${VERSION}-with-dependencies.tar.gz .
141 | curl -X POST -d @${PROJECT_NAME}-${VERSION}-with-dependencies.tar.gz -H "Content-Type: application/gzip" "https://uploads.github.com/repos/ovh/${PROJECT_NAME}/releases/${ID}/assets?name=${PROJECT_NAME}-${VERSION}-with-dependencies.tar.gz" -i -u "${USER}:${TOKEN}"
142 | rm -f ${PROJECT_NAME}-${VERSION}-with-dependencies.tar.gz
143 |
144 |
--------------------------------------------------------------------------------
/scripts/update-copyright.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # Usage: ./scripts/update-copyright.sh
4 | #
5 |
6 | PCRE_MATCH_COPYRIGHT="Copyright \(c\) 2013-[0-9]{4}, OVH SAS."
7 | PCRE_MATCH_DEBIAN="Copyright: [-0-9]* OVH SAS"
8 | YEAR=$(date +%Y)
9 |
10 | echo -n "Updating copyright headers to ${YEAR}... "
11 | grep -rPl "${PCRE_MATCH_COPYRIGHT}" | xargs sed -ri "s/${PCRE_MATCH_COPYRIGHT}/Copyright (c) 2013-${YEAR}, OVH SAS./g"
12 | grep -rPl "${PCRE_MATCH_DEBIAN}" | xargs sed -ri "s/${PCRE_MATCH_DEBIAN}/Copyright: 2013-${YEAR} OVH SAS/g"
13 | echo "[OK]"
14 |
15 |
--------------------------------------------------------------------------------
/src/Api.php:
--------------------------------------------------------------------------------
1 | 'https://eu.api.ovh.com/1.0',
62 | 'ovh-ca' => 'https://ca.api.ovh.com/1.0',
63 | 'ovh-us' => 'https://api.us.ovhcloud.com/1.0',
64 | 'kimsufi-eu' => 'https://eu.api.kimsufi.com/1.0',
65 | 'kimsufi-ca' => 'https://ca.api.kimsufi.com/1.0',
66 | 'soyoustart-eu' => 'https://eu.api.soyoustart.com/1.0',
67 | 'soyoustart-ca' => 'https://ca.api.soyoustart.com/1.0',
68 | 'runabove-ca' => 'https://api.runabove.com/1.0',
69 | ];
70 |
71 | private static $OAUTH2_TOKEN_URLS = [
72 | "ovh-eu" => "https://www.ovh.com/auth/oauth2/token",
73 | "ovh-ca" => "https://ca.ovh.com/auth/oauth2/token",
74 | "ovh-us" => "https://us.ovhcloud.com/auth/oauth2/token",
75 | ];
76 |
77 | /**
78 | * Contain endpoint selected to choose API
79 | *
80 | * @var string
81 | */
82 | private ?string $endpoint;
83 |
84 | /**
85 | * Contain key of the current application
86 | *
87 | * @var string
88 | */
89 | private ?string $application_key;
90 |
91 | /**
92 | * Contain secret of the current application
93 | *
94 | * @var string
95 | */
96 | private ?string $application_secret;
97 |
98 | /**
99 | * Contain consumer key of the current application
100 | *
101 | * @var string
102 | */
103 | private ?string $consumer_key;
104 |
105 | /**
106 | * Contain delta between local timestamp and api server timestamp
107 | *
108 | * @var string
109 | */
110 | private ?string $time_delta;
111 |
112 | /**
113 | * Contain http client connection
114 | *
115 | * @var Client
116 | */
117 | private ?Client $http_client;
118 |
119 | /**
120 | * OAuth2 wrapper if built with `withOAuth2`
121 | *
122 | * @var \Ovh\OAuth2
123 | */
124 | private ?OAuth2 $oauth2;
125 |
126 | /**
127 | * Construct a new wrapper instance
128 | *
129 | * @param string $application_key key of your application.
130 | * For OVH APIs, you can create a application's credentials on
131 | * https://api.ovh.com/createApp/
132 | * @param string $application_secret secret of your application.
133 | * @param string $api_endpoint name of api selected
134 | * @param string $consumer_key If you have already a consumer key, this parameter prevent to do a
135 | * new authentication
136 | * @param Client $http_client instance of http client
137 | *
138 | * @throws Exceptions\InvalidParameterException if one parameter is missing or with bad value
139 | */
140 | public function __construct(
141 | $application_key,
142 | $application_secret,
143 | $api_endpoint,
144 | $consumer_key = null,
145 | ?Client $http_client = null
146 | ) {
147 | if (!isset($api_endpoint)) {
148 | throw new Exceptions\InvalidParameterException("Endpoint parameter is empty");
149 | }
150 |
151 | if (preg_match('/^https?:\/\/..*/', $api_endpoint)) {
152 | $this->endpoint = $api_endpoint;
153 | } else {
154 | if (!array_key_exists($api_endpoint, $this->endpoints)) {
155 | throw new Exceptions\InvalidParameterException("Unknown provided endpoint");
156 | }
157 |
158 | $this->endpoint = $this->endpoints[$api_endpoint];
159 | }
160 |
161 | if (!isset($http_client)) {
162 | $http_client = new Client([
163 | 'timeout' => 30,
164 | 'connect_timeout' => 5,
165 | ]);
166 | }
167 |
168 | $this->application_key = $application_key;
169 | $this->application_secret = $application_secret;
170 | $this->http_client = $http_client;
171 | $this->consumer_key = $consumer_key;
172 | $this->oauth2 = null;
173 | }
174 |
175 | /**
176 | * Alternative constructor to build a client using OAuth2
177 | *
178 | * @throws Exceptions\InvalidParameterException if one parameter is missing or with bad value
179 | * @return Ovh\Api
180 | */
181 | public static function withOAuth2($clientId, $clientSecret, $apiEndpoint)
182 | {
183 | if (!array_key_exists($apiEndpoint, self::$OAUTH2_TOKEN_URLS)) {
184 | throw new Exceptions\InvalidParameterException(
185 | "OAuth2 authentication is not compatible with endpoint $apiEndpoint (it can only be used with ovh-eu, ovh-ca and ovh-us)"
186 | );
187 | }
188 |
189 | $instance = new self("", "", $apiEndpoint);
190 | $instance->oauth2 = new Oauth2($clientId, $clientSecret, self::$OAUTH2_TOKEN_URLS[$apiEndpoint]);
191 | return $instance;
192 | }
193 |
194 | /**
195 | * Calculate time delta between local machine and API's server
196 | *
197 | * @throws ClientException if http request is an error
198 | * @return int
199 | */
200 | private function calculateTimeDelta()
201 | {
202 | if (!isset($this->time_delta)) {
203 | $response = $this->rawCall(
204 | 'GET',
205 | "/auth/time",
206 | null,
207 | false
208 | );
209 | $serverTimestamp = (int)(string)$response->getBody();
210 | $this->time_delta = $serverTimestamp - (int)\time();
211 | }
212 |
213 | return $this->time_delta;
214 | }
215 |
216 | /**
217 | * Request a consumer key from the API and the validation link to
218 | * authorize user to validate this consumer key
219 | *
220 | * @param array $accessRules list of rules your application need.
221 | * @param string $redirection url to redirect on your website after authentication
222 | *
223 | * @return mixed
224 | * @throws ClientException if http request is an error
225 | */
226 | public function requestCredentials(
227 | array $accessRules,
228 | $redirection = null
229 | ) {
230 | $parameters = new \StdClass();
231 | $parameters->accessRules = $accessRules;
232 | $parameters->redirection = $redirection;
233 |
234 | //bypass authentication for this call
235 | $response = $this->decodeResponse(
236 | $this->rawCall(
237 | 'POST',
238 | '/auth/credential',
239 | $parameters,
240 | true
241 | )
242 | );
243 |
244 | $this->consumer_key = $response["consumerKey"];
245 |
246 | return $response;
247 | }
248 |
249 | /**
250 | * getTarget returns the URL to target given an endpoint and a path.
251 | * If the path starts with `/v1` or `/v2`, then remove the trailing `/1.0` from the endpoint.
252 | *
253 | * @param string path to use prefix from
254 | * @return string
255 | */
256 | protected function getTarget($path) : string
257 | {
258 | $endpoint = $this->endpoint;
259 | if (substr($endpoint, -4) == '/1.0' && (
260 | substr($path, 0, 3) == '/v1' ||
261 | substr($path, 0, 3) == '/v2')) {
262 | $endpoint = substr($endpoint, 0, strlen($endpoint)-4);
263 | }
264 | return $endpoint . $path;
265 | }
266 |
267 | /**
268 | * This is the main method of this wrapper. It will
269 | * sign a given query and return its result.
270 | *
271 | * @param string $method HTTP method of request (GET,POST,PUT,DELETE)
272 | * @param string $path relative url of API request
273 | * @param \stdClass|array|null $content body of the request
274 | * @param bool $is_authenticated if the request use authentication
275 | *
276 | * @param null $headers
277 | * @return ResponseInterface
278 | * @throws Exceptions\InvalidParameterException
279 | * @throws GuzzleException
280 | * @throws \JsonException
281 | */
282 | protected function rawCall($method, $path, $content = null, $is_authenticated = true, $headers = null): ResponseInterface
283 | {
284 | if ($is_authenticated) {
285 | if (!isset($this->application_key)) {
286 | throw new Exceptions\InvalidParameterException("Application key parameter is empty");
287 | }
288 |
289 | if (!isset($this->application_secret)) {
290 | throw new Exceptions\InvalidParameterException("Application secret parameter is empty");
291 | }
292 | }
293 |
294 | $url = $this->getTarget($path);
295 | $request = new Request($method, $url);
296 | if (isset($content) && $method === 'GET') {
297 | $query_string = $request->getUri()->getQuery();
298 |
299 | $query = [];
300 | if (!empty($query_string)) {
301 | $queries = explode('&', $query_string);
302 | foreach ($queries as $element) {
303 | $key_value_query = explode('=', $element, 2);
304 | $query[$key_value_query[0]] = $key_value_query[1];
305 | }
306 | }
307 |
308 | $query = array_merge($query, (array)$content);
309 |
310 | // rewrite query args to properly dump true/false parameters
311 | foreach ($query as $key => $value) {
312 | if ($value === false) {
313 | $query[$key] = "false";
314 | } elseif ($value === true) {
315 | $query[$key] = "true";
316 | }
317 | }
318 |
319 | $query = \GuzzleHttp\Psr7\Query::build($query);
320 |
321 | $url = $request->getUri()->withQuery($query);
322 | $request = $request->withUri($url);
323 | $body = "";
324 | } elseif (isset($content)) {
325 | $body = json_encode($content, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
326 |
327 | $request->getBody()->write($body);
328 | } else {
329 | $body = "";
330 | }
331 | if (!is_array($headers)) {
332 | $headers = [];
333 | }
334 | $headers['Content-Type'] = 'application/json; charset=utf-8';
335 |
336 | if ($is_authenticated) {
337 | if (!is_null($this->oauth2)) {
338 | $headers['Authorization'] = $this->oauth2->getAuthorizationHeader();
339 | } else {
340 | $headers['X-Ovh-Application'] = $this->application_key ?? '';
341 |
342 | if (!isset($this->time_delta)) {
343 | $this->calculateTimeDelta();
344 | }
345 | $now = time() + $this->time_delta;
346 |
347 | $headers['X-Ovh-Timestamp'] = $now;
348 |
349 | if (isset($this->consumer_key)) {
350 | $toSign = $this->application_secret . '+' . $this->consumer_key . '+' . $method
351 | . '+' . $url . '+' . $body . '+' . $now;
352 | $signature = '$1$' . sha1($toSign);
353 | $headers['X-Ovh-Consumer'] = $this->consumer_key;
354 | $headers['X-Ovh-Signature'] = $signature;
355 | }
356 | }
357 | } else {
358 | $headers['X-Ovh-Application'] = $this->application_key ?? '';
359 | }
360 |
361 | /** @var Response $response */
362 | return $this->http_client->send($request, ['headers' => $headers]);
363 | }
364 |
365 | /**
366 | * Decode a Response object body to an Array
367 | *
368 | * @param Response $response
369 | *
370 | * @throws \JsonException
371 | */
372 | private function decodeResponse(ResponseInterface $response)
373 | {
374 | if ($response->getStatusCode() === 204 || $response->getBody()->getSize() === 0) {
375 | return null;
376 | }
377 | return json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR);
378 | }
379 |
380 | /**
381 | * Wrap call to Ovh APIs for GET requests
382 | *
383 | * @param string $path path ask inside api
384 | * @param array $content content to send inside body of request
385 | * @param array headers custom HTTP headers to add on the request
386 | * @param bool is_authenticated if the request need to be authenticated
387 | *
388 | * @throws ClientException if http request is an error
389 | * @throws \JsonException
390 | */
391 | public function get($path, $content = null, $headers = null, $is_authenticated = true)
392 | {
393 | if (preg_match('/^\/[^\/]+\.json$/', $path)) {
394 | // Schema description must be access without authentication
395 | return $this->decodeResponse(
396 | $this->rawCall("GET", $path, $content, false, $headers)
397 | );
398 | }
399 |
400 | return $this->decodeResponse(
401 | $this->rawCall("GET", $path, $content, $is_authenticated, $headers)
402 | );
403 | }
404 |
405 | /**
406 | * Wrap call to Ovh APIs for POST requests
407 | *
408 | * @param string $path path ask inside api
409 | * @param array $content content to send inside body of request
410 | * @param array headers custom HTTP headers to add on the request
411 | * @param bool is_authenticated if the request need to be authenticated
412 | *
413 | * @throws ClientException if http request is an error
414 | */
415 | public function post($path, $content = null, $headers = null, $is_authenticated = true)
416 | {
417 | return $this->decodeResponse(
418 | $this->rawCall("POST", $path, $content, $is_authenticated, $headers)
419 | );
420 | }
421 |
422 | /**
423 | * Wrap call to Ovh APIs for PUT requests
424 | *
425 | * @param string $path path ask inside api
426 | * @param array $content content to send inside body of request
427 | * @param array headers custom HTTP headers to add on the request
428 | * @param bool is_authenticated if the request need to be authenticated
429 | *
430 | * @throws ClientException if http request is an error
431 | */
432 | public function put($path, $content, $headers = null, $is_authenticated = true)
433 | {
434 | return $this->decodeResponse(
435 | $this->rawCall("PUT", $path, $content, $is_authenticated, $headers)
436 | );
437 | }
438 |
439 | /**
440 | * Wrap call to Ovh APIs for DELETE requests
441 | *
442 | * @param string $path path ask inside api
443 | * @param array $content content to send inside body of request
444 | * @param array headers custom HTTP headers to add on the request
445 | * @param bool is_authenticated if the request need to be authenticated
446 | *
447 | * @throws ClientException if http request is an error
448 | */
449 | public function delete($path, $content = null, $headers = null, $is_authenticated = true)
450 | {
451 | return $this->decodeResponse(
452 | $this->rawCall("DELETE", $path, $content, $is_authenticated, $headers)
453 | );
454 | }
455 |
456 | /**
457 | * Get the current consumer key
458 | */
459 | public function getConsumerKey(): ?string
460 | {
461 | return $this->consumer_key;
462 | }
463 |
464 | /**
465 | * Get the current consumer key
466 | */
467 | public function setConsumerKey($consumer_key): void
468 | {
469 | $this->consumer_key = $consumer_key;
470 | }
471 |
472 | /**
473 | * Return instance of http client
474 | */
475 | public function getHttpClient(): ?Client
476 | {
477 | return $this->http_client;
478 | }
479 | }
480 |
--------------------------------------------------------------------------------
/src/Exceptions/ApiException.php:
--------------------------------------------------------------------------------
1 | = 400)
40 | *
41 | * @package Ovh
42 | * @category Exceptions
43 | */
44 | class ApiException extends Exception
45 | {
46 | }
47 |
--------------------------------------------------------------------------------
/src/Exceptions/InvalidParameterException.php:
--------------------------------------------------------------------------------
1 | provider = new \League\OAuth2\Client\Provider\GenericProvider([
44 | 'clientId' => $clientId,
45 | 'clientSecret' => $clientSecret,
46 | # Do not configure `scopes` here as this GenericProvider ignores it when using client credentials flow
47 | 'urlAccessToken' => $tokenUrl,
48 | 'urlAuthorize' => null, # GenericProvider wants it but OVHcloud doesn't provide it, as it's not needed for client credentials flow
49 | 'urlResourceOwnerDetails' => null, # GenericProvider wants it but OVHcloud doesn't provide it, as it's not needed for client credentials flow
50 | ]);
51 | }
52 |
53 | public function getAuthorizationHeader()
54 | {
55 | if (is_null($this->token) ||
56 | $this->token->hasExpired() ||
57 | $this->token->getExpires() - 10 <= time()) {
58 | try {
59 | $this->token = $this->provider->getAccessToken('client_credentials', ['scope' => 'all']);
60 | } catch (UnexpectedValueException | IdentityProviderException $e) {
61 | throw new Exceptions\OAuth2FailureException('OAuth2 failure: ' . $e->getMessage(), $e->getCode(), $e);
62 | }
63 | }
64 |
65 | return 'Bearer ' . $this->token->getToken();
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/tests/ApiFunctionalTest.php:
--------------------------------------------------------------------------------
1 | markTestSkipped("Skip test due to missing $envName variable");
92 | return;
93 | }
94 | }
95 |
96 | $this->application_key = getenv('APP_KEY');
97 | $this->application_secret = getenv('APP_SECRET');
98 | $this->consumer_key = getenv('CONSUMER');
99 | $this->endpoint = getenv('ENDPOINT');
100 | $this->rangeIP = '127.0.0.20/32';
101 | $this->alternativeRangeIP = '127.0.0.30/32';
102 |
103 | $this->client = new Client();
104 | $this->api = new Api(
105 | $this->application_key,
106 | $this->application_secret,
107 | $this->endpoint,
108 | $this->consumer_key,
109 | $this->client
110 | );
111 | }
112 |
113 | /**
114 | * Get private and protected method to unit test it
115 | *
116 | * @param string $name
117 | *
118 | * @return \ReflectionMethod
119 | */
120 | protected static function getPrivateMethod($name)
121 | {
122 | $class = new \ReflectionClass(\Ovh\Api::class);
123 | $method = $class->getMethod($name);
124 | $method->setAccessible(true);
125 |
126 | return $method;
127 | }
128 |
129 | /**
130 | * Get private and protected property to unit test it
131 | *
132 | * @param string $name
133 | *
134 | * @return \ReflectionProperty
135 | */
136 | protected static function getPrivateProperty($name)
137 | {
138 | $class = new \ReflectionClass(\Ovh\Api::class);
139 | $property = $class->getProperty($name);
140 | $property->setAccessible(true);
141 |
142 | return $property;
143 | }
144 |
145 | /**
146 | * Test if result contains consumerKey and validationUrl
147 | */
148 | public function testIfConsumerKeyIsReplace()
149 | {
150 | $property = self::getPrivateProperty('consumer_key');
151 | $accessRules = json_decode(' [
152 | { "method": "GET", "path": "/*" },
153 | { "method": "POST", "path": "/*" },
154 | { "method": "PUT", "path": "/*" },
155 | { "method": "DELETE", "path": "/*" }
156 | ] ');
157 |
158 | $credentials = $this->api->requestCredentials($accessRules);
159 | $consumer_key = $property->getValue($this->api);
160 |
161 | $this->assertSame($consumer_key, $credentials["consumerKey"]);
162 | $this->assertNotEquals($consumer_key, $this->consumer_key);
163 | }
164 |
165 | /**
166 | * Test if post request on me
167 | */
168 | public function testPostRestrictionAccessIp()
169 | {
170 | $this->assertNull(
171 | $this->api->post('/me/accessRestriction/ip', ['ip' => $this->rangeIP, 'rule' => 'deny', 'warning' => true])
172 | );
173 |
174 | $this->assertNull(
175 | $this->api->post('/me/accessRestriction/ip', ['ip' => $this->alternativeRangeIP,
176 | 'rule' => 'deny',
177 | 'warning' => true,
178 | ])
179 | );
180 | }
181 |
182 | /**
183 | * Test if get request on /me
184 | */
185 | public function testGetRestrictionAccessIP()
186 | {
187 | $result = $this->api->get('/me/accessRestriction/ip');
188 |
189 | $restrictionIps = [];
190 |
191 | foreach ($result as $restrictionId) {
192 | $restriction = $this->api->get('/me/accessRestriction/ip/' . $restrictionId);
193 |
194 | $restrictionIps[] = $restriction['ip'];
195 | }
196 |
197 | $this->assertContains($this->rangeIP, $restrictionIps);
198 | $this->assertContains($this->alternativeRangeIP, $restrictionIps);
199 | }
200 |
201 | /**
202 | * Test if delete request on /me
203 | */
204 | public function testPutRestrictionAccessIP()
205 | {
206 | $result = $this->api->get('/me/accessRestriction/ip');
207 |
208 | foreach ($result as $restrictionId) {
209 | $restriction = $this->api->get('/me/accessRestriction/ip/' . $restrictionId);
210 |
211 | if (in_array($restriction["ip"], [$this->rangeIP, $this->alternativeRangeIP])) {
212 | $this->assertNull(
213 | $this->api->put('/me/accessRestriction/ip/' . $restrictionId, ['rule' => 'accept', 'warning' => true])
214 | );
215 |
216 | $restriction = $this->api->get('/me/accessRestriction/ip/' . $restrictionId);
217 | $this->assertSame('accept', $restriction['rule']);
218 | }
219 | }
220 | }
221 |
222 | /**
223 | * Test if delete request on /me
224 | */
225 | public function testDeleteRestrictionAccessIP()
226 | {
227 | $result = $this->api->get('/me/accessRestriction/ip');
228 | foreach ($result as $restrictionId) {
229 | $restriction = $this->api->get('/me/accessRestriction/ip/' . $restrictionId);
230 |
231 | if (in_array($restriction["ip"], [$this->rangeIP, $this->alternativeRangeIP])) {
232 | $result = $this->api->delete('/me/accessRestriction/ip/' . $restrictionId);
233 | $this->assertNull($result);
234 | }
235 | }
236 | }
237 |
238 | /**
239 | * Test if request without authentication works
240 | */
241 | public function testIfRequestWithoutAuthenticationWorks()
242 | {
243 | $api = new Api($this->application_key, $this->application_secret, $this->endpoint, null, $this->client);
244 | $invoker = self::getPrivateMethod('rawCall');
245 | $result = $invoker->invokeArgs($api, ['GET', '/xdsl/incidents']);
246 | $this->assertIsObject($result);
247 | }
248 |
249 | /**
250 | * Test Api::get
251 | */
252 | public function testApiGetWithParameters()
253 | {
254 | $this->expectException(ClientException::class);
255 |
256 | $this->api->get('/me/accessRestriction/ip', ['foo' => 'bar']);
257 | }
258 |
259 | /**
260 | * Test Api::get, should build valide signature
261 | */
262 | public function testApiGetWithQueryString()
263 | {
264 | $result = $this->api->get('/me/api/credential', ['status' => 'pendingValidation']);
265 | $this->assertIsArray($result);
266 | }
267 |
268 | /**
269 | * Test APi::get without authentication
270 | */
271 | public function testApiGetWithoutAuthentication()
272 | {
273 | $api = new Api(null, null, $this->endpoint, null, $this->client);
274 | $result = $api->get('/hosting/web/moduleList', null, null, false);
275 | $this->assertIsArray($result);
276 | }
277 | }
278 |
--------------------------------------------------------------------------------
/tests/ApiTest.php:
--------------------------------------------------------------------------------
1 | calls);
58 |
59 | $handlerStack = HandlerStack::create($mock);
60 | $handlerStack->push($history);
61 |
62 | parent::__construct(['handler' => $handlerStack]);
63 | }
64 | }
65 |
66 | /**
67 | * Get private and protected property to unit test it
68 | *
69 | * @param string $name
70 | *
71 | * @return \ReflectionProperty
72 | */
73 | function getPrivateProperty($name)
74 | {
75 | $class = new \ReflectionClass(\Ovh\Api::class);
76 | $property = $class->getProperty($name);
77 | $property->setAccessible(true);
78 |
79 | return $property;
80 | }
81 |
82 | function mockOauth2HttpClient($api, $client)
83 | {
84 | $httpClientProperty = getPrivateProperty('http_client');
85 | $httpClient = $httpClientProperty->setValue($api, $client);
86 |
87 | $oauth2Property = getPrivateProperty('oauth2');
88 | $oauth2 = $oauth2Property->getValue($api);
89 |
90 | $class = new \ReflectionClass(\Ovh\Oauth2::class);
91 | $providerProperty = $class->getProperty('provider');
92 | $providerProperty->setAccessible(true);
93 | $provider = $providerProperty->getValue($oauth2);
94 |
95 | $provider->setHttpClient($client);
96 | }
97 |
98 |
99 | /**
100 | * Test Api class
101 | *
102 | * @package Ovh
103 | * @category Ovh
104 | */
105 | class ApiTest extends TestCase
106 | {
107 | /**
108 | * Test missing $application_key
109 | */
110 | public function testMissingApplicationKey()
111 | {
112 | $this->expectException(InvalidParameterException::class);
113 | $this->expectExceptionMessage('Application key parameter is empty');
114 | $api = new Api(null, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, new MockClient());
115 | $api->get('/me');
116 | }
117 |
118 | /**
119 | * Test missing $application_secret
120 | */
121 | public function testMissingApplicationSecret()
122 | {
123 | $this->expectException(InvalidParameterException::class);
124 | $this->expectExceptionMessage('Application secret parameter is empty');
125 | $api = new Api(MOCK_APPLICATION_KEY, null, 'ovh-eu', MOCK_CONSUMER_KEY, new MockClient());
126 | $api->get('/me');
127 | }
128 |
129 | /**
130 | * Test we don't check Application Key for unauthenticated call
131 | */
132 | public function testNoCheckAppKeyForUnauthCall()
133 | {
134 | $client = new MockClient(new Response(200, [], '{}'));
135 |
136 | $api = new Api(null, null, 'ovh-eu', null, $client);
137 | $api->get("/me", null, null, false);
138 |
139 | $calls = $client->calls;
140 | $this->assertCount(1, $calls);
141 |
142 | $req = $calls[0]['request'];
143 | $this->assertSame('GET', $req->getMethod());
144 | $this->assertSame('https://eu.api.ovh.com/1.0/me', $req->getUri()->__toString());
145 | $this->assertSame('', $req->getHeaderLine('X-Ovh-Application'));
146 | }
147 |
148 | /**
149 | * Test missing $api_endpoint
150 | */
151 | public function testMissingApiEndpoint()
152 | {
153 | $this->expectException(InvalidParameterException::class);
154 | $this->expectExceptionMessage('Endpoint parameter is empty');
155 | new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, null, MOCK_CONSUMER_KEY, new MockClient());
156 | }
157 |
158 | /**
159 | * Test bad $api_endpoint
160 | */
161 | public function testBadApiEndpoint()
162 | {
163 | $this->expectException(InvalidParameterException::class);
164 | $this->expectExceptionMessage('Unknown provided endpoint');
165 | new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'i_am_invalid', MOCK_CONSUMER_KEY, new MockClient());
166 | }
167 |
168 | /**
169 | * Test creating Client if none is provided
170 | */
171 | public function testClientCreation()
172 | {
173 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY);
174 | $this->assertInstanceOf('\\GuzzleHttp\\Client', $api->getHttpClient());
175 | }
176 |
177 | /**
178 | * Test the compute of time delta
179 | */
180 | public function testTimeDeltaCompute()
181 | {
182 | $client = new MockClient(
183 | new Response(200, [], time() - 10),
184 | new Response(200, [], '{}'),
185 | );
186 |
187 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
188 | $api->get("/me");
189 |
190 | $property = getPrivateProperty('time_delta');
191 | $time_delta = $property->getValue($api);
192 | $this->assertSame('-10', $time_delta);
193 |
194 | $calls = $client->calls;
195 | $this->assertCount(2, $calls);
196 |
197 | $req = $calls[0]['request'];
198 | $this->assertSame('GET', $req->getMethod());
199 | $this->assertSame('https://eu.api.ovh.com/1.0/auth/time', $req->getUri()->__toString());
200 |
201 | $req = $calls[1]['request'];
202 | $this->assertSame('GET', $req->getMethod());
203 | $this->assertSame('https://eu.api.ovh.com/1.0/me', $req->getUri()->__toString());
204 | }
205 |
206 | /**
207 | * Test if consumer key is replaced
208 | */
209 | public function testIfConsumerKeyIsReplace()
210 | {
211 | $client = new MockClient(
212 | new Response(200, [], MOCK_TIME),
213 | new Response(
214 | 200,
215 | ['Content-Type' => 'application/json; charset=utf-8'],
216 | '{"validationUrl":"https://api.ovh.com/login/?credentialToken=token","consumerKey":"consumer_remote","state":"pendingValidation"}'
217 | ),
218 | );
219 |
220 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
221 | $this->assertNotEquals('consumer_remote', $api->getConsumerKey());
222 | $credentials = $api->requestCredentials(['method' => 'GET', 'path' => '/*']);
223 | $this->assertSame('consumer_remote', $api->getConsumerKey());
224 |
225 | $calls = $client->calls;
226 | $this->assertCount(2, $calls);
227 |
228 | $req = $calls[0]['request'];
229 | $this->assertSame('GET', $req->getMethod());
230 | $this->assertSame('https://eu.api.ovh.com/1.0/auth/time', $req->getUri()->__toString());
231 |
232 | $req = $calls[1]['request'];
233 | $this->assertSame('POST', $req->getMethod());
234 | $this->assertSame('https://eu.api.ovh.com/1.0/auth/credential', $req->getUri()->__toString());
235 | }
236 |
237 | /**
238 | * Test invalid applicationKey
239 | */
240 | public function testInvalidApplicationKey()
241 | {
242 | $error = '{"class":"Client::Forbidden","message":"Invalid application key"}';
243 | $this->expectException(ClientException::class);
244 | $this->expectExceptionCode(403);
245 | $this->expectExceptionMessage($error);
246 |
247 | $client = new MockClient(new Response(403, ['Content-Type' => 'application/json; charset=utf-8'], $error));
248 |
249 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
250 | $api->requestCredentials(['method' => 'GET', 'path' => '/*']);
251 | }
252 |
253 | /**
254 | * Test invalid rights
255 | */
256 | public function testInvalidRight()
257 | {
258 | $error = '{"message": "Invalid credentials"}';
259 | $this->expectException(ClientException::class);
260 | $this->expectExceptionCode(403);
261 | $this->expectExceptionMessage($error);
262 |
263 | $client = new MockClient(new Response(403, ['Content-Type' => 'application/json; charset=utf-8'], $error));
264 |
265 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
266 | $api->get('/me', null, null, false);
267 | }
268 |
269 | public function testGetConsumerKey()
270 | {
271 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY);
272 | $this->assertSame(MOCK_CONSUMER_KEY, $api->getConsumerKey());
273 | }
274 |
275 | /**
276 | * Test GET query args
277 | */
278 | public function testGetQueryArgs()
279 | {
280 | $client = new MockClient(new Response(200, [], '{}'));
281 |
282 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
283 | $api->get('/me/api/credential?applicationId=49', ['status' => 'pendingValidation'], null, false);
284 |
285 | $calls = $client->calls;
286 | $this->assertCount(1, $calls);
287 |
288 | $req = $calls[0]['request'];
289 | $this->assertSame('GET', $req->getMethod());
290 | $this->assertSame('https://eu.api.ovh.com/1.0/me/api/credential?applicationId=49&status=pendingValidation', $req->getUri()->__toString());
291 | }
292 |
293 | /**
294 | * Test GET overlapping query args
295 | */
296 | public function testGetOverlappingQueryArgs()
297 | {
298 | $client = new MockClient(new Response(200, [], '{}'));
299 |
300 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
301 | $api->get('/me/api/credential?applicationId=49&status=pendingValidation', ['status' => 'expired', 'test' => "success"], null, false);
302 |
303 | $calls = $client->calls;
304 | $this->assertCount(1, $calls);
305 |
306 | $req = $calls[0]['request'];
307 | $this->assertSame('GET', $req->getMethod());
308 | $this->assertSame('https://eu.api.ovh.com/1.0/me/api/credential?applicationId=49&status=expired&test=success', $req->getUri()->__toString());
309 | }
310 |
311 | /**
312 | * Test GET boolean query args
313 | */
314 | public function testGetBooleanQueryArgs()
315 | {
316 | $client = new MockClient(new Response(200, [], '{}'));
317 |
318 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
319 | $api->get('/me/api/credential', ['dryRun' => true, 'notDryRun' => false], null, false);
320 |
321 | $calls = $client->calls;
322 | $this->assertCount(1, $calls);
323 |
324 | $req = $calls[0]['request'];
325 | $this->assertSame('GET', $req->getMethod());
326 | $this->assertSame('https://eu.api.ovh.com/1.0/me/api/credential?dryRun=true¬DryRun=false', $req->getUri()->__toString());
327 | }
328 |
329 | /**
330 | * Test valid provided endpoint
331 | */
332 | public function testProvidedUrl()
333 | {
334 | foreach ([
335 | ['endpoint' => 'http://api.ovh.com/1.0', 'expectedUrl' => 'http://api.ovh.com/1.0'],
336 | ['endpoint' => 'https://api.ovh.com/1.0', 'expectedUrl' => 'https://api.ovh.com/1.0'],
337 | ['endpoint' => 'ovh-eu', 'expectedUrl' => 'https://eu.api.ovh.com/1.0'],
338 | ] as $test) {
339 | $client = new MockClient(new Response(200, [], '{}'));
340 |
341 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, $test['endpoint'], MOCK_CONSUMER_KEY, $client);
342 | $api->get('/me/api/credential', null, null, false);
343 |
344 | $calls = $client->calls;
345 | $this->assertCount(1, $calls);
346 |
347 | $req = $calls[0]['request'];
348 | $this->assertSame('GET', $req->getMethod());
349 | $this->assertSame($test['expectedUrl'] . '/me/api/credential', $req->getUri()->__toString());
350 | }
351 | }
352 |
353 | /**
354 | * Test missing header X-OVH-Application on requestCredentials
355 | */
356 | public function testMissingOvhApplicationHeaderOnRequestCredentials()
357 | {
358 | $client = new MockClient(
359 | new Response(200, [], MOCK_TIME),
360 | new Response(200, [], '{"validationUrl":"https://api.ovh.com/login/?credentialToken=token","consumerKey":"consumer_remote","state":"pendingValidation"}'),
361 | );
362 |
363 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
364 | $api->requestCredentials([]);
365 |
366 | $calls = $client->calls;
367 | $this->assertCount(2, $calls);
368 |
369 | $req = $calls[0]['request'];
370 | $this->assertSame('GET', $req->getMethod());
371 | $this->assertSame('https://eu.api.ovh.com/1.0/auth/time', $req->getUri()->__toString());
372 |
373 | $req = $calls[1]['request'];
374 | $this->assertSame('POST', $req->getMethod());
375 | $this->assertSame('https://eu.api.ovh.com/1.0/auth/credential', $req->getUri()->__toString());
376 | $this->assertSame(MOCK_APPLICATION_KEY, $req->getHeaderLine('X-Ovh-Application'));
377 | $this->assertSame(MOCK_CONSUMER_KEY, $req->getHeaderLine('X-Ovh-Consumer'));
378 | $this->assertSame(MOCK_TIME, $req->getHeaderLine('X-Ovh-Timestamp'));
379 | }
380 |
381 | public function testCallSignature()
382 | {
383 | // GET /auth/time
384 | $mocks = [new Response(200, [], MOCK_TIME)];
385 | // (GET,POST,PUT,DELETE) x (/auth,/unauth)
386 | for ($i = 0; $i < 8; $i++) {
387 | $mocks[] = new Response(200, [], '{}');
388 | }
389 | $client = new MockClient(...$mocks);
390 |
391 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
392 | $body = ["a" => "b", "c" => "d"];
393 |
394 | // Authenticated calls
395 | $api->get('/auth');
396 | $api->post('/auth', $body);
397 | $api->put('/auth', $body);
398 | $api->delete('/auth');
399 |
400 | // Unauth calls
401 | $api->get('/unauth', null, null, false);
402 | $api->post('/unauth', $body, null, false);
403 | $api->put('/unauth', $body, null, false);
404 | $api->delete('/unauth', null, null, false);
405 |
406 | $calls = $client->calls;
407 | $this->assertCount(9, $calls);
408 |
409 | $req = $calls[0]['request'];
410 | $this->assertSame('GET', $req->getMethod());
411 | $this->assertSame('https://eu.api.ovh.com/1.0/auth/time', $req->getUri()->__toString());
412 |
413 | foreach ([
414 | 1 => ['method' => 'GET', 'sig' => '$1$e9556054b6309771395efa467c22e627407461ad'],
415 | 2 => ['method' => 'POST', 'sig' => '$1$ec2fb5c7a81f64723c77d2e5b609ae6f58a84fc1'],
416 | 3 => ['method' => 'PUT', 'sig' => '$1$8a75a9e7c8e7296c9dbeda6a2a735eb6bd58ec4b'],
417 | 4 => ['method' => 'DELETE', 'sig' => '$1$a1eecd00b3b02b6cf5708b84b9ff42059a950d85'],
418 | ] as $i => $test) {
419 | $req = $calls[$i]['request'];
420 | $this->assertSame($test['method'], $req->getMethod());
421 | $this->assertSame('https://eu.api.ovh.com/1.0/auth', $req->getUri()->__toString());
422 | $this->assertSame(MOCK_APPLICATION_KEY, $req->getHeaderLine('X-Ovh-Application'));
423 | $this->assertSame(MOCK_CONSUMER_KEY, $req->getHeaderLine('X-Ovh-Consumer'));
424 | $this->assertSame(MOCK_TIME, $req->getHeaderLine('X-Ovh-Timestamp'));
425 | $this->assertSame($test['sig'], $req->getHeaderLine('X-Ovh-Signature'));
426 | if ($test['method'] == 'POST' || $test['method'] == 'PUT') {
427 | $this->assertSame('application/json; charset=utf-8', $req->getHeaderLine('Content-Type'));
428 | }
429 | }
430 |
431 | foreach (['GET', 'POST', 'PUT', 'DELETE'] as $i => $method) {
432 | $req = $calls[$i + 5]['request'];
433 | $this->assertSame($method, $req->getMethod());
434 | $this->assertSame('https://eu.api.ovh.com/1.0/unauth', $req->getUri()->__toString());
435 | $this->assertSame(MOCK_APPLICATION_KEY, $req->getHeaderLine('X-Ovh-Application'));
436 | $this->assertNotTrue($req->hasHeader('X-Ovh-Consumer'));
437 | $this->assertNotTrue($req->hasHeader('X-Ovh-Timestamp'));
438 | $this->assertNotTrue($req->hasHeader('X-Ovh-Signature'));
439 | if ($method == 'POST' || $method == 'PUT') {
440 | $this->assertSame('application/json; charset=utf-8', $req->getHeaderLine('Content-Type'));
441 | }
442 | }
443 | }
444 |
445 | public function testVersionInUrl()
446 | {
447 | // GET /auth/time
448 | $mocks = [new Response(200, [], MOCK_TIME)];
449 | // GET x (/1.0/call,/v1/call,/v2/call)
450 | for ($i = 0; $i < 3; $i++) {
451 | $mocks[] = new Response(200, [], '{}');
452 | }
453 | $client = new MockClient(...$mocks);
454 |
455 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
456 |
457 | $api->get('/call');
458 | $api->get('/v1/call');
459 | $api->get('/v2/call');
460 |
461 | $calls = $client->calls;
462 | $this->assertCount(4, $calls);
463 |
464 | $req = $calls[0]['request'];
465 | $this->assertSame('GET', $req->getMethod());
466 | $this->assertSame('https://eu.api.ovh.com/1.0/auth/time', $req->getUri()->__toString());
467 |
468 | foreach ([
469 | 1 => ['path' => '1.0/call', 'sig' => '$1$7f2db49253edfc41891023fcd1a54cf61db05fbb'],
470 | 2 => ['path' => 'v1/call', 'sig' => '$1$e6e7906d385eb28adcbfbe6b66c1528a42d741ad'],
471 | 3 => ['path' => 'v2/call', 'sig' => '$1$bb63b132a6f84ad5433d0c534d48d3f7c3804285'],
472 | ] as $i => $test) {
473 | $req = $calls[$i]['request'];
474 | $this->assertSame('GET', $req->getMethod());
475 | $this->assertSame('https://eu.api.ovh.com/' . $test['path'], $req->getUri()->__toString());
476 | $this->assertSame(MOCK_APPLICATION_KEY, $req->getHeaderLine('X-Ovh-Application'));
477 | $this->assertSame(MOCK_CONSUMER_KEY, $req->getHeaderLine('X-Ovh-Consumer'));
478 | $this->assertSame(MOCK_TIME, $req->getHeaderLine('X-Ovh-Timestamp'));
479 | $this->assertSame($test['sig'], $req->getHeaderLine('X-Ovh-Signature'));
480 | }
481 | }
482 |
483 | public function testEmptyResponseBody()
484 | {
485 | $client = new MockClient(
486 | // GET /auth/time
487 | new Response(200, [], MOCK_TIME),
488 | // POST /domain/zone/nonexisting.ovh/refresh
489 | new Response(204, [], ''),
490 | );
491 |
492 | $api = new Api(MOCK_APPLICATION_KEY, MOCK_APPLICATION_SECRET, 'ovh-eu', MOCK_CONSUMER_KEY, $client);
493 | $response = $api->post('/domain/zone/nonexisting.ovh/refresh');
494 | $this->assertSame(null, $response);
495 |
496 | $calls = $client->calls;
497 | $this->assertCount(2, $calls);
498 |
499 | $req = $calls[0]['request'];
500 | $this->assertSame('GET', $req->getMethod());
501 | $this->assertSame('https://eu.api.ovh.com/1.0/auth/time', $req->getUri()->__toString());
502 |
503 | $req = $calls[1]['request'];
504 | $this->assertSame('POST', $req->getMethod());
505 | $this->assertSame('https://eu.api.ovh.com/1.0/domain/zone/nonexisting.ovh/refresh', $req->getUri()->__toString());
506 | }
507 |
508 | public function testOauth2500()
509 | {
510 | $client = new MockClient(
511 | // POST https://www.ovh.com/auth/oauth2/token
512 | new Response(500, [], 'test
'),
513 | );
514 |
515 | $api = Api::withOauth2('client_id', 'client_secret', 'ovh-eu');
516 | mockOauth2HttpClient($api, $client);
517 |
518 | $this->expectException(OAuth2FailureException::class);
519 | $this->expectExceptionMessage('OAuth2 failure: An OAuth server error was encountered that did not contain a JSON body');
520 |
521 | $api->get('/call');
522 | }
523 |
524 | public function testOauth2BadJSON()
525 | {
526 | $client = new MockClient(
527 | // POST https://www.ovh.com/auth/oauth2/token
528 | new Response(200, [], 'test
'),
529 | );
530 |
531 | $api = Api::withOauth2('client_id', 'client_secret', 'ovh-eu');
532 | mockOauth2HttpClient($api, $client);
533 |
534 | $this->expectException(OAuth2FailureException::class);
535 | $this->expectExceptionMessage('OAuth2 failure: Invalid response received from Authorization Server. Expected JSON.');
536 |
537 | $api->get('/call');
538 | }
539 |
540 | public function testOauth2UnknownClient()
541 | {
542 | $client = new MockClient(
543 | // POST https://www.ovh.com/auth/oauth2/token
544 | new Response(200, [], 'test
'),
545 | );
546 |
547 | $this->expectException(InvalidParameterException::class);
548 | $this->expectExceptionMessage('OAuth2 authentication is not compatible with endpoint unknown (it can only be used with ovh-eu, ovh-ca and ovh-us)');
549 |
550 | Api::withOauth2('client_id', 'client_secret', 'unknown');
551 | }
552 |
553 | public function testOauth2InvalidCredentials()
554 | {
555 | $client = new MockClient(
556 | // POST https://www.ovh.com/auth/oauth2/token
557 | new Response(400, [], '{"error":"invalid_client_credentials","error_description":"client secret invalid"}'),
558 | );
559 |
560 | $api = Api::withOauth2('client_id', 'client_secret', 'ovh-eu');
561 | mockOauth2HttpClient($api, $client);
562 |
563 | $this->expectException(OAuth2FailureException::class);
564 | $this->expectExceptionMessage('OAuth2 failure: invalid_client_credentials');
565 |
566 | $api->get('/call');
567 | }
568 |
569 | public function testOauth2OK()
570 | {
571 | $client = new MockClient(
572 | // POST https://www.ovh.com/auth/oauth2/token
573 | new Response(200, [], '{"access_token":"cccccccccccccccc", "token_type":"Bearer", "expires_in":11,"scope":"all"}'),
574 | // GET /1.0/call
575 | new Response(200, [], '{}'),
576 | // GET /1.0/call
577 | new Response(200, [], '{}'),
578 | // POST https://www.ovh.com/auth/oauth2/token
579 | new Response(200, [], '{"access_token":"cccccccccccccccd", "token_type":"Bearer", "expires_in":11,"scope":"all"}'),
580 | // GET /1.0/call
581 | new Response(200, [], '{}'),
582 | );
583 |
584 | $api = Api::withOauth2('client_id', 'client_secret', 'ovh-eu');
585 | mockOauth2HttpClient($api, $client);
586 |
587 | $api->get('/call');
588 |
589 | $calls = $client->calls;
590 | $this->assertCount(2, $calls);
591 |
592 | $req = $calls[0]['request'];
593 | $this->assertSame('POST', $req->getMethod());
594 | $this->assertSame('https://www.ovh.com/auth/oauth2/token', $req->getUri()->__toString());
595 |
596 | $req = $calls[1]['request'];
597 | $this->assertSame('GET', $req->getMethod());
598 | $this->assertSame('https://eu.api.ovh.com/1.0/call', $req->getUri()->__toString());
599 | $this->assertSame('Bearer cccccccccccccccc', $req->getHeaderLine('Authorization'));
600 |
601 | $api->get('/call');
602 |
603 | $calls = $client->calls;
604 | $this->assertCount(3, $calls);
605 |
606 | $req = $calls[2]['request'];
607 | $this->assertSame('GET', $req->getMethod());
608 | $this->assertSame('https://eu.api.ovh.com/1.0/call', $req->getUri()->__toString());
609 | $this->assertSame('Bearer cccccccccccccccc', $req->getHeaderLine('Authorization'));
610 |
611 | sleep(2);
612 |
613 | $api->get('/call');
614 |
615 | $calls = $client->calls;
616 | $this->assertCount(5, $calls);
617 |
618 | $req = $calls[3]['request'];
619 | $this->assertSame('POST', $req->getMethod());
620 | $this->assertSame('https://www.ovh.com/auth/oauth2/token', $req->getUri()->__toString());
621 |
622 | $req = $calls[4]['request'];
623 | $this->assertSame('GET', $req->getMethod());
624 | $this->assertSame('https://eu.api.ovh.com/1.0/call', $req->getUri()->__toString());
625 | $this->assertSame('Bearer cccccccccccccccd', $req->getHeaderLine('Authorization'));
626 | }
627 | }
628 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 |