Post form data to external url + curl - php

I have this form that I am trying to use to post data to an external url. I do have some very basic knowlegde of using php curl. So far if I use this code that I have written:
<?php
if ($_POST['request_callback'])
{
$customer_name = cleaninput($_REQUEST['customer_name'],"text");
$debtor_id = cleaninput($_REQUEST['debtor_id'],"number");
$telephone_number = cleaninput($_REQUEST['customer_number'],"number");
if ($_POST['callme_now'] == '1') {
$callback_time = date('y-m-d ' . $_POST['hour_select'] . ':' . $_POST['minute_select'] . ':s');
} else {
$callback_time = date('y-m-d H:i:s');
}
// Send using CURL
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, "http://www.myjoomla.eo/test.php"); // URL to post
curl_setopt ($ch, CURLOPT_POST, 1);
curl_setopt ($ch, CURLOPT_POSTFIELDS, "Name=$customer_name&Debtor_ID=$debtor_id&Telephone_Number=$telephone_number&CallBack_Time=$callback_time");
curl_setopt ($ch, CURLOPT_FOLLOWLOCATION, 1);
$result = curl_exec( $ch ); // runs the post
curl_close($ch);
echo "Reply Response: " . $result; // echo reply response
}
?>
So far, it does post to the file and the results are display. Now how do I format the data that has been posted into xml format? Ideally, I am trying to acheive something like this an xml that looks like this:
<?xml version="1.0" encoding="utf-8"?>
<CallRequest>
<ProjectName>Test</ProjectName>
<ContactNumberToDial>07843088348</ContactNumberToDial>
<DateTimeToDial></DateTimeToDial>
<ListSource>WebLead</ListSource>
<AgentName></AgentName>
<AddToList>False</AddToList>
<SpecificAgent>False</SpecificAgent>
<DBField>
<FieldName>Name</FieldName>
<FieldValue>Testing</FieldValue>
</DBField>
</CallRequest>
Anyone have an Idea of what to do here?
Thanks,
James

An XML library I have used in the past that allows you to create XML using PHP is XmlWriter. This library was originally written to work with PHP4. You'll find that its name conflicts with that of a built-in PHP5 class so you'll need to change both the class declaration and the constructor to something else.
Hope that helps!

I agree with jkndrkn - it seems cURL is correct, it's a matter of the output from test.php. IBM has a great article about reading/writing/parsing XML with PHP, check it out here.

Hi Sorry for taking a while in getting back. Been trying to figure this out in a few way. What I have been told is that the client wants to post an xml string to the given URL. When looking at sample pages, they have got 3 examples of what could be then. There is an example with SOAP 1.1 which display the request and response, an example with SOAP 1.2 request and response, an HTTP GET request and response sample and an HTTP POST request and response sample.
I have opted for the latter which I feel will be the easiest to work with and I am using PHP curl.
The HTTP POST example is this:
Request:
POST /ClickToCall/CallRequest.asmx/Call HTTP/1.1
Host: 194.217.1.2
Content-Type: application/x-www-form-urlencoded
Content-Length: length
xmlString=string
Response:
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length
<?xml version="1.0" encoding="utf-8"?>
<string xmlns="http://tempuri.org/">string</string>
When I enter the xmlString manual on the url's test page, I get the necessary responses.
The xmlString looks like this:
<?xml version="1.0" encoding="utf-8"?><CallRequest><ProjectName>Noble Test</ProjectName><ContactNumberToDial>07843088348</ContactNumberToDial><DateTimeToDial>2009-12-10 18:30:53</DateTimeToDial><ListSource>WebLead</ListSource><AgentName></AgentName><AddToList>False</AddToList><SpecificAgent>False</SpecificAgent><DBField><FieldName>Name</FieldName><FieldValue>NobleTesting</FieldValue></DBField></CallRequest>
When I use my code however, I get no response at all.
This is the code I am using:
<?php
if ($_POST['request_callback'])
{
$customer_name = cleaninput($_REQUEST['customer_name'],"text");
$debtor_id = cleaninput($_REQUEST['debtor_id'],"number");
$telephone_number = cleaninput($_REQUEST['customer_number'],"number");
if ($_POST['callme_now'] == '1') {
$callback_time = date('y-m-d ' . $_POST['hour_select'] . ':' . $_POST['minute_select'] . ':s');
} else {
$callback_time = date('y-m-d H:i:s');
}
// XML data as string
$request = '<?xml version="1.0" encoding="utf-8"?>';
$request .= '<CallRequest>';
$request .= '<ProjectName>Nobel Test</ProjectName>';
$request .= '<ContactNumberToDial>' . $telephone_number . '</ContactNumberToDial>';
if (isset($_POST['callme_now'])) {
$request .= '<DateTimeToDial></DateTimeToDial>';
} else {
$request .= '<DateTimeToDial>' . date('Y-m-d ' . $_POST['hour_select'] . ':' . $_POST['minute_select'] . ':s') . '</DateTimeToDial>';
}
$request .= '<ListSource>WebLead</ListSource>';
$request .= '<AgentName></AgentName>';
$request .= '<AddToList>False</AddToList>';
$request .= '<SpecificAgent>False</SpecificAgent>';
$request .= '<DBField>';
$request .= '<FieldName>Customer Name</FieldName>';
$request .= '<FieldValue>' . $customer_name . '</FieldValue>';
$request .= '</DBField>';
$request .= '</CallRequest>';
// Create Headers
$header[] = "Host: www.myjoomla.eo";
$header[] = "Content-type: application/x-www-form-urlencoded";
$header[] = "Content-length: ". strlen($request) . "\r\n";
$header[] = $request;
$loginUsername = "username";
$loginPassword = "password";
// Send using CURL
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, "http://194.217.1.2/ClickToCall/CallRequest.asmx/Call"); // URL to post
curl_setopt( $ch, CURLOPT_USERPWD, "$loginUsername:$loginPassword"); //login
curl_setopt( $ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); // return into a variable
curl_setopt( $ch, CURLOPT_POST, 1);
curl_setopt( $ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt( $ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt ($ch, CURLOPT_POSTFIELDS, $header);
$result = curl_exec( $ch ); // runs the post
curl_close($ch);
echo "Reply Response: " . $result; // echo reply response
echo " ";
echo "";
print_r($header);
echo "";
// return $result;
}
Does anyone see anything with with the above code? Thanks

Related

Workflow Max add job via API

I'm trying to add a job to the Workflow Max API. I seem to be hitting the API but I keep getting the error message:
Message not in expected format. The following required element was missing - Job/ClientID
I'm sure that the client ID is added but something seems to be wrong. This is the code:
function post_job_to_workflow_max($job_data) {
// configure our connection to the api
$api_token = 'API_KEY';
$acc_key = 'ACC_TOKEN';
$url = 'https://api.workflowmax.com/job.api/add?apiKey=' . $api_token . '&accountKey=' . $acc_key;
// Job data must match the format required by WorkflowMax
// currently accepts XML data
// see: https://www.workflowmax.com/api/job-methods#POST%20add
$xml = new SimpleXMLElement("<Job></Job>");
$xml->addChild('Name', $job_data[0]);
$xml->addChild('Description', $job_data[1]);
$xml->addChild('ClientID', 18754031);
// $clientID = $xml->addChild('Client');
// $clientID->addChild('ID', 18754031);
// $clientID->addChild('Name', "TEST CLIENT");
$xml->addChild('State', 'Planned');
$xml->addChild('StartDate', $job_data[2]);
$xml->addChild('DueDate', $job_data[3]);
// print_r($xml);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml->asXML());
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: text/xml',
'Content-Length: ' . strlen($xml->asXML()))
);
$output = curl_exec($ch);
curl_close($ch);
$result = simplexml_load_string($output);
print_r($result);
}
If there's anyone with experience of using WFM, would be good to hear how you approached it.
Thanks
So in answer to my own question, I did finally work this out.
The way I did this was to return the ID of the client from the function I used to post a client to WorkFlow Max. See code:
1) post the client
function post_client_to_workflowmax($client_data) {
// configure our connection to the api
$api_token = 'YOUR_TOKEN';
$acc_key = 'YOUR_KEY';
$url = 'https://api.workflowmax.com/client.api/add?apiKey=' . $api_token . '&accountKey=' . $acc_key;
// Client data must match the format required by WorkflowMax
// currently accepts XML data
// These indexes match up with how the data has been stored
// see: https://www.workflowmax.com/api/client-methods#POST%20add
$xml = new SimpleXMLElement("<Client></Client>");
$xml->addChild('Name', htmlspecialchars($client_data[2]));
$xml->addChild('Email', htmlspecialchars($client_data[9]));
$xml->addChild('Phone', htmlspecialchars($client_data[10]));
$xml->addChild('Address', htmlspecialchars($client_data[3]) . ' ' . htmlspecialchars($client_data[4]));
$xml->addChild('City', htmlspecialchars($client_data[5]));
$xml->addChild('Postcode', htmlspecialchars($client_data[7]));
$xml->addChild('Country', htmlspecialchars($client_data[8]));
$xml->addChild('IsProspect', 'No');
$contacts = $xml->addChild('Contacts');
$contact = $contacts->addChild('Contact');
$name = $contact->addChild('Name', htmlspecialchars($client_data[0]) . ' ' . htmlspecialchars($client_data[1]));
// POST request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml->asXML());
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: text/xml',
'Content-Length: ' . strlen($xml->asXML()))
);
$output = curl_exec($ch);
curl_close($ch);
// Create an array from the data that is sent back from the API
$result = simplexml_load_string($output);
$clientID = NULL;
// here we get the ID created for this client and pass it into the variable $clientID
foreach($result->Client as $k => $v) {
$clientID = $v->ID;
}
return $clientID;
}
We then get that ID passed into our job posting function like so:
2) post a job to WFM
function post_job_to_workflow_max($job_data, $clientID) {
// configure our connection to the api
$api_token = 'YOUR_TOKEN';
$acc_key = 'YOUR_KEY';
$url = 'https://api.workflowmax.com/job.api/add?apiKey=' . $api_token . '&accountKey=' . $acc_key;
// Job data must match the format required by WorkflowMax
// currently accepts XML data
// see: https://www.workflowmax.com/api/job-methods#POST%20add
$xml = new SimpleXMLElement("<Job></Job>");
$xml->addChild('ClientID', $clientID);
$xml->addChild('Name', htmlspecialchars($job_data[0]));
$xml->addChild('Description', htmlspecialchars($job_data[1]));
$xml->addChild('State', 'Planned');
$xml->addChild('StartDate', htmlspecialchars($job_data[2]));
$xml->addChild('DueDate', htmlspecialchars($job_data[3]));
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml->asXML());
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: text/xml',
'Content-Length: ' . strlen($xml->asXML()))
);
$output = curl_exec($ch);
curl_close($ch);
$result = simplexml_load_string($output);
}
And then calling these functions looks something like this:
$id = post_client_to_workflowmax($client);
post_job_to_workflow_max($job, $id);
Where $client must be an array of data. This worked for my case but might not work for your particular case so you may need to edit the fields etc.
Hopefully this helps someone who is stuck with the same problem. Not the most elegant code but it gets the job done.

Get JSON generated by a webhook hosted online

I am making a card payment and then it communicates to the server of the company via webhook, the response is then recorded in RequestBin, which generates a JSON response in their website, how do I extract the information from the website to my PHP code?
The webpage looks like this:
my requestb.in online webhook
What I need is to get that raw JSON.
You could try using CURL to retrieve the JSON object. Are you using CURL to send the payment payload out to the processor, etc? Below is an example (Obviously you would need to fill in the appropriate PHP variables where applicable).
$reqbody = json_encode($_REQUEST);
$serviceURL = "http://www.url.com/payment_processor";
$curl = curl_init($serviceURL);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_POSTFIELDS, $reqbody);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($curl, CURLOPT_VERBOSE, true);
$headers = array(
'Content-type: application/json',
"Authorization: ".$hmac_enc,
"apikey: ".$apikey,
"token: ".$token,
"timestamp: ".$timestamp,
"nonce: ".$nonce,
);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
$json_response = curl_exec($curl);
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
if ( $status != 201 ) {
die("Error: call to URL $serviceURL failed with status $status, response $json_response, curl_error " . curl_error($curl) . ", curl_errno " . curl_errno($curl));
}
curl_close($curl);
$response = json_decode($json_response, true);
echo "<hr/><br/><strong>PROCESSOR RESPONSE:</strong><br/>";
echo "<pre>";
print_r($response);
echo "</pre>";
You could get the json from requestbin and resend it to your localhost using a request client like Postman.
if (!empty($_POST)) {
$data = json_decode($_POST);
}
I found the solution, first you download HTML dom and then you just change the fields. The reason the for loop goes from 0-19 is because requestb.in saves 20 entries, for the rest just substitute the variables.
include('../simple_html_dom.php');
// get DOM from URL or file
// asegurese de incluir el ?inspect en el URL
$html = file_get_html('https://requestb.in/YOURURL?inspect');
for ($x = 0; $x <= 19; $x++) {
$result = $html->find('pre[class=body prettyprint]', $x)->plaintext;
if($result){
$json_a = str_replace('"', '"', $result);
$object = json_decode($json_a);
if(isset($object->type)) echo $object->type . "<br>";
if(isset($object->transaction->customer_id)) echo $object->transaction->customer_id . "<br>";
}
}

Square-Connect Inventory Update cURL Call

I am trying to update my square inventory from my inventory database website and I keep getting this error.
Response:{"type":"bad_request","message":"Missing required parameter `quantity_delta`"}
I am adding the quantity_delta field and adjustment_type to the cURL call because that is what the documentation says, there are 3 options in the documentation and only 1 of them has (optional) next to it so I am using the 2 that appear to be required. I can't capture the POST body to see exactly how the call is going out, maybe a type or json_encode issue, so debugging this is giving me an issue.
I am also writing the headers and the response to a text file fore easy reading.
Here is the code:
$i = $_GET['id'];
$n = $_GET['name'];
$q = $_GET['qty'];
$s = $_GET['sku'];
$c = $_GET['current'];
$sync = $_GET['sync'];
if($c > $q){
$up = $q - $c;
$reason = "SALE";
}else{
$up = $c + $q;
$reason = "RECEIVE_STOCK";
}
$postData = array(
"quantity_delta" => $up,
"adjustment_type" => $reason);
$b = json_encode($postData);
$fp = fopen('curlOut.txt', 'rw+');
fopen('curlOut.txt', 'rw+');
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPHEADER, array('Authorization: Bearer *****_******' ));
curl_setopt($curl, CURLOPT_URL, "https://connect.squareup.com/v1/me/inventory/".$i."");
curl_setopt($curl, CURLOPT_POST, TRUE);
curl_setopt($curl, CURLOPT_POSTFIELDS, $b);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($curl, CURLINFO_HEADER_OUT, TRUE);
curl_setopt($curl, CURLOPT_VERBOSE, 1);
curl_setopt($curl, CURLOPT_STDERR, $fp);
if(!curl_exec($curl)){
die('Error: "' . curl_error($curl) . '" - Code: ' . curl_errno($curl));
}
$filename = 'curlOut.txt';
if (is_writable($filename)){
echo 'The file is writeable';
}else{
echo 'nope';
}
$ch = curl_exec ($curl);
$sentCall = curl_getinfo($curl, CURLINFO_HEADER_OUT);
$dump = fopen("curlOut.txt","a") or die("Unable to open file!");
$dumptxt = "Header Info:".$sentCall . "Response:".$ch."\n\n";
fwrite($dump,$dumptxt);
curl_close ($curl);
fclose('curlOut.txt');
var_dump(json_decode($ch,true));
Can you please tell me what I am doing wrong? I have been trying for days to figure out what is wrong with my cURL call. I can do cURL calls to read data from the square-connect API with no issues. I also have some repetitive code in here to display output/response in different ways hoping for more information. I will also post the header info that I get using CULINFO_HEADER_OUT.
Header Info:POST /v1/me/inventory/011a799a-****-****-****-4f5b70dc1494 HTTP/1.1
Host: connect.squareup.com
Accept: */*
Authorization: Bearer *****_*****
Content-Length: 47
Content-Type: application/x-www-form-urlencoded
Thank You.
I believe this error is occurring because your request's Content-Type header is currently application/x-www-form-urlencoded. Requests to the Connect API must have a Content-Type of application/json to match your request body.
This was clearly an unhelpful error message to receive in this case; I will work with the API engineering team to improve it.

how to post xml with curl with php GET variables?

the script will post xml content to a url, and i need to include the php GET variables (eg. $RegionId), pls advise how to.
$RegionId = $_GET["RegionId"];
// xml data
$xml_data ='<AvailabilitySearch>
<RegionId xmlns="http://www.reservwire.com/namespace/WebServices/Xml">$RegionId</RegionId>
<HotelId xmlns="http://www.reservwire.com/namespace/WebServices/Xml">0</HotelId>
<HotelStayDetails xmlns="http://www.reservwire.com/namespace/WebServices/Xml">
</AvailabilitySearch>
';
// assigning url and posting with curl
$URL = "http://roomsxmldemo.com/RXLStagingServices/ASMX/XmlService.asmx";
$ch = curl_init($URL);
............
............
............
$RegionId is not posted on the script, how to use the GET or POST variable on that xml content?
Hope this will help You
$RegionId = $_GET["RegionId"];
// xml data
$xml_data ='<?xml version="1.0" encoding="UTF-8"?><AvailabilitySearch>
<RegionId xmlns="http://www.reservwire.com/namespace/WebServices/Xml">{$RegionId}</RegionId>
<HotelId xmlns="http://www.reservwire.com/namespace/WebServices/Xml">0</HotelId>
<HotelStayDetails xmlns="http://www.reservwire.com/namespace/WebServices/Xml">
</AvailabilitySearch>
';
// assigning url and posting with curl
$URL = "http://roomsxmldemo.com/RXLStagingServices/ASMX/XmlService.asmx";
$headers = array(
"Content-type: text/xml",
"Content-length: " . strlen($xml_data),
"Connection: close",
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL,$UR);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml_data);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$data = curl_exec($ch);
curl_close($ch);
echo $data;
Just add your GET vars to url.
$URL = "http://roomsxmldemo.com/RXLStagingServices/ASMX/XmlService.asmx?var1=val1&var2=val2";
Well, merely you can wrap your xml content in double quotes and substitute necessary variables.
$id = (isset($_GET['id'])) ? $_GET['id'] : '';
xml_data = "<AvailabilitySearch>
<RegionId xmlns=\"http://www.reservwire.com/namespace/WebServices/Xml\">$rid</RegionId>
<HotelId xmlns=\"http://www.reservwire.com/namespace/WebServices/Xml\">0</HotelId>
<HotelStayDetails xmlns=\"http://www.reservwire.com/namespace/WebServices/Xml\">
</AvailabilitySearch>";
Value of $id variable will be in xml content. And you can post this message. But, don't forget to shield inner double quotes with additional slashes in your xml.

Passing a parameter in the header (XML RPC)

I'm trying to set up a server status for the MMORPG Champions Online. I got some basic information from the web master and this is all he told me:
XML-RPC call to server: http://www.champions-online.com/xmlrpc.php
function name: wgsLauncher.getServerStatus
Parameter (language): en-US
Now, I found a nice example to start with here, and I ended up with this code:
<?php
ini_set('display_errors', 1);
error_reporting(E_ALL);
# Using the XML-RPC extension to format the XML package
$request = xmlrpc_encode_request("wgsLauncher.getServerStatus", "<param><value><string>en-US</string></value></param>", null );
# Using the cURL extension to send it off,
# first creating a custom header block
$header[] = "Host: http://www.champions-online.com:80/";
$header[] = "Content-type: text/xml";
$header[] = "Content-length: ".strlen($request) . "\r\n";
$header[] = $request;
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, "http://www.champions-online.com/xmlrpc.php"); # URL to post to
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 ); # return into a variable
curl_setopt( $ch, CURLOPT_HTTPHEADER, $header ); # custom headers, see above
curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'POST' ); # This POST is special, and uses its specified Content-type
$result = curl_exec( $ch ); # run!
curl_close($ch);
echo $result;
?>
But I'm getting a "400 Bad Request" error. I'm new to XML RPC and I barely know php, so I'm at a loss. The examples from the php site show how to use an array as a parameter, but nothing else.
I obtained the parameter string <param><value><string>en-US</string></value></param> from this XMLRPC Debugger (very nice btw). I entered the parameter I needed in the "payload" box and this was the output.
So, I would appreciate any help on how to pass this parameter to the header.
Note: My host supports xmlrpc but it seems the function "xmlrpc_client" doesn't exist.
Update: The web master replied with this information, but it's still not working... it's getting to the point I may just scrape the status off the page.
$request = xmlrpc_encode_request("wgsLauncher.getServerStatus", "en-US" );
Ok, I finally figured out my answer... It seemed to be a problem in the header, because it worked when I changed the cURL code to match the code I found it on this site. The post is about how to remotely post to wordpress using XMLRPC in php.
This is the code I ended up with:
<?php
// ini_set('display_errors', 1);
// error_reporting(E_ALL);
# Using the XML-RPC extension to format the XML package
$request = xmlrpc_encode_request( "wgsLauncher.getServerStatus", "en-US" );
$ch = curl_init();
curl_setopt($ch, CURLOPT_POSTFIELDS, $request);
curl_setopt($ch, CURLOPT_URL, "http://www.champions-online.com/xmlrpc.php");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
$result = curl_exec($ch);
curl_close($ch);
$method = null;
$params = xmlrpc_decode_request($result, &$method);
# server status result = true (up) or false (down)
$status = ($params['status']) ? 'up' : 'down';
$notice = ($params['notice'] == "") ? "" : "Notice: " + $params['notice'];
echo "Server Status: " . $status . "<br>";
echo $notice;
?>

Categories