Search code examples
phpmagentocakephpoauthmagento-rest-api

Use CakePHP Http Client with Magento2 rest API search criteria


I'm trying to send a GET request to a local Magento2 rest API to get all the orders after a certain time. I'm following http://devdocs.magento.com/guides/v2.1/howdoi/webapi/search-criteria.html#simple-search-using-a-timestamp. I'm using CakePHP 3.4's Http Client (https://book.cakephp.org/3.0/en/core-libraries/httpclient.html) and have successfully integrated with Magento using Oauth1 and have no problems with simpler GET requests like http://www.magento.dev.com/rest/V1/stockItems/:productSku. It is a problem with passing the search criteria. The response is always a 401 Invalid Signature.

Using Postman, I can get a valid response to http://www.magento.dev.com/rest/V1/orders?searchCriteria[filter_groups][0][filters][0][field]=created_at&searchCriteria[filter_groups][0][filters][0][value]=2016-07-01 00:00:00&searchCriteria[filter_groups][0][filters][0][condition_type]=gt

This is what I have so far/how I'm sending the request:

In Model/Table/OrdersTable.php:

public function importNewOrders(\App\Model\Entity\OauthIntegration $integrationDetails)
    {
       $this->OauthIntegrations = TableRegistry::get('OauthIntegrations');
       $this->Orders = TableRegistry::get('Orders');
       $timeCutOff = '2015-01-01 00:00:00';
       $search = [
           'searchCriteria' => [
                'filterGroups' => [
                    0 => [
                        'filters' => [
                            0 => [
                                'field' => 'created_at',
                                'value' => $timeCutOff,
                                'condition_type' => 'gt'
                            ]
                        ]
                    ]
                ]
           ]
        ];
//           'searchCriteria[filter_groups][0][filters][0][field]' => 'created_at',
//           'searchCriteria[filter_groups][0][filters][0][value]' => $timeCutOff, 
//           'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'gt'

       $action = '/V1/orders';

       $type = "GET";
       $response = $this->OauthIntegrations->sendRequest(
               $integrationDetails, 
               $action, 
               $type,
               '',
               $search);

       Log::write('debug', $response->body());
       return $response;
    }

and in Model\Table\OauthIntegrationsTable.php:

public function sendRequest(\App\Model\Entity\OauthIntegration $integrationDetails, 
            string $action, string $method = "GET", string $data = '', array $search = null)
    {

        $http = new Client([
            'auth' => [
                'type' => 'oauth',
                'consumerKey' => $integrationDetails->oauth_consumer_key,
                'consumerSecret' => $integrationDetails->oauth_consumer_secret,
                'token' => $integrationDetails->oauth_token,
                'tokenSecret' => $integrationDetails->oauth_token_secret
              ]
          ]);
        $url = $integrationDetails->store_base_url . 'rest' . $action;
        if ($method == 'GET'){
            if (!isset($search)){
                $search = [];
            }
            $response = $http->get($url, $search, []);

        } else if ($method == 'POST'){

            $response = $http->post($url, $data, [
              'type' => 'json',

            ]);

        } else if($method == 'PUT'){
            $response = $http->put($url, $data, [
              'type' => 'json',
            ]);
        }
        Log::write('debug', 'url: ' . $url . ' and status code: ' . $response->getStatusCode());
        return $response;
    }

and this is the error (I'm hoping) is the cause of the Invalid Signature response:

2017-03-28 10:07:01 Notice: Notice (8): Array to string conversion in [/var/www/cakephp/html/beacon/vendor/cakephp/cakephp/src/Http/Client/Auth/Oauth.php, line 315]
Trace:
Cake\Error\BaseErrorHandler::handleError() - CORE/src/Error/BaseErrorHandler.php, line 153
Cake\Http\Client\Auth\Oauth::_normalizedParams() - CORE/src/Http/Client/Auth/Oauth.php, line 315
Cake\Http\Client\Auth\Oauth::baseString() - CORE/src/Http/Client/Auth/Oauth.php, line 246
Cake\Http\Client\Auth\Oauth::_hmacSha1() - CORE/src/Http/Client/Auth/Oauth.php, line 143
Cake\Http\Client\Auth\Oauth::authentication() - CORE/src/Http/Client/Auth/Oauth.php, line 61
Cake\Http\Client::_addAuthentication() - CORE/src/Http/Client.php, line 501
Cake\Http\Client::_createRequest() - CORE/src/Http/Client.php, line 448
Cake\Http\Client::_doRequest() - CORE/src/Http/Client.php, line 341
Cake\Http\Client::get() - CORE/src/Http/Client.php, line 211
App\Model\Table\OauthIntegrationsTable::sendRequest() - APP/Model/Table/OauthIntegrationsTable.php, line 134
App\Model\Table\OrdersTable::importNewOrders() - APP/Model/Table/OrdersTable.php, line 672
App\Shell\MagentoShell::main() - APP/Shell/MagentoShell.php, line 36
Cake\Console\Shell::runCommand() - CORE/src/Console/Shell.php, line 472
Cake\Console\ShellDispatcher::_dispatch() - CORE/src/Console/ShellDispatcher.php, line 227
Cake\Console\ShellDispatcher::dispatch() - CORE/src/Console/ShellDispatcher.php, line 182
Cake\Console\ShellDispatcher::run() - CORE/src/Console/ShellDispatcher.php, line 128
[main] - ROOT/bin/cake.php, line 33

Code from Http\Client\Oauth.php where error occurs:

        $pairs = [];
        foreach ($args as $k => $val) {
            if (is_array($val)) {
                sort($val, SORT_STRING);
                Log::write('debug', 'about to go through foreach($val as $nestedVal)');
                foreach ($val as $nestedVal) {
                    Log::write('debug', $nestedVal);
                    $pairs[] = "$k=$nestedVal"; // <<< HERE
                }
            } else {
                $pairs[] = "$k=$val";
            }
        }

debugging from above results in:

2017-03-28 10:07:01 Debug: about to go through foreach($val as $nestedVal)
2017-03-28 10:07:01 Debug: Array
(
    [0] => Array
        (
            [filters] => Array
                (
                    [0] => Array
                        (
                            [field] => created_at
                            [value] => 2015-01-01 00:00:00
                            [condition_type] => gt
                        )

                )

        )

)

In summary, is it possible to pass a multi-dimensional array to the 2nd parameter in a get request using Cake's Http Client?

// Is it possible to replace ['q' => 'widget'] with a multi-dimensional array??
$response = $http->get('http://example.com/search', ['q' => 'widget']);

If not, what would be the best way to use Cake's Http Client to send GET request to: http://www.magento.dev.com/rest/V1/orders?searchCriteria[filter_groups][0][filters][0][field]=created_at&searchCriteria[filter_groups][0][filters][0][value]=2016-07-01 00:00:00&searchCriteria[filter_groups][0][filters][0][condition_type]=gt ?

Thanks in advance!!!


Solution

  • Possible bug

    This may be considered as a possible bug. I don't think the OAuth specs take this PHP style bracket stuff in URLs into account, and therefore sorting/encoding the parameters is limited to flat key=value sets, ie a key would be

    searchCriteria[filter_groups][0][filters][0][field]
    

    and the value would be

    created_at
    

    The CakePHP OAuth adapter however parses the requests query string into a possibly deeply nested array structure, which will then fail, as it doesn't handle that case.

    I'd suggest that you report this as a possible bug. Further problems may occour as encoding seems to be ment to be applied before sorting, where in the CakePHP implementation, additonal parameter encoding is applied after sorting (that may actually be fine though, I'm not sure).

    Try a custom OAuth adapter as a workaround

    Until this is being fixed/enhanced, you could use a custom OAuth adapter that handles things "properly" (whatever that means in this context). Here's a quick and dirty example (works for me with the Magento API).

    Create src/Http/Client/Auth/AppOAuth.php

    <?php
    namespace App\Http\Client\Auth;
    
    use Cake\Http\Client\Auth\Oauth;
    
    class AppOAuth extends Oauth
    {
        protected function _normalizedParams($request, $oauthValues)
        {
            $query = parse_url($request->url(), PHP_URL_QUERY);
            parse_str($query, $queryArgs);
    
            $post = [];
            $body = $request->body();
            if (is_string($body) &&
                $request->getHeaderLine('content-type') === 'application/x-www-form-urlencoded'
            ) {
                parse_str($body, $post);
            }
            if (is_array($body)) {
                $post = $body;
            }
    
            $args = array_merge($queryArgs, $oauthValues, $post);
            $query = http_build_query($args);
    
            $args = [];
            foreach (explode('&', $query) as $value) {
                $pair = explode('=', $value, 2);
                $args[] =
                    rawurlencode(rawurldecode($pair[0])) .
                    '=' .
                    rawurlencode(rawurldecode($pair[1]));
            }
            usort($args, 'strcmp');
    
            return implode('&', $args);
        }
    }
    

    Compare to \Cake\Http\Client\Auth\Oauth::_normalizedParams()

    Use it by specifying the classname in the type option for your client instance:

    'type' => 'AppOAuth',
    

    ps

    shouldn't it be filter_groups instead of filterGroups in your $search array?