Use CakePHP Http Client with Magento2 rest API search criteria - rest

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

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?

Related

Is there a way to fix a date error using Symfony 4?

I'm working on a project as a back-end developer using PHP 7.2.11 and Symfony 4.2.3
My goal is to showcase events (concerts, festivals...) from data I grab from an API on an interactive map.
This is the result I get:
https://imgur.com/HmpBK5B.png
To import the data from the API, I use a for loop going from 1 to 7 (corresponding to days) and adding days to the today's date.
This date will be used as a parameter to grab the events occuring from today to the next 7 days.
Problem is: I get the following error running my custom symfony command
php bin/console import:mapado :
2019-03-26T12:08:34+01:00 [error] Error thrown while running command "import:mapado". Message: "Notice: Undefined index: address"
The error is due to a date not existing and then refering to an inexistant address.
I tried to change the parameters in my first for loop, with days going either to 8 or to 6 but it didn't change the output error.
I change the custom date parameter (from my loop) from the API url to the default parameter and everything is working with default (but it only gets the events for the 3 next days).
Here is the parameter I use:
https://imgur.com/tx1OyrM.png
From: https://api.mapado.net/v2/docs#operation/getActivityCollection
And how an element from the API looks like:
https://imgur.com/l1nTOCC.png
This is the code I've written:
protected function execute(InputInterface $input, OutputInterface $output) {
for ($jour = 0; $jour <= 7; $jour++) {
// problem is here
$futureDay = date('Y-m-d', strtotime('+'.$jour.' days'));
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => "https://api.mapado.net/v2/activities?fields=#id,title,shortDate,nextDate,activityType,locale,description,address&itemsPerPage=1000&when=".$futureDay."&periodOfDay=evening",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => array(
"Authorization: Bearer MTMwZWJiODFiZjA4YTcyOGY2ZmMzMGYwOTQyYWM2NDZjODVlNDg1MzU0MzE3M2I4MTdiMDQyZjU5MDVkZjFjZA",
"Cache-Control: no-cache",
"Conent-Type: application/json",
"Content-Type: application/x-www-form-urlencoded",
"Postman-Token: 55672a19-0ffc-4fe6-a866-3e15c3df9dae"
),
));
$response = curl_exec($curl);
$err = curl_error($curl);
$mapado_events = json_decode($response, JSON_PRETTY_PRINT);
for ($i = 0; $i < count($mapado_events['hydra:member']); $i++) {
if ($mapado_events['hydra:member'][$i]['locale'] == 'fr') {
$mapado_id = $mapado_events['hydra:member'][$i]['#id'];
$mapado_date = \date('Y-m-d', strtotime($mapado_events['hydra:member'][$i]['nextDate']));
$result = $this->getContainer()
->get('doctrine')
->getRepository(MapadoIDs::class)
->findOneBy(['mapado_id' => $mapado_id]);
if ($result == null) {
echo 'event existe pas, ajout en bdd'.PHP_EOL;
$MapadoIDs = new MapadoIDs();
$MapadoIDs->setMapadoId($mapado_id);
$this->em->persist($MapadoIDs);
$mapado = json_decode($response, JSON_PRETTY_PRINT);
$event = new Event();
$event->setLongitude($mapado['hydra:member'][$i]['address']['longitude']);
$event->setLatitude($mapado['hydra:member'][$i]['address']['latitude']);
$event->setTitle($mapado['hydra:member'][$i]['title']);
$event->setDate($mapado_date);
$event->setFormattedAddress($mapado['hydra:member'][$i]['address']['formattedAddress']);
$event->setCity($mapado['hydra:member'][$i]['address']['city']);
$event->setLocale($mapado['hydra:member'][$i]['locale']);
$event->setActivityType($mapado['hydra:member'][$i]['activityType']);
$event->setDescription($mapado['hydra:member'][$i]['description']);
$this->em->persist($event);
}
}
}
}
$this->em->flush();
curl_close($curl);
if ($err) {
echo "cURL Error #: " . $err;
} else {
echo $response;
}
}
}
for better readability:
https://pastebin.com/CTu5gb8t
Expected result is a json output in my console.
Actual result is an error thrown preventing me from inserting results into my database.
Could you tell me if I'm missing something that could result in this error ?
It's a long and detailed post so that you can understand better my problem.
Well I solved my problem and it was not a date problem.
The problem was with the itemsPerPage query parameter requesting too much data and then throwing an error.
I set it up to 400 and everything is working as expected.

Guzzle HTTP send file stream throws error- "json_encode error: Type is not supported"

I am using Guzzle client in my Laravel application to send a request to API endpoint along with a file. I am achieving this by creating a multipart data as follow-
$rid = $this->wsl->curlWSl('POST', '/throttle', [], [
'verify' => false,
'multipart' => [
[
'name' => 'csv',
'contents' => fopen($dest, 'rb')
],
[
'name' => 'name',
'contents' => $request->input('name')
],
[
'name' => 'description',
'contents' => $request->input('description')
],
[
'name' => 'header',
'contents' => '1'
]
]
]);
The curlWSL method I have defined as given below -
public function curlWSl(string $method, string $path, Array $headers = [], Array $data = null, Array $options = [])
{
$endPoint = $this->getUri() . $path;
if (!empty($headers)) {
$options['headers'] = $headers;
}
if ($method == 'GET' && $data) {
$endPoint .= http_build_query($data);
}
if ($method == 'POST') {
$options['json'] = $data;
}
try {
$response = $this->getClient()->request(
$method,
$endPoint,
$options
);
} catch (\Exception $ex) {
return ['statusCode'=>$ex->getCode(), 'errorMsg' => $ex->getMessage()];
}
return json_decode($response->getBody()) ?? (string)$response->getBody();
}
Doing this, throws me an exception -
InvalidArgumentException {#296 ▼
#message: "json_encode error: Type is not supported"
#code: 0
#file: "/var/www/html/vendor/guzzlehttp/guzzle/src/functions.php"
#line: 327
trace: {▶}
}
I am sure, this is because of fopen file stream because when I remove that, my request is received at the endpoint.
I am also looking for some help on how can I validate the request data at the API endpoint using laravel validators.
Your help is much appreciated.
Note: I am much looking to pass the file object and not only the file data, which I am able to do with file_get_contents.
I got it fixed.
The Guzzle don't process too many parameters to send as in request to the endpoint. As we are setting up $options['json'] = $data and this considers the data in json format which is actually not. So, I had to turn it to multipart instead of json to make things working. I had to modify the call as below -
$rid = $this->wsl->curlWSl('POST', '/throttle', [], [
[
'name' => 'csv',
'contents' => fopen($dest, 'rb')
],
[
'name' => 'name',
'contents' => $request->input('name')
],
[
'name' => 'description',
'contents' => $request->input('description')
],
[
'name' => 'header',
'contents' => '1'
]
]);
And in the method curlWSL, I modified the code as to accept the data as multipart-
public function curlWSl(string $method, string $path, Array $headers = [], Array $data = null, Array $options = [])
{
$endPoint = $this->getUri() . $path;
if (!empty($headers)) {
$options['headers'] = $headers;
}
if ($method == 'GET' && $data) {
$endPoint .= http_build_query($data);
}
if ($method == 'POST') {
$options['multipart'] = $data;
}
try {
$response = $this->getClient()->request(
$method,
$endPoint,
$options
);
} catch (\Exception $ex) {
return ['statusCode'=>$ex->getCode(), 'errorMsg' => $ex->getMessage()];
}
return json_decode($response->getBody()) ?? (string)$response->getBody();
}

How to Retrieve HTTP Status Code with Guzzle?

New to Guzzle/Http.
I have a API rest url login that answer with 401 code if not authorized, or 400 if missing values.
I would get the http status code to check if there is some issues, but cannot have only the code (integer or string).
This is my piece of code, I did use instruction here ( http://docs.guzzlephp.org/en/stable/quickstart.html#exceptions )
namespace controllers;
use GuzzleHttp\Psr7;
use GuzzleHttp\Exception\ClientException;
$client = new \GuzzleHttp\Client();
$url = $this->getBaseDomain().'/api/v1/login';
try {
$res = $client->request('POST', $url, [
'form_params' => [
'username' => 'abc',
'password' => '123'
]
]);
} catch (ClientException $e) {
//echo Psr7\str($e->getRequest());
echo Psr7\str($e->getResponse());
}
You can use the getStatusCode function.
$response = $client->request('GET', $url);
$statusCode = $response->getStatusCode();
Note: If your URL redirects to some other URL then you need to set false value for allow_redirects property to be able to detect initial status code for parent URL.
// On client creation
$client = new GuzzleHttp\Client([
'allow_redirects' => false
]);
// Using with request function
$client->request('GET', '/url/with/redirect', ['allow_redirects' => false]);
If you want to check status code in catch block, then you need to use $exception->getCode()
More about responses
More about allow_redirects
you can also use this code :
$client = new \GuzzleHttp\Client(['base_uri' 'http://...', 'http_errors' => false]);
hope help you

Mapquest API key not recognized

I signed up for the Mapquest API service, and I am now testing my application, using PHP.
Under Manage Keys, I created a new key, and Mapquest gave me:
Consumer Key
Consumer Secret
I clicked on Approve All Keys
I looked up the documentation for Geocoding API Post Batch, and it says that I should include the key as one of the params.
I assumed it's the Consumer Key, so I included mine. However, when I make the call, I get the following response:
The AppKey submitted with this request is invalid.
My code:
$results = mapquest_v1_geocoding_batch_get_location(array('123 Main St, Anytown, WA', '123 Main St, Anytown, WA 98052'));
pretty_print($results);
function mapquest_v1_geocoding_batch_get_location($locations)
{
//&location=Denver, CO&location=1555 Blake St, Denver, CO 80202&location=Boulder&key=KEY
$postfields = array (
'inFormat' => 'kvp',
'outFormat' => 'json',
'thumbMaps' => FALSE,
'maxResults' => 1
);
$postfields_string = http_build_query($postfields);
foreach ($locations as $location) {
$postfields_string .= '&'.http_build_query(array('location' => $location));
}
$postfields_string .= '&'.http_build_query(array('key' => PARN_MAPQUEST_TW_TO_FB_KEY));
pretty_echo($postfields_string);
$url = 'https://www.mapquestapi.com/geocoding/v1/batch';
return jhm_curl_post_call($url, $postfields);
}
function jhm_curl_post_call($url, $postfields, $setopts_array = FALSE)
{
$results = array();
if (!$setopts_array) {
$setopts_array = array();
}
if (!isset($setopts_array[CURLOPT_RETURNTRANSFER])) {
$setopts_array[CURLOPT_RETURNTRANSFER] = TRUE;
}
if (!isset($setopts_array[CURLOPT_POST])) {
$setopts_array[CURLOPT_POST] = TRUE;
}
$setopts_array[CURLOPT_URL] = $url;
$setopts_array[CURLOPT_POSTFIELDS] = http_build_query($postfields);
$ch = curl_init();
curl_setopt_array ($ch , $setopts_array);
$results['json_response'] = curl_exec($ch);
$results['response'] = json_decode($results['json_response'], TRUE);
$results['info'] = curl_getinfo($ch);
$results['curl_errno'] = curl_errno($ch);
$results['curl_error'] = curl_error($ch);
curl_close($ch);
return $results;
}
This is the $postfields_string:
inFormat=kvp&outFormat=json&thumbMaps=0&maxResults=1&location=123+Main+St%2C+Anytown%2C+WA&location=123+Main+St%2C+Anytown%2C+WA+98052&key=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
and the results of the call:
Array
(
[json_response] => The AppKey submitted with this request is invalid.
[info] => Array
(
[url] => https://www.mapquestapi.com/geocoding/v1/batch
[content_type] => text/plain
[http_code] => 403
[header_size] => 236
[request_size] => 198
[filetime] => -1
[ssl_verify_result] => 0
[redirect_count] => 0
[total_time] => 0.265
[namelookup_time] => 0.062
[connect_time] => 0.109
[pretransfer_time] => 0.203
[size_upload] => 52
[size_download] => 50
[speed_download] => 188
[speed_upload] => 196
[download_content_length] => 50
[upload_content_length] => 52
[starttransfer_time] => 0.265
[redirect_time] => 0
[redirect_url] =>
[primary_ip] => 207.200.103.5
[certinfo] => Array
(
)
[primary_port] => 443
[local_ip] => 192.168.1.4
[local_port] => 50514
)
[curl_errno] => 0
[curl_error] =>
)
The key needs to be in the url parameters after mapquestapi.com rather than in the post data. Then you should be good to go.

Drupal7 REST: I am not able to perform POST and PUT methods. Error is :Not Acceptable : Node type is required, Code:406?

I'm using drupal7. my drupal_http_request() for get and delete are working fine for authenticated users, but the post and put methods are not working.
The error is :Not Acceptable : Node type is required, and http error code is :406. My code is below:
function ws_form_post_auth() {
$base_url = 'http://localhost/drupalws/api/v1';
$data = array(
'username' => 'student1',
'password' => 'welcome',
);
$data = http_build_query($data, '', '&');
$options = array(
'headers' => array(
'Accept' => 'application/json',
),
'method' => 'POST',
'data' => $data
);
$response = drupal_http_request($base_url . '/user/login', $options);
$data = json_decode($response->data);
// Check if login was successful
if ($response->code == 200) {
$options['headers']['Cookie'] = $data->session_name . '=' . $data->sessid;
$options['headers']['X-CSRF-Token'] = $data->token;
$data = array(
'title' => 'First forum post',
'type'=> 'forum',
'body'=> array(
'und'=>array(
0=> array(
'value'=>'This is my first forum post via httprequest.'
)
)
)
);
$data = json_encode($data);
$options['data'] = $data;
$options['method'] = 'POST';
$response = drupal_http_request($base_url . '/node', $options);
return $response->status_message;
}
return $response->status_message;
}
I got the solution for my issue,I just missed a Content-Type in Headers.
[....]
if ($response->code == 200) {
$options['headers']['Cookie'] = $data->session_name . '=' . $data->sessid;
$options['headers']['X-CSRF-Token'] = $data->token;
$options['headers']['Content-Type'] = 'application/json';
[....]