2

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!!!

ndm
  • 59,784
  • 9
  • 71
  • 110
BSounder
  • 134
  • 9

1 Answers1

1

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?

Community
  • 1
  • 1
ndm
  • 59,784
  • 9
  • 71
  • 110
  • Bug submitted: https://github.com/cakephp/cakephp/issues/10458. Creating custom Oauth adapter like above solves the array to string notice but still has 401 invalid signature. The url is being passed with the square brackets being encoded as %5D and %5B. I tried using urldecode on the keys that include brackets from within AppOAuth.php but they are being encoded again before request is sent as debugging $url in cakephp/src//Http/Client.php protected function _doRequest($method, $url, $data, $options) has the brackets encoded. Could that encoding cause the 401? I appreciate the help! – BSounder Mar 29 '17 at 22:17
  • @BSounder AFAICT that should be fine, it's just percent encoded, totally valid for a URL. I can have another look at your specific conditions, I tried it with different ones. – ndm Mar 29 '17 at 22:50
  • That's what I thought too, especially since everything I've seen points to square brackets in url's as going against URI standards (http://stackoverflow.com/questions/40568/are-square-brackets-permitted-in-urls / http://www.ietf.org/rfc/rfc3986.txt) so therefore they should be encoded – BSounder Mar 29 '17 at 23:05
  • @BSounder Looks like the space in the date value is the problem in this case, it should end up as `%252B` when generating the signature, however it makes it there as `%2B` only, which seems to stem from the fact the when reading the URL from the request object in `_normalizedParams()`, it is present as `+`, and not already as `%2B`. Not sure who's correct here, I haven't delved too deeply into the OAuth RFC, but I could imagine that Magento is correct in requiring this. – ndm Mar 29 '17 at 23:08
  • @BSounder Updated the code example... works now, but things really get out of hand here, I have no idea if it works by chance, or if it really decodes/encodes things in the right place in the right way - I hate reading RFCs, and it's already 2 in the morning :) – ndm Mar 29 '17 at 23:52
  • @BSounder I couldn't stop thinking about it, so I came up with something new... I think this follows the RFC now, but I'm not 100% sure, after all it's already 4 in the morning now :) so, good night everyone, I'm out. – ndm Mar 30 '17 at 01:49
  • Haha @ndm you're the greatest!! Copied in your updated code and it worked immediately. Can't thank you enough! Get some sleep ;) – BSounder Mar 30 '17 at 15:23