├── .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 | [![PHP Wrapper for OVHcloud APIs](https://github.com/ovh/php-ovh/blob/master/img/logo.png)](https://packagist.org/packages/ovh/ovh) 4 | 5 | [![Source Code](https://img.shields.io/badge/source-ovh/php--ovh-blue.svg?style=flat-square)](https://github.com/ovh/php-ovh) 6 | [![Build Status](https://img.shields.io/github/actions/workflow/status/ovh/php-ovh/ci.yaml?label=CI&logo=github&style=flat-square)](https://github.com/ovh/php-ovh/actions?query=workflow%3ACI) 7 | [![Codecov Code Coverage](https://img.shields.io/codecov/c/gh/ovh/php-ovh?label=codecov&logo=codecov&style=flat-square)](https://codecov.io/gh/ovh/php-ovh) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/ovh/ovh.svg?style=flat-square)](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 | docs 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 |