├── .github ├── FUNDING.yml └── workflows │ └── run-tests.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── _config.yml ├── composer.json ├── extend_doc.md ├── phpspec.yml ├── spec └── Omniphx │ └── Forrest │ ├── Authentications │ ├── ClientCredentialsSpec.php │ ├── OAuthJWTSpec.php │ ├── UserPasswordSoapSpec.php │ ├── UserPasswordSpec.php │ └── WebServerSpec.php │ ├── Providers │ ├── Laravel │ │ ├── LaravelCacheSpec.php │ │ ├── LaravelEventSpec.php │ │ ├── LaravelInputSpec.php │ │ ├── LaravelRedirectSpec.php │ │ └── LaravelSessionSpec.php │ └── Lumen │ │ └── LumenRedirectSpec.php │ └── Repositories │ ├── InstanceURLRepositorySpec.php │ ├── RefreshTokenRepositorySpec.php │ ├── ResourceRepositorySpec.php │ ├── StateRepositorySpec.php │ ├── TokenRepositorySpec.php │ └── VersionRepositorySpec.php └── src ├── Omniphx └── Forrest │ ├── Authentications │ ├── ClientCredentials.php │ ├── OAuthJWT.php │ ├── UserPassword.php │ ├── UserPasswordSoap.php │ └── WebServer.php │ ├── Client.php │ ├── Exceptions │ ├── InvalidLoginCreditialsException.php │ ├── MissingInstanceURLException.php │ ├── MissingKeyException.php │ ├── MissingRefreshTokenException.php │ ├── MissingResourceException.php │ ├── MissingStateException.php │ ├── MissingTokenException.php │ ├── MissingVersionException.php │ ├── SalesforceException.php │ └── TokenExpiredException.php │ ├── Formatters │ ├── BaseFormatter.php │ ├── CsvFormatter.php │ ├── CsvHeadersFormatter.php │ ├── JSONFormatter.php │ ├── URLEncodedFormatter.php │ └── XMLFormatter.php │ ├── Interfaces │ ├── AuthenticationInterface.php │ ├── ClientCredentialsInterface.php │ ├── EncryptorInterface.php │ ├── EventInterface.php │ ├── FormatterInterface.php │ ├── InputInterface.php │ ├── RedirectInterface.php │ ├── RepositoryInterface.php │ ├── ResourceRepositoryInterface.php │ ├── StorageInterface.php │ ├── UserPasswordInterface.php │ ├── UserPasswordSoapInterface.php │ └── WebServerInterface.php │ ├── Providers │ ├── BaseServiceProvider.php │ ├── Laravel │ │ ├── Facades │ │ │ └── Forrest.php │ │ ├── ForrestServiceProvider.php │ │ ├── LaravelCache.php │ │ ├── LaravelEncryptor.php │ │ ├── LaravelEvent.php │ │ ├── LaravelInput.php │ │ ├── LaravelRedirect.php │ │ └── LaravelSession.php │ ├── Lumen │ │ ├── ForrestServiceProvider.php │ │ ├── LumenCache.php │ │ └── LumenRedirect.php │ └── ObjectStorage.php │ └── Repositories │ ├── InstanceURLRepository.php │ ├── RefreshTokenRepository.php │ ├── ResourceRepository.php │ ├── StateRepository.php │ ├── TokenRepository.php │ └── VersionRepository.php ├── config ├── .gitkeep └── config.php └── lang └── .gitkeep /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [omniphx] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | fail-fast: true 13 | matrix: 14 | os: [ubuntu-latest, windows-latest] 15 | php: ["7.3", "7.4", "8.0", "8.1", "8.2", "8.3"] 16 | laravel: [8.x, 9.x, 10.x, 11.x, 12.x] 17 | dependency-version: [prefer-stable] 18 | exclude: 19 | - laravel: 9.x 20 | php: 7.3 21 | - laravel: 10.x 22 | php: 7.3 23 | - laravel: 11.x 24 | php: 7.3 25 | - laravel: 12.x 26 | php: 7.3 27 | - laravel: 9.x 28 | php: 7.4 29 | - laravel: 10.x 30 | php: 7.4 31 | - laravel: 11.x 32 | php: 7.4 33 | - laravel: 12.x 34 | php: 7.4 35 | - laravel: 9.x 36 | php: 8.0 37 | - laravel: 10.x 38 | php: 8.0 39 | - laravel: 11.x 40 | php: 8.0 41 | - laravel: 12.x 42 | php: 8.0 43 | - laravel: 10.x 44 | php: 8.1 45 | - laravel: 11.x 46 | php: 8.1 47 | - laravel: 12.x 48 | php: 8.1 49 | 50 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 51 | 52 | steps: 53 | - name: Checkout code 54 | uses: actions/checkout@v2 55 | 56 | - name: Cache dependencies 57 | uses: actions/cache@v2 58 | with: 59 | path: ~/.composer/cache/files 60 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 61 | 62 | - name: Setup PHP 63 | uses: shivammathur/setup-php@v2 64 | with: 65 | php-version: ${{ matrix.php }} 66 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 67 | coverage: none 68 | 69 | - name: Install dependencies 70 | run: | 71 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 72 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest 73 | - name: Execute tests 74 | run: vendor/bin/phpspec 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | /.idea 6 | /src/config/local 7 | /src/config/production 8 | mm.log 9 | /vin 10 | /bin 11 | /tests -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | ## Build 4 | 5 | After you've forked the repo, clone forrest into a new Laravel application or existing project. I recommend creating a new directory to store this project to seperate it from the rest of your codebase. This guide will assume it is named `library` but you can call it anything you like. 6 | 7 | `git clone git@github.com:/forrest.git libraries/forrest` 8 | 9 | Next, update your `composer.json` to include the psr-4 auto-loader location. Your should already see the `App\\` namespace unless you've named it something else: 10 | 11 | ``` 12 | "autoload": { 13 | "psr-4": { 14 | "App\\": "app/", 15 | "Database\\Factories\\": "database/factories/", 16 | "Database\\Seeders\\": "database/seeders/", 17 | "Omniphx\\Forrest\\": "libraries/forrest/src/Omniphx/Forrest" 18 | } 19 | }, 20 | ``` 21 | 22 | Add required dependencies: 23 | 24 | ``` 25 | "require": { 26 | "firebase/php-jwt": "^5.2", 27 | "nesbot/carbon": "^2.0|^3.0" 28 | }, 29 | ``` 30 | 31 | Next run: `composer update` 32 | 33 | From your project's root, add the service provider and alias to your `config/app.php` (same as the install guide): 34 | 35 | ``` 36 | Omniphx\Forrest\Providers\Laravel\ForrestServiceProvider::class 37 | 'Forrest' => Omniphx\Forrest\Providers\Laravel\Facades\Forrest::class 38 | ``` 39 | 40 | And publish the forrest configuration: `php artisan vendor:publish` 41 | 42 | For more details on configuration, see the README.md 43 | 44 | ## Testing 45 | 46 | This project uses the PHPSpec testing framework. PHPSpec leverages mocks so that we only test the code that we've written and assume that other libraries and integrations (such as the Salesforce REST API) are working perfectly fine. You can read more about PHPSpec here: `http://www.phpspec.net/en/stable/` 47 | 48 | You'll also need to be in the forrest directory, not your root/project directory to run tests. 49 | 50 | 1. `cd libary/forrest` 51 | 2. `composer update` 52 | 3. `vendor/bin/phpspec run` (it should be fast!) 53 | 54 | All test are located in the `spec` folder and have a similar namespace to the files in our `src` folder. 55 | 56 | If you add new test methods, please use descriptive method naming. For instance, `it_should_not_call_refresh_method_if_there_is_no_token` 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Matthew Mitchener 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salesforce REST API Client for Laravel 2 | 3 | [![Laravel](https://img.shields.io/badge/Laravel-10.x-orange.svg?style=flat-square)](http://laravel.com) 4 | [![Latest Stable Version](https://img.shields.io/packagist/v/omniphx/forrest.svg?style=flat-square)](https://packagist.org/packages/omniphx/forrest) 5 | [![Total Downloads](https://img.shields.io/packagist/dt/omniphx/forrest.svg?style=flat-square)](https://packagist.org/packages/omniphx/forrest) 6 | [![License](https://img.shields.io/packagist/l/omniphx/forrest.svg?style=flat-square)](https://packagist.org/packages/omniphx/forrest) 7 | [![Actions Status](https://github.com/omniphx/forrest/workflows/Tests/badge.svg)](https://github.com/omniphx/forrest/actions) 8 | 9 | Forrest is a Salesforce/Force.com REST API client for Laravel and Lumen. 10 | 11 | Interested in Eloquent Salesforce Models? Check out [@roblesterjr04](https://github.com/roblesterjr04)'s [EloquentSalesForce](https://github.com/roblesterjr04/EloquentSalesForce) project that utilizes Forrest as it's API layer. 12 | 13 | ## Installation 14 | 15 | > If you are upgrading to Version 2.0, be sure to re-publish your config file. 16 | 17 | Forrest can be installed through composer. Open your `composer.json` file and add the following to the `require` key: 18 | 19 | ```php 20 | "omniphx/forrest": "2.*" 21 | ``` 22 | 23 | Next run `composer update` from the command line to install the package. 24 | 25 | ### Laravel Installation 26 | 27 | The package will automatically register the service provider and `Forrest` alias for Laravel `>=5.5`. For earlier versions, add the service provider and alias to your `config/app.php` file: 28 | 29 | ```php 30 | Omniphx\Forrest\Providers\Laravel\ForrestServiceProvider::class 31 | 'Forrest' => Omniphx\Forrest\Providers\Laravel\Facades\Forrest::class 32 | ``` 33 | 34 | > For Laravel 4, add `Omniphx\Forrest\Providers\Laravel4\ForrestServiceProvider` in `app/config/app.php`. Alias will remain the same. 35 | 36 | ### Lumen Installation 37 | 38 | ```php 39 | class_alias('Omniphx\Forrest\Providers\Laravel\Facades\Forrest', 'Forrest'); 40 | $app->register(Omniphx\Forrest\Providers\Lumen\ForrestServiceProvider::class); 41 | $app->configure('forrest'); 42 | $app->withFacades(); 43 | ``` 44 | 45 | Then you'll utilize the Lumen service provider by registering it in the `bootstrap/app.php` file. 46 | 47 | ### Configuration 48 | 49 | You will need a configuration file to add your credentials. Publish a config file using the `artisan` command: 50 | 51 | ```bash 52 | php artisan vendor:publish 53 | ``` 54 | 55 | This will publish a `config/forrest.php` file that can switch between authentication types as well as other settings. 56 | 57 | After adding the config file, update your `.env` to include the following values (details for getting a consumer key and secret are outlined below): 58 | 59 | ```txt 60 | SF_CONSUMER_KEY=123455 61 | SF_CONSUMER_SECRET=ABCDEF 62 | SF_CALLBACK_URI=https://test.app/callback 63 | 64 | SF_LOGIN_URL=https://login.salesforce.com 65 | # For sandbox: SF_LOGIN_URL=https://test.salesforce.com 66 | 67 | SF_USERNAME=mattjmitchener@gmail.com 68 | SF_PASSWORD=password123 69 | ``` 70 | 71 | > For Lumen, you should copy the config file from `src/config/config.php` and add it to a `forrest.php` configuration file under a config directory in the root of your application. 72 | > For Laravel 4, run `php artisan config:publish omniphx/forrest` which create `app/config/omniphx/forrest/config.php` 73 | 74 | ## Getting Started 75 | 76 | ### Setting up a Connected App 77 | 78 | 1. Log into to your Salesforce org 79 | 2. Click on Setup in the upper right-hand menu 80 | 3. Search App in quick find box, and select `App Manager` 81 | 4. Click New Connected App. 82 | 5. Enter the following details for the remote application: 83 | - Connected App Name 84 | - API Name 85 | - Contact Email 86 | - Enable OAuth Settings under the API dropdown 87 | - Callback URL 88 | - Select access scope (If you need a refresh token, specify it here) 89 | 6. Click `Save` 90 | 91 | After saving, you will now be given a Consumer Key and Consumer Secret. Update your config file with values for `consumerKey`, `consumerSecret`, `loginURL` and `callbackURI`. 92 | 93 | ### Setup 94 | 95 | Creating authentication routes 96 | 97 | #### Web Server authentication flow 98 | 99 | ```php 100 | Route::get('/authenticate', function() 101 | { 102 | return Forrest::authenticate(); 103 | }); 104 | 105 | Route::get('/callback', function() 106 | { 107 | Forrest::callback(); 108 | 109 | return Redirect::to('/'); 110 | }); 111 | ``` 112 | 113 | #### Username-Password authentication flow 114 | 115 | With the Username Password flow, you can directly authenticate with the `Forrest::authenticate()` method. 116 | 117 | > To use this authentication you must add your username, and password to the config file. Security token might need to be amended to your password unless your IP address is whitelisted. 118 | 119 | ```php 120 | Route::get('/authenticate', function() 121 | { 122 | Forrest::authenticate(); 123 | return Redirect::to('/'); 124 | }); 125 | ``` 126 | 127 | #### Client Credentials authentication flow 128 | 129 | With the Client Credentials flow, you can directly authenticate with the `Forrest::authenticate()` method. 130 | 131 | > Using this authentication method only requires your consumer secret and key. Your Salesforce Connected app must also have the "Client Credentials Flow" Enabled in its settings. 132 | 133 | ```php 134 | Route::get('/authenticate', function() 135 | { 136 | Forrest::authenticate(); 137 | return Redirect::to('/'); 138 | }); 139 | ``` 140 | 141 | #### SOAP authentication flow 142 | 143 | (When you cannot create a connected App in Salesforce) 144 | 145 | 1. Salesforce allows individual logins via a SOAP Login 146 | 2. The Bearer access token returned from the SOAP login can be used similar to Oauth key 147 | 3. Update your config file and set the `authentication` value to `UserPasswordSoap` 148 | 4. Update your config file with values for `loginURL`, `username`, and `password`. 149 | With the Username Password SOAP flow, you can directly authenticate with the `Forrest::authenticate()` method. 150 | 151 | > To use this authentication you can add your username, and password to the config file. Security token might need to be amended to your password unless your IP address is whitelisted. 152 | 153 | ```php 154 | Route::get('/authenticate', function() 155 | { 156 | Forrest::authenticate(); 157 | return Redirect::to('/'); 158 | }); 159 | ``` 160 | 161 | If your application requires logging in to salesforce as different users, you can alternatively pass in the login url, username, and password to the `Forrest::authenticateUser()` method. 162 | 163 | > Security token might need to be amended to your password unless your IP address is whitelisted. 164 | 165 | ```php 166 | Route::Post('/authenticate', function(Request $request) 167 | { 168 | Forrest::authenticateUser('https://login.salesforce.com',$request->username, $request->password); 169 | return Redirect::to('/'); 170 | }); 171 | ``` 172 | 173 | #### JWT authentication flow 174 | 175 | Initial setup 176 | 177 | 1. Set `authentication` to `OAuthJWT` in `config/forrest.php` 178 | 2. Generate a key and cert: `openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt` 179 | 3. Configure private key in `config/forrest.php` (e.g., `file_get_contents('./../server.key'),`) 180 | 181 | Setting up a Connected App 182 | 183 | 1. App Manager > Create Connected App 184 | 2. Enable Oauth Settings 185 | 3. Check "Use digital signatures" 186 | 4. Add `server.crt` or whatever you choose to name it 187 | 5. Scope must includes "refresh_token, offline_access" 188 | 6. Click Save 189 | 190 | Next you need to pre-authorize a profile (As of now, can only do this step in Classic but it's important) 191 | 192 | 1. Manage Apps > Connected Apps 193 | 2. Click 'Edit' next to your application 194 | 3. Set 'Permitted Users' = 'Admin approved users are pre-authorized' 195 | 4. Save 196 | 5. Go to Settings > Manage Users > Profiles and edit the profile of the associated user (i.e., Salesforce Administrator) 197 | 6. Under 'Connected App Access' check the corresponding app name 198 | 199 | The implementation is exactly the same as UserPassword (e.g., will need to explicitly specify a username and password) 200 | 201 | ```php 202 | Route::get('/authenticate', function() 203 | { 204 | Forrest::authenticate(); 205 | return Redirect::to('/'); 206 | }); 207 | ``` 208 | 209 | For connecting to Lightning orgs you will need to configure an `instanceUrl` inside your `forrest.php` config: 210 | 211 | ```txt 212 | Lightning: https://.my.salesforce.com 213 | Lightning Sandbox: https://--.sandbox.my.salesforce.com 214 | Developer Org: https://.develop.my.salesforce.com 215 | ``` 216 | 217 | #### Custom login urls 218 | 219 | Sometimes users will need to connect to a sandbox or custom url. To do this, simply pass the url as an argument for the authenticatation method: 220 | 221 | ```php 222 | Route::get('/authenticate', function() 223 | { 224 | $loginURL = 'https://test.salesforce.com'; 225 | 226 | return Forrest::authenticate($loginURL); 227 | }); 228 | ``` 229 | 230 | > Note: You can specify a default login URL in your config file. 231 | 232 | ## Basic usage 233 | 234 | After authentication, your app will store an encrypted authentication token which can be used to make API requests. 235 | 236 | ### Query a record 237 | 238 | ```php 239 | Forrest::query('SELECT Id FROM Account'); 240 | ``` 241 | 242 | Sample result: 243 | 244 | ```php 245 | ( 246 | [totalSize] => 2 247 | [done] => 1 248 | [records] => Array 249 | ( 250 | [0] => Array 251 | ( 252 | [attributes] => Array 253 | ( 254 | [type] => Account 255 | [url] => /services/data/v48.0/sobjects/Account/0013I000004zuIXQAY 256 | ) 257 | 258 | [Id] => 0013I000004zuIXQAY 259 | ) 260 | 261 | [1] => Array 262 | ( 263 | [attributes] => Array 264 | ( 265 | [type] => Account 266 | [url] => /services/data/v48.0/sobjects/Account/0013I000004zuIcQAI 267 | ) 268 | [Id] => 0013I000004zuIcQAI 269 | ) 270 | ) 271 | ) 272 | ``` 273 | 274 | If you are querying more than 2000 records, your response will include: 275 | 276 | ```php 277 | ( 278 | [nextRecordsUrl] => /services/data/v20.0/query/01gD0000002HU6KIAW-2000 279 | ) 280 | ``` 281 | 282 | Simply, call `Forrest::next($nextRecordsUrl)` to return the next 2000 records. 283 | 284 | ### Create a new record 285 | 286 | Records can be created using the following format. 287 | 288 | ```php 289 | Forrest::sobjects('Account',[ 290 | 'method' => 'post', 291 | 'body' => ['Name' => 'Dunder Mifflin'] 292 | ]); 293 | ``` 294 | 295 | ### Update a record 296 | 297 | Update a record with the PUT method. 298 | 299 | ```php 300 | Forrest::sobjects('Account/001i000000xxx',[ 301 | 'method' => 'put', 302 | 'body' => [ 303 | 'Name' => 'Dunder Mifflin', 304 | 'Phone' => '555-555-5555' 305 | ] 306 | ]); 307 | ``` 308 | 309 | ### Upsert a record 310 | 311 | Update a record with the PATCH method and if the external Id doesn't exist, it will insert a new record. 312 | 313 | ```php 314 | $externalId = 'XYZ1234'; 315 | 316 | Forrest::sobjects('Account/External_Id__c/' . $externalId, [ 317 | 'method' => 'patch', 318 | 'body' => [ 319 | 'Name' => 'Dunder Mifflin', 320 | 'Phone' => '555-555-5555' 321 | ] 322 | ]); 323 | ``` 324 | 325 | ### Delete a record 326 | 327 | Delete a record with the DELETE method. 328 | 329 | ```php 330 | Forrest::sobjects('Account/001i000000xxx', ['method' => 'delete']); 331 | ``` 332 | 333 | ### Setting headers 334 | 335 | Sometimes you need the ability to set custom headers (e.g., creating a Lead with an assignment rule) 336 | 337 | ```php 338 | Forrest::sobjects('Lead',[ 339 | 'method' => 'post', 340 | 'body' => [ 341 | 'Company' => 'Dunder Mifflin', 342 | 'LastName' => 'Scott' 343 | ], 344 | 'headers' => [ 345 | 'Sforce-Auto-Assign' => '01Q1N000000yMQZUA2' 346 | ] 347 | ]); 348 | ``` 349 | 350 | > To disable assignment rules, use `'Sforce-Auto-Assign' => 'false'` 351 | 352 | ### XML format 353 | 354 | Change the request/response format to XML with the `format` key or make it default in your config file. 355 | 356 | ```php 357 | Forrest::sobjects('Account',['format'=>'xml']); 358 | ``` 359 | 360 | ## API Requests 361 | 362 | With the exception of the `search` and `query` resources, all resources are requested dynamically using method overloading. 363 | 364 | You can determine which resources you have access to by calling with the resource method 365 | 366 | ```php 367 | Forrest::resources(); 368 | ``` 369 | 370 | This sample output shows the resourses available to call via the API: 371 | 372 | ```php 373 | Array 374 | ( 375 | [sobjects] => /services/data/v30.0/sobjects 376 | [connect] => /services/data/v30.0/connect 377 | [query] => /services/data/v30.0/query 378 | [theme] => /services/data/v30.0/theme 379 | [queryAll] => /services/data/v30.0/queryAll 380 | [tooling] => /services/data/v30.0/tooling 381 | [chatter] => /services/data/v30.0/chatter 382 | [analytics] => /services/data/v30.0/analytics 383 | [recent] => /services/data/v30.0/recent 384 | [process] => /services/data/v30.0/process 385 | [identity] => https://login.salesforce.com/id/00Di0000000XXXXXX/005i0000000aaaaAAA 386 | [flexiPage] => /services/data/v30.0/flexiPage 387 | [search] => /services/data/v30.0/search 388 | [quickActions] => /services/data/v30.0/quickActions 389 | [appMenu] => /services/data/v30.0/appMenu 390 | ) 391 | ``` 392 | 393 | From the list above, I can call resources by referring to the specified key. 394 | 395 | ```php 396 | Forrest::theme(); 397 | ``` 398 | 399 | Or... 400 | 401 | ```php 402 | Forrest::appMenu(); 403 | ``` 404 | 405 | Additional resource url parameters can also be passed in 406 | 407 | ```php 408 | Forrest::sobjects('Account/describe/approvalLayouts/'); 409 | ``` 410 | 411 | As well as new formatting options, headers or other configurations 412 | 413 | ```php 414 | Forrest::theme(['format'=>'xml']); 415 | ``` 416 | 417 | ### Upsert multiple records (Bulk API 2.0) 418 | 419 | Bulk API requests are especially handy when you need to quickly load large amounts of data into your Salesforce org. The key differences is that it requires at least three separate requests (Create, Add, Close), and the data being loaded is sent in a CSV format. 420 | 421 | To illustrate, following are three requests to upsert a CSV of `Contacts` records. 422 | 423 | #### Create 424 | 425 | Create a bulk upload job with the POST method, the body contains the following job properties: 426 | 427 | - `object` is the type of objects you're loading (they must all be the same type per job) 428 | - `externalIdFieldName` is the external ID, if this exists it'll update and if it doesn't a new record will be inserted. Only needed for upsert operations. 429 | - `contentType` is CSV, this is currently the only valid value. 430 | - `operation` is set to `upsert` to both add and update records. 431 | 432 | We're storing the response in `$bulkJob` in order to reference the unique Job ID in the Add and Close requests below. 433 | 434 | > See [Create a Job](https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/create_job.htm) for the full list of options available here. 435 | 436 | ```php 437 | $bulkJob = Forrest::jobs('ingest', [ 438 | 'method' => 'post', 439 | 'body' => [ 440 | "object" => "Contact", 441 | "externalIdFieldName" => "externalId", 442 | "contentType" => "CSV", 443 | "operation" => "upsert" 444 | ] 445 | ]); 446 | ``` 447 | 448 | #### Add Data 449 | 450 | Using the Job ID from the Create POST request, you then send the CSV data to be processed using a PUT request. This assumes you've loaded your CSV contents to `$csv` 451 | 452 | > See [Prepare CSV Files](https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/datafiles_prepare_csv.htm) for details on how it should be formatted. 453 | 454 | ```php 455 | Forrest::jobs('ingest/' . $bulkJob['id'] . '/batches', [ 456 | 'method' => 'put', 457 | 'headers' => [ 458 | 'Content-Type' => 'text/csv' 459 | ], 460 | 'body' => $csv 461 | ]); 462 | ``` 463 | 464 | #### Close 465 | 466 | You must close the job before the records can be processed, to do so you send an `UploadComplete` state using a PATCH request to the Job ID. 467 | 468 | > See [Close or Abort a Job](https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/close_job.htm) for more options and details on how to abort a job. 469 | 470 | ```php 471 | $response = Forrest::jobs('ingest/' . $bulkJob['id'] . '/', [ 472 | 'method' => 'patch', 473 | 'body' => [ 474 | "state" => "UploadComplete" 475 | ] 476 | ]); 477 | ``` 478 | 479 | > **Bulk API 2.0 is available in API version 41.0 and later**. For more information on Salesforce Bulk API, check out the [official documentation](https://developer.salesforce.com/docs/atlas.en-us.api_bulk_v2.meta/api_bulk_v2/introduction_bulk_api_2.htm) and [this tutorial](https://trailhead.salesforce.com/en/content/learn/modules/api_basics/api_basics_bulk) on how to perform a successful Bulk Upload. 480 | 481 | ### Additional API Requests 482 | 483 | #### Refresh 484 | 485 | If a refresh token is set, the server can refresh the access token on the user's behalf. Refresh tokens are only for the Web Server flow. 486 | 487 | ```php 488 | Forrest::refresh(); 489 | ``` 490 | 491 | > If you need a refresh token, be sure to specify this under `access scope` in your [Connected App](#setting-up-connected-app). You can also specify this in your configuration file by adding `'scope' => 'full refresh_token'`. Setting scope access in the config file is optional, the default scope access is determined by your Salesforce org. 492 | 493 | #### Revoke 494 | 495 | This will revoke the authorization token. The session will continue to store a token, but it will become invalid. 496 | 497 | ```php 498 | Forrest::revoke(); 499 | ``` 500 | 501 | #### Versions 502 | 503 | Returns all currently supported versions. Includes the verison, label and link to each version's root: 504 | 505 | ```php 506 | Forrest::versions(); 507 | ``` 508 | 509 | #### Resources 510 | 511 | Returns list of available resources based on the logged in user's permission and API version. 512 | 513 | ```php 514 | Forrest::resources(); 515 | ``` 516 | 517 | #### Identity 518 | 519 | Returns information about the logged-in user. 520 | 521 | ```php 522 | Forrest::identity(); 523 | ``` 524 | 525 | #### Base URL 526 | 527 | Returns the URL of the Salesforce instance with api info. 528 | 529 | ```php 530 | Forrest::getBaseUrl(); // https://my-instance.my.salesforce.com/services/data/v50.0 531 | ``` 532 | 533 | #### Instance URL 534 | 535 | Returns the URL of the Salesforce instance. 536 | 537 | ```php 538 | Forrest::getInstanceURL(); // https://my-instance.my.salesforce.com 539 | ``` 540 | 541 | For a complete listing of API resources, refer to the [Force.com REST API Developer's Guide](http://www.salesforce.com/us/developer/docs/api_rest/api_rest.pdf) 542 | 543 | ### Custom Apex endpoints 544 | 545 | If you create a custom API using Apex, you can use the `custom()` method for consuming them. 546 | 547 | ```php 548 | Forrest::custom('/myEndpoint'); 549 | ``` 550 | 551 | Additional options and parameters can be passed in like this: 552 | 553 | ```php 554 | Forrest::custom('/myEndpoint', [ 555 | 'method' => 'post', 556 | 'body' => ['foo' => 'bar'], 557 | 'parameters' => ['flim' => 'flam']]); 558 | ``` 559 | 560 | > Read [Creating REST APIs using Apex REST](https://developer.salesforce.com/page/Creating_REST_APIs_using_Apex_REST) for more information. 561 | 562 | ### Raw Requests 563 | 564 | If needed, you can make raw requests to an endpoint of your choice. 565 | 566 | ```php 567 | Forrest::get('/services/data/v20.0/endpoint'); 568 | Forrest::head('/services/data/v20.0/endpoint'); 569 | Forrest::post('/services/data/v20.0/endpoint', ['my'=>'param']); 570 | Forrest::put('/services/data/v20.0/endpoint', ['my'=>'param']); 571 | Forrest::patch('/services/data/v20.0/endpoint', ['my'=>'param']); 572 | Forrest::delete('/services/data/v20.0/endpoint'); 573 | ``` 574 | ### Get file body from ContentVersion and Attachment 575 | You can use the Forrest::getContentVersionBody() and Forrest::getAttachmentBody() to retrieve the content of the 576 | uploaded files. They return a streamed response, so it may be a bit cumbersome to use if now used to streams. 577 | Bellow you can find an example to retrieve the content of a uploaded content version. 578 | 579 | ```php 580 | # example 581 | $data = Forrest::getContentVersionBody($version->Id); 582 | $content = $data->getBody()->getContents(); 583 | ``` 584 | 585 | ### Raw response output 586 | 587 | By default, this package will return the body of a response as either a deserialized JSON object or a SimpleXMLElement object. 588 | 589 | There might be times, when you would rather handle this differently. To do this, simply use the format of 'none' and the code will return the entire response body as a string. 590 | 591 | ```php 592 | $response = Forrest::sobjects($resource, ['format'=> 'none']); 593 | echo $response; // Unformatted string 594 | ``` 595 | 596 | ### Event Listener 597 | 598 | This package makes use of Guzzle's event listers 599 | 600 | ```php 601 | Event::listen('forrest.response', function($request, $response) { 602 | dd((string) $response); 603 | }); 604 | ``` 605 | 606 | ### Creating multiple instances of Forrest 607 | 608 | There might be situations where you need to make calls to multiple Salesforce orgs. This can only be achieved only with the UserPassword flows. 609 | 610 | 1. Set storage = `object` in the config file. This will store the token inside the object instance: 611 | 612 | ```php 613 | 'storage'=> [ 614 | 'type' => 'object' 615 | ], 616 | ``` 617 | 618 | 2. Create a multiple instance with the laravel `app()->make()` helper function: 619 | 620 | ```php 621 | $forrest1 = app()->make('forrest'); 622 | $forrest1->setCredentials(['username' => 'user@email.com.org1', 'password'=> '1234']); 623 | $forrest1->authenticate(); 624 | 625 | $forrest2 = app()->make('forrest'); 626 | $forrest2->setCredentials(['username' => 'user@email.com.org2', 'password'=> '1234']); 627 | $forrest2->authenticate(); 628 | ``` 629 | 630 | For more information about Guzzle responses and event listeners, refer to their [documentation](http://guzzle.readthedocs.org). 631 | 632 | ### Creating a custom store 633 | 634 | If you'd prefer to use storage other than `session`, `cache` or `object`, you can implement a custom implementation by configuring a custom class instance in `storage.type`: 635 | 636 | ```php 637 | 'storage' => [ 638 | 'type' => App\Storage\CustomStorage::class, 639 | ], 640 | ``` 641 | 642 | You class can be named anything but it must implement `Omniphx\Forrest\Interfaces\StorageInterface`: 643 | 644 | ```php 645 | path = 'app.custom.path'; 660 | } 661 | 662 | /** 663 | * Store into session. 664 | * 665 | * @param $key 666 | * @param $value 667 | * 668 | * @return void 669 | */ 670 | public function put($key, $value) 671 | { 672 | return Session::put($this->path.$key, $value); 673 | } 674 | 675 | /** 676 | * Get from session. 677 | * 678 | * @param $key 679 | * 680 | * @return mixed 681 | */ 682 | public function get($key) 683 | { 684 | if(!$this->has($key)) { 685 | throw new MissingKeyException(sprintf('No value for requested key: %s', $key)); 686 | } 687 | 688 | return Session::get($this->path.$key); 689 | } 690 | 691 | /** 692 | * Check if storage has a key. 693 | * 694 | * @param $key 695 | * 696 | * @return bool 697 | */ 698 | public function has($key) 699 | { 700 | return Session::has($this->path.$key); 701 | } 702 | } 703 | ``` 704 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "omniphx/forrest", 3 | "description": "A Laravel library for Salesforce", 4 | "license": "MIT", 5 | "keywords": [ 6 | "salesforce", 7 | "laravel", 8 | "rest", 9 | "force.com", 10 | "force" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Matthew Mitchener", 15 | "homepage": "http://mattmitchener.com", 16 | "email": "mattjmitchener@gmail.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "~7.2 || ~8.0", 22 | "firebase/php-jwt": "^5.2|~6.0", 23 | "illuminate/cache": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0", 24 | "illuminate/contracts": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0", 25 | "illuminate/config": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0", 26 | "illuminate/http": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0", 27 | "illuminate/routing": "~6.0|~7.0|~8.0|~9.0|~10.0|~11.0|~12.0", 28 | "nesbot/carbon": "^2.0|^3.0", 29 | "guzzlehttp/guzzle": "~6.0|~7.0" 30 | }, 31 | "require-dev": { 32 | "phpspec/phpspec": "~6.0|~7.0" 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Omniphx\\Forrest\\": "src/Omniphx/Forrest/" 37 | } 38 | }, 39 | "scripts": { 40 | "test": "vendor/bin/phpspec run" 41 | }, 42 | "config": { 43 | "sort-packages": true 44 | }, 45 | "minimum-stability": "dev", 46 | "prefer-stable": true, 47 | "extra": { 48 | "laravel": { 49 | "providers": [ 50 | "Omniphx\\Forrest\\Providers\\Laravel\\ForrestServiceProvider" 51 | ], 52 | "aliases": { 53 | "Forrest": "Omniphx\\Forrest\\Providers\\Laravel\\Facades\\Forrest" 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /extend_doc.md: -------------------------------------------------------------------------------- 1 | #### Query 2 | Returns results for a specified SOQL query. 3 | ```php 4 | Forrest::query('SELECT Id FROM Account'); 5 | ``` 6 | 7 | #### Query Explain 8 | Returns details of how Salesforce will process your query. Available for API verison 30.0 or later. 9 | ```php 10 | Forrest::queryExplain('SELECT Id FROM Account'); 11 | ``` 12 | 13 | #### Query All 14 | Returns results for a specified SOQL query, but will also inlcude deleted records. 15 | ```php 16 | Forrest::queryAll('SELECT Id FROM Account'); 17 | ``` 18 | 19 | #### Search 20 | Returns the specified SOSL query 21 | ```php 22 | Forrest::search('Find {foo}'); 23 | ``` 24 | 25 | #### Scope Order 26 | Global search keeps track of which objects the user interacts with and arranges them when the user performs a global search. This call will return this ordered list of objects. 27 | ```php 28 | Forrest::scopeOrder(); 29 | ``` 30 | 31 | #### Search Layouts 32 | Returns the search results layout for the objects in the query string. List should be formatted as a string, but delimited by a comma. 33 | ```php 34 | Forrest::searchLayouts('Account,Contact,Lead'); 35 | ``` 36 | 37 | #### Suggested Articles 38 | Returns a list of Salesforce Knowledge articles based on the a search query. Pass additional parameters into the second argument. Available for API verison 30.0 or later. 39 | 40 | > Salesforce Knowledge must be enabled for this to work. 41 | 42 | ```php 43 | Forrest::suggestedArticles('foo', [ 44 | 'parameters' => [ 45 | 'channel' => 'App', 46 | 'publishStatus' => 'Draft']]); 47 | ``` 48 | 49 | #### Suggested Queries 50 | Returns a list of suggested searches based on a search text query. Matches search queries that other users have performed in Salesforce Knowledge. Like Suggest Articles, additional parameters can be passed into the second argument with the `parameters` key. Available for API version 30.0 or later. 51 | 52 | ```php 53 | Forrest::suggestedQueries('app, [ 54 | 'parameters' => ['foo' => 'bar']]); 55 | ``` -------------------------------------------------------------------------------- /phpspec.yml: -------------------------------------------------------------------------------- 1 | suites: 2 | forrest_suits: 3 | namespace: Omniphx\Forrest 4 | spec_prefix: spec -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Authentications/OAuthJWTSpec.php: -------------------------------------------------------------------------------- 1 | "Spring 15", 34 | "url" => "/services/data/v33.0", 35 | "version" => "33.0" 36 | ], 37 | [ 38 | "label" => "Summer 15", 39 | "url" => "/services/data/v34.0", 40 | "version" => "34.0" 41 | ], 42 | [ 43 | "label" => "Winter 16", 44 | "url" => "/services/data/v35.0", 45 | "version" => "35.0" 46 | ] 47 | ]; 48 | 49 | protected $authenticationJSON = '{ 50 | "access_token": "00Do0000000secret", 51 | "id": "https://login.salesforce.com/id/00Do0000000xxxxx/005o0000000xxxxx", 52 | "instance_url": "https://na17.salesforce.com", 53 | "issued_at": "1447000236011", 54 | "signature": "secretsig", 55 | "token_type": "Bearer" 56 | }'; 57 | 58 | protected $token = [ 59 | 'access_token' => '00Do0000000secret', 60 | 'instance_url' => 'https://na17.salesforce.com', 61 | 'id' => 'https://login.salesforce.com/id/00Do0000000xxxxx/005o0000000xxxxx', 62 | 'token_type' => 'Bearer', 63 | 'issued_at' => '1447000236011', 64 | 'signature' => 'secretsig']; 65 | 66 | protected $settings = [ 67 | 'authenticationFlow' => 'OAuthJWT', 68 | 'credentials' => [ 69 | 'consumerKey' => 'testingClientId', 70 | 'privateKey' => '-----BEGIN RSA PRIVATE KEY----- 71 | MIIEowIBAAKCAQEAxxceYYRDCpErWPqwLE9DjvAmTDoIKmX1PxawLPLY9TPeFgrG 72 | FHEuf/BjP30z3RUcHclCYsNeMT33Ou/T7QHpgPG6b5Er2X0+xjj89YUhLj5T3tWG 73 | vUGtfpuortbLDdFKgVSZYk24P0L/pgRMOTmDSEMh+rLueio0YiGFc4aE0IEWNqOL 74 | ZEzGGef0rew1z7Sui1lFAoPxm3WJU+0umtfwVwOnPkmUtLIGQGB2Q7n8CDyw9lk3 75 | 4Iojjv1gEWp4bCMo6tAdjWg2DuNUmsZpIwzXpC4Xi6WJ2qUjc4exfltgDZjWCSzN 76 | u68oEDFWkL32zrALnHrLjbGyG9vln2TvGy1+GQIDAQABAoIBAChu+46Wi/8TaJhT 77 | oX/+QRxAjaahipMBzgMYGoOmdoWmGQ6k9YGlUupM6fs09FmMNf+epkrknralfRaN 78 | Kp9R6hhz/4c1FpC/LQaZAFbkyM5ZfjMdbpX1RsUV2/ZWTTrrLJSDl/stCaRfeQhA 79 | izJ8CbudVsNRn7lT5PuhDzddNJAbq4I7Hr3LoEiQy+Wxv3hkNFSTHDzP2mwyqh52 80 | JLGeeYk/F81sQ3ltvxQUdrD7V5vQ2h9VkQEQky65wAsm2STbSdu9hTcNCcyVv5f6 81 | wAkJzru/nVkoqn5hBSybLlWk7l1x6RVxKfB6xzvbPk5JDFlnkLWj2jBXkeIct1Jc 82 | 23XibQECgYEA8Jp6nfIbCjf//QIrkVl5ad9JIcDe/FI/KQ7r1CQnNdRQzwCJg4eQ 83 | o9ndCeK+cTTYzX3W+q2NsSBdV6A+xuKFZjza2YJ3Q3m8RrKtA33lWURxsD3PwuzS 84 | sTtwXNdsW+h9HYJH7OhmjhqlBF4iTnWcNlgEqtg4HyG+2sG0bBE36ykCgYEA09SU 85 | T0A32USN1GMOXMtnh75/6HrX8StDkHKLqN1WTuJkqK+JCqSMRnn8lKBWbeBEk80k 86 | kIuzKXkb2C/MLGhpH5jGR2DfUC5Mtdw0yRZUATW5EwHcoYTG8/n/gFXIICRVaV+n 87 | ErlrdVN55GHvbV5tEzcQYo+qieejOjLQcXHwSXECgYEAhYJS8/36Pytf4vcnUdpC 88 | YxtBq3coxP6miZP8DJWbJGWSCauUouXAvwsPeoLVhl/6xdxERIm1jEoXQZ5r91SP 89 | DXJLRlL89vZAIULYeo2LjINMSq2h8doT98Cx0vK+8CkL9Cns22sCLWxfkRLjGoJs 90 | kkM5I8wjKDNDgoPmJ+lODDECgYB/w2/QfQMyYE7LExPOlEBVd2jeZ3lnVJjjvrLN 91 | nvI3kgT0WStm5+hTebAGVM7MZr/2BX1QUXI2SX2p3upevnrpO9QbqSoHymUqKy8L 92 | OhRgxm5iMHVKVjNJZDfex950xHVfoPm8KWnO0hJq1Ub7yEAxnrybNdu+YZ/pskxW 93 | oEo1gQKBgA1UiBkEnFvX6eYplJVe4fsvDFjaPLKDMdfKqPJTsUfzbwrzuKqBWWU/ 94 | oKYBQx5bP3wfNtI9j5dp1kIePcDBIuaNoPzpG1+UV36Wofae2OaQGN5eA89X9hja 95 | jrskEKQvdXS8iJl4zv2NtM5sCmHBrEzuIu0Hm5Mkp3IeDpi+TPtE 96 | -----END RSA PRIVATE KEY-----', 97 | 'callbackURI' => 'callbackURL', 98 | 'loginURL' => 'https://login.salesforce.com', 99 | 'username' => 'user@email.com', 100 | 'password' => 'mypassword', 101 | ], 102 | 'parameters' => [ 103 | 'display' => 'popup', 104 | 'immediate' => 'false', 105 | 'state' => '', 106 | 'scope' => '', 107 | ], 108 | 'instanceURL' => '', 109 | 'authRedirect' => 'redirectURL', 110 | 'version' => '30.0', 111 | 'defaults' => [ 112 | 'method' => 'get', 113 | 'format' => 'json', 114 | 'compression' => false, 115 | 'compressionType' => 'gzip', 116 | ], 117 | 'language' => 'en_US', 118 | ]; 119 | 120 | public function let( 121 | ClientInterface $mockedHttpClient, 122 | EncryptorInterface $mockedEncryptor, 123 | EventInterface $mockedEvent, 124 | InputInterface $mockedInput, 125 | RedirectInterface $mockedRedirect, 126 | ResponseInterface $mockedResponse, 127 | RepositoryInterface $mockedInstanceURLRepo, 128 | RepositoryInterface $mockedRefreshTokenRepo, 129 | ResourceRepositoryInterface $mockedResourceRepo, 130 | RepositoryInterface $mockedStateRepo, 131 | RepositoryInterface $mockedTokenRepo, 132 | RepositoryInterface $mockedVersionRepo, 133 | FormatterInterface $mockedFormatter) 134 | { 135 | $this->beConstructedWith( 136 | $mockedHttpClient, 137 | $mockedEncryptor, 138 | $mockedEvent, 139 | $mockedInput, 140 | $mockedRedirect, 141 | $mockedInstanceURLRepo, 142 | $mockedRefreshTokenRepo, 143 | $mockedResourceRepo, 144 | $mockedStateRepo, 145 | $mockedTokenRepo, 146 | $mockedVersionRepo, 147 | $mockedFormatter, 148 | $this->settings); 149 | 150 | $mockedInstanceURLRepo->get() 151 | ->willReturn('https://instance.salesforce.com'); 152 | 153 | $mockedResourceRepo->get(Argument::any()) 154 | ->willReturn('/services/data/v30.0/resource'); 155 | $mockedResourceRepo->put(Argument::any())->willReturn(null); 156 | 157 | $mockedTokenRepo->get()->willReturn($this->token); 158 | $mockedTokenRepo->put($this->token)->willReturn(null); 159 | 160 | $mockedFormatter->setBody(Argument::any())->willReturn(null); 161 | $mockedFormatter->setHeaders()->willReturn([ 162 | 'Authorization' => 'Oauth accessToken', 163 | 'Accept' => 'application/json', 164 | 'Content-Type' => 'application/json', 165 | ]); 166 | $mockedFormatter->getDefaultMIMEType()->willReturn('application/json'); 167 | 168 | $mockedVersionRepo->get()->willReturn(['url' => '/resources']); 169 | 170 | $mockedFormatter->formatResponse($mockedResponse) 171 | ->willReturn(['foo' => 'bar']); 172 | 173 | // Fake the current timestamp 174 | Carbon::setTestNow($this->currentTime); 175 | } 176 | 177 | public function letGo() 178 | { 179 | // Reset Carbon 180 | Carbon::setTestNow(); 181 | } 182 | 183 | public function it_is_initializable() 184 | { 185 | $this->shouldHaveType(OAuthJWT::class); 186 | } 187 | 188 | public function it_should_authenticate( 189 | ClientInterface $mockedHttpClient, 190 | ResponseInterface $mockedResponse, 191 | ResponseInterface $mockedVersionRepo, 192 | FormatterInterface $mockedFormatter, 193 | ResponseInterface $versionResponse, 194 | Stream $body) 195 | { 196 | $url = 'url'; 197 | $mockedHttpClient->request( 198 | 'post', 199 | $url, 200 | Argument::any()) 201 | ->shouldBeCalled() 202 | ->willReturn($mockedResponse); 203 | 204 | $mockedHttpClient->request( 205 | 'get', 206 | 'https://instance.salesforce.com/resources', 207 | ['headers' => [ 208 | 'Authorization' => 'Oauth accessToken', 209 | 'Accept' => 'application/json', 210 | 'Content-Type' => 'application/json' 211 | ]]) 212 | ->shouldBeCalled() 213 | ->willReturn($mockedResponse); 214 | 215 | $body->getContents()->shouldBeCalled() 216 | ->willReturn($this->authenticationJSON); 217 | $mockedResponse->getBody()->shouldBeCalled()->willReturn($body); 218 | 219 | $mockedHttpClient->request( 220 | 'get', 221 | 'https://instance.salesforce.com/services/data', 222 | ['headers' => [ 223 | 'Authorization' => 'Oauth accessToken', 224 | 'Accept' => 'application/json', 225 | 'Content-Type' => 'application/json' 226 | ]]) 227 | ->shouldBeCalled() 228 | ->willReturn($versionResponse); 229 | 230 | $mockedFormatter->formatResponse($versionResponse)->shouldBeCalled() 231 | ->willReturn($this->versionArray); 232 | 233 | $mockedVersionRepo->has()->willReturn(false); 234 | $mockedVersionRepo->put([ 235 | "label" => "Winter 16", 236 | "url" => "/services/data/v35.0", 237 | "version" => "35.0" 238 | ])->shouldBeCalled(); 239 | 240 | $this->authenticate($url)->shouldReturn(null); 241 | } 242 | 243 | public function it_should_refresh( 244 | ClientInterface $mockedHttpClient, 245 | ResponseInterface $mockedResponse, 246 | ResponseInterface $mockedVersionRepo, 247 | FormatterInterface $mockedFormatter, 248 | ResponseInterface $versionResponse, 249 | Stream $body) 250 | { 251 | $url = 'https://login.salesforce.com/services/oauth2/token'; 252 | $mockedHttpClient->request( 253 | 'post', 254 | $url, 255 | Argument::any()) 256 | ->shouldBeCalled() 257 | ->willReturn($mockedResponse); 258 | 259 | $mockedHttpClient->request( 260 | 'get', 261 | 'https://instance.salesforce.com/resources', 262 | ['headers' => [ 263 | 'Authorization' => 'Oauth accessToken', 264 | 'Accept' => 'application/json', 265 | 'Content-Type' => 'application/json' 266 | ]]) 267 | ->shouldBeCalled() 268 | ->willReturn($mockedResponse); 269 | 270 | $body->getContents()->shouldBeCalled() 271 | ->willReturn($this->authenticationJSON); 272 | $mockedResponse->getBody()->shouldBeCalled()->willReturn($body); 273 | 274 | $mockedHttpClient->request( 275 | 'get', 276 | 'https://instance.salesforce.com/services/data', 277 | ['headers' => [ 278 | 'Authorization' => 'Oauth accessToken', 279 | 'Accept' => 'application/json', 280 | 'Content-Type' => 'application/json' 281 | ]]) 282 | ->shouldBeCalled() 283 | ->willReturn($versionResponse); 284 | 285 | $mockedFormatter->formatResponse($versionResponse)->shouldBeCalled() 286 | ->willReturn($this->versionArray); 287 | 288 | $mockedVersionRepo->has()->willReturn(false); 289 | $mockedVersionRepo->put([ 290 | "label" => "Winter 16", 291 | "url" => "/services/data/v35.0", 292 | "version" => "35.0" 293 | ])->shouldBeCalled(); 294 | 295 | $this->refresh()->shouldReturn(null); 296 | } 297 | 298 | public function it_should_revoke_the_authentication_token( 299 | ClientInterface $mockedHttpClient, 300 | ResponseInterface $mockedResponse) 301 | { 302 | $mockedHttpClient->request( 303 | 'post', 304 | 'https://login.salesforce.com/services/oauth2/revoke', 305 | [ 306 | 'headers' => [ 307 | 'content-type' => 'application/x-www-form-urlencoded' 308 | ], 309 | 'form_params' => [ 310 | 'token' => $this->token 311 | ] 312 | ]) 313 | ->shouldBeCalled() 314 | ->willReturn($mockedResponse); 315 | $this->revoke()->shouldReturn($mockedResponse); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Authentications/WebServerSpec.php: -------------------------------------------------------------------------------- 1 | "Spring 15", 45 | "url" => "/services/data/v33.0", 46 | "version" => "33.0" 47 | ], 48 | [ 49 | "label" => "Summer 15", 50 | "url" => "/services/data/v34.0", 51 | "version" => "34.0" 52 | ], 53 | [ 54 | "label" => "Winter 16", 55 | "url" => "/services/data/v35.0", 56 | "version" => "35.0" 57 | ] 58 | ]; 59 | 60 | protected $tokenJSON = '{ 61 | "access_token": "00Do0000000secret", 62 | "id": "https://login.salesforce.com/id/00Do0000000xxxxx/005o0000000xxxxx", 63 | "instance_url": "https://na17.salesforce.com", 64 | "issued_at": "1447000236011", 65 | "signature": "secretsig", 66 | "token_type": "Bearer" 67 | }'; 68 | 69 | protected $responseXML = ' 70 | 71 | I\'m Mr. Meseeks, look at me! 72 | Get 2 strokes off Gary\'s golf swing 73 | Have you tried squring your shoulders, Gary? 74 | '; 75 | 76 | protected $token = [ 77 | 'access_token' => '00Do0000000secret', 78 | 'instance_url' => 'https://na17.salesforce.com', 79 | 'id' => 'https://login.salesforce.com/id/00Do0000000xxxxx/005o0000000xxxxx', 80 | 'token_type' => 'Bearer', 81 | 'issued_at' => '1447000236011', 82 | 'signature' => 'secretsig']; 83 | 84 | protected $decodedResponse = ['foo' => 'bar']; 85 | 86 | protected $settings = [ 87 | 'authentication' => 'WebServer', 88 | 'credentials' => [ 89 | 'consumerKey' => 'testingClientId', 90 | 'consumerSecret' => 'testingClientSecret', 91 | 'callbackURI' => 'callbackURL', 92 | 'loginURL' => 'https://login.salesforce.com', 93 | ], 94 | 'parameters' => [ 95 | 'display' => '', 96 | 'immediate' => false, 97 | 'state' => '', 98 | 'scope' => '', 99 | 'prompt' => '', 100 | ], 101 | 'defaults' => [ 102 | 'method' => 'get', 103 | 'format' => 'json', 104 | 'compression' => false, 105 | 'compressionType' => 'gzip', 106 | ], 107 | 'storage' => [ 108 | 'type' => 'session', 109 | 'path' => 'forrest_', 110 | 'expire_in' => 60, 111 | 'store_forever' => false, 112 | ], 113 | 'version' => '', 114 | 'instanceURL' => '', 115 | 'language' => 'en_US', 116 | ]; 117 | 118 | public function let( 119 | ClientInterface $mockedHttpClient, 120 | EncryptorInterface $mockedEncryptor, 121 | EventInterface $mockedEvent, 122 | InputInterface $mockedInput, 123 | RedirectInterface $mockedRedirect, 124 | ResponseInterface $mockedResponse, 125 | RepositoryInterface $mockedInstanceURLRepo, 126 | ResourceRepositoryInterface $mockedResourceRepo, 127 | RepositoryInterface $mockedStateRepo, 128 | RepositoryInterface $mockedRefreshTokenRepo, 129 | RepositoryInterface $mockedTokenRepo, 130 | RepositoryInterface $mockedVersionRepo, 131 | FormatterInterface $mockedFormatter) 132 | { 133 | $this->beConstructedWith( 134 | $mockedHttpClient, 135 | $mockedEncryptor, 136 | $mockedEvent, 137 | $mockedInput, 138 | $mockedRedirect, 139 | $mockedInstanceURLRepo, 140 | $mockedRefreshTokenRepo, 141 | $mockedResourceRepo, 142 | $mockedStateRepo, 143 | $mockedTokenRepo, 144 | $mockedVersionRepo, 145 | $mockedFormatter, 146 | $this->settings); 147 | 148 | $mockedInstanceURLRepo->get()->willReturn('https://instance.salesforce.com'); 149 | $mockedRefreshTokenRepo->get()->willReturn('refreshToken'); 150 | 151 | $mockedFormatter->setHeaders()->willReturn([ 152 | 'Authorization' => 'Oauth accessToken', 153 | 'Accept' => 'application/json', 154 | 'Content-Type' => 'application/json', 155 | ]); 156 | 157 | $mockedFormatter->formatResponse($mockedResponse)->willReturn(['foo' => 'bar']); 158 | 159 | } 160 | 161 | public function it_is_initializable() 162 | { 163 | $this->shouldHaveType('Omniphx\Forrest\Authentications\WebServer'); 164 | } 165 | 166 | public function it_should_authenticate(RedirectInterface $mockedRedirect) 167 | { 168 | $mockedRedirect->to(Argument::any())->willReturn('redirectURL'); 169 | $this->authenticate()->shouldReturn('redirectURL'); 170 | } 171 | 172 | public function it_should_callback( 173 | ClientInterface $mockedHttpClient, 174 | InputInterface $mockedInput, 175 | RepositoryInterface $mockedInstanceURLRepo, 176 | RepositoryInterface $mockedTokenRepo, 177 | ResourceRepositoryInterface $mockedResourceRepo, 178 | ResponseInterface $mockedResponse, 179 | ResponseInterface $resourceResponse, 180 | ResponseInterface $versionResponse, 181 | ResponseInterface $tokenResponse, 182 | ResponseInterface $mockedVersionRepo, 183 | FormatterInterface $mockedFormatter) 184 | { 185 | $mockedInput->get('code')->shouldBeCalled()->willReturn('callbackCode'); 186 | $stateOptions = ['loginUrl' => 'https://login.salesforce.com']; 187 | $mockedInput->get('state')->shouldBeCalled()->willReturn(urlencode(json_encode($stateOptions))); 188 | 189 | $mockedHttpClient->request( 190 | 'post', 191 | 'https://login.salesforce.com/services/oauth2/token', 192 | ['form_params' => [ 193 | 'code' => 'callbackCode', 194 | 'grant_type' => 'authorization_code', 195 | 'client_id' => 'testingClientId', 196 | 'client_secret' => 'testingClientSecret', 197 | 'redirect_uri' => 'callbackURL' 198 | ]]) 199 | ->shouldBeCalled() 200 | ->willReturn($tokenResponse); 201 | 202 | $tokenResponse->getBody()->shouldBeCalled()->willReturn($this->tokenJSON); 203 | $mockedTokenRepo->put($this->token)->shouldBeCalled(); 204 | $mockedVersionRepo->put(["label" => "Winter 16", "url" => "/services/data/v35.0", "version" => "35.0"])->shouldBeCalled(); 205 | $mockedInstanceURLRepo->get()->shouldBeCalled()->willReturn('https://instance.salesforce.com'); 206 | 207 | $mockedHttpClient->request( 208 | 'get', 209 | 'https://instance.salesforce.com/services/data', 210 | ['headers' => [ 211 | 'Authorization' => 'Oauth accessToken', 212 | 'Accept' => 'application/json', 213 | 'Content-Type' => 'application/json' 214 | ]]) 215 | ->shouldBeCalled() 216 | ->willReturn($versionResponse); 217 | 218 | $mockedFormatter->formatResponse($versionResponse)->shouldBeCalled()->willReturn($this->versionArray); 219 | 220 | $mockedVersionRepo->get()->shouldBeCalled()->willReturn(['url' => '/services/data/v35.0']); 221 | 222 | $mockedHttpClient->request( 223 | 'get', 224 | 'https://instance.salesforce.com/services/data/v35.0', 225 | ['headers' => [ 226 | 'Authorization' => 'Oauth accessToken', 227 | 'Accept' => 'application/json', 228 | 'Content-Type' => 'application/json' 229 | ]]) 230 | ->shouldBeCalled() 231 | ->willReturn($resourceResponse); 232 | 233 | $mockedFormatter->formatResponse($resourceResponse)->shouldBeCalled()->willReturn('resources'); 234 | 235 | $this->callback()->shouldReturn($stateOptions); 236 | } 237 | 238 | public function it_should_refresh( 239 | ClientInterface $mockedHttpClient, 240 | EncryptorInterface $mockedEncryptor, 241 | RepositoryInterface $mockedTokenRepo, 242 | ResponseInterface $mockedResponse) 243 | { 244 | $mockedHttpClient->request( 245 | 'post', 246 | 'https://instance.salesforce.com/services/oauth2/token', 247 | ['form_params'=> [ 248 | 'refresh_token' => 'refreshToken', 249 | 'grant_type' => 'refresh_token', 250 | 'client_id' => 'testingClientId', 251 | 'client_secret' => 'testingClientSecret' 252 | ]]) 253 | ->shouldBeCalled() 254 | ->willReturn($mockedResponse); 255 | 256 | $mockedResponse->getBody()->willReturn($this->tokenJSON); 257 | 258 | $mockedTokenRepo->put($this->token)->shouldBeCalled(); 259 | 260 | $this->refresh('token')->shouldReturn(null); 261 | } 262 | 263 | public function it_should_return_the_request( 264 | ClientInterface $mockedHttpClient, 265 | RequestInterface $mockedRequest, 266 | ResponseInterface $mockedResponse) 267 | { 268 | $mockedHttpClient->send($mockedRequest)->willReturn($mockedResponse); 269 | $mockedHttpClient->request( 270 | 'get', 271 | 'url', 272 | ['headers' => [ 273 | 'Authorization' => 'Oauth accessToken', 274 | 'Accept' => 'application/json', 275 | 'Content-Type' => 'application/json']]) 276 | ->willReturn($mockedResponse); 277 | 278 | $this->request('url', ['key' => 'value'])->shouldReturn(['foo' => 'bar']); 279 | } 280 | 281 | public function it_should_refresh_the_token_if_response_throws_error( 282 | ClientInterface $mockedHttpClient, 283 | FormatterInterface $mockedFormatter, 284 | RepositoryInterface $mockedTokenRepo, 285 | RequestInterface $mockedRequest, 286 | ResponseInterface $mockedResponse) 287 | { 288 | //Testing that we catch 401 errors and refresh the salesforce token. 289 | $failedRequest = new Request('GET', 'fakeurl'); 290 | $failedResponse = new Response(401); 291 | $requestException = new RequestException('Salesforce token has expired', $failedRequest, $failedResponse); 292 | 293 | //First request throws an exception 294 | $mockedHttpClient->request( 295 | 'get', 296 | 'url', 297 | ['headers' => [ 298 | 'Authorization' => 'Oauth accessToken', 299 | 'Accept' => 'application/json', 300 | 'Content-Type' => 'application/json']]) 301 | ->shouldBeCalled() 302 | ->willThrow($requestException); 303 | 304 | $mockedHttpClient->request( 305 | 'post', 306 | 'https://instance.salesforce.com/services/oauth2/token', 307 | ['form_params'=> [ 308 | 'refresh_token' => 'refreshToken', 309 | 'grant_type' => 'refresh_token', 310 | 'client_id' => 'testingClientId', 311 | 'client_secret' => 'testingClientSecret' 312 | ]]) 313 | ->shouldBeCalled() 314 | ->willReturn($mockedResponse); 315 | 316 | $mockedResponse->getBody()->shouldBeCalled()->willReturn($this->tokenJSON); 317 | 318 | $mockedTokenRepo->put($this->token)->shouldBeCalled(); 319 | 320 | //This might seem counter-intuitive. We are throwing an exception with the send() method, but we can't stop it. Basically creating an infinite loop of the token being expired. What we can do is verify the methods in the refresh() method are being fired. 321 | $tokenException = new TokenExpiredException('Salesforce token has expired', $requestException); 322 | 323 | $this->shouldThrow($tokenException)->duringRequest('url', ['key' => 'value']); 324 | } 325 | 326 | public function it_should_not_call_refresh_method_if_there_is_no_token( 327 | ClientInterface $mockedHttpClient, 328 | RequestInterface $failedRequest, 329 | RepositoryInterface $mockedRefreshTokenRepo) 330 | { 331 | $failedRequest = new Request('GET', 'fakeurl'); 332 | $failedResponse = new Response(401); 333 | $requestException = new RequestException('Salesforce token has expired', $failedRequest, $failedResponse); 334 | 335 | //First request throws an exception 336 | $mockedHttpClient->request('get', 'url', ['headers' => ['Authorization' => 'Oauth accessToken', 'Accept' => 'application/json', 'Content-Type' => 'application/json']])->shouldBeCalled(1)->willThrow($requestException); 337 | 338 | $mockedRefreshTokenRepo->get()->willThrow('\Omniphx\Forrest\Exceptions\MissingRefreshTokenException'); 339 | 340 | $this->shouldThrow('\Omniphx\Forrest\Exceptions\MissingRefreshTokenException')->duringRequest('url', ['key' => 'value']); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Providers/Laravel/LaravelCacheSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($config, $cache); 16 | } 17 | 18 | public function it_is_initializable(Config $config) 19 | { 20 | $config->get(Argument::any())->shouldBeCalled(); 21 | $this->shouldHaveType('Omniphx\Forrest\Interfaces\StorageInterface'); 22 | } 23 | 24 | public function it_should_allow_a_get(Cache $cache, Config $config) 25 | { 26 | $cache->has(Argument::any())->shouldBeCalled()->willReturn(true); 27 | $cache->get(Argument::any())->shouldBeCalled()->willReturn('morty'); 28 | 29 | $this->get('rick')->shouldReturn('morty'); 30 | } 31 | 32 | public function it_should_allow_storing_cache_forever(Cache $cache, Config $config) 33 | { 34 | $config->get(Argument::any())->shouldBeCalled()->willReturn(10); 35 | $config->get('forrest.storage.store_forever')->shouldBeCalled()->willReturn(true); 36 | $cache->forever(Argument::any(), Argument::any())->shouldBeCalled(); 37 | 38 | $this->put('rick','morty'); 39 | } 40 | 41 | public function it_should_allow_a_put(Cache $cache, Config $config) 42 | { 43 | $config->get(Argument::any())->shouldBeCalled()->willReturn(10); 44 | $config->get('forrest.storage.store_forever')->shouldBeCalled()->willReturn(false); 45 | $cache->put(Argument::any(), Argument::any(), 10)->shouldBeCalled(); 46 | 47 | $this->put('rick','morty'); 48 | } 49 | 50 | public function it_should_not_allow_an_non_integer_to_be_set_for_expiration(Cache $cache, Config $config) 51 | { 52 | $config->get('forrest.storage.path')->shouldBeCalled()->willReturn('path'); 53 | $config->get('forrest.storage.expire_in')->shouldBeCalled()->willReturn('asdfa'); 54 | $config->get('forrest.storage.store_forever')->shouldBeCalled()->willReturn(false); 55 | $cache->put(Argument::any(), Argument::any(), 20)->shouldBeCalled(); 56 | 57 | $this->put('rick','morty'); 58 | } 59 | 60 | public function it_should_not_allow_an_negative_integer_to_be_set_for_expiration(Cache $cache, Config $config) 61 | { 62 | $config->get('forrest.storage.path')->shouldBeCalled()->willReturn('path'); 63 | $config->get('forrest.storage.expire_in')->shouldBeCalled()->willReturn(-15); 64 | $config->get('forrest.storage.store_forever')->shouldBeCalled()->willReturn(false); 65 | $cache->put(Argument::any(), Argument::any(), 20)->shouldBeCalled(); 66 | 67 | $this->put('rick','morty'); 68 | } 69 | 70 | public function it_should_not_allow_string_integer_to_be_set_for_expiration(Cache $cache, Config $config) 71 | { 72 | $config->get('forrest.storage.path')->shouldBeCalled()->willReturn('path'); 73 | $config->get('forrest.storage.expire_in')->shouldBeCalled()->willReturn('45'); 74 | $config->get('forrest.storage.store_forever')->shouldBeCalled()->willReturn(false); 75 | $cache->put(Argument::any(), Argument::any(), 45)->shouldBeCalled(); 76 | 77 | $this->put('rick','morty'); 78 | } 79 | 80 | public function it_should_allow_a_has(Cache $cache) 81 | { 82 | $cache->has(Argument::any())->shouldBeCalled(); 83 | 84 | $this->has('rick'); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Providers/Laravel/LaravelEventSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($event); 15 | } 16 | 17 | public function it_is_initializable() 18 | { 19 | $this->shouldHaveType('Omniphx\Forrest\Providers\Laravel\LaravelEvent'); 20 | } 21 | 22 | public function it_should_fire_an_event(Event $event) 23 | { 24 | $event->dispatch(Argument::type('string'), Argument::type('array'), Argument::type('bool'))->shouldBeCalled(); 25 | $this->fire('event',[]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Providers/Laravel/LaravelInputSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($request); 15 | } 16 | 17 | public function it_is_initializable() 18 | { 19 | $this->shouldHaveType('Omniphx\Forrest\Providers\Laravel\LaravelInput'); 20 | } 21 | 22 | public function it_should_allow_getting_input_from_request(Request $request) 23 | { 24 | $request->input('rick')->shouldBeCalled()->willReturn('morty'); 25 | $this->get('rick')->shouldReturn('morty'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Providers/Laravel/LaravelRedirectSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($redirector); 15 | } 16 | 17 | public function it_is_initializable() 18 | { 19 | $this->shouldHaveType('Omniphx\Forrest\Providers\Laravel\LaravelRedirect'); 20 | } 21 | 22 | public function it_should_allow_a_redirect(Redirector $redirector) 23 | { 24 | $redirector->to('wubbadubbadubdub')->shouldBeCalled(); 25 | $this->to('wubbadubbadubdub'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Providers/Laravel/LaravelSessionSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($config, $session); 16 | } 17 | 18 | public function it_is_initializable(Config $config) 19 | { 20 | $config->get(Argument::any())->shouldBeCalled(); 21 | $this->shouldHaveType('Omniphx\Forrest\Providers\Laravel\LaravelSession'); 22 | } 23 | 24 | public function it_should_allow_a_get(Session $session) 25 | { 26 | $session->has(Argument::any())->shouldBeCalled()->willReturn(true); 27 | $session->get(Argument::any())->shouldBeCalled(); 28 | $this->get('test'); 29 | } 30 | 31 | public function it_should_allow_a_put(Session $session) 32 | { 33 | $session->put(Argument::any(), Argument::any())->shouldBeCalled(); 34 | $this->put('test', 'value'); 35 | } 36 | 37 | public function it_should_allow_a_has(Session $session) 38 | { 39 | $session->has(Argument::any())->shouldBeCalled(); 40 | $this->has('test'); 41 | } 42 | 43 | public function it_should_throw_an_exception_if_token_does_not_exist(Session $session) 44 | { 45 | $session->has(Argument::any())->shouldBeCalled()->willReturn(false); 46 | $this->shouldThrow('\Omniphx\Forrest\Exceptions\MissingKeyException')->duringGet('test'); 47 | } 48 | } -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Providers/Lumen/LumenRedirectSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($redirector); 15 | // } 16 | 17 | // function it_is_initializable() 18 | // { 19 | // $this->shouldHaveType('Omniphx\Forrest\Providers\Lumen\LumenRedirect'); 20 | // } 21 | 22 | // public function it_should_allow_a_redirect(Redirector $redirector) 23 | // { 24 | // $redirector->to('wubbadubbadubdub')->shouldBeCalled(); 25 | // $this->to('wubbadubbadubdub'); 26 | // } 27 | } -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Repositories/InstanceURLRepositorySpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(InstanceURLRepository::class); 18 | } 19 | 20 | public function let( 21 | RepositoryInterface $mockedTokenRepo) 22 | { 23 | $this->beConstructedWith($mockedTokenRepo, $this->settings); 24 | 25 | $mockedTokenRepo->get()->willReturn(['instance_url' => 'tokenInstanceURL']); 26 | } 27 | 28 | public function it_should_return_when_put(RepositoryInterface $mockedTokenRepo) 29 | { 30 | $mockedTokenRepo->get()->shouldBeCalled()->willReturn([]); 31 | $mockedTokenRepo->put(['instance_url'=>'this'])->shouldBeCalled(); 32 | $this->put('this')->shouldReturn(null); 33 | } 34 | 35 | public function it_should_return_instance_url_when_setting_is_set(RepositoryInterface $mockedTokenRepo) 36 | { 37 | $this->settings['instanceURL'] = 'settingInstanceURL'; 38 | $this->beConstructedWith($mockedTokenRepo, $this->settings); 39 | 40 | $this->get()->shouldReturn('settingInstanceURL'); 41 | } 42 | 43 | public function it_should_return_instance_url_when_setting_is_not_set() 44 | { 45 | $this->get()->shouldReturn('tokenInstanceURL'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Repositories/RefreshTokenRepositorySpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($mockedEncryptor, $mockedStorage); 17 | } 18 | 19 | function it_is_initializable() 20 | { 21 | $this->shouldHaveType(RefreshTokenRepository::class); 22 | } 23 | 24 | function it_should_store_refresh_token($mockedEncryptor, $mockedStorage) { 25 | $mockedEncryptor->encrypt('token')->willReturn('encryptedToken'); 26 | $mockedStorage->put('refresh_token', 'encryptedToken')->shouldBeCalled(); 27 | 28 | $this->put('token'); 29 | } 30 | 31 | function it_should_retrieve_refresh_token($mockedEncryptor, $mockedStorage) { 32 | $mockedStorage->has('refresh_token')->willReturn(true); 33 | $mockedStorage->get('refresh_token')->willReturn('encryptedToken'); 34 | $mockedEncryptor->decrypt('encryptedToken')->willReturn('decryptedToken'); 35 | 36 | $this->get()->shouldReturn('decryptedToken'); 37 | } 38 | 39 | function it_should_throw_an_error_if_storage_does_not_have_refresh_token($mockedEncryptor, $mockedStorage) { 40 | $mockedStorage->has('refresh_token')->willReturn(false); 41 | 42 | $missingTokenException = new MissingRefreshTokenException('No refresh token stored in current session. Verify you have added refresh_token to your scope items on your connected app settings in Salesforce.'); 43 | 44 | $this->shouldThrow($missingTokenException)->duringGet(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Repositories/ResourceRepositorySpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($mockedStorage); 16 | } 17 | 18 | function it_is_initializable() 19 | { 20 | $this->shouldHaveType(ResourceRepository::class); 21 | } 22 | 23 | function it_should_store_resource($mockedStorage) { 24 | $mockedStorage->put('resources', 'resources')->shouldBeCalled(); 25 | 26 | $this->put('resources'); 27 | } 28 | 29 | function it_should_get_resource($mockedStorage) { 30 | $mockedStorage->has('resources')->shouldBeCalled()->willReturn(true); 31 | $mockedStorage 32 | ->get('resources') 33 | ->shouldBeCalled() 34 | ->willReturn(['resource' => 'resources']); 35 | 36 | $this->get('resource')->shouldReturn('resources'); 37 | } 38 | 39 | function it_should_throw_exception_if_resource_doesnt_exist($mockedStorage) { 40 | $mockedStorage->has('resources')->shouldBeCalled()->willReturn(false); 41 | $missingResourcesException = new MissingResourceException('No resources available'); 42 | 43 | $this->shouldThrow($missingResourcesException)->duringGet('resource'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Repositories/StateRepositorySpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType(StateRepository::class); 17 | } 18 | 19 | function let(StorageInterface $mockedStorage) 20 | { 21 | $this->beConstructedWith($mockedStorage); 22 | } 23 | 24 | function it_should_store_state($mockedStorage) 25 | { 26 | $mockedStorage->put('stateOptions','foo')->shouldBeCalled(); 27 | 28 | $this->put('foo'); 29 | } 30 | 31 | function it_should_get_state($mockedStorage) 32 | { 33 | $mockedStorage->has('stateOptions')->shouldBeCalled()->willReturn(true); 34 | $mockedStorage->get('stateOptions')->shouldBeCalled()->willReturn('foo'); 35 | 36 | $this->get()->shouldReturn('foo'); 37 | } 38 | 39 | function it_should_throw_an_error_if_storage_does_not_have_state($mockedStorage) 40 | { 41 | $mockedStorage->has('stateOptions')->shouldBeCalled()->willReturn(false); 42 | 43 | $missingStateException = new MissingStateException('No state available'); 44 | 45 | $this->shouldThrow($missingStateException)->duringGet(); 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Repositories/TokenRepositorySpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($mockedEncryptor, $mockedStorage); 18 | } 19 | 20 | function it_is_initializable() 21 | { 22 | $this->shouldHaveType(TokenRepository::class); 23 | } 24 | 25 | function it_should_store_token($mockedEncryptor, $mockedStorage) { 26 | $mockedEncryptor->encrypt('token')->willReturn('encryptedToken'); 27 | $mockedStorage->put('token', 'encryptedToken')->shouldBeCalled(); 28 | 29 | $this->put('token'); 30 | } 31 | 32 | function it_should_retrieve_token($mockedEncryptor, $mockedStorage) { 33 | $mockedStorage->has('token')->willReturn(true); 34 | $mockedStorage->get('token')->willReturn('encryptedToken'); 35 | $mockedEncryptor->decrypt('encryptedToken')->willReturn('decryptedToken'); 36 | 37 | $this->get()->shouldReturn('decryptedToken'); 38 | } 39 | 40 | function it_should_throw_an_error_if_storage_does_not_have_token($mockedEncryptor, $mockedStorage) { 41 | $mockedStorage->has('token')->willReturn(false); 42 | 43 | $missingTokenException = new MissingTokenException('No token available'); 44 | 45 | $this->shouldThrow($missingTokenException)->duringGet(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /spec/Omniphx/Forrest/Repositories/VersionRepositorySpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($mockedStorage); 16 | } 17 | 18 | function it_is_initializable() 19 | { 20 | $this->shouldHaveType(VersionRepository::class); 21 | } 22 | 23 | function it_should_store_version($mockedStorage) { 24 | $mockedStorage->put('version', '39.0')->shouldBeCalled(); 25 | $this->put('39.0'); 26 | } 27 | 28 | function it_should_get_version($mockedStorage) { 29 | $mockedStorage->has('version')->shouldBeCalled()->willReturn(true); 30 | $mockedStorage->get('version')->shouldBeCalled()->willReturn('39.0'); 31 | $this->get()->shouldReturn('39.0'); 32 | } 33 | 34 | function it_should_throw_exception_if_version_doesnt_exist($mockedStorage) { 35 | $mockedStorage->has('version')->shouldBeCalled()->willReturn(false); 36 | $missingVersionException = new MissingVersionException('No version available'); 37 | 38 | $this->shouldThrow($missingVersionException)->duringGet(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Authentications/ClientCredentials.php: -------------------------------------------------------------------------------- 1 | credentials['loginURL'] : $url; 13 | $loginURL .= '/services/oauth2/token'; 14 | 15 | $authToken = $this->getAuthToken($loginURL); 16 | 17 | $this->tokenRepo->put($authToken); 18 | 19 | $this->storeVersion(); 20 | $this->storeResources(); 21 | } 22 | 23 | /** 24 | * Refresh authentication token by re-authenticating. 25 | * 26 | * @return void 27 | */ 28 | public function refresh() 29 | { 30 | $tokenURL = $this->credentials['loginURL'] . '/services/oauth2/token'; 31 | $authToken = $this->getAuthToken($tokenURL); 32 | 33 | $this->tokenRepo->put($authToken); 34 | } 35 | 36 | /** 37 | * @param String $tokenURL 38 | * @param Array $parameters 39 | * @return String 40 | */ 41 | private function getAuthToken($url) 42 | { 43 | $parameters['form_params'] = [ 44 | 'grant_type' => 'client_credentials', 45 | 'client_id' => $this->credentials['consumerKey'], 46 | 'client_secret' => $this->credentials['consumerSecret'], 47 | ]; 48 | 49 | // \Psr\Http\Message\ResponseInterface 50 | $response = $this->httpClient->request('post', $url, $parameters); 51 | 52 | $authTokenDecoded = json_decode($response->getBody()->getContents(), true); 53 | 54 | $this->handleAuthenticationErrors($authTokenDecoded); 55 | 56 | return $authTokenDecoded; 57 | } 58 | 59 | /** 60 | * Revokes access token from Salesforce. Will not flush token from storage. 61 | * 62 | * @return \Psr\Http\Message\ResponseInterface 63 | */ 64 | public function revoke() 65 | { 66 | $accessToken = $this->tokenRepo->get(); 67 | $url = $this->credentials['loginURL'].'/services/oauth2/revoke'; 68 | 69 | $options['headers']['content-type'] = 'application/x-www-form-urlencoded'; 70 | $options['form_params']['token'] = $accessToken; 71 | 72 | return $this->httpClient->request('post', $url, $options); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Authentications/OAuthJWT.php: -------------------------------------------------------------------------------- 1 | $iss, 16 | 'aud' => $aud, 17 | 'sub' => $sub, 18 | 'exp' => Carbon::now()->addMinutes(3)->timestamp 19 | ]; 20 | 21 | return JWT::encode($payload, $privateKey, 'RS256'); 22 | } 23 | 24 | private function getDefaultInstanceURL() 25 | { 26 | if (isset($this->settings['instanceURL']) && !empty($this->settings['instanceURL'])) { 27 | return $this->settings['instanceURL']; 28 | } else { 29 | return $this->credentials['loginURL']; 30 | } 31 | } 32 | 33 | public function authenticate($fullInstanceUrl = null) 34 | { 35 | $fullInstanceUrl = $fullInstanceUrl ?? $this->getDefaultInstanceURL() . '/services/oauth2/token'; 36 | 37 | $consumerKey = $this->credentials['consumerKey']; 38 | $loginUrl = $this->credentials['loginURL']; 39 | $username = $this->credentials['username']; 40 | $privateKey = $this->credentials['privateKey']; 41 | 42 | // Generate the form parameters 43 | $assertion = static::getJWT($consumerKey, $loginUrl, $username, $privateKey); 44 | $parameters = [ 45 | 'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 46 | 'assertion' => $assertion 47 | ]; 48 | 49 | // \Psr\Http\Message\ResponseInterface 50 | $response = $this->httpClient->request('post', $fullInstanceUrl, ['form_params' => $parameters]); 51 | 52 | $authToken = json_decode($response->getBody()->getContents(), true); 53 | 54 | $this->handleAuthenticationErrors($authToken); 55 | 56 | $this->tokenRepo->put($authToken); 57 | 58 | $this->storeVersion(); 59 | $this->storeResources(); 60 | } 61 | 62 | /** 63 | * @return void 64 | */ 65 | public function refresh() 66 | { 67 | $this->authenticate(); 68 | } 69 | 70 | /** 71 | * @return \Psr\Http\Message\ResponseInterface 72 | */ 73 | public function revoke() 74 | { 75 | $accessToken = $this->tokenRepo->get()['access_token']; 76 | $url = $this->credentials['loginURL'].'/services/oauth2/revoke'; 77 | 78 | $options['headers']['content-type'] = 'application/x-www-form-urlencoded'; 79 | $options['form_params']['token'] = $accessToken; 80 | 81 | return $this->httpClient->request('post', $url, $options); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Authentications/UserPassword.php: -------------------------------------------------------------------------------- 1 | credentials['loginURL'] : $url; 13 | $loginURL .= '/services/oauth2/token'; 14 | 15 | $authToken = $this->getAuthToken($loginURL); 16 | 17 | $this->tokenRepo->put($authToken); 18 | 19 | $this->storeVersion(); 20 | $this->storeResources(); 21 | } 22 | 23 | /** 24 | * Refresh authentication token by re-authenticating. 25 | * 26 | * @return void 27 | */ 28 | public function refresh() 29 | { 30 | $tokenURL = $this->credentials['loginURL'] . '/services/oauth2/token'; 31 | $authToken = $this->getAuthToken($tokenURL); 32 | 33 | $this->tokenRepo->put($authToken); 34 | } 35 | 36 | /** 37 | * @param String $tokenURL 38 | * @param Array $parameters 39 | * @return String 40 | */ 41 | private function getAuthToken($url) 42 | { 43 | $parameters['form_params'] = [ 44 | 'grant_type' => 'password', 45 | 'client_id' => $this->credentials['consumerKey'], 46 | 'client_secret' => $this->credentials['consumerSecret'], 47 | 'username' => $this->credentials['username'], 48 | 'password' => $this->credentials['password'], 49 | ]; 50 | 51 | // \Psr\Http\Message\ResponseInterface 52 | $response = $this->httpClient->request('post', $url, $parameters); 53 | 54 | $authTokenDecoded = json_decode($response->getBody()->getContents(), true); 55 | 56 | $this->handleAuthenticationErrors($authTokenDecoded); 57 | 58 | return $authTokenDecoded; 59 | } 60 | 61 | /** 62 | * Revokes access token from Salesforce. Will not flush token from storage. 63 | * 64 | * @return \Psr\Http\Message\ResponseInterface 65 | */ 66 | public function revoke() 67 | { 68 | $accessToken = $this->tokenRepo->get(); 69 | $url = $this->credentials['loginURL'].'/services/oauth2/revoke'; 70 | 71 | $options['headers']['content-type'] = 'application/x-www-form-urlencoded'; 72 | $options['form_params']['token'] = $accessToken; 73 | 74 | return $this->httpClient->request('post', $url, $options); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Authentications/UserPasswordSoap.php: -------------------------------------------------------------------------------- 1 | credentials['loginURL'] : $url; 31 | $this->authenticateUser($loginURL); 32 | } 33 | 34 | public function authenticateUser($url = null, $username = null, $password = null) 35 | { 36 | $loginURL = null === $url ? $this->credentials['loginURL'] : $url; 37 | $loginURL .= '/services/Soap/u/46.0'; 38 | 39 | $loginUser = null === $username ? $this->credentials['username'] : $username; 40 | $loginPassword = null === $password ? $this->credentials['password'] : $password; 41 | $this->credentials['username'] = $loginUser; 42 | $this->credentials['password'] = $loginPassword; 43 | $authToken = $this->getAuthUser($loginURL, $loginUser, $loginPassword); 44 | $this->tokenRepo->put($authToken); 45 | 46 | $this->storeVersion(); 47 | $this->storeResources(); 48 | } 49 | 50 | /** 51 | * Refresh authentication token by re-authenticating. 52 | * 53 | * @return void 54 | */ 55 | public function refresh() 56 | { 57 | $this->authenticate(); 58 | /* Per the SOAP Documenetationthe token life is extended at every call, 59 | * so the refresh is not needed. Token will expire in two hours from 60 | * last call by default. 61 | */ 62 | } 63 | 64 | /** 65 | * Perform the actual SOAP login. 66 | * 67 | * @param string $tokenURL 68 | * @param string $username 69 | * @param string $password 70 | * 71 | * @return string 72 | */ 73 | private function getAuthUser($url, $username, $password) 74 | { 75 | /* 76 | SOAP Login method - Does not require a connected/defined application 77 | https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_calls_login.htm 78 | */ 79 | 80 | // Required to avoid a 500 error on invalid login and guzzle unhandled error 81 | $parameters['http_errors'] = false; 82 | 83 | $parameters['headers'] = [ 84 | 'Content-Type' => 'text/xml; charset=UTF-8', 85 | 'SOAPAction' => 'login', 86 | ]; 87 | 88 | /* Create a SOAP envelope containing the username and password 89 | * and put the resulting SOAP message in the POST Body. 90 | */ 91 | $parameters['body'] = ''.$username.''.$password.''; 92 | 93 | $response = $this->httpClient->request('post', $url, $parameters); 94 | $xmlResponseBody = $response->getBody(); 95 | $authTokenDecoded = $this->convertSoapToJSON($xmlResponseBody); 96 | $this->handleAuthenticationErrors($authTokenDecoded); 97 | 98 | return $authTokenDecoded; 99 | } 100 | 101 | /** 102 | * Revokes access token from Salesforce. Will not flush token from storage. 103 | * 104 | * @return void 105 | */ 106 | public function revoke() 107 | { 108 | /* 109 | Session Expiration 110 | Client apps aren’t required to explicitly log out to end a session. Sessions expire automatically after a predetermined length of inactivity. The default is two hours. If you make an API call, the inactivity timer is reset to zero. To change the session expiration (timeout) value, from Setup, enter Session Settings in the Quick Find box, and select Session Settings. 111 | 112 | Another thread states that the session keys are unique to a user, so for example 113 | a background task issuing logout has the ability to kill a web session. 114 | 115 | Here is the gist of it. When you make an API call to Salesforce, the they authenticate the calls by examining the SOAP header that contains a session key. Session keys are allocated on a per-user basis. What this means is that if you create two connections at the same time (i.e. using login() from two different threads) - they will both be using the same session key. 116 | 117 | Decision: Dont invoke any type of logout/revoke method. Protect your session keys! 118 | */ 119 | } 120 | 121 | protected function extractToken($response) 122 | { 123 | $sessionId = $response->sessionId; 124 | $serverUrl = $response->serverUrl; 125 | } 126 | 127 | protected function convertSoapToJSON($response) 128 | { 129 | // make the result look like standard xml instead of SOAP 130 | // handle the result from a SOAP Fault the same as a regular result. 131 | if (false === strpos($response, '')) { 132 | $posResult1 = strpos($response, ''); 133 | $posResult2 = strpos($response, ''); 134 | $len = 9; 135 | } else { 136 | $posResult1 = strpos($response, ''); 137 | $posResult2 = strpos($response, ''); 138 | $len = 16; 139 | } 140 | 141 | // Start building a simple XML String 142 | $result = ''.substr($response, $posResult1, ($posResult2 - $posResult1 + $len)); 143 | 144 | // replace namespaces 145 | $result = preg_replace("/(<\/?)(\w+):([^>]*>)/", '$1$2$3', $result); 146 | $result = str_replace('xsi:', '', $result); 147 | 148 | // Disable libxml errors to fetch error information as needed 149 | libxml_use_internal_errors(true); 150 | 151 | // Convert the XML to a standard object via JSON intermediary. 152 | $xml = simplexml_load_string($result); 153 | $json = json_encode($xml); 154 | $data = json_decode($json); 155 | 156 | // Create an empty response Object, then pick and choose items to insert from XML, 157 | $tokenResponse = []; 158 | $tokenResponse['signature'] = 'SOAPHasNoSecretSig'; 159 | // $tokenResponse['issued_at'] = time(); // including this causes phpspec to fail 160 | 161 | // Handle errors from Login 162 | if (isset($data->faultcode)) { 163 | // Forrest is looking for error 164 | $tokenResponse['error'] = $data->faultcode; 165 | } 166 | if (isset($data->faultstring)) { 167 | // Forrest is looking for error_description 168 | $tokenResponse['error_description'] = $data->faultstring; 169 | } 170 | 171 | // Handle successful SOAP login 172 | if (isset($data->userId)) { 173 | // Forrest is looking for id. SOAP doesnt return this, so build it 174 | // based on well known format. 175 | // https://login.salesforce.com/id// 176 | if ('false' == $data->sandbox) { 177 | $base = 'https://login.salesforce.com'; 178 | } else { 179 | $base = 'https://test.salesforce.com'; 180 | } 181 | $tokenResponse['id'] = $base.'/id/'.$data->userInfo->organizationId.'/'.$data->userId; 182 | } 183 | // The SOAP session id is what you put in the REST calls header 184 | // as "Authorization: Bearer " 185 | if (isset($data->sessionId)) { 186 | // Forrest is looking for access_token 187 | $tokenResponse['access_token'] = $data->sessionId; 188 | $tokenResponse['token_type'] = 'Bearer'; 189 | } 190 | if (isset($data->serverUrl)) { 191 | // Forrest is looking for instance_url 192 | // Extract the base URI 193 | $servicesPosition = strpos($data->serverUrl, '/services'); 194 | $url = substr($data->serverUrl, 0, $servicesPosition); 195 | $tokenResponse['instance_url'] = $url; 196 | } 197 | 198 | return $tokenResponse; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Authentications/WebServer.php: -------------------------------------------------------------------------------- 1 | credentials['loginURL'] : $url; 28 | 29 | $stateOptions['loginUrl'] = $loginURL; 30 | $state = '&state='.urlencode(json_encode($stateOptions)); 31 | $parameters = $this->settings['parameters']; 32 | 33 | $loginURL .= '/services/oauth2/authorize'; 34 | $loginURL .= '?response_type=code'; 35 | $loginURL .= '&client_id='.$this->credentials['consumerKey']; 36 | $loginURL .= '&redirect_uri='.urlencode($this->credentials['callbackURI']); 37 | $loginURL .= !empty($parameters['display']) ? '&display='.$parameters['display'] : ''; 38 | $loginURL .= $parameters['immediate'] ? '&immediate=true' : ''; 39 | $loginURL .= !empty($parameters['scope']) ? '&scope='.rawurlencode($parameters['scope']) : ''; 40 | $loginURL .= !empty($parameters['prompt']) ? '&prompt='.rawurlencode($parameters['prompt']) : ''; 41 | $loginURL .= $state; 42 | 43 | return $this->redirect->to($loginURL); 44 | } 45 | 46 | /** 47 | * When settings up your callback route, you will need to call this method to 48 | * acquire an authorization token. This token will be used for the API requests. 49 | * 50 | * @return RedirectInterface 51 | */ 52 | public function callback() 53 | { 54 | //Salesforce sends us an authorization code as part of the Web Server OAuth Authentication Flow 55 | $code = $this->input->get('code'); 56 | $state = stripslashes($this->input->get('state')); 57 | 58 | $stateOptions = json_decode(urldecode($state), true); 59 | 60 | //Store instance URL 61 | $loginURL = $stateOptions['loginUrl']; 62 | 63 | // Store user options so they can be used later 64 | $this->stateRepo->put($stateOptions); 65 | 66 | $tokenURL = $loginURL.'/services/oauth2/token'; 67 | 68 | $jsonResponse = $this->httpClient->request('post', $tokenURL, [ 69 | 'form_params' => [ 70 | 'code' => $code, 71 | 'grant_type' => 'authorization_code', 72 | 'client_id' => $this->credentials['consumerKey'], 73 | 'client_secret' => $this->credentials['consumerSecret'], 74 | 'redirect_uri' => $this->credentials['callbackURI'], 75 | ], 76 | ]); 77 | 78 | // Response returns an json of access_token, instance_url, id, issued_at, and signature. 79 | $response = json_decode($jsonResponse->getBody(), true); 80 | $this->handleAuthenticationErrors($response); 81 | 82 | // Encrypt token and store token in storage. 83 | $this->tokenRepo->put($response); 84 | 85 | if (isset($response['refresh_token'])) { 86 | $this->refreshTokenRepo->put($response['refresh_token']); 87 | } 88 | 89 | $this->storeVersion(); 90 | $this->storeResources(); 91 | 92 | // Return settings 93 | return $stateOptions; 94 | } 95 | 96 | /** 97 | * Refresh authentication token. 98 | * 99 | * @return void 100 | */ 101 | public function refresh() 102 | { 103 | $refreshToken = $this->refreshTokenRepo->get(); 104 | $tokenURL = $this->getLoginURL(); 105 | $tokenURL .= '/services/oauth2/token'; 106 | 107 | $response = $this->httpClient->request('post', $tokenURL, [ 108 | 'form_params' => [ 109 | 'refresh_token' => $refreshToken, 110 | 'grant_type' => 'refresh_token', 111 | 'client_id' => $this->credentials['consumerKey'], 112 | 'client_secret' => $this->credentials['consumerSecret'], 113 | ], 114 | ]); 115 | 116 | // Response returns an json of access_token, instance_url, id, issued_at, and signature. 117 | $token = json_decode($response->getBody(), true); 118 | 119 | // Encrypt token and store token and in storage. 120 | $this->tokenRepo->put($token); 121 | } 122 | 123 | /** 124 | * Revokes access token from Salesforce. Will not flush token from storage. 125 | * 126 | * @return \Psr\Http\Message\ResponseInterface 127 | */ 128 | public function revoke() 129 | { 130 | $accessToken = $this->tokenRepo->get()['access_token']; 131 | $url = $this->getLoginURL(); 132 | $url .= '/services/oauth2/revoke'; 133 | 134 | $options['headers']['content-type'] = 'application/x-www-form-urlencoded'; 135 | $options['form_params']['token'] = $accessToken; 136 | 137 | return $this->httpClient->post($url, $options); 138 | } 139 | 140 | /** 141 | * Retrieve login URL. 142 | * 143 | * @return string 144 | */ 145 | private function getLoginURL() 146 | { 147 | try { 148 | return $this->instanceURLRepo->get(); 149 | } catch (MissingKeyException $e) { 150 | return $loginURL = $this->credentials['loginURL']; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Client.php: -------------------------------------------------------------------------------- 1 | httpClient = $httpClient; 163 | $this->encryptor = $encryptor; 164 | $this->event = $event; 165 | $this->input = $input; 166 | $this->redirect = $redirect; 167 | $this->instanceURLRepo = $instanceURLRepo; 168 | $this->refreshTokenRepo = $refreshTokenRepo; 169 | $this->resourceRepo = $resourceRepo; 170 | $this->stateRepo = $stateRepo; 171 | $this->tokenRepo = $tokenRepo; 172 | $this->versionRepo = $versionRepo; 173 | $this->formatter = $formatter; 174 | $this->settings = $settings; 175 | $this->credentials = $settings['credentials']; 176 | } 177 | 178 | /** 179 | * Try requesting token, if token expired try refreshing token. 180 | * 181 | * @param string $url 182 | * @param array $options 183 | * @return string|array 184 | */ 185 | public function request($url, $options) 186 | { 187 | $this->url = $url; 188 | $this->options = array_replace_recursive($this->settings['defaults'], $options); 189 | 190 | try { 191 | return $this->handleRequest(); 192 | } catch (TokenExpiredException $e) { 193 | $this->refresh(); 194 | 195 | $this->url = $url; 196 | 197 | return $this->handleRequest(); 198 | } 199 | } 200 | 201 | public function setCredentials($credentials) 202 | { 203 | $this->credentials = array_replace_recursive($this->credentials, $credentials); 204 | } 205 | 206 | private function handleRequest() 207 | { 208 | if (isset($this->options['format'])) { 209 | $this->setFormatter($this->options['format']); 210 | } else { 211 | $this->setFormatter($this->settings['defaults']['format']); 212 | } 213 | 214 | if (isset($this->options['headers'])) { 215 | $this->parameters['headers'] = array_replace_recursive($this->formatter->setHeaders(), $this->options['headers']); 216 | } else { 217 | $this->parameters['headers'] = $this->formatter->setHeaders(); 218 | } 219 | 220 | if (isset($this->options['body'])) { 221 | if ($this->parameters['headers']['Content-Type'] == $this->formatter->getDefaultMIMEType()) { 222 | $this->parameters['body'] = $this->formatter->setBody($this->options['body']); 223 | } else { 224 | $this->parameters['body'] = $this->options['body']; 225 | } 226 | } else { 227 | unset($this->parameters['body']); 228 | } 229 | 230 | if (isset($this->options['query'])) { 231 | $this->parameters['query'] = http_build_query($this->options['query']); 232 | } else { 233 | unset($this->parameters['query']); 234 | } 235 | 236 | if (isset($this->options['json'])) { 237 | $this->parameters['json'] = $this->options['json']; 238 | } else { 239 | unset($this->parameters['json']); 240 | } 241 | 242 | try { 243 | $response = $this->httpClient->request($this->options['method'], $this->url, $this->parameters); 244 | } catch (RequestException $ex) { 245 | $this->assignExceptions($ex); 246 | } 247 | 248 | // If the format is raw, return the response as is 249 | if($this->options['format'] == 'raw') { 250 | return $response; 251 | } 252 | 253 | $formattedResponse = $this->formatter->formatResponse($response); 254 | 255 | $this->event->fire('forrest.response', [$formattedResponse]); 256 | 257 | return $formattedResponse; 258 | } 259 | 260 | /** 261 | * GET method call using any custom path. 262 | * 263 | * @param string $path 264 | * @param array $requestBody 265 | * @param array $options 266 | * @return mixed 267 | */ 268 | public function get($path, $requestBody = [], $options = []) 269 | { 270 | return $this->sendRequest($path, $requestBody, $options, 'GET'); 271 | } 272 | 273 | /** 274 | * POST method call using any custom path. 275 | * 276 | * @param string $path 277 | * @param array $requestBody 278 | * @param array $options 279 | * @return mixed 280 | */ 281 | public function post($path, $requestBody = [], $options = []) 282 | { 283 | return $this->sendRequest($path, $requestBody, $options, 'POST'); 284 | } 285 | 286 | /** 287 | * PUT method call using any custom path. 288 | * 289 | * @param string $path 290 | * @param array $requestBody 291 | * @param array $options 292 | * @return mixed 293 | */ 294 | public function put($path, $requestBody = [], $options = []) 295 | { 296 | return $this->sendRequest($path, $requestBody, $options, 'PUT'); 297 | } 298 | 299 | /** 300 | * DELETE method call using any custom path. 301 | * 302 | * @param string $path 303 | * @param array $requestBody 304 | * @param array $options 305 | * @return mixed 306 | */ 307 | public function delete($path, $requestBody = [], $options = []) 308 | { 309 | return $this->sendRequest($path, $requestBody, $options, 'DELETE'); 310 | } 311 | 312 | /** 313 | * HEAD method call using any custom path. 314 | * 315 | * @param string $path 316 | * @param array $requestBody 317 | * @param array $options 318 | * @return mixed 319 | */ 320 | public function head($path, $requestBody = [], $options = []) 321 | { 322 | return $this->sendRequest($path, $requestBody, $options, 'HEAD'); 323 | } 324 | 325 | /** 326 | * PATCH method call using any custom path. 327 | * 328 | * @param string $path 329 | * @param array $requestBody 330 | * @param array $options 331 | * @return mixed 332 | */ 333 | public function patch($path, $requestBody = [], $options = []) 334 | { 335 | return $this->sendRequest($path, $requestBody, $options, 'PATCH'); 336 | } 337 | 338 | /** 339 | * Prepares options and sends the request. 340 | * 341 | * 342 | * @return mixed 343 | */ 344 | private function sendRequest($path, $requestBody, $options, $method) 345 | { 346 | $url = $this->instanceURLRepo->get(); 347 | $url .= '/'.trim($path, "/\t\n\r\0\x0B"); 348 | 349 | $options['method'] = $method; 350 | if (! empty($requestBody)) { 351 | $options['body'] = $requestBody; 352 | } 353 | 354 | return $this->request($url, $options); 355 | } 356 | 357 | /** 358 | * Request that returns all currently supported versions. 359 | * Includes the verison, label and link to each version's root. 360 | * Formats: json, xml 361 | * Methods: get. 362 | * 363 | * @param array $options 364 | * @return array $versions 365 | */ 366 | public function versions($options = []) 367 | { 368 | $url = $this->instanceURLRepo->get(); 369 | $url .= '/services/data'; 370 | 371 | $versions = $this->request($url, $options); 372 | 373 | return $versions; 374 | } 375 | 376 | /** 377 | * Lists availabe resources for specified API version. 378 | * Includes resource name and URI. 379 | * Formats: json, xml 380 | * Methods: get. 381 | * 382 | * @param array $options 383 | * @return array $resources 384 | */ 385 | public function resources($options = []) 386 | { 387 | $url = $this->getBaseUrl(); 388 | $resources = $this->request($url, $options); 389 | 390 | return $resources; 391 | } 392 | 393 | /** 394 | * Returns information about the logged-in user. 395 | * 396 | * @param array 397 | * @return array $identity 398 | */ 399 | public function identity($options = []) 400 | { 401 | $token = $this->tokenRepo->get(); 402 | $url = $token['id']; 403 | $accessToken = $token['access_token']; 404 | $tokenType = $token['token_type']; 405 | 406 | $options['headers']['Authorization'] = "$tokenType $accessToken"; 407 | 408 | $identity = $this->request($url, $options); 409 | 410 | return $identity; 411 | } 412 | 413 | /** 414 | * Lists information about organizational limits. 415 | * Available for API version 29.0 and later. 416 | * Returns limits for daily API calls, Data storage, etc. 417 | * 418 | * @param array $options 419 | * @return array $limits 420 | */ 421 | public function limits($options = []) 422 | { 423 | $url = $this->getBaseUrl(); 424 | $url .= '/limits'; 425 | 426 | $limits = $this->request($url, $options); 427 | 428 | return $limits; 429 | } 430 | 431 | /** 432 | * Describes all global objects available in the organization. 433 | * 434 | * @param string $object_name 435 | * @param array $options 436 | * @return array 437 | */ 438 | public function describe($object_name = null, $options = []) 439 | { 440 | $url = sprintf('%s/sobjects', $this->getBaseUrl()); 441 | 442 | if (! empty($object_name)) { 443 | $url .= sprintf('/%s/describe', $object_name); 444 | } 445 | 446 | return $this->request($url, $options); 447 | } 448 | 449 | /** 450 | * Executes a specified SOQL query. 451 | * 452 | * @param string $query 453 | * @param array $options 454 | * @return array $queryResults 455 | */ 456 | public function query($query, $options = []) 457 | { 458 | $url = $this->instanceURLRepo->get(); 459 | $url .= $this->resourceRepo->get('query'); 460 | $url .= '?q='; 461 | $url .= urlencode($query); 462 | 463 | $queryResults = $this->request($url, $options); 464 | 465 | return $queryResults; 466 | } 467 | 468 | /** 469 | * Calls next query. 470 | * 471 | * @param array $options 472 | * @return mixed 473 | */ 474 | public function next($nextUrl, $options = []) 475 | { 476 | $url = $this->instanceURLRepo->get(); 477 | $url .= $nextUrl; 478 | 479 | $queryResults = $this->request($url, $options); 480 | 481 | return $queryResults; 482 | } 483 | 484 | /** 485 | * Details how Salesforce will process your query. 486 | * Available for API verison 30.0 or later. 487 | * 488 | * @param string $query 489 | * @param array $options 490 | * @return array $queryExplain 491 | */ 492 | public function queryExplain($query, $options = []) 493 | { 494 | $url = $this->instanceURLRepo->get(); 495 | $url .= $this->resourceRepo->get('query'); 496 | $url .= '?explain='; 497 | $url .= urlencode($query); 498 | 499 | $queryExplain = $this->request($url, $options); 500 | 501 | return $queryExplain; 502 | } 503 | 504 | /** 505 | * Executes a SOQL query, but will also returned records that have 506 | * been deleted. 507 | * Available for API version 29.0 or later. 508 | * 509 | * @param string $query 510 | * @param array $options 511 | * @return array $queryResults 512 | */ 513 | public function queryAll($query, $options = []) 514 | { 515 | $url = $this->instanceURLRepo->get(); 516 | $url .= $this->resourceRepo->get('queryAll'); 517 | $url .= '?q='; 518 | $url .= urlencode($query); 519 | 520 | $queryResults = $this->request($url, $options); 521 | 522 | return $queryResults; 523 | } 524 | 525 | /** 526 | * Executes the specified SOSL query. 527 | * 528 | * @param string $query 529 | * @param array $options 530 | * @return array 531 | */ 532 | public function search($query, $options = []) 533 | { 534 | $url = $this->instanceURLRepo->get(); 535 | $url .= $this->resourceRepo->get('search'); 536 | $url .= '?q='; 537 | $url .= urlencode($query); 538 | 539 | $searchResults = $this->request($url, $options); 540 | 541 | return $searchResults; 542 | } 543 | 544 | /** 545 | * Returns an ordered list of objects in the default global search 546 | * scope of a logged-in user. Global search keeps track of which 547 | * objects the user interacts with and how often and arranges the 548 | * search results accordingly. Objects used most frequently appear 549 | * at the top of the list. 550 | * 551 | * @param array $options 552 | * @return array 553 | */ 554 | public function scopeOrder($options = []) 555 | { 556 | $url = $this->instanceURLRepo->get(); 557 | $url .= $this->resourceRepo->get('search'); 558 | $url .= '/scopeOrder'; 559 | 560 | $scopeOrder = $this->request($url, $options); 561 | 562 | return $scopeOrder; 563 | } 564 | 565 | /** 566 | * Returns search result layout information for the objects in the query string. 567 | * 568 | * @param array $objectList 569 | * @param array $options 570 | * @return array 571 | */ 572 | public function searchLayouts($objectList, $options = []) 573 | { 574 | $url = $this->instanceURLRepo->get(); 575 | $url .= $this->resourceRepo->get('search'); 576 | $url .= '/layout/?q='; 577 | $url .= urlencode($objectList); 578 | 579 | $searchLayouts = $this->request($url, $options); 580 | 581 | return $searchLayouts; 582 | } 583 | 584 | /** 585 | * Returns a list of Salesforce Knowledge articles whose titles match the user’s 586 | * search query. Provides a shortcut to navigate directly to likely 587 | * relevant articles, before the user performs a search. 588 | * Available for API version 30.0 or later. 589 | * 590 | * @param string $query 591 | * @param array $options 592 | * @return array 593 | */ 594 | public function suggestedArticles($query, $options = []) 595 | { 596 | $url = $this->instanceURLRepo->get(); 597 | $url .= $this->resourceRepo->get('search'); 598 | $url .= '/suggestTitleMatches?q='; 599 | $url .= urlencode($query); 600 | 601 | $parameters = [ 602 | 'language' => $this->settings['language'], 603 | 'publishStatus' => 'Online', 604 | ]; 605 | 606 | if (isset($options['parameters'])) { 607 | $parameters = array_replace_recursive($parameters, $options['parameters']); 608 | $url .= '&'.http_build_query($parameters); 609 | } 610 | 611 | $suggestedArticles = $this->request($url, $options); 612 | 613 | return $suggestedArticles; 614 | } 615 | 616 | /** 617 | * Returns a list of suggested searches based on the user’s query string text 618 | * matching searches that other users have performed in Salesforce Knowledge. 619 | * Available for API version 30.0 or later. 620 | * 621 | * Tested this and can't get it to work. I think the request is set up correctly. 622 | * 623 | * @param string $query 624 | * @param array $options 625 | * @return array 626 | */ 627 | public function suggestedQueries($query, $options = []) 628 | { 629 | $url = $this->instanceURLRepo->get(); 630 | $url .= $this->resourceRepo->get('search'); 631 | $url .= '/suggestSearchQueries?q='; 632 | $url .= urlencode($query); 633 | 634 | $parameters = ['language' => $this->settings['language']]; 635 | 636 | if (isset($options['parameters'])) { 637 | $parameters = array_replace_recursive($parameters, $options['parameters']); 638 | $url .= '&'.http_build_query($parameters); 639 | } 640 | 641 | $suggestedQueries = $this->request($url, $options); 642 | 643 | return $suggestedQueries; 644 | } 645 | 646 | /** 647 | * Request to a custom Apex REST endpoint. 648 | * 649 | * @param string $customURI 650 | * @param array $options 651 | * @return mixed 652 | */ 653 | public function custom($customURI, $options = []) 654 | { 655 | $url = $this->instanceURLRepo->get(); 656 | $url .= '/services/apexrest'; 657 | $url .= $customURI; 658 | 659 | $parameters = []; 660 | 661 | if (isset($options['parameters'])) { 662 | $parameters = array_replace_recursive($parameters, $options['parameters']); 663 | $url .= '?'.http_build_query($parameters); 664 | } 665 | 666 | return $this->request($url, $options); 667 | } 668 | 669 | /** 670 | * Public accessor to the Guzzle Client Object. 671 | * 672 | * @return HttpClientInterface 673 | */ 674 | public function getClient() 675 | { 676 | return $this->httpClient; 677 | } 678 | 679 | /** 680 | * Accessor to get instance URL 681 | * 682 | * @return string 683 | */ 684 | public function getInstanceURL() 685 | { 686 | return $this->instanceURLRepo->get(); 687 | } 688 | 689 | /** 690 | * Accessor to get the token object 691 | * 692 | * @return mixed 693 | */ 694 | public function getToken() 695 | { 696 | return $this->tokenRepo->get(); 697 | } 698 | 699 | /** 700 | * Determine whether token exists 701 | * 702 | * @return bool 703 | */ 704 | public function hasToken() 705 | { 706 | return $this->tokenRepo->has(); 707 | } 708 | 709 | /** 710 | * Returns any resource that is available to the authenticated 711 | * user. Reference Force.com's REST API guide to read about more 712 | * methods that can be called or refence them by calling the 713 | * Session::get('resources') method. 714 | * 715 | * @param string $name 716 | * @param array $arguments 717 | * @return string|array 718 | */ 719 | public function __call($name, $arguments) 720 | { 721 | $url = $this->instanceURLRepo->get(); 722 | $url .= $this->resourceRepo->get($name); 723 | $url .= $this->appendURL($arguments); 724 | 725 | $options = $this->setOptions($arguments); 726 | 727 | return $this->request($url, $options); 728 | } 729 | 730 | private function appendURL($arguments) 731 | { 732 | if (! isset($arguments[0])) { 733 | return ''; 734 | } 735 | if (! is_string($arguments[0])) { 736 | return ''; 737 | } 738 | 739 | return "/$arguments[0]"; 740 | } 741 | 742 | private function setOptions($arguments) 743 | { 744 | $options = []; 745 | 746 | foreach ($arguments as $argument) { 747 | $this->setArgument($argument, $options); 748 | } 749 | 750 | return $options; 751 | } 752 | 753 | private function setArgument($argument, &$options) 754 | { 755 | if (! is_array($argument)) { 756 | return; 757 | } 758 | foreach ($argument as $key => $value) { 759 | $options[$key] = $value; 760 | } 761 | } 762 | 763 | /** 764 | * Refresh authentication token. 765 | * 766 | * @return void 767 | */ 768 | abstract public function refresh(); 769 | 770 | /** 771 | * Revokes access token from Salesforce. Will not flush token from storage. 772 | * 773 | * @return \Psr\Http\Message\ResponseInterface|void 774 | */ 775 | abstract public function revoke(); 776 | 777 | public function getBaseUrl() 778 | { 779 | $url = $this->instanceURLRepo->get(); 780 | $url .= $this->versionRepo->get()['url']; 781 | 782 | return $url; 783 | } 784 | 785 | /** 786 | * Checks to see if version is specified in configuration and if not then 787 | * assign the latest version number available to the user's instance. 788 | * Once a version number is determined, it will be stored in the user's 789 | * storage with the 'version' key. 790 | * 791 | * @return void 792 | */ 793 | protected function storeVersion() 794 | { 795 | $versions = $this->versions(); 796 | 797 | $this->storeLatestVersion($versions); 798 | $this->storeConfiguredVersion($versions); 799 | } 800 | 801 | /** 802 | * Overrides the default formatter set during register. 803 | * 804 | * @param string $formatter - Name of the formatter to use 805 | */ 806 | protected function setFormatter($formatter) 807 | { 808 | if ($formatter === 'json' && strpos(get_class($this->formatter), 'JSONFormatter') === false) { 809 | $this->formatter = new JSONFormatter($this->tokenRepo, $this->settings); 810 | } elseif ($formatter === 'xml' && strpos(get_class($this->formatter), 'XMLFormatter') === false) { 811 | $this->formatter = new XMLFormatter($this->tokenRepo, $this->settings); 812 | } elseif ($formatter === 'none' && strpos(get_class($this->formatter), 'BaseFormatter') === false) { 813 | $this->formatter = new BaseFormatter($this->tokenRepo, $this->settings); 814 | } elseif ($formatter === 'csv' && strpos(get_class($this->formatter), 'CsvFormatter') === false) { 815 | $this->formatter = new CsvFormatter($this->tokenRepo, $this->settings); 816 | } 817 | } 818 | 819 | private function storeConfiguredVersion($versions) 820 | { 821 | $configVersion = $this->settings['version']; 822 | if (empty($configVersion)) { 823 | return; 824 | } 825 | 826 | foreach ($versions as $version) { 827 | $this->determineIfConfiguredVersionExists($version, $configVersion); 828 | } 829 | } 830 | 831 | private function determineIfConfiguredVersionExists($version, $configVersion) 832 | { 833 | if ($version['version'] !== $configVersion) { 834 | return; 835 | } 836 | $this->versionRepo->put($version); 837 | } 838 | 839 | private function storeLatestVersion($versions) 840 | { 841 | $latestVersion = end($versions); 842 | $this->versionRepo->put($latestVersion); 843 | } 844 | 845 | /** 846 | * Checks to see if version is specified. If not then call storeVersion. 847 | * Once a version is determined, determine the available resources the 848 | * user has access to and store them in teh user's sesion. 849 | * 850 | * @return void 851 | */ 852 | protected function storeResources() 853 | { 854 | $resources = $this->resources(['format' => 'json']); 855 | $this->resourceRepo->put($resources); 856 | } 857 | 858 | protected function handleAuthenticationErrors(array $response) 859 | { 860 | if (! isset($response['error'])) { 861 | return; 862 | } 863 | 864 | throw new InvalidLoginCreditialsException($response['error_description']); 865 | } 866 | 867 | /** 868 | * Method will elaborate on RequestException. 869 | * 870 | * 871 | * @throws SalesforceException 872 | * @throws TokenExpiredException 873 | */ 874 | private function assignExceptions(RequestException $ex) 875 | { 876 | if ($ex->hasResponse() && $ex->getResponse()->getStatusCode() == 401) { 877 | throw new TokenExpiredException('Salesforce token has expired', $ex); 878 | } elseif ($ex->hasResponse() && $ex->getResponse()->getStatusCode() == 403 && $ex->getResponse()->getBody()->getContents() == 'Bad_OAuth_Token') { 879 | throw new TokenExpiredException('Salesforce token has expired', $ex); 880 | } elseif ($ex->hasResponse()) { 881 | $error = json_decode($ex->getResponse()->getBody()->getContents(), true); 882 | $ex->getResponse()->getBody()->rewind(); 883 | $jsonError = json_encode($error, JSON_PRETTY_PRINT); 884 | throw new SalesforceException($jsonError, $ex); 885 | } else { 886 | throw new SalesforceException(sprintf('Invalid request: %s', $ex->getMessage()), $ex); 887 | } 888 | } 889 | 890 | /** 891 | * @param string $id Attachment.Id 892 | * 893 | * @throws GuzzleException 894 | */ 895 | public function getAttachmentBody(string $id): ResponseInterface 896 | { 897 | $url = $this->getBaseUrl().sprintf('/sobjects/Attachment/%s/body', $id); 898 | 899 | $parameters = [ 900 | 'headers' => $this->formatter->setHeaders(), 901 | ]; 902 | 903 | $response = $this->httpClient->request('get', $url, $parameters); 904 | 905 | $this->event->fire('forrest.response', json_encode([ 906 | 'url' => $url, 907 | 'method' => 'get', 908 | 'status_code' => $response->getStatusCode(), 909 | 'body_size' => (string) $response->getBody()->getSize(), 910 | ])); 911 | 912 | return $response; 913 | } 914 | 915 | /** 916 | * @param string $id ContentVersion.Id 917 | * 918 | * @throws GuzzleException 919 | */ 920 | public function getContentVersionBody(string $id): ResponseInterface 921 | { 922 | 923 | $url = $this->getBaseUrl().sprintf('/sobjects/ContentVersion/%s/VersionData', $id); 924 | 925 | $parameters = [ 926 | 'headers' => $this->formatter->setHeaders(), 927 | ]; 928 | 929 | $response = $this->httpClient->request('get', $url, $parameters); 930 | 931 | $this->event->fire( 932 | 'forrest.response', 933 | json_encode([ 934 | 'url' => $url, 935 | 'method' => 'get', 936 | 'status_code' => $response->getStatusCode(), 937 | 'body_size' => (string) $response->getBody()->getSize(), 938 | ])); 939 | 940 | return $response; 941 | } 942 | } 943 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Exceptions/InvalidLoginCreditialsException.php: -------------------------------------------------------------------------------- 1 | getRequest(), $e->getResponse(), $e); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Exceptions/TokenExpiredException.php: -------------------------------------------------------------------------------- 1 | getRequest(), $e->getResponse(), $e); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Formatters/BaseFormatter.php: -------------------------------------------------------------------------------- 1 | tokenRepository = $tokenRepository; 16 | $this->settings = $settings; 17 | } 18 | 19 | public function setHeaders() 20 | { 21 | $accessToken = $this->tokenRepository->get()['access_token']; 22 | $tokenType = $this->tokenRepository->get()['token_type']; 23 | 24 | $this->headers['Accept'] = $this->getDefaultMIMEType(); 25 | $this->headers['Content-Type'] = $this->getDefaultMIMEType(); 26 | $this->headers['Authorization'] = "$tokenType $accessToken"; 27 | 28 | $this->setCompression(); 29 | 30 | return $this->headers; 31 | } 32 | 33 | private function setCompression() 34 | { 35 | if (!$this->settings['defaults']['compression']) return; 36 | 37 | $this->headers['Accept-Encoding'] = $this->settings['defaults']['compressionType']; 38 | $this->headers['Content-Encoding'] = $this->settings['defaults']['compressionType']; 39 | } 40 | 41 | public function setBody($data) 42 | { 43 | return $data; 44 | } 45 | 46 | public function formatResponse($response) 47 | { 48 | $body = $response->getBody(); 49 | $contents = (string) $body; 50 | return $contents; 51 | } 52 | 53 | public function getDefaultMIMEType() 54 | { 55 | return $this->mimeType; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Formatters/CsvFormatter.php: -------------------------------------------------------------------------------- 1 | tokenRepository = $tokenRepository; 17 | $this->settings = $settings; 18 | } 19 | 20 | public function setHeaders() 21 | { 22 | $accessToken = $this->tokenRepository->get()['access_token']; 23 | $tokenType = $this->tokenRepository->get()['token_type']; 24 | 25 | $this->headers['Accept'] = $this->getDefaultAcceptMIMEType(); 26 | $this->headers['Content-Type'] = $this->getDefaultMIMEType(); 27 | $this->headers['Authorization'] = "$tokenType $accessToken"; 28 | 29 | $this->setCompression(); 30 | 31 | return $this->headers; 32 | } 33 | 34 | private function setCompression() 35 | { 36 | if (!$this->settings['defaults']['compression']) return; 37 | 38 | $this->headers['Accept-Encoding'] = $this->settings['defaults']['compressionType']; 39 | $this->headers['Content-Encoding'] = $this->settings['defaults']['compressionType']; 40 | } 41 | 42 | public function setBody($data) 43 | { 44 | return $data; 45 | } 46 | 47 | public function formatResponse($response) 48 | { 49 | $body = $response->getBody(); 50 | $contents = (string) $body; 51 | return $contents; 52 | } 53 | 54 | public function getDefaultMIMEType() 55 | { 56 | return $this->mimeType; 57 | } 58 | 59 | public function getDefaultAcceptMIMEType() 60 | { 61 | return $this->acceptMimeType; 62 | } 63 | } -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Formatters/CsvHeadersFormatter.php: -------------------------------------------------------------------------------- 1 | tokenRepository = $tokenRepository; 17 | $this->settings = $settings; 18 | } 19 | 20 | public function setHeaders() 21 | { 22 | $accessToken = $this->tokenRepository->get()['access_token']; 23 | $tokenType = $this->tokenRepository->get()['token_type']; 24 | 25 | $this->headers['Accept'] = $this->getDefaultAcceptMIMEType(); 26 | $this->headers['Content-Type'] = $this->getDefaultMIMEType(); 27 | $this->headers['Authorization'] = "$tokenType $accessToken"; 28 | 29 | $this->setCompression(); 30 | 31 | return $this->headers; 32 | } 33 | 34 | private function setCompression() 35 | { 36 | if (!$this->settings['defaults']['compression']) return; 37 | 38 | $this->headers['Accept-Encoding'] = $this->settings['defaults']['compressionType']; 39 | $this->headers['Content-Encoding'] = $this->settings['defaults']['compressionType']; 40 | } 41 | 42 | public function setBody($data) 43 | { 44 | return $data; 45 | } 46 | 47 | public function formatResponse($response) 48 | { 49 | $body = $response->getBody(); 50 | $header = $response->getHeaders(); 51 | $contents = (string) $body; 52 | 53 | return [ 54 | 'header' => $header, 55 | 'body' => $contents, 56 | 57 | ]; 58 | } 59 | 60 | public function getDefaultMIMEType() 61 | { 62 | return $this->mimeType; 63 | } 64 | 65 | public function getDefaultAcceptMIMEType() 66 | { 67 | return $this->acceptMimeType; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Formatters/JSONFormatter.php: -------------------------------------------------------------------------------- 1 | tokenRepository = $tokenRepository; 17 | $this->settings = $settings; 18 | } 19 | 20 | public function setHeaders() 21 | { 22 | $accessToken = $this->tokenRepository->get()['access_token']; 23 | $tokenType = $this->tokenRepository->get()['token_type']; 24 | 25 | $this->headers['Accept'] = $this->getDefaultMIMEType(); 26 | $this->headers['Content-Type'] = $this->getDefaultMIMEType(); 27 | $this->headers['Authorization'] = "$tokenType $accessToken"; 28 | 29 | $this->setCompression(); 30 | 31 | return $this->headers; 32 | } 33 | 34 | private function setCompression() 35 | { 36 | if (!$this->settings['defaults']['compression']) return; 37 | 38 | $this->headers['Accept-Encoding'] = $this->settings['defaults']['compressionType']; 39 | $this->headers['Content-Encoding'] = $this->settings['defaults']['compressionType']; 40 | } 41 | 42 | public function setBody($data) 43 | { 44 | return json_encode($data); 45 | } 46 | 47 | public function formatResponse($response) 48 | { 49 | return json_decode($response->getBody()->getContents(), true); 50 | } 51 | 52 | public function getDefaultMIMEType() 53 | { 54 | return $this->mimeType; 55 | } 56 | } -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Formatters/URLEncodedFormatter.php: -------------------------------------------------------------------------------- 1 | getDefaultMIMEType(); 14 | $headers['Content-Type'] = $this->getDefaultMIMEType(); 15 | 16 | return $headers; 17 | } 18 | 19 | public function setBody($data) 20 | { 21 | return $data; 22 | } 23 | 24 | public function formatResponse($response) 25 | { 26 | return $response->getBody()->getContents(); 27 | } 28 | 29 | public function getDefaultMIMEType() 30 | { 31 | return $this->mimeType; 32 | } 33 | } -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Formatters/XMLFormatter.php: -------------------------------------------------------------------------------- 1 | tokenRepository = $tokenRepository; 17 | $this->settings = $settings; 18 | } 19 | 20 | public function setHeaders() 21 | { 22 | $accessToken = $this->tokenRepository->get()['access_token']; 23 | $tokenType = $this->tokenRepository->get()['token_type']; 24 | 25 | $this->headers['Accept'] = $this->getDefaultMIMEType(); 26 | $this->headers['Content-Type'] = $this->getDefaultMIMEType(); 27 | $this->headers['Authorization'] = "$tokenType $accessToken"; 28 | 29 | $this->setCompression(); 30 | 31 | return $this->headers; 32 | } 33 | 34 | private function setCompression() 35 | { 36 | if (!$this->settings['defaults']['compression']) return; 37 | 38 | $this->headers['Accept-Encoding'] = $this->settings['defaults']['compressionType']; 39 | $this->headers['Content-Encoding'] = $this->settings['defaults']['compressionType']; 40 | } 41 | 42 | public function setBody($data) 43 | { 44 | return $data; 45 | } 46 | 47 | public function formatResponse($response) 48 | { 49 | $body = $response->getBody(); 50 | $contents = (string) $body; 51 | $decodedXML = simplexml_load_string($contents); 52 | 53 | return $decodedXML; 54 | } 55 | 56 | public function getDefaultMIMEType() 57 | { 58 | return $this->mimeType; 59 | } 60 | } -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Interfaces/AuthenticationInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | public function setHeaders(); 11 | 12 | /** 13 | * @param string|array $data 14 | * @return string 15 | */ 16 | public function setBody($data); 17 | 18 | /** 19 | * @param \Psr\Http\Message\ResponseInterface $response 20 | * @return string|array 21 | */ 22 | public function formatResponse($response); 23 | 24 | /** 25 | * @return string 26 | */ 27 | public function getDefaultMIMEType(); 28 | } -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Interfaces/InputInterface.php: -------------------------------------------------------------------------------- 1 | publishes([ 70 | __DIR__.'/../../../config/config.php' => $this->getConfigPath(), 71 | ]); 72 | } 73 | 74 | /** 75 | * Register the service provider. 76 | * 77 | * @return void 78 | */ 79 | public function register() 80 | { 81 | $this->app->singleton('forrest', function ($app) { 82 | 83 | // Config options 84 | $settings = config('forrest'); 85 | $storageType = config('forrest.storage.type'); 86 | $authenticationType = config('forrest.authentication'); 87 | 88 | // Dependencies 89 | $httpClient = $this->getClient(); 90 | $input = new LaravelInput(app('request')); 91 | $event = new LaravelEvent(app('events')); 92 | $encryptor = new LaravelEncryptor(app('encrypter')); 93 | $redirect = $this->getRedirect(); 94 | $storage = $this->getStorage($storageType); 95 | 96 | $refreshTokenRepo = new RefreshTokenRepository($encryptor, $storage); 97 | $tokenRepo = new TokenRepository($encryptor, $storage); 98 | $resourceRepo = new ResourceRepository($storage); 99 | $versionRepo = new VersionRepository($storage); 100 | $instanceURLRepo = new InstanceURLRepository($tokenRepo, $settings); 101 | $stateRepo = new StateRepository($storage); 102 | 103 | $formatter = new JSONFormatter($tokenRepo, $settings); 104 | 105 | switch ($authenticationType) { 106 | case 'OAuthJWT': 107 | $forrest = new OAuthJWT( 108 | $httpClient, 109 | $encryptor, 110 | $event, 111 | $input, 112 | $redirect, 113 | $instanceURLRepo, 114 | $refreshTokenRepo, 115 | $resourceRepo, 116 | $stateRepo, 117 | $tokenRepo, 118 | $versionRepo, 119 | $formatter, 120 | $settings); 121 | break; 122 | case 'WebServer': 123 | $forrest = new WebServer( 124 | $httpClient, 125 | $encryptor, 126 | $event, 127 | $input, 128 | $redirect, 129 | $instanceURLRepo, 130 | $refreshTokenRepo, 131 | $resourceRepo, 132 | $stateRepo, 133 | $tokenRepo, 134 | $versionRepo, 135 | $formatter, 136 | $settings); 137 | break; 138 | case 'UserPassword': 139 | $forrest = new UserPassword( 140 | $httpClient, 141 | $encryptor, 142 | $event, 143 | $input, 144 | $redirect, 145 | $instanceURLRepo, 146 | $refreshTokenRepo, 147 | $resourceRepo, 148 | $stateRepo, 149 | $tokenRepo, 150 | $versionRepo, 151 | $formatter, 152 | $settings); 153 | break; 154 | case 'UserPasswordSoap': 155 | $forrest = new UserPasswordSoap( 156 | $httpClient, 157 | $encryptor, 158 | $event, 159 | $input, 160 | $redirect, 161 | $instanceURLRepo, 162 | $refreshTokenRepo, 163 | $resourceRepo, 164 | $stateRepo, 165 | $tokenRepo, 166 | $versionRepo, 167 | $formatter, 168 | $settings); 169 | break; 170 | case 'ClientCredentials': 171 | $forrest = new ClientCredentials( 172 | $httpClient, 173 | $encryptor, 174 | $event, 175 | $input, 176 | $redirect, 177 | $instanceURLRepo, 178 | $refreshTokenRepo, 179 | $resourceRepo, 180 | $stateRepo, 181 | $tokenRepo, 182 | $versionRepo, 183 | $formatter, 184 | $settings); 185 | break; 186 | default: 187 | $forrest = new WebServer( 188 | $httpClient, 189 | $encryptor, 190 | $event, 191 | $input, 192 | $redirect, 193 | $instanceURLRepo, 194 | $refreshTokenRepo, 195 | $resourceRepo, 196 | $stateRepo, 197 | $tokenRepo, 198 | $versionRepo, 199 | $formatter, 200 | $settings); 201 | break; 202 | } 203 | 204 | return $forrest; 205 | }); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/Laravel/Facades/Forrest.php: -------------------------------------------------------------------------------- 1 | get('forrest.client', []); 27 | return new Client($client_config); 28 | } 29 | 30 | protected function getRedirect() 31 | { 32 | return new LaravelRedirect(app('redirect')); 33 | } 34 | 35 | protected function getStorage($storageType) 36 | { 37 | switch ($storageType) { 38 | case 'session': 39 | return new LaravelSession(app('config'), app('request')->session()); 40 | case 'cache': 41 | return new LaravelCache(app('config'), app('cache')->store()); 42 | case 'object': 43 | return new ObjectStorage(); 44 | default: 45 | if($storageType !== null && class_exists($storageType) && new $storageType() instanceof StorageInterface) { 46 | return new $storageType(); 47 | } else { 48 | return new LaravelSession(app('config'), app('request')->session()); 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/Laravel/LaravelCache.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 21 | $this->path = $config->get('forrest.storage.path'); 22 | $this->storeForever = $config->get('forrest.storage.store_forever'); 23 | $this->expirationConfig = $config->get('forrest.storage.expire_in'); 24 | $this->setSeconds(); 25 | } 26 | 27 | /** 28 | * Store into session. 29 | * 30 | * @param $key 31 | * @param $value 32 | * 33 | * @return void 34 | */ 35 | public function put($key, $value) 36 | { 37 | if ($this->storeForever) { 38 | return $this->cache->forever($this->path.$key, $value); 39 | } else { 40 | return $this->cache->put($this->path.$key, $value, $this->seconds); 41 | } 42 | } 43 | 44 | /** 45 | * Get from session. 46 | * 47 | * @param $key 48 | * 49 | * @return mixed 50 | */ 51 | public function get($key) 52 | { 53 | $this->checkForKey($key); 54 | 55 | return $this->cache->get($this->path.$key); 56 | } 57 | 58 | /** 59 | * Check if storage has a key. 60 | * 61 | * @param $key 62 | * 63 | * @return bool 64 | */ 65 | public function has($key) 66 | { 67 | return $this->cache->has($this->path.$key); 68 | } 69 | 70 | /** 71 | * @return void 72 | */ 73 | protected function setSeconds() { 74 | if(!$this->checkIfPositiveInteger($this->expirationConfig)) return; 75 | $this->seconds = $this->expirationConfig; 76 | } 77 | 78 | /** 79 | * @return mixed 80 | */ 81 | protected function checkForKey($key) { 82 | if($this->cache->has($this->path.$key)) return; 83 | 84 | throw new MissingKeyException(sprintf('No value for requested key: %s', $key)); 85 | } 86 | 87 | protected function checkIfPositiveInteger($integer) { 88 | return $this->checkIfInteger($integer) && $this->checkIfPositive($integer); 89 | } 90 | 91 | protected function checkIfInteger($integer) { 92 | return filter_var($integer, FILTER_VALIDATE_INT) !== false; 93 | } 94 | 95 | protected function checkIfPositive($integer) { 96 | return $integer > 0; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/Laravel/LaravelEncryptor.php: -------------------------------------------------------------------------------- 1 | encryptor = $encryptor; 15 | } 16 | 17 | /** 18 | * Encrypt the given value. 19 | * 20 | * @param string $value 21 | * @return string 22 | */ 23 | public function encrypt($value) { 24 | return $this->encryptor->encrypt($value); 25 | } 26 | 27 | /** 28 | * Decrypt the given value. 29 | * 30 | * @param string $payload 31 | * @return string 32 | */ 33 | public function decrypt($payload) { 34 | return $this->encryptor->decrypt($payload); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/Laravel/LaravelEvent.php: -------------------------------------------------------------------------------- 1 | event = $event; 15 | } 16 | 17 | /** 18 | * Fire an event and call the listeners. 19 | * 20 | * @param string $event 21 | * @param mixed $payload 22 | * @param bool $halt 23 | * 24 | * @return array|null 25 | */ 26 | public function fire($event, $payload = [], $halt = false) 27 | { 28 | if (method_exists($this->event, 'dispatch')) { 29 | return $this->event->dispatch($event, $payload, $halt); 30 | } 31 | 32 | return $this->event->fire($event, $payload, $halt); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/Laravel/LaravelInput.php: -------------------------------------------------------------------------------- 1 | request = $request; 17 | } 18 | 19 | /** 20 | * Get input from response. 21 | * 22 | * @param string $parameter 23 | * 24 | * @return mixed 25 | */ 26 | public function get($parameter) 27 | { 28 | return $this->request->input($parameter); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/Laravel/LaravelRedirect.php: -------------------------------------------------------------------------------- 1 | redirector = $redirector; 15 | } 16 | 17 | /** 18 | * Redirect to new url. 19 | * 20 | * @param string $parameter 21 | * 22 | * @return \Illuminate\Http\RedirectResponse 23 | */ 24 | public function to($parameter) 25 | { 26 | return $this->redirector->to($parameter); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/Laravel/LaravelSession.php: -------------------------------------------------------------------------------- 1 | path = $config->get('forrest.storage.path'); 19 | $this->session = $session; 20 | } 21 | 22 | /** 23 | * Store into session. 24 | * 25 | * @param $key 26 | * @param $value 27 | * 28 | * @return void 29 | */ 30 | public function put($key, $value) 31 | { 32 | return $this->session->put($this->path.$key, $value); 33 | } 34 | 35 | /** 36 | * Get from session. 37 | * 38 | * @param $key 39 | * 40 | * @return mixed 41 | */ 42 | public function get($key) 43 | { 44 | if(!$this->has($key)) { 45 | throw new MissingKeyException(sprintf('No value for requested key: %s', $key)); 46 | } 47 | 48 | return $this->session->get($this->path.$key); 49 | } 50 | 51 | /** 52 | * Check if storage has a key. 53 | * 54 | * @param $key 55 | * 56 | * @return bool 57 | */ 58 | public function has($key) 59 | { 60 | return $this->session->has($this->path.$key); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/Lumen/ForrestServiceProvider.php: -------------------------------------------------------------------------------- 1 | true]); 26 | } 27 | 28 | protected function getRedirect() 29 | { 30 | return new LumenRedirect(redirect()); 31 | } 32 | 33 | protected function getStorage($storageType) 34 | { 35 | switch ($storageType) { 36 | case 'object': 37 | return new ObjectStorage(); 38 | default: 39 | return new LumenCache(app('config'), app('request')->session()); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/Lumen/LumenCache.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 21 | $this->path = $config->get('forrest.storage.path'); 22 | $this->storeForever = $config->get('forrest.storage.store_forever'); 23 | $this->expirationConfig = $config->get('forrest.storage.expire_in'); 24 | $this->setMinutes(); 25 | } 26 | 27 | /** 28 | * Store into session. 29 | * 30 | * @param $key 31 | * @param $value 32 | * 33 | * @return void 34 | */ 35 | public function put($key, $value) 36 | { 37 | if ($this->storeForever) { 38 | return $this->cache->forever($this->path.$key, $value); 39 | } else { 40 | return $this->cache->put($this->path.$key, $value, $this->minutes); 41 | } 42 | } 43 | 44 | /** 45 | * Get from session. 46 | * 47 | * @param $key 48 | * 49 | * @return mixed 50 | */ 51 | public function get($key) 52 | { 53 | $this->checkForKey($key); 54 | 55 | return $this->cache->get($this->path.$key); 56 | } 57 | 58 | /** 59 | * Check if storage has a key. 60 | * 61 | * @param $key 62 | * 63 | * @return bool 64 | */ 65 | public function has($key) 66 | { 67 | return $this->cache->has($this->path.$key); 68 | } 69 | 70 | /** 71 | * @return void 72 | */ 73 | protected function setMinutes() { 74 | if(!$this->checkIfPositiveInteger($this->expirationConfig)) return; 75 | $this->minutes = $this->expirationConfig; 76 | } 77 | 78 | /** 79 | * @return mixed 80 | */ 81 | protected function checkForKey($key) { 82 | if($this->cache->has($this->path.$key)) return; 83 | 84 | throw new MissingKeyException(sprintf('No value for requested key: %s', $key)); 85 | } 86 | 87 | protected function checkIfPositiveInteger($integer) { 88 | return $this->checkIfInteger($integer) && $this->checkIfPositive($integer); 89 | } 90 | 91 | protected function checkIfInteger($integer) { 92 | return filter_var($integer, FILTER_VALIDATE_INT) !== false; 93 | } 94 | 95 | protected function checkIfPositive($integer) { 96 | return $integer > 0; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/Lumen/LumenRedirect.php: -------------------------------------------------------------------------------- 1 | redirector = $redirector; 16 | } 17 | 18 | /** 19 | * Redirect to new url. 20 | * 21 | * @param string $parameter 22 | * 23 | * @return \Illuminate\Http\RedirectResponse 24 | */ 25 | public function to($parameter) 26 | { 27 | return $this->redirector->to($parameter); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Providers/ObjectStorage.php: -------------------------------------------------------------------------------- 1 | store[$key] = $value; 22 | } 23 | 24 | /** 25 | * @param $key 26 | * 27 | * @return mixed 28 | */ 29 | public function get($key) 30 | { 31 | if(!$this->has($key)) { 32 | throw new MissingKeyException(sprintf('No value for requested key: %s', $key)); 33 | } 34 | 35 | return $this->store[$key]; 36 | } 37 | 38 | /** 39 | * @param $key 40 | * 41 | * @return bool 42 | */ 43 | public function has($key) 44 | { 45 | return array_key_exists($key, $this->store); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Repositories/InstanceURLRepository.php: -------------------------------------------------------------------------------- 1 | tokenRepo = $tokenRepo; 15 | $this->settings = $settings; 16 | } 17 | 18 | /** 19 | * Store the instance URL. 20 | * 21 | * @parameter $instanceURL Override the instance URL returned from authentication 22 | */ 23 | public function put($instanceURL) 24 | { 25 | $token = $this->tokenRepo->get(); 26 | $token['instance_url'] = $instanceURL; 27 | $this->tokenRepo->put($token); 28 | } 29 | 30 | /** 31 | * Is there a Token Repo? 32 | * 33 | * @return bool 34 | */ 35 | public function has() 36 | { 37 | return $this->tokenRepo->has(); 38 | } 39 | 40 | /** 41 | * Get Instance URL. 42 | * 43 | * @return string 44 | */ 45 | public function get() 46 | { 47 | if (isset($this->settings['instanceURL']) && !empty($this->settings['instanceURL'])) { 48 | return $this->settings['instanceURL']; 49 | } else { 50 | return $this->tokenRepo->get()['instance_url']; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Repositories/RefreshTokenRepository.php: -------------------------------------------------------------------------------- 1 | encryptor = $encryptor; 17 | $this->storage = $storage; 18 | } 19 | 20 | /** 21 | * Encrypt refresh token and pass into session. 22 | * 23 | * @param array $token 24 | * 25 | * @return void 26 | */ 27 | public function put($token) 28 | { 29 | $encryptedToken = $this->encryptor->encrypt($token); 30 | 31 | $this->storage->put('refresh_token', $encryptedToken); 32 | } 33 | 34 | public function has() 35 | { 36 | return $this->storage->has('refresh_token'); 37 | } 38 | 39 | /** 40 | * Get refresh token from session and decrypt it. 41 | * 42 | * @return mixed 43 | */ 44 | public function get() 45 | { 46 | $this->verifyRefreshTokenExists(); 47 | 48 | $token = $this->storage->get('refresh_token'); 49 | 50 | return $this->encryptor->decrypt($token); 51 | } 52 | 53 | private function verifyRefreshTokenExists() { 54 | if ($this->storage->has('refresh_token')) return; 55 | 56 | throw new MissingRefreshTokenException(sprintf('No refresh token stored in current session. Verify you have added refresh_token to your scope items on your connected app settings in Salesforce.')); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Repositories/ResourceRepository.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 15 | } 16 | 17 | public function put($resource) 18 | { 19 | $this->storage->put('resources', $resource); 20 | } 21 | 22 | public function has() 23 | { 24 | return $this->storage->has('resources'); 25 | } 26 | 27 | public function get($resource) { 28 | $this->verify(); 29 | 30 | return $this->storage->get('resources')[$resource]; 31 | } 32 | 33 | private function verify() { 34 | if ($this->storage->has('resources')) return; 35 | 36 | throw new MissingResourceException('No resources available'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Repositories/StateRepository.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 15 | } 16 | 17 | public function put($state) { 18 | $this->storage->put('stateOptions', $state); 19 | } 20 | 21 | public function get() 22 | { 23 | $this->verify(); 24 | 25 | return $this->storage->get('stateOptions'); 26 | } 27 | 28 | public function has() { 29 | return $this->storage->has('stateOptions'); 30 | } 31 | 32 | private function verify() { 33 | if ($this->storage->has('stateOptions')) return; 34 | 35 | throw new MissingStateException('No state available'); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Repositories/TokenRepository.php: -------------------------------------------------------------------------------- 1 | encryptor = $encryptor; 17 | $this->storage = $storage; 18 | } 19 | 20 | /** 21 | * Encrypt authentication token and store it in session/cache. 22 | * 23 | * @param array $token 24 | * 25 | * @return void 26 | */ 27 | public function put($token) 28 | { 29 | $encryptedToken = $this->encryptor->encrypt($token); 30 | 31 | $this->storage->put('token', $encryptedToken); 32 | } 33 | 34 | /** 35 | * Get refresh token from session and decrypt it. 36 | * 37 | * @return mixed 38 | */ 39 | public function get() 40 | { 41 | $this->verify(); 42 | 43 | $token = $this->storage->get('token'); 44 | 45 | return $this->encryptor->decrypt($token); 46 | } 47 | 48 | public function has() { 49 | return $this->storage->has('token'); 50 | } 51 | 52 | private function verify() { 53 | if ($this->storage->has('token')) return; 54 | 55 | throw new MissingTokenException('No token available'); 56 | } 57 | } -------------------------------------------------------------------------------- /src/Omniphx/Forrest/Repositories/VersionRepository.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 15 | } 16 | 17 | public function put($versions) 18 | { 19 | $this->storage->put('version', $versions); 20 | } 21 | 22 | /** 23 | * Get version 24 | * 25 | * @return mixed 26 | */ 27 | public function get() 28 | { 29 | $this->verify(); 30 | 31 | return $this->storage->get('version'); 32 | } 33 | 34 | public function has() 35 | { 36 | return $this->storage->has('version'); 37 | } 38 | 39 | private function verify() { 40 | if ($this->storage->has('version')) return; 41 | 42 | throw new MissingVersionException('No version available'); 43 | } 44 | } -------------------------------------------------------------------------------- /src/config/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omniphx/forrest/b0146867caeb221be66df6df22ddace52d12e31b/src/config/.gitkeep -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | env('SF_AUTH_METHOD', 'WebServer'), 11 | 12 | /* 13 | * Enter your credentials 14 | * Username and Password are only necessary for UserPassword & UserPasswordSoap flows. 15 | * Likewise, callbackURI is only necessary for WebServer flow. 16 | * OAuthJWT requires a key, username, and private key (SF_CONSUMER_SECRET) 17 | */ 18 | 'credentials' => [ 19 | //Required: 20 | 'consumerKey' => env('SF_CONSUMER_KEY'), 21 | 'consumerSecret' => env('SF_CONSUMER_SECRET'), 22 | 'callbackURI' => env('SF_CALLBACK_URI'), 23 | 'loginURL' => env('SF_LOGIN_URL'), 24 | 25 | // Only required for UserPassword authentication: 26 | 'username' => env('SF_USERNAME'), 27 | // Security token might need to be amended to password unless IP Address is whitelisted 28 | 'password' => env('SF_PASSWORD'), 29 | // Only required for OAuthJWT authentication: 30 | 'privateKey' => '', 31 | ], 32 | 33 | /* 34 | * These are optional authentication parameters that can be specified for the WebServer flow. 35 | * https://help.salesforce.com/apex/HTViewHelpDoc?id=remoteaccess_oauth_web_server_flow.htm&language=en_US 36 | */ 37 | 'parameters' => [ 38 | 'display' => '', 39 | 'immediate' => false, 40 | 'state' => '', 41 | 'scope' => '', 42 | 'prompt' => '', 43 | ], 44 | 45 | /* 46 | * Default settings for resource requests. 47 | * Format can be 'json', 'xml' or 'none' 48 | * Compression can be set to 'gzip' or 'deflate' 49 | */ 50 | 'defaults' => [ 51 | 'method' => 'get', 52 | 'format' => 'json', 53 | 'compression' => false, 54 | 'compressionType' => 'gzip', 55 | ], 56 | 57 | 'client' => [ 58 | 'http_errors' => true, 59 | 'verify' => false, 60 | ], 61 | 62 | /* 63 | * Where do you want to store access tokens fetched from Salesforce. The type of storage will persist 64 | * Salesforce token when user refreshes the page. If you choose 'object', the token is stored on the object 65 | * instance and will persist as long as the object remains in memory. 66 | */ 67 | 'storage' => [ 68 | 'type' => 'session', // Options include: 'session', 'cache', 'object', or class instance of Omniphx\Forrest\Interfaces\StorageInterface 69 | 'path' => 'forrest_', // unique storage path to avoid collisions 70 | 'expire_in' => 3600, // number of seconds to expire cache/session 71 | 'store_forever' => false, // never expire cache/session 72 | ], 73 | 74 | /* 75 | * If you'd like to specify an API version manually it can be done here. 76 | * Format looks like '32.0' 77 | */ 78 | 'version' => '', 79 | 80 | /* 81 | * Optional (and not recommended) if you need to override the instance_url returned from Salesforce 82 | * 83 | * This is useful for configuring lightning or lightning sandboxes with OAuthJWT: 84 | * Lightning: https://.my.salesforce.com 85 | * Lightning Sandbox: https://--.sandbox.my.salesforce.com 86 | * Developer Org: https://.develop.my.salesforce.com 87 | */ 88 | 'instanceURL' => '', 89 | 90 | /* 91 | * Language 92 | */ 93 | 'language' => 'en_US', 94 | ]; 95 | -------------------------------------------------------------------------------- /src/lang/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/omniphx/forrest/b0146867caeb221be66df6df22ddace52d12e31b/src/lang/.gitkeep --------------------------------------------------------------------------------