I'm trying to import a TMX file via the Microsoft Custom Translator API in PHP. Unfortunately, I keep running into the following error:
"DocumentDetails must follow type IEnumerable[ImportDocumentRequestDetails]."
I've managed to make other (though only GET) requests to the API successfully, so it's specifically this request I'm having trouble figuring out.
So far, I've tried various permutations of the request, mostly by trial and error. I've tried replicating the request by uploading the same file in the portal, which succeeds without problems, but I've not been able to replicate this in PHP (7.3).
I've also tried to reverse-engineer the C# API samples on GitHub. Unfortunately, my C# knowledge isn't that sharp and I'm sure there are nuances I'm missing. I have noticed the sample uses a 'Language' string, whereas the portal seems to use a 'LanguageCode', as well as other inconsistencies which haven't made solving this much easier.
A stripped-down version of my code, with only the relevant parts (one can assume a valid access token and local filepath to a valid .tmx) is the following:
Class CustomTranslator {
private $curl;
private $aAccessToken; // valid, working token
// Set up connection and login with initial user
public function __construct() {
$this->curl = curl_init();
$aOptions = array(
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 60
);
curl_setopt_array($this->curl, $aOptions);
}
function ImportParallelDocument($strFilePath) {
$aRequestContent = [
'Files' => new CURLFile($strFilePath, mime_content_type($strFilePath), basename($strFilePath)),
'DocumentDetails' => [
'DocumentName' => basename($strFilePath),
'DocumentType' => 'training',
'IsParallel' => true,
'FileDetails' => [
'Name' => $strFilePath,
'Language' => 'Dutch',
'Type' => pathinfo($strFilePath, PATHINFO_EXTENSION),
'OverwriteIfExists' => true
]
]
];
return $this->Request("v1.0/documents/import?workspaceId=".CUSTOMTRANSLATOR_WORKSPACEID, $aRequestContent, 'POST');
}
// Prototype request function
private function Request($strRequest, $aData = array(), $strMethod = 'GET') {
$strRequest = CUSTOMTRANSLATOR_API_URL.$strRequest;
// Reset between requests
curl_setopt($this->curl, CURLOPT_POST, false);
curl_setopt($this->curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer '.$this->aAccessToken['access_token']]);
if(isset($aData['authorization'])) $aData['authorization'] = $this->aAccessToken['access_token'];
if ($strMethod == 'GET') {
$strRequest .= "?".http_build_query($aData);
}
else {
curl_setopt($this->curl, CURLOPT_POST, true);
curl_setopt($this->curl, CURLOPT_HTTPHEADER, ['Authorization: Bearer '.$this->aAccessToken['access_token'],
'X-HTTP-Method-Override: '.$strMethod]);
curl_setopt($this->curl, CURLOPT_POSTFIELDS, $aData);
}
curl_setopt($this->curl, CURLOPT_URL, $strRequest);
$strResponse = curl_exec($this->curl);
// Return the JSON array if it can be decoded, otherwise the actual curl response
return json_decode($strResponse, true)?:$strResponse;
}
}
As stated, when I try to upload a file using the above code, the exact error I'm receiving is {"message":"DocumentDetails must follow type IEnumerable[ImportDocumentRequestDetails].","display":false}, unfortunately without further specification of what's missing or incorrect. I'm hoping to achieve a successful file import of a TMX file which does successfully import via the portal itself, which I understand implements the same API.
I expect I'm simply missing something, or doing something not quite right, so any help would be appreciated!
With help from a colleague, figured out a will-do-for-now workaround, by simply providing the equivalent JSON of the call as it's used in the portal:
$aRequestContent = [
'Files' => new CURLFile($strFilePath, mime_content_type($strFilePath), basename($strFilePath)),
'DocumentDetails' =>
'[{ "DocumentName": "",
"DocumentType": "training",
"FileDetails": [{
"Name": "'.basename($strFilePath).'",
"LanguageCode": "en",
"OverwriteIfExists": true }]
}]'
];
It's not the best solution, but for the purposes of a preview API, it'll work for now (until it inevitably ends up in production, sigh).
Examining the JSON a little closer led me to discover the nested array structure (presumably for multi-file uploads), which wasn't inherently obvious to me at first. However, the following array structure is sufficient for requests to be processed:
$aDocumentDetails = [[ // Note the nested array here, for index numbering
'DocumentName' => '',
'DocumentType' => ucfirst($strType),
'FileDetails' => [[ // As well as here
'Name' => basename($strFilePath),
'LanguageCode' => $strLanguageCode,
'OverwriteIfExists' => $bOverwrite
]]
]];
$aRequestContent = [
'Files' => new CURLFile($strFilePath, mime_content_type($strFilePath), basename($strFilePath)),
'DocumentDetails' => json_encode($aDocumentDetails)
];
In short, the API expects DocumentDetails (and FileDetails) in indexed sub-arrays:
[0 => [ 'DocumentName' => ...,
'FileDetails' => [0 => ['Name' => ...]
];
Understanding this tidbit helped me tremendously.
Related
I'm using Laravel 9 with a PHP Linnworks API repository (https://github.com/booni3/linnworks) which uses Guzzle for the API requests like so: linnworks->api->Orders()->post($url, $parameters );
I'm trying to re-create the following CURL POST request (which works fine) with the above repository's Guzzle implementation:
curl_setopt_array($curl, array(
CURLOPT_URL => "https://eu-ext.linnworks.net//api/Orders/SetExtendedProperties",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "orderId=bf812fc0-d655-ecda-26da-bd64bcb3898a&extendedProperties=[
{
\"RowId\": \"ea89429e-5c71-4069-8340-7fad55ffe056\",
\"Name\": \"Net-Total\",
\"Value\": \"123\",
\"Type\": \"PROFITCALC\"
}
]"
As you can see, the postfields are just sent as a string, which was nice and easy as Linnworks API can be a bit all over the place with the request formats, but with this Guzzle implementation I'm having to send $parameters as an array. I've tried json encoding the extendedProperties sub-array like so:
$parameters = [
"orderId" => "bf812fc0-d655-ecda-26da-bd64bcb3898a",
"extendedProperties" => json_encode([
"RowId" => "ea89429e-5c71-4069-8340-7fad55ffe056",
"Name" => "Net-Total",
"Value" => "123",
"Type" => "PROFITCALC"
])
];
$url = "Orders/SetExtendedProperties/";
$extendedPropertiesNew = $this->linnworks->api->Orders()->post($url, $parameters );
Unfortunately though, I'm getting the following error returned by the API:
Client error: `POST https://eu-ext.linnworks.net/api/Orders/SetExtendedProperties/` resulted in a `400 Bad Request` response: {"Code":null,"Message":"Invalid parameter extendedProperties"}
Which doesn't make much sense to me as the request documentation here (https://apps.linnworks.net/Api/Method/Orders-SetExtendedProperties) states extendedProperties is a legit parameter and my Curl request also works fine.
I've also tried not using json_encode and just sending $parameters as a multi dimensional array but that just returns an empty array, deletes any existing order extended properties but no extended properties are written as intended, and as my old Curl request does successfully.
If it's of any help, please see booni3/Linnworks post and parse class methods:
public function post($url = null, array $parameters = []): array
{
return $this->parse(function() use($url, $parameters){
return $this->client->post($this->server.$url, [
'form_params' => $parameters,
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'Accept' => 'application/json',
'Authorization' => $this->bearer ?? ''
]
]);
});
}
private function parse(callable $callback)
{
$response = call_user_func($callback);
$json = json_decode((string) $response->getBody(), true);
if(json_last_error() !== JSON_ERROR_NONE){
throw new LinnworksResponseCouldNotBeParsed((string) $response->getBody());
}
return $json;
}
Any ideas on how to get the form_params to match exactly with my curl request so I can get this to work? I've used this repository successfully with countless other Linnworks API requests already but I'm simply stuck on this one so any help would be greatly appreciated.
Running XAMPP which loads and runs fine, and can see every other localhost/folder , however as soon as I upload this
<?php
$data = array (
'number' => 1,
'current_date' => date('Y-m-d'),
'name' => 'RS'
);
$ch = curl_init();
$options = array(
CURLOPT_URL => 'http://localhost/fake_api/',
CURLOPT_POST => 1,
CURLOPT_POSTFIELDS => $data,
CURLOPT_RETURNTRANSFER => 1,
);
curl_setopt_array($ch, $options);
$result = curl_exec($ch);
curl_close($ch);
print_r($result);
?>
php file, it returns 'object not found' to the browser.
I am trying to teach myself the POST method using cURL scripting, as eventually on a live server I will need to POST to a live API, but I am seriously stumbling at this hurdle.
I do have a separate /directory with a fake_api php document which is seen by apache and can be seen in the browser returning array().
EDIT
Apache stops seeing the entire /directory which that particular php file is in, not just the /directory/file.php
I have found the answer after more reseaerch, I didnt specify an array with [] within the curl_opt_array()
or the initial data variable
$data = array ();
should have been
$data = array ([]);
SOLUTION: I had malformed my JSON data for the payload body. The "ttl" => 30 was in the incorrect array() method. This probably won't help anyone in the future, moving the ttl key/value pair made this work correctly as seen below.
$data = array(
"statement" => array(
"actor" => array(
"mbox" => "mailto:test#example.com"
),
),
"ttl" => 30
);
I have checked numerous other StackOverflow questions and cannot find a solution that works. I should note that I am testing this using a local XAMPP server running on port 8080. Not sure if that matters. I have been able to get this working using Postman, but translating it to PHP has been problematic. Am I missing something? I am not all that familiar with PHP, but need this for work.
EDIT: Some more information about what the API is expecting. It's a fairly simple API that requires a JSON body, a Basic Authorization header, and a Content-Type: application/json.
Here is the JSON body I am using in Postman. This is a direct copy/paste from Postman, which is successfully communicating with the API:
{
"statement": {
"actor": {
"mbox": "mailto:test#example.com"
}
},
"ttl": 30
}
Is there a syntax error in my below PHP code for this? Again, I am learning PHP on the fly so I'm unsure if I am properly constructing a JSON payload using the array() method in PHP.
My code below has the $https_user,$https_password, and $url domain changed for obvious security reasons. In my actual PHP code, I have the same credentials and domain used in Postman.
The $randomSessionID serves no real purpose other than an identification number for future requests. Has no affect on the API response failing or succeeding.
<?php
$https_user = 'username';
$https_password = 'password';
$randomSessionID = floor((mt_rand() / mt_getrandmax()) * 10000000);
$url = 'https://www.example.com/session/' . $randomSessionID . '/launch';
$json = json_encode(array(
"statement" => array(
"actor" => array(
"mbox" => "mailto:test#example.com"
),"ttl" => 30
)
));
$options = array(
'http' => array(
'method' => 'POST',
'header' => 'Content-Type: application/json\r\n'.
"Authorization: Basic ".base64_encode("$https_user:$https_password")."\r\n",
'content' => $json
)
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
if ($result === FALSE) { /* Handle error */ }
?>
SOLUTION: I had malformed my JSON data for the payload body. The "ttl" => 30 was in the incorrect array() method. This probably won't help anyone in the future, but moving the ttl key/value pair made this work correctly as seen below.
$data = array(
"statement" => array(
"actor" => array(
"mbox" => "mailto:test#example.com"
),
),
"ttl" => 30
);
I'm new developer, I need to connect XML API Hotel, In the documentation is mentioned about Startup guide, first we need to add customer login and user, there're two steps at first, but I don't know how to use them, and how to connect Main XML URL into PHP using the password login info like below:
General Request / Request Format:
<customer>
<username>username</username>
<password>md5(password)</password>
<id>company code</id>
<source>1</source>
<request command="getallcities">
<return>
<filters>
<countryCode></countryCode>
<countryName></countryName>
</filters>
<fields>
<field>countryName</field>
<field>countryCode</field>
</fields>
</return>
</request>
</customer>
General Response / Response Format:
<result command=" " date="" ip="">
<request>
<error>
<class></class>
<code>error code</code>
<details></details>
<extraDetails></extraDetails>
<postXml></postXml>
</error>
<successful>FALSE</successful>
</request>
</result>
General Request XSD
getallcities.xsd
I highly appreciate of someone will guide me how to use them and how to connect PHP into Main XML Url.
I highly appreciate your help in this issue.
Thanks in advance
Okay, so, you didn't give very much to work with, but I'm going to try and help you solve this.
First off, let's use a PHP package to simplify converting an array into valid XML. It will save you time down the road. I recommend Spatie's Array to Xml package.
If you're familiar with composer, go ahead and install that. If you've never used composer, go ahead and download src/ArrayToXml.php and put it in your project. (I highly recommend you use composer if you don't, I'm just providing some extra instructions since I don't know your level of experience.)
Now, I'm going to assume you got the Array To Xml package set up. Let's go ahead and structure an array before we convert it to xml.
// Structure the payload as an array and pass in any data you need dynamically
$payload = [
'username' => 'username',
'password' => 'md5(password)',
'id' => 'company code',
'source' => '1',
'request' => [
'return' => [
'filters' => [
'countryCode' => [],
'countryName' => [],
],
'fields' => [
'field' => [
'countryName',
'countryCode',
],
],
],
'#attributes' => [
'command' => 'getallcities',
],
],
];
// Convert the array to xml with a root of 'customer'
$xml = \Spatie\ArrayToXml\ArrayToXml::convert($payload, 'customer');
From there, we're going to initiate a curl call to the endpoint with your payload.
// Set the url you're making an api call to
$endpoint = 'https://yoursite.com/your-endpoint';
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $endpoint,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => $xml,
CURLOPT_HTTPHEADER => [
"Content-Type: application/xml"
]
]);
$response = curl_exec($curl);
$error = curl_error($curl);
curl_close($curl);
if ($error) {
echo "cURL Error #:" . $error;
} else {
echo $response;
}
Now you'll want to parse the response xml back into an array so you can process it. I created a package to help you do this. You can find it here: https://github.com/mtownsend5512/xml-to-array
The basic premise of installing it is the same. You'll do $response = \Mtownsend\XmlToArray\XmlToArray::convert($response);
See how that works.
Sorry, I can only post 2 hyperlinks so I'm going to have to remove the http : //
Background
I'm, trying to convert the code here: https://github.com/RusticiSoftware/TinCan_Prototypes/blob/92969623efebe2588fdbf723dd9f33165694970c/ClientPrototypes/StatementIssuer/StatementIssuer.java
into PHP, specifically the makeRequest function. This code posts data to a Tin Can Compliant Learner Record Store.
The current version of my PHP code is here:
tincanapi.co.uk/wiki/tincanapi.co.uk:MediaWikiTinCan
The specification for the Tin Can API which everything should conform to is here:
scorm.com/wp-content/assets/tincandocs/TinCanAPI.pdf
There is also a working java script function that Posts data in the right format here (see the XHR_request function I think):
https://github.com/RusticiSoftware/TinCan_Prototypes/blob/92969623efebe2588fdbf723dd9f33165694970c/ClientPrototypes/GolfExample_TCAPI/scripts/TCDriver.js
I don't have access to the code or server that I'm posting to, but the end result should be an output here: beta.projecttincan.com/ClientPrototypes/ReportSample/index.html
Problem
I'm trying to use Curl to POST the data as JSON in PHP. Curl is returning 'false' but no error and is not posting the data.
On the recommendation of other questions on this site, I've tried adding 'json=' to the start of the POSTFIELDS, but since the Java and JavaScript versions does have this, I'm not sure this is right.
Can anybody suggest either how I might fix this or how I might get useful errors out of curl? My backup is to output the relevant JavaScript to the user's browser, but surely PHP should be able to do this server side?
Very grateful for any help.
Andrew
At least one thing is wrong: you should not be using rawurlencode on your Authorization header value.
Consider using php streams and json_encode() and json_decode() instead. The following code works.
function fopen_request_json($data, $url)
{
$streamopt = array(
'ssl' => array(
'verify-peer' => false,
),
'http' => array(
'method' => 'POST',
'ignore_errors' => true,
'header' => array(
'Authorization: Basic VGVzdFVzZXI6cGFzc3dvcmQ=',
'Content-Type: application/json',
'Accept: application/json, */*; q=0.01',
),
'content' => json_encode($data),
),
);
$context = stream_context_create($streamopt);
$stream = fopen($url, 'rb', false, $context);
$ret = stream_get_contents($stream);
$meta = stream_get_meta_data($stream);
if ($ret) {
$ret = json_decode($ret);
}
return array($ret, $meta);
}
function make_request()
{
$url = 'https://cloud.scorm.com/ScormEngineInterface/TCAPI/public/statements';
$statements = array(
array(
'actor' => array(
'name' => array('Example Name'),
'mbox' => array('mailto:example#example.com'),
'objectType' => 'Person',
),
'verb' => 'experienced',
'object' => array(
'objectType' => 'Activity',
'id'=> 'http://www.thincanapi.co.uk/wiki/index.php?Main_Page',
'definition' => array(
'name' => array('en-US'=>'TinCanAPI.co.uk-tincanapi.co.uk'),
'description' => array('en-US'=> 'TinCanAPI.co.uk-tincanapi.co.uk'),
),
),
),
);
return fopen_request_json($statements, $url);
}
list($resp, $meta) = make_request();
var_export($resp); // Returned headers, including errors, are in $meta
We've now released an open source library specifically for PHP, it uses a similar method as the accepted answer but rounds out the rest of the library as well. See:
http://rusticisoftware.github.io/TinCanPHP/
https://github.com/RusticiSoftware/TinCanPHP