Amazon Flexible Payments Exception: Caller Input Exception: Invalid Signature - php

I am trying to work with the PHP API for Amazon's Flexible Payments.
Here is my PHP snippet to send a payment request:
<?php
$string_to_sign = 'GET
authorize.payments-sandbox.amazon.com
/cobranded-ui/actions/start
SignatureMethod=HmacSHA256&SignatureVersion=2&callerKey=my_access_key&callerReference=YourCallerReference&paymentReason=donation&pipelineName=SingleUse&returnUrl=http%3A%2F%2Fproblemio.com&transactionAmount=4.0';
$encoded_string_to_sign = URLEncode(Base64_Encode(hash_hmac('sha256', $string_to_sign, 'my_secret_key')));
$amazon_request_sandbox = 'https://authorize.payments-sandbox.amazon.com/cobranded-ui/actions/start?SignatureVersion=2&returnUrl='.$return_url.'&paymentReason='.$payment_reason.'&callerReference=YourCallerReference&callerKey='.$my_access_key_id.'&transactionAmount=4.0&pipelineName=SingleUse&SignatureMethod=HmacSHA256&Signature='.$encoded_string_to_sign;
// When it goes to the url, it gets the invalid signature error
header('Location: '.$amazon_request_sandbox);
?>
This seems to be following their instructions, but I can't get past that error.
Thanks!!

<?php
$method = 'GET';
$host = 'authorize.payments-sandbox.amazon.com';
$path = '/cobranded-ui/actions/start';
$params = array(
'signatureMethod' => 'HmacSHA256',
'signatureVersion' => '2',
'currencyCode' => 'USD',
'callerKey' => 'Your_Key_ID',
'callerReference' => 'YourCallerReference',
'paymentReason' => 'donation',
'pipelineName' => 'SingleUse',
'returnUrl' => 'http://yourcallback.com',
'transactionAmount'=> '5',
'version' => '2009-01-09',
);
$params = array_map('rawurlencode', $params);
$paramStringArray = array();
foreach($params as $key => $value){
$paramStringArray[] = $key . '=' . $value;
}
$paramString = implode('&', $paramStringArray);
$string_to_sign = $method . "\n"
. $host . "\n"
. $path . "\n"
. $paramString;
$signature = base64_encode(hash_hmac(
'sha256',
$string_to_sign,
'Your_Super_Secret_Key',
true
));
$amazon_request_sandbox = "https://{$host}{$path}?" . $paramString .
'&signature=' . rawurlencode($signature);
header('Location: '.$amazon_request_sandbox);
?>
Okay... using the structure from the code below, I've finally figured this whole thing out via the code above. There are three things of note to keep track of while forming your signature/URL...
It seems that the parameter "transactionAmount" is necessary for a valid Co-branded UI Pipeline, even though there's no specific instruction alluding to the issue.
If any of your parameters have/had spaces in them, and you tried to use html_build_query() in all but the latest (5.4) version of PHP, you would be given an encoding scheme that featured "+" marks for spaces instead of "%20" which is what Amazon appears to like. My code above takes care of that by implementing rawurlencode() on the entire parameter array.
The ordering of the parameters is paramount in the construction of the signature. The keys (not the values) need to be in case-insensitive alphabetical order. It's also worth noting that despite what the documentation says for the API, both the ampersands (&) and the equals (=) must be present in the creation of the query string for the signature.
Ex:
Query String for Signature: callerKey=1111111111111&currencyCode=USD&signatureVersion=2
Some Other Things I Noticed...
In the sample code included with the PHP SDK (2010-8-28), the "paymentReason" attribute in the file "CBUISingleUsePipelineSample.php" is listed as "HarryPotter 1-5 DVD set". Since this attribute has spaces in it, it throws that ever-annoying "invalid signature" error when you try to visit the generated link because html_build_query() is used to generate the query string for the URL. To fix this issue, open up "CBUIPipeline.php", and look for the following line in the constructUrl() method...
$queryString = http_build_query($parameters, '', '&');
replace it with:
$queryString = str_replace('+', '%20', http_build_query($parameters, '', '&'));
That'll solve the space-encoding problem for older versions of PHP (< 5.4). With the latest version, there's an "enc_type" flag you can set.
Last things Last...
This is my first post on StackOverflow so don't kill me if I broke protocol. Hope it helps!

Try this piece of code:
<?php
$method = 'GET';
$host = 'authorize.payments-sandbox.amazon.com';
$path = '/cobranded-ui/actions/start';
$params = array(
'SignatureMethod' => 'HmacSHA256'
'SignatureVersion' => 2,
'callerKey' => 'my_access_key',
'callerReference' => 'YourCallerReference',
'paymentReason' => 'donation',
'pipelineName' => 'SingleUse',
'returnUrl' => 'http://problemio.com&transactionAmount=4.0',
);
$string_to_sign = $method . "\n"
. $host . "\n"
. $path . "\n"
. http_build_query($params);
$signature = base64_encode(hash_hmac(
'sha256',
$string_to_sign,
'my_secret_key'
));
$params['Signature'] = $signature;
$amazon_request_sandbox = "https://{$host}{$path}?" . http_build_query($params);
header('Location: ' . $amazon_request_sandbox);
So I made a few changes:
PHP's http_build_query() to build the query string (ensure correct encoding)
trying to re-use your vars vs. duplicating the efforts (makes it easier to spot mistakes, etc.)
explicit \n - maybe your editor entered \r or \r\n
HTH

Related

OAuth: invalid oauth_signature

I am stuck generating the OAuth signature programmatically.
I am on FreeBSD with PHP71 and can not use the PECL extension, because it doesn't work with PHP > 5.6 yet.
I read the RFC and many many SO threads as well as blogs that all have the same problem I have.
But I can't figure out where I failed.
I am trying to obtain a request token from the XING.com Api.
My Code:
$strConsumerKey = '12345';
$strConsumerSecret = '1234567890';
$arrQueryParams = [
'oauth_callback' => 'http://xing.dev/endpoint?hauth.done=XING',
'oauth_consumer_key' => $strConsumerKey,
'oauth_nonce' => md5(microtime() . mt_rand()),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_timestamp' => time(),
'oauth_version' => '1.0'
];
uksort($arrQueryParams, 'strcmp');
$strNormalizedParams = [];
foreach($arrQueryParams AS $k => $v)
$strNormalizedParams[] = $k . '=' . $v;
$strRequestTokenUrl = 'https://api.xing.com/v1/request_token';
$strBaseString = 'GET&' . rawurlencode($strRequestTokenUrl) . '&' . rawurlencode(implode('&', $strNormalizedParams));
$strKey = rawurlencode($strConsumerSecret) . '&';# . rawurlencode($strConsumerKey);
$strOAuthSignature = base64_encode(hash_hmac('sha1', $strBaseString, $strKey, true));
$arrQueryParams['oauth_signature'] = $strOAuthSignature;
uksort($arrQueryParams, 'strcmp');
$params = [];
foreach($arrQueryParams AS $k => $v)
$params[] = $k . '=' . rawurlencode($v);
$strFinalRequest = $strRequestTokenUrl . '?' . implode('&', $params);
$ch = curl_init($strFinalRequest);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$mixResponse = curl_exec($ch);
var_dump($mixResponse);
curl_close($ch);
exit;
I tried it as GET/POST request (Api Docs say both is possible)
I tried all combinations in creating the $strKey, but I think the current is the correct one
I checked my system time, I tried different timezones
$strBaseString example:
GET&https%3A%2F%2Fapi.xing.com%2Fv1%2Frequest_token&oauth_callback%3Dhttp%3A%2F%2Fxing.dev%2Fendpoint%3Fhauth.done%3DXING%26oauth_consumer_key%3D12345%26oauth_nonce%3Daadab05f87028514358e5995dc6728fd%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1503318691%26oauth_version%3D1.0
Final Request example:
https://api.xing.com/v1/request_token?oauth_callback=http%3A%2F%2Fxing.dev%2Fendpoint%3Fhauth.done%3DXING&oauth_consumer_key=12345&oauth_nonce=71f83f72b929a87e5fdd6bf1a7fb2511&oauth_signature=56qc3%252FCrMptnUoy6TgsJtsZclfY%253D&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1503318773&oauth_version=1.0
The response is always:
{"message":"Invalid OAuth signature","error_name":"INVALID_OAUTH_SIGNATURE"}
The real funny think is though, that the https://github.com/xing/xing-api-samples/tree/master/php XING client works on my machine. I debugged the code and looked how they are generating the signature, dumped out the generated base string, the final string, params, etc. and everything looks like what I do. (Except for the values of oauth_timestamp and oauth_nonce of course)
Also I can reproduce the results on the oauth example: https://oauth.net/core/1.0/#sig_base_example
This makes it even more strange.
Has anyone an idea what I am doing wrong?
Thank you and best regards
Found the mistake:
$strBaseString = 'GET&' . rawurlencode($strRequestTokenUrl) . '&' . rawurlencode(implode('&', $strNormalizedParams));
should be
$strBaseString = 'GET&' . rawurlencode($strRequestTokenUrl) . '&' . rawurlencode(http_build_query($arrQueryParams));

How to assign a variable to a hash_hmac sha256 string or data in php?

I am using mastercard payment gateway .
All works fine if I hardcode the data or string of the hash_hmac sha256.
the working version:
$vpcURL = 'https://migs.mastercard.com.au/vpcpay?';
$secret = strtoupper("MYSECRET CODE");
$data ="vpc_AccessCode=0E5AC9E6&vpc_Amount=1000&vpc_Command=pay&vpc_Locale=en&vpc_MerchTxnRef=TEST_TRN&vpc_Merchant=TESTSITE&vpc_OrderInfo=123&vpc_ReturnURL=https://www.examplesite.com/payment-confirmation/&vpc_Version=1";
$sha256_hmac = strtoupper(hash_hmac('sha256', $data, pack('H*', $secret)));
header("Location: " . $vpcURL . "&" . $data . "&vpc_SecureHash=" . $sha256_hmac."&vpc_SecureHashType=SHA256");
but I can not pass the hardcoded value to the vpc_Amount
I am getting the amount from a form where user can input the amount they wish.
So I am getting the amount from:
$totalAmount = $_POST['totalAmount'];
Now I want to pass this $totalAmount to the $data.
So I change the $data to this:
$data ="vpc_AccessCode=0E5AC9E6&vpc_Amount=$totalAmount&vpc_Command=pay&vpc_Locale=en&vpc_MerchTxnRef=TEST_TRN&vpc_Merchant=TESTSITE&vpc_OrderInfo=123&vpc_ReturnURL=https://www.examplesite.com/payment-confirmation/&vpc_Version=1";
When I use this the payment gateway directly goes to the confirmation page : https://www.examplesite.com/payment-confirmation/ and the all the values are empty.
I think it is a simple syntex error..
How can I fix this?
how to pass the $totalAmount to $data correctly?
print_r ($data); gives this:
vpc_AccessCode=0E5AC9E6&vpc_Amount=58,258.00&vpc_Command=pay&vpc_Locale=en&vpc_MerchTxnRef=TEST_TRN&vpc_Merchant=TESTSITE&vpc_OrderInfo=123&vpc_ReturnURL=https://www.examplesite.com/payment-confirmation/?vpc_Version=1
UPDATE
if I update the code to
$real_integer_amount = filter_var($totalAmount, FILTER_SANITIZE_NUMBER_INT);
$data ="vpc_AccessCode=0E5BC9E7&vpc_Amount={$real_integer_amount}&vpc_Command=pay&vpc_Locale=en&vpc_MerchTxnRef=TEST_TRN&vpc_Merchant=TESTSITE&vpc_OrderInfo=123&vpc_ReturnURL=https://www.examplesite.com/payment-confirmation/?vpc_Version=1";
in confirmation page it shows the real amount and others are empty but still not going to the payment gateway where user can input their card details
I can't imagine the receiving server wants commas in the value. Also, you should build a query string like this to avoid problems with unescaped values:
<?php
$vpcURL = 'https://migs.mastercard.com.au/vpcpay?';
$secret = strtoupper("MYSECRET CODE");
$totalAmount = str_replace(",", "", $_POST["totalAmount"]);
$data = [
"vpc_AccessCode" => "0E5AC9E6",
"vpc_Amount" => $totalAmount,
"vpc_Command" => "pay",
"vpc_Locale" => "en",
"vpc_MerchTxnRef" => "TEST_TRN",
"vpc_Merchant" => "TESTSITE",
"vpc_OrderInfo" => "123",
"vpc_ReturnURL" => "https://www.examplesite.com/payment-confirmation/",
"vpc_Version" => "1",
];
$data = http_build_query($data);
$sha256_hmac = strtoupper(hash_hmac('sha256', $data, pack('H*', $secret)));
header("Location: " . $vpcURL . "&" . $data . "&vpc_SecureHash=" . $sha256_hmac."&vpc_SecureHashType=SHA256");
what I posted in first works very fine..
if I change vpc_amount to any (hardcoded) values that works..
the problem was when I assign the $totalAmount to vpc_amount the variable ($totalAmount) holds decimal points and thousand separators..
that makes this issue..
I just wanted to sanitize the variable before passing this totalAmount to the datato make it works..
so I've updated it as:
$real_integer_amount = filter_var($totalAmount, FILTER_SANITIZE_NUMBER_INT);
and now this works fine..
so the final working code is :
$vpcURL = 'https://migs.mastercard.com.au/vpcpay?';
$secret = strtoupper("My Secret Code");
$real_integer_amount = filter_var($totalAmount, FILTER_SANITIZE_NUMBER_INT);
$data ="vpc_AccessCode=0E5AC9E6&vpc_Amount=$real_integer_amount&vpc_Command=pay&vpc_Locale=en&vpc_MerchTxnRef=TEST_TRN&vpc_Merchant=TESTSITE&vpc_OrderInfo=123&vpc_ReturnURL=https://www.trinitycollege.lk/payment-confirmation/&vpc_Version=1";
$sha256_hmac = strtoupper(hash_hmac('sha256', $data, pack('H*', $secret)));
header("Location: " . $vpcURL . "&" . $data . "&vpc_SecureHash=" . $sha256_hmac."&vpc_SecureHashType=SHA256");
#MagnusEriksson : thanks for the time and suggestions.. URL encoding did nothing with this issue.
#pvg there was no any spelling mistakes.. if I just use {$totalAmount} in the $data it did not work.. but this needed this : FILTER_SANITIZE_NUMBER_INT
#miken32 thank you for your answer. I've tried to use your code just by replacing the params with my actual detail.. but it gave me this error "HTTP Status - 400 E5000: Cannot form a matching secure hash based on the merchant's request using either of the two merchant's secrets"
I've double checked the spelling and values/params
This may help someone else in future..

Signing URLs with JWT for Google Cloud Storage using PHP

I've just started to upgrade my Google Cloud Storage code from API version 1.0 to version 2.0 and I'm having some troubles.
With version 1.0 I used Signed URLs with great success, using .p12 files. However that's deprecated in the new version and I have to use Firebase/php-jwt instead, using JSON files.
The problem is that it's just not working, I get the error:
<?xml version='1.0' encoding='UTF-8'?><Error><Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Message>
<StringToSign>PUT
image/png
1483626991
/myBucket/folder/test.PNG</StringToSign></Error>
This is the simplified code used to sign it.
$string = ($method . "\n" .
$contentMd5 . "\n" .
$contentType . "\n" .
$expiration . "\n" .
$file);
$signedURL = base64_encode(Firebase\JWT\JWT::encode($string,
file_get_contents($credentialsFilePath)));
After the signedURL is received I build an URL with the correct data. The only part I've changed from 1.0 and 2.0 is the part where you sign the URL. Furthermore I've checked that the string in "StringToSign"-field of the response is exactly the same as the one I'm signing.
In version 1.0 I signed the URL like this:
$signedURL = base64_encode((new Google_Signer_P12(
file_get_contents($p12FilePath),
'notasecret'
))->sign($string));
All of this leads me to believe that I'm singing the correct contents but using the JWT function the wrong way. Has anyone else done this? How did yo do it?
In case it's interesting this is the URL I build (works with 1.0):
$returnArr['url'] = "https://{$bucket}.commondatastorage.googleapis.com/"
. $prefix . '/' . rawurlencode($file)
. "?GoogleAccessId=" . rawurlencode($serviceEmail)
. "&Expires={$expiration}"
. "&Signature=" . rawurlencode($signature);
Looking at the source for that JWT library the first thing that jumps out at me, and I see was noted in comments, is that your payload should be an array or object, not a string... "JSON web tokens".
* #param object|array $payload PHP object or array
public static function encode($payload, $key, $alg = 'HS256', $keyId = null, $head = null)
Second, it looks like you are double base64 encoding it... base128? :)
The return value of encode should be the three Base64url strings concatenated together, so you shouldn't need to do it again.
I'd give this a try:
$payload = ['HTTP_Verb' => $method,
'Content_MD5' => $contentMd5,
'Content_Type' => $contentType,
'Expiration' => $expiration,
'Canonicalized_Resource' => $file];
$key = file_get_contents($credentialsFilePath);
$signedURL = Firebase\JWT\JWT::encode($payload, $key); //think base64_encode here is redundant.
Ref: Overview of Signed URLs page. They sure don't explain things very well in those docs.
I assume you've looked at SDK?
If you wanted to go the string route you would need to sign using RSA signatures with SHA256... opensssl_sign or also maybe easier to lean on Google's PHP SDKs?
Later...
OK, decided to test it. Saw Google Cloud had a free trial. Installed gsutil, read a bunch of docs. Damned if I understand this JWT approach though. Share if anyone can even provide the docs on that topic.
This code works:
<?php
$method = 'GET';
$expires = '1503532674';
$container = '/example-bucket/cat.jpeg';
$payload = "{$method}\n\n\n{$expires}\n{$container}";
//assume you have this 'json' formatted key too? Otherwise just load the private key file as is.
$key = file_get_contents('~/oas_private_key.json');
$key = json_decode($key, true);
$key = $key['private_key'];
//if sucessful the encypted string is assigned to $signature
openssl_sign($payload, $signature, $key, OPENSSL_ALGO_SHA256);
$signature = urlencode(base64_encode($signature));
die("https://storage.googleapis.com/{$container}?GoogleAccessId=oastest#foo.iam.gserviceaccount.com&Expires={$expires}&Signature={$signature}");
Finally no "SignatureDoesNotMatch" error! Personally I'd use the SDK. Little bit of init and you can just do something like the following:
$url = $object->signedUrl(new Timestamp(new DateTime('tomorrow')), [
'method' => 'PUT'
]);
It would also make upgrades easier in the future.

Twitter API 1.1 returns authentification error when the search contains the "#" character

If I pass test to the q parameter of the tweets.json endpoint, it returns data fine. But if I include the # symbol at the front, i.e. #test, I get the following error:
Could not authenticate you.
The same issue happens when I use %40 instead of #.
Here is my code:
$query = array( // query parameters
'q' => '#test',
'count' => '100'
);
$method = "GET";
$path = "/1.1/search/tweets.json";
$token = 'xxxxxx';
$token_secret = 'xxxxxx';
$consumer_key = 'xxxxxxx';
$consumer_secret = 'xxxxxx';
$host = 'api.twitter.com';
$oauth = array(
'oauth_consumer_key' => $consumer_key,
'oauth_token' => $token,
'oauth_nonce' => (string)mt_rand(), // a stronger nonce is recommended
'oauth_timestamp' => time(),
'oauth_signature_method' => 'HMAC-SHA1',
'oauth_version' => '1.0'
);
$oauth = array_map("rawurlencode", $oauth); // must be encoded before sorting
$query = array_map("rawurlencode", $query);
$arr = array_merge($oauth, $query); // combine the values THEN sort
asort($arr); // secondary sort (value)
ksort($arr); // primary sort (key)
// http_build_query automatically encodes, but our parameters
// are already encoded, and must be by this point, so we undo
// the encoding step
$querystring = urldecode(http_build_query($arr, '', '&'));
$url = "https://$host$path";
// mash everything together for the text to hash
$base_string = $method."&".rawurlencode($url)."&".rawurlencode($querystring);
// same with the key
$key = rawurlencode($consumer_secret)."&".rawurlencode($token_secret);
// generate the hash
$signature = rawurlencode(base64_encode(hash_hmac('sha1', $base_string, $key, true)));
// this time we're using a normal GET query, and we're only encoding the query params
// (without the oauth params)
$url .= "?".http_build_query($query);
$url=str_replace("&","&",$url); //Patch by #Frewuill
$oauth['oauth_signature'] = $signature; // don't want to abandon all that work!
ksort($oauth); // probably not necessary, but twitter's demo does it
// also not necessary, but twitter's demo does this too
function add_quotes($str) { return '"'.$str.'"'; }
$oauth = array_map("add_quotes", $oauth);
// this is the full value of the Authorization line
$auth = "OAuth " . urldecode(http_build_query($oauth, '', ', '));
// if you're doing post, you need to skip the GET building above
// and instead supply query parameters to CURLOPT_POSTFIELDS
$options = array( CURLOPT_HTTPHEADER => array("Authorization: $auth"),
//CURLOPT_POSTFIELDS => $postfields,
CURLOPT_HEADER => false,
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false);
// do our business
$feed = curl_init();
curl_setopt_array($feed, $options);
$json = curl_exec($feed);
curl_close($feed);
return $json;
Why can't I retrieve data with the # symbol at the front of the q parameter?
As I suspected, this is a double (even triple) encoding issue. I managed to get it working in 2 separate ways (they must not be used together):
Merging before rawurlencode(), my preferred solution:
//NOPE $oauth = array_map("rawurlencode", $oauth); // must be encoded before sorting
//NOPE $query = array_map("rawurlencode", $query);
$arr = array_merge($oauth, $query); // combine the values THEN sort
$arr = array_map("rawurlencode", $arr);
asort($arr); // secondary sort (value)
ksort($arr); // primary sort (key)
Removing urldecode() after the http_build_query():
$querystring = (http_build_query($arr, '', '&'));
But at this point, I could not explain why either of them worked. It turns out that later in the code, you use this:
// this time we're using a normal GET query, and we're only encoding the query params
// (without the oauth params)
$url .= "?".http_build_query($query);
The subtle difference is that this time you did not use urldecode() after http_build_query().
Because of all the encoding that goes on, the url you end up using for the signature does not match the url you use for the request, therefore failing the authentification.
Signature
The oauth_signature parameter contains a value which is generated by running all of the other request parameters and two secret values through a signing algorithm. The purpose of the signature is so that Twitter can verify that the request has not been modified in transit, verify the application sending the request, and verify that the application has authorization to interact with the user’s account.
From Authorizing a request - Twitter Developers.
This works, the url is encoded 2 times and the signature url 3 times:
// end of $url, encoded twice
?q=%2540test&count=100
// end of $base_string, used in signature, encoded thrice
%26oauth_version%3D1.0%26q%3D%252540test
This doesn't, the url is encoded 1 time and the signature url 3 times:
// end of $url
?q=%40test&count=100
// end of $base_string, used in signature
%26oauth_version%3D1.0%26q%3D%252540test
Because the signature must only be encoded one more time than the request url, both previously mentioned solutions work (independently) because:
Solution 1 doesn't encode $query, making $url single encoded and the signature double encoded.
Solution 2 keeps the double encoding, leaving $url double encoded and making the signature triple encoded.
But wait, why does it only fail when using # ?
Because that's the only character in all the parameters that actually needs encoding. It is encoded when using http_build_query(), generating a % character that will get caught up in subsequent encodings.

Mixing multiple values for the same key and file uploads using cURL and PHP

I’ve run into a limitation in the cURL bindings for PHP. It appears there is no easy way to send the same multiple values for the same key for postfields. Most of the workarounds I have come across for this have involved creating the URL encoded post fields by hand tag=foo&tag=bar&tag=baz) instead of using the associative array version of CURLOPT_POSTFIELDS.
It seems like a pretty common thing to need to support so I feel like I must have missed something. Is this really the only way to handle multiple values for the same key?
While this workaround might be considered workable (if not really annoying), my main problem is that I need to be able to do multiple values for the same key and also support file upload. As far as I can tell, file upload more or less requires to use the associate arravy version of CURLOPT_POSTFIELDS. So I feel like I am stuck.
I have posted about this problem in more detail on the cURL PHP mailing list in the hopes that someone there has some ideas about this.
Suggestions or hints on where I can look for more information on this are greatly appreciated!
I ended up writing my own function to build a custom CURLOPT_POSTFIELDS string with multipart/form-data. What a pain.
function curl_setopt_custom_postfields($ch, $postfields, $headers = null) {
// $postfields is an assoc array.
// Creates a boundary.
// Reads each postfields, detects which are #files, and which values are arrays
// and dumps them into a new array (not an assoc array) so each key can exist
// multiple times.
// Sets content-length, content-type and sets CURLOPT_POSTFIELDS with the
// generated body.
}
I was able to use this method like this:
curl_setopt_custom_postfields($ch, array(
'file' => '#/path/to/file',
'tag' => array('a', 'b', 'c'),
));
I am not certain of CURLOPT_HTTPHEADER stacks, so since this method calls it, I made certain that the function would allow for the user to specify additonal headers if needed.
I have the full code available in this blog post.
If you use tag[] rather than tag for the name, PHP will generate an array for you, in other words, rather than
tag=foo&tag=bar&tag=baz
You need
tag[]=foo&tag[]=bar&tag[]=baz
Note that when urlencoded for transmission this should become
tag%5B%5D=foo&tag%5B%5D=bar&tag%5B%5D=baz
Vote for PHP Bug #51634.
Try #BeauSimensen's answer.
Guzzle can do this. See an example below.
$client = new \GuzzleHttp\Client();
$client->request('POST', $url, [
'multipart' => [
[ 'name' => 'foo', 'contents' => 'bar' ],
[ 'name' => 'foo', 'contents' => 'baz' ],
]
]);
I ran into the same issue. But I was able to solve it this way.
for($cnt = 0; $cnt < count($siteRows); $cnt++)
{
$curlParams['site_ids['.$cnt.']'] = $siteRows[$cnt]->site_id;
}
Works for files too:
for($cnt = 0; $cnt < count($imageRows); $cnt++)
{
$curlParams['product_images['.$cnt.']'] = '#'.$imageRows[$cnt]->full_path;
}
I got it working using:
curl_setopt($ch, CURLOPT_POSTFIELDS,array('tag[0]'=>'val0','tag[1]'=>'val1'));
then $_POST results in: $_POST['tag'][0] = 'val0' and $_POST['tag'][1] = 'val1'
I think the established standard for multiple values in one key (or the same key) is to have it concatenated with a delimiter, such as for multiple selections of option lists in form elements. I believe this delimiter is the tab character (\t) or the pipe symbol (|).
If the keyname is terminated with [] (like tag[]), PHP will automatically convert the values into an array for your convenience.
lImbus and paul, thank you for your input.
If I had control over the form I am posting to, I could probably find an alternate solution to this problem. However, I do not have any control over the form. And I am almost positive that the software reading the post is not PHP and does not obey the tag[] standards.
Even if it did, cURL does not seem to obey the tag[] syntax either. Basically, I tried the following and neither worked...
curl_setopt($ch, CURLOPT_POSTFIELDS, array('file' => '#/pathtofile', 'tag[]' => array('a', 'b', 'c'));
curl_setopt($ch, CURLOPT_POSTFIELDS, array('file' => '#/pathtofile', 'tag' => array('a', 'b', 'c'));
And again, I don't think that passing tag[] would work anyway as the form I am posting to is actually looking for 'tag' and not 'tag[]'.
I am really starting to get the feeling that the cURL PHP bindings really have no support for this. Which seems so surprising to me. It seems like it can do quite literally anything else, yet it is unable to do something simple like this?
DON'T USE GUZZLE:
# at your command line start php interactive
user#group:~:php -a
php > $arr=array('var' => array(1,2,3,4));
php > echo http_build_query($arr);
var%5B0%5D=1&var%5B1%5D=2&var%5B2%5D=3&var%5B3%5D=4
php > echo urldecode(http_build_query($arr));
var[0]=1&var[1]=2&var[2]=3&var[3]=4
So, you need http_build_query where you pass a hash array of key-values; your (array) variable is entered as a key with value a array instead a scalar value like 'var' => array(1,2,3,4). Now, http_build_query can format the post fields of curl command:
$fields = array('key1' => 'value1', 'var' => array(1,2,3,4));
$curlPost = \http_build_query($fields);
curl_setopt($ch, CURLOPT_POSTFIELDS, $curlPost);
that's 3 lines of code! how many 1000s of code lines are in Guzzle? (*)
So far, I used curl to:
manage Google OAuth protocol with success
connect with APIs like mailgun
handle paypal smart buttons
that's a replacement of million of lines with some 100s!
(*): the result of http_build_query can be formatted further according your needs.
I ran into the same problem in which I had to send a parameter which has to be an array from a PHP server to another server that does not use '[]' for mixing values with the same key along with a file.
In Laravel 8 I could achieve this goal with Http client (of course Http client uses guzzle).
Here is a sample of my code.
Illuminate\Support\Facades\Http::attach('file', $fileContents, 'file-name')
->post('https://destination' , [['name' => 'tag', 'content' => 'foo'], ['name' => 'tag', 'content' => 'bar']])
I found this answer online and want to post it here before it disappears:
http://yeehuichan.wordpress.com/2011/08/07/sending-multiple-values-with-the-same-namekey-in-curl-post/
function curl_setopt_custom_postfields($ch, $postfields, $headers = null) {
$algos = hash_algos();
$hashAlgo = null;
foreach ( array('sha1', 'md5') as $preferred ) {
if ( in_array($preferred, $algos) ) {
$hashAlgo = $preferred;
break;
}
}
if ( $hashAlgo === null ) { list($hashAlgo) = $algos; }
$boundary =
'----------------------------' .
substr(hash($hashAlgo, 'cURL-php-multiple-value-same-key-support' . microtime()), 0, 12);
$body = array();
$crlf = "\r\n";
$fields = array();
foreach ( $postfields as $key => $value ) {
if ( is_array($value) ) {
foreach ( $value as $v ) {
$fields[] = array($key, $v);
}
} else {
$fields[] = array($key, $value);
}
}
foreach ( $fields as $field ) {
list($key, $value) = $field;
if ( strpos($value, '#') === 0 ) {
preg_match('/^#(.*?)$/', $value, $matches);
list($dummy, $filename) = $matches;
$body[] = '--' . $boundary;
$body[] = 'Content-Disposition: form-data; name="' . $key . '"; filename="' . basename($filename) . '"';
$body[] = 'Content-Type: application/octet-stream';
$body[] = '';
$body[] = file_get_contents($filename);
} else {
$body[] = '--' . $boundary;
$body[] = 'Content-Disposition: form-data; name="' . $key . '"';
$body[] = '';
$body[] = $value;
}
}
$body[] = '--' . $boundary . '--';
$body[] = '';
$contentType = 'multipart/form-data; boundary=' . $boundary;
$content = join($crlf, $body);
$contentLength = strlen($content);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Length: ' . $contentLength,
'Expect: 100-continue',
'Content-Type: ' . $contentType,
));
curl_setopt($ch, CURLOPT_POSTFIELDS, $content);
}
And to use it:
curl_setopt_custom_postfields($ch, array(
'file' => '#a.csv',
'name' => array('James', 'Peter', 'Richard'),
));

Categories