cURL, php request_headers from Apache2.2 to Oracle App Server - php

I have banged my head on this long enough. I hope someone can help me figure this out. I'm not sure anymore whether my problems are caused by cURL, php, Apache, Oracle or a brain fart.
I'm trying to post to a form on an Oracle server. By hand, I can make a GET-like url that will bring up the correct page. I want to do POST to hide the variables (and there could be a lot of them) and because the originating form's method is POST. Either way I can't get the response via my php/cURL program.
My specific questions:
The obvious one, why doesn't it work?
Why can I do the request by hand and not by program?
Why does the access.log have a GET in there?
Why is my request header being rewritten to include the
/DAD/scheme/app of the Oracle server?
Will my programming ego ever be the same?
Here's my current code:
*<?php
$error_dump = 'stderr.txt';
$error_dump_handle = fopen($error_dump,'a');
$ch = curl_init();
$url = 'http://www2.blah.com/pls/blah/blah.blaQuery';
$url_enc_fields = array(
'LAST_NAME' => 'MacBlahBlah',
'FIRST_NAME' => 'Blahberina',
'CONTAINS' => 'Y',
... and more fields ...
);
$url_enc_fields = http_build_query($url_enc_fields);
//$url = $url.'?'.$url_enc_fields; //previous GET attempt
$content_length = strlen($url_enc_fields);// number of bytes
$content_length = 'Content-Length:' . $content_length;
$headers = array(
'Request: POST ' . $url_enc_fields . 'HTTP/1.1', //An attempt to force the request
'Accept: */*',
'Content-Type: application/x-www-form-urlencoded',
'Referer:http://blah.com/pls/blah/blah.startup?code1=MM&code2=bleep',
'Expect: ',
$content_length
);
curl_setopt($ch,CURLOPT_HEADER,1);
curl_setopt($ch,CURLOPT_HTTPHEADER,$headers);
curl_setopt($ch,CURLINFO_HEADER_OUT,TRUE);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,FALSE);
curl_setopt($ch,CURLOPT_FOLLOWLOCATION,TRUE); // one of many guesses
curl_setopt($ch,CURLOPT_FRESH_CONNECT,TRUE);
// NOTE: no cookies or passwords involved
curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,180);
curl_setopt($ch,CURLOPT_TIMEOUT,180);
curl_setopt($ch,CURLOPT_VERBOSE,TRUE);
curl_setopt($ch,CURLOPT_STDERR,$error_dump_handle);
curl_setopt($ch,CURLOPT_ENCODING,'chunked'); // Added because the response was chunked, no difference
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_POST,TRUE); // Apparently futile
curl_setopt($ch,CURLOPT_POSTFIELDS,$url_enc_fields);
$postResult = curl_exec($ch);
$info = curl_getinfo($ch);
$pretty_info = print_r($info,true);
> echo '<br/><pre>'; print_r($info); print_r($url_enc_fields);
> print_r($headers);
echo '</pre>';
$now_time = getdate();$format = '-------------- %f -----------';
fprintf($error_dump_handle,$format,$now_time[0]);
fprintf($error_dump_handle,$pretty_info);
fprintf($error_dump_handle,curl_error($ch));
fclose($error_dump_handle);
curl_close($ch);
?>*
------------ Here is the current logging -------------
From my error dump:
-------------- 1323725891.000000 -----------Array
(
[url] => http://www2.blah.com/pls/blah/blah.blaQuery
[content_type] => text/html; charset=iso-8859-1
[http_code] => 404
[header_size] => 205
[request_size] => 601
[filetime] => -1
[ssl_verify_result] => 0
[redirect_count] => 0
[total_time] => 0.203
[namelookup_time] => 0
[connect_time] => 0.078
[pretransfer_time] => 0.078
[size_upload] => 146
[size_download] => 336
[speed_download] => 1655
[speed_upload] => 719
[download_content_length] => -1
[upload_content_length] => 0
[starttransfer_time] => 0.203
[redirect_time] => 0
**[request_header] => POST /pls/blah/blah.blaQuery HTTP/1.1**
Host: blah.com
Accept-Encoding: chunked
Request: POST LAST_NAME=MacBlahBlah&FIRST_NAME=Blahberina&CONTAINS=Y&...some other fields... HTTP/1.1
Accept: */*
Content-Type: application/x-www-form-urlencoded
Referer:http://blah.com/pls/wllpub/blah.startup?code1=MM&code2=bleep
Content-Length:146
From my Apache access.log:
*##.##.##.## - - [12/Dec/2011:14:38:06 -0700] "GET /cgi-bin/mydir/mysubmit_form.php HTTP/1.1" 200 2698 "-" "Mozilla/5.0 (Windows NT 6.0; WOW64; rv:8.0) Gecko/20100101 Firefox/8.0"*
Why is it doing a GET?
The HTML code returned:
http://www2.blah.com/pls/blah/blah.blaQuery
HTTP/1.1 404 Not Found
Date: Mon, 12 Dec 2011 22:03:53 GMT
Server: Oracle-Application-Server-10g/10.1.2.2.0 Oracle-HTTP-Server
Transfer-Encoding: chunked Content-Type: text/html; charset=iso-8859-1
Not Found
The requested URL pls/blah/blah.blaQuery was not found on this server.
Oracle-Application-Server-10g/10.1.2.2.0 Oracle-HTTP-Server Server at www2 Port 80

try
$fields = array(
'LAST_NAME' => 'MacBlahBlah',
'FIRST_NAME' => 'Blahberina',
'CONTAINS' => 'Y',
... and more fields ...
);
$url_enc_fields = http_build_query($fields);
and then
curl_setopt($ch,CURLOPT_POST,count($fields)); // Apparently futile
curl_setopt($ch,CURLOPT_POSTFIELDS,$url_enc_fields);
I didn't check these changes, but this is how I usually do it

Related

Curl http_code 400

I am trying to send the push Notification but it is getting problem
$url = 'https://android.googleapis.com/gcm/send';
$fields = array(
'registration_ids' => $id,
'data' => $load,
);
$headers = array(
'Content-Type: application/json',
'Authorization: key=' . GOOGLE_API_KEY,
);
// Open connection
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
//curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11');
// Disabling SSL Certificate support temporarly
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields, true));
// Execute post
$result = curl_exec($ch);
$httpcode = curl_getinfo($ch);
And when I try to see the curl info I see there is http_code 400. I did everything but still I am getting problem and push notification is not working.
Can you guys please help me ? I am stuck here.
Array
(
[url] => https://android.googleapis.com/gcm/send
[content_type] => text/plain; charset=UTF-8
[http_code] => 400
[header_size] => 406
[request_size] => 698
[filetime] => -1
[ssl_verify_result] => 0
[redirect_count] => 0
[total_time] => 0.22549
[namelookup_time] => 0.028427
[connect_time] => 0.030052
[pretransfer_time] => 0.189248
[size_upload] => 382
[size_download] => 41
[speed_download] => 181
[speed_upload] => 1694
[download_content_length] => -1
[upload_content_length] => 382
[starttransfer_time] => 0.225382
[redirect_time] => 0
[certinfo] => Array
(
)
[primary_ip] => 172.217.6.234
[primary_port] => 443
[local_ip] => 162.243.229.189
[local_port] => 54327
[redirect_url] =>
)
Error 400 means Bad Request. I see in your code, in the $headers array that you have the GOOGLE_API_KEY which is not defined in your script. You also have not defined $id and $load from what I can see.
Firstly, you will need a Google Cloud Messaging API Key, which you can obtain from the Google Developer's Console.
Please note that the method which you are employing requires a client side app installed on the mobile device. An alternative would be to send a web-push notification, which pushes the notification to Chrome (or any other compatible web browser), so long as it is running and has any tab open. If this is an option you think is worth looking at, I suggest you look at the following web-push library on Git-Hub.

Pasting url in browser returns json, curl returns 400 error

I am creating a loyalty discount for a client, and they have api that will return JSON with discount data.
Now I know how I need to generate api link, and when I output it in my error log, and then paste it to browser or postman I get the JSON just fine. But when I try to fetch the JSON using curl I get 400 error
Status 400: Request authentication failed
The curl part looks like this:
$ch = curl_init();
curl_setopt( $ch, CURLOPT_URL, $api_url );
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1 );
$json = json_decode( curl_exec( $ch ), true );
error_log(print_r($api_url, true));
error_log(print_r($json, true));
if ( curl_errno( $ch ) ) {
wp_die( curl_error( $ch ) );
}
$httpcode = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( $httpcode >= 200 && $httpcode < 300) {
wp_die( $json );
} else {
wp_die( 'httpcode: ' . $httpcode . ' ' . __( 'Probably entered the wrong data on option menu, or REST API not responsive' ) );
}
When I look at error log I can see the url that works when I paste it to postman or browser.
Now, the postman is set to GET, and when I tried to switch it to POST I got the same error. Postman returns headers
CONTENT-LENGTH → 238
I tried setting
curl_setopt( $ch, CURLOPT_CUSTOMREQUEST, 'GET');
before geting curl_exec, but I still get the same error.
Any way to see what could be wrong here?
Oh and I am generating the request using ajax. So user inputs their ID, and submit it to ajax and then I should check and return the json.
EDIT:
I've outputted the headers in my error log
[25-Oct-2016 12:07:53 UTC] Array
(
[url] => this is the url
[content_type] =>
[http_code] => 400
[header_size] => 48
[request_size] => 248
[filetime] => -1
[ssl_verify_result] => 0
[redirect_count] => 0
[total_time] => 0.564651
[namelookup_time] => 0.510374
[connect_time] => 0.520541
[pretransfer_time] => 0.52061
[size_upload] => 0
[size_download] => 41
[speed_download] => 72
[speed_upload] => 0
[download_content_length] => 41
[upload_content_length] => -1
[starttransfer_time] => 0.564589
[redirect_time] => 0
[redirect_url] =>
[primary_ip] => 213.191.137.78
[certinfo] => Array
(
)
[primary_port] => 80
[local_ip] => xxx.xxx.xxx.xxx
[local_port] => 60192
[request_header] => GET /rest/api/v1/webshop/loycard/customer/the endpoint goes here HTTP/1.1
Host: the client host
Accept: */*
)
And when I look in the inspector of pasted link I have
GET /rest/api/v1/webshop/loycard/customer/the endpoint goes here HTTP/1.1
Host: the client host
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.59 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, sdch
Accept-Language: hr-HR,hr;q=0.8,en-US;q=0.6,en;q=0.4

PHP cURL not returning Content-Encoding header

When I run curl -I http://api.stackoverflow.com/1.1/badges fro my terminal, it shows me the following headers:
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 42804
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
X-AspNetMvc-Version: 4.0
X-RateLimit-Max: 300
X-RateLimit-Current: 297
X-AspNet-Version: 4.0.30319
Set-Cookie: .ASPXBrowserOverride=; expires=Mon, 08-Oct-2012 04:29:28 GMT; path=/
Date: Tue, 09 Oct 2012 04:29:27 GMT
Yet, when I run the same cURL request through PHP, I get this:
Array
(
[url] => http://api.stackoverflow.com/1.1/badges?10102
[content_type] => application/json; charset=utf-8
[http_code] => 200
[header_size] => 277
[request_size] => 85
[filetime] => -1
[ssl_verify_result] => 0
[redirect_count] => 0
[total_time] => 0.168343
[namelookup_time] => 0.023417
[connect_time] => 0.046293
[pretransfer_time] => 0.046365
[size_upload] => 0
[size_download] => 42804
[speed_download] => 254266
[speed_upload] => 0
[download_content_length] => 42804
[upload_content_length] => 0
[starttransfer_time] => 0.097563
[redirect_time] => 0
[certinfo] => Array
(
)
[redirect_url] =>
)
The major difference that matters to me is that when run through PHP, I do not get the Content-Encoding header, without which I do not know if the content needs to be gzip inflated or not.
Is there a way to get the Content-Encoding header, or to check for gzip compression some other way?
There is no header_response nor accept-encoding in the returned getinfo array. I thought CURLINFO_HEADER_OUT on getinfo would give response headers, but only request headers are given.
But you can get raw headers using the CURLOPT_HEADER option set to true. So I suggest you to do something less natural :
$curl = curl_init();
$opts = array (
CURLOPT_URL => 'http://api.stackoverflow.com/1.1/badges',
CURLOPT_TIMEOUT => 120,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_ENCODING => 'gzip',
CURLOPT_HEADER => true,
);
curl_setopt_array($curl, $opts);
$return = curl_exec($curl);
list($rawHeader, $response) = explode("\r\n\r\n", $return, 2);
$cutHeaders = explode("\r\n", $rawHeader);
$headers = array();
foreach ($cutHeaders as $row)
{
$cutRow = explode(":", $row, 2);
$headers[$cutRow[0]] = trim($cutRow[1]);
}
echo $headers['Content-Encoding']; // gzip
If you set CURLOPT_HEADER to true, curl returns the header alongside the body. If you're just interested in the header, you can set CURLOPT_NOBODY to true and the body is not returned (which emulates the -I flag on the command line).
This example sets just the CURLOPT_HEADER, reads the Content-Encoding header (if it is set) and uncompresses the body:
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, "http://api.stackoverflow.com/1.1/badges");
curl_setopt($curl, CURLOPT_HEADER, 1);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($curl);
curl_close($curl);
list($header, $body) = explode("\r\n\r\n", $response, 2);
if(preg_match('#Content-Encoding:\s+(\w+)#i', $header, $match)) {
switch (strtolower($match[1])) {
case 'gzip':
$body = gzdecode($body);
break;
case 'compress':
$body = gzuncompress($body);
break;
case 'deflate':
$body = gzdeflate($body);
break;
}
}
echo $header;
echo $body;
Disclaimer: gzdecode might not be available in your PHP-version. I've tested it with PHP 5.4.4 and it worked.
You could also install the HTTP_Request2-PEAR package which does that for you (plus you get easy access to the headers without HTTP-header parsing):
include 'HTTP/Request2.php';
$request = new HTTP_Request2('http://api.stackoverflow.com/1.1/badges',
HTTP_Request2::METHOD_GET);
$response = $request->send();
echo $response->getBody();

How to use php curl to get contacts from icontact?

I am trying to get the list of contacts for my account in iContact using a php script. I can get the contact list using RestClient by entering the set of headers, the url and pressing go so I'm pretty sure my headers and url are correct. iContact provides example code for this task however when I run that (with my account and ap details) I get the same response as when I run the following code:
<?php
$Headers = array(
"Accept: text/xml",
"Content-Type: text/xml",
"API-Version: 2.2",
"API-AppId: grBddgWuirhAYT41K6gvrvRGaUGJFVQL",
"API-Username: <removed>",
"API-Password: <removed>");
$Url = "https://app.sandbox.icontact.com/icp/a/412608/c/123920/contacts";
$Handle = curl_init();
if(!$Handle)
{
die("Could not create a cURL handle.");
}
curl_setopt($Handle, CURLOPT_URL, $Url);
curl_setopt($Handle, CURLOPT_HTTPHEADER, $Headers);
curl_setopt($Handle, CURLOPT_USERAGENT, "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.15) Gecko/20110303 Ubuntu/10.04 (lucid) Firefox/3.6.15");
curl_setopt($Handle, CURLOPT_RETURNTRANSFER, true);
$Response = curl_exec($Handle);
$iCode = curl_getinfo($Handle);
echo "Response: <br />";
print_r($Response);
echo "<br /><br /><br /><br /><br /><br /><br /><br /><br /><br />";
echo "Code: <br />";
print_r($iCode);
curl_close($Handle);
?>
This gives the out put:
Response:
Code:
Array ( [url] => https://app.sandbox.icontact.com/icp/a/412608/c/123920/contacts [content_type] => [http_code] => 0 [header_size] => 0 [request_size] => 0 [filetime] => -1 [ssl_verify_result] => 0 [redirect_count] => 0 [total_time] => 0.094 [namelookup_time] => 0 [connect_time] => 0.094 [pretransfer_time] => 0 [size_upload] => 0 [size_download] => 0 [speed_download] => 0 [speed_upload] => 0 [download_content_length] => -1 [upload_content_length] => -1 [starttransfer_time] => 0 [redirect_time] => 0 )
From what I understand of curl (which isn't very much at the moment) the http_code should be 200 and the xml should be in $Response.
Could anyone point out what I'm doing wrong please?
The iContact examples are available here:
http://developer.icontact.com/documentation/code-library-zip-file/
I am trying to do what get_contacts.php should do.
The problem wasn't with the code, that was fine.
I couldn't run it and get a response from my localhost though, had to use a public server.

Curl Errors While Pulling from Twitter

I'm trying to pull some data from twitter via PHP. I'm using the tmhOAuth plugin, which can be found here. https://github.com/themattharris/tmhOAuth/
I wrote my code based off the example file "streaming.php", which can also be found on the above github page. Here is my code:
require 'tmhOAuth.php';
$tmhOAuth = new tmhOAuth(array(
'consumer_key' => 'xxxhiddenxxx',
'consumer_secret' => 'xxxhiddenxxx',
'user_token' => 'xxxhiddenxxx',
'user_secret' => 'xxxhiddenxxx'
));
$method = 'http://stream.twitter.com/1/statuses/filter.json';
$params = array(
'follow' => '1307392917',
'count' => '5'
);
$tmhOAuth->streaming_request('POST', $method, $params, 'my_streaming_callback');
$tmhOAuth->pr($tmhOAuth);
That was not printing out any of the twitter data I wanted to pull, and was only showing the debug information that the pr() command writes.
While trying to debug why I wasn't getting any data, I went in and added a line to tmhOAuth.php so that I could see what error cURL was giving. I did this by using
echo curl_error($C);
The error that cURL outputed was :
transfer closed with outstanding read data remaining
I've done some research on that error, but I can't find anything that helps. There were a couple things that I found regarding content-length, but when I dug into the code I saw that the author of tmhOAuth had already addressed those issues (and commenting out his fixes didn't help).
Any help?
Update 1 Here is the response info gathered using curl_getinfo:
//Removed - an updated version is below
Update 2 Thanks to the comments below I realized that twitter was sending me data with transfer-encoding: chunked. I put this line into tmhOAuth.php to force out chunked data:
curl_setopt($c, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
That worked, somewhat. I'm no longer getting any cURL errors, but my WRITEFUNCTION callback is still never getting called - so I'm never getting any actual data. Here's the output of my cURL object again:
[response] => Array
(
[content-length] => 0
[headers] => Array
(
[content_type] => text/html; charset=iso-8859-1
[server] => Jetty(6.1.25)
)
[code] => 416
[response] => 1
[info] => Array
(
[url] => http://stream.twitter.com/1/statuses/filter.json
[content_type] => text/html; charset=iso-8859-1
[http_code] => 416
[header_size] => 116
[request_size] => 532
[filetime] => -1
[ssl_verify_result] => 0
[redirect_count] => 0
[total_time] => 0.118553
[namelookup_time] => 0.043927
[connect_time] => 0.070477
[pretransfer_time] => 0.07049
[size_upload] => 25
[size_download] => 0
[speed_download] => 0
[speed_upload] => 210
[download_content_length] => -1
[upload_content_length] => -1
[starttransfer_time] => 0.118384
[redirect_time] => 0
[request_header] => POST /1/statuses/filter.json HTTP/1.0
User-Agent: themattharris' HTTP Client
Host: stream.twitter.com
Accept: */*
Authorization: OAuth oauth_consumer_key="xxxhiddenxxx", oauth_nonce="xxxhidden", oauth_signature="xxxhidden", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1308226585", oauth_token="xxxhiddenxxx", oauth_version="1.0"
Content-Length: 25
Content-Type: application/x-www-form-urlencoded
)
)
)
Update 3: Couple things I've figured out so far... I removed the 'count' parameters from my POST request, and now the page seems to take forever. I figured this meant it was just downloading tons and tons of data, so I put a break into the streaming callback function, setup so that it kills the page after 5 loops.
I did this, and let it sit for quite awhile. After about 5 minutes, the page finished loading, and showed me what data I had gathered. It looked like I had gotten no data each time it ran through - only an end of line character. So, it's taking a minute for every piece of data I am downloading, and even then the only data that shows is an end of line character. Weird? Is this a twitter issue or a cURL issue?
I tried with the token api but never got something good, so this is the script I found here :
<?php
/**
* API Streaming for Twitter.
*
* #author Loïc Gerbaud <gerbaudloic#gmail.com>
* #version 0.1 "itjustworks"
*/
define('TWITTER_LOGIN','login'); //login twitter
define('TWITTER_PASSWORD','myp4ssw0rd'); //password twitter
$sTrackingList = 504443371;//read my account but could be keywords
// ?
while(1){
echo 'Connexion ';
read_the_stream($sTrackingList);
echo 'Deconnexion ';
}
/**read the stream
*
*/
function read_the_stream($sTrackingList){
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,'https://stream.twitter.com/1/statuses/filter.json');
curl_setopt($ch,CURLOPT_USERPWD,TWITTER_LOGIN.':'.TWITTER_PASSWORD);//Le couple login:password
curl_setopt($ch, CURLOPT_NOBODY, 0);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_USERAGENT, '');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION,1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('X-Twitter-Client: ItsMe','X-Twitter-Client-Version: 0.1','X-Twitter-Client-URL: http://blog.loicg.net/'));
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS,"follow=".$sTrackingList);//read the doc for your request
curl_setopt($ch, CURLOPT_WRITEFUNCTION, 'write_callback');//function callback
curl_exec($ch);
curl_close($ch);
}
/** a demo with a writting log or put in MySQL
*/
function write_callback($ch, $data) {
if(strlen($data)>2){
$oData = json_decode($data);
if(isset($oData->text)){
file_put_contents('log',$oData->text."\n",FILE_APPEND);
}
}
return strlen($data);
}
?>
run this script in your browser (you can close it after), update your twitter account and check the .log
After about 5 minutes, the page finished loading
Are you running streaming.php in the browser? If so, you have to run it via ssl, otherwise it doesn't work. I have a server chron job pointing to the file but you can do it also with the terminal:
php /path/to/here/streaming.php
For view the data you are getting, you can store it into a database or log:
function my_streaming_callback($data, $length, $metrics) {
$ddf = fopen('/twitter/mydata.log','a');
fwrite($ddf,$data);
fclose($ddf);
}

Categories