User Name Password Register
DaniWeb IT Discussion Community
All
What is DaniWeb IT Discussion Community?
You're currently browsing the PHP section within the Web Development category of DaniWeb, a massive community of 427,396 software developers, web developers, Internet marketers, and tech gurus who are all enthusiastic about making contacts, networking, and learning from each other. In fact, there are 3,233 IT professionals currently interacting right now! Registration is free, only takes a minute and lets you enjoy all of the interactive features of the site.
Please support our PHP advertiser: Lunarpages PHP Web Hosting
Jun 21st, 2005
Views: 12,329
class_http.php is a "screen-scraping" utility that makes it easy to scrape content and cache scraped content for any number of seconds desired before hitting the live source again. Caching makes you a good neighbor!

The class has 2 static methods that make it easy to extract individual tables of data out of web pages. The class even comes with a companion script that makes it easy to use and cache external images directly within img elements.

The class cloaks itself as the User Agent of the user making the request to your script. It also sends your script as the Referer, since in essence, it is the referrer. This means you should be able to screen-scrape sites that normally block screen-scraping. This class is not meant to help you break any company's usage policies. Be a good neighbor, and always use caching when you can.

Need to access protected content? The class can do basic authentication. However, a lot of sites that require login do not use basic authentication.

Most current information and documentation and downloads found at
http://www.troywolf.com/articles/php/class_http.

There are three complete PHP files listed below. First is the class file, class_http.php. The second is example.php to show you how to use the class. The third file is image_cache.php--a companion script to cache images for use within the src attribute of img elements.

Troy Wolf operates ShinySolutions Webhosting, and is the author of SnippetEdit--a PHP application providing browser-based website editing that even non-technical people can use. "Website editing as easy as it gets." Troy has been a professional Internet and database application developer for over 10 years. He has many years' experience with ASP, VBScript, PHP, Javascript, DHTML, CSS, SQL, and XML on Windows and Linux platforms.
php Syntax | 5 stars
  1. =====================================================
  2. class_http.php
  3. =====================================================
  4. <?php
  5. /*
  6. * Filename.......: class_http.php
  7. * Author.........: Troy Wolf [troy@troywolf.com]
  8. * Last Modified..: Date: 2006/03/06 10:15:00
  9. * Description....: Screen-scraping class with caching. Includes image_cache.php
  10.   companion script. Includes static methods to extract data
  11.   out of HTML tables into arrays or XML. Now supports sending
  12.   XML requests and custom verbs with support for making
  13.   WebDAV requests to Microsoft Exchange Server.
  14. */
  15.  
  16. class http {
  17. var $log;
  18. var $dir;
  19. var $name;
  20. var $filename;
  21. var $url;
  22. var $port;
  23. var $verb;
  24. var $status;
  25. var $header;
  26. var $body;
  27. var $ttl;
  28. var $headers;
  29. var $postvars;
  30. var $xmlrequest;
  31. var $connect_timeout;
  32. var $data_ts;
  33.  
  34. /*
  35.   The class constructor. Configure defaults.
  36.   */
  37. function http() {
  38. $this->log = "New http() object instantiated.<br />\n";
  39.  
  40. /*
  41.   Seconds to attempt socket connection before giving up.
  42.   */
  43. $this->connect_timeout = 30;
  44.  
  45. /*
  46.   Set the 'dir' property to the directory where you want to store the cached
  47.   content. I suggest a folder that is not web-accessible.
  48.   End this value with a "/".
  49.   */
  50. $this->dir = realpath("./")."/"; //Default to current dir.
  51.  
  52. $this->clean();
  53.  
  54. return true;
  55. }
  56.  
  57. /*
  58.   fetch() method to get the content. fetch() will use 'ttl' property to
  59.   determine whether to get the content from the url or the cache.
  60.   */
  61. function fetch($url="", $ttl=0, $name="", $user="", $pwd="", $verb="GET") {
  62. $this->log .= "--------------------------------<br />fetch() called<br />\n";
  63. $this->log .= "url: ".$url."<br />\n";
  64. $this->status = "";
  65. $this->header = "";
  66. $this->body = "";
  67. if (!$url) {
  68. $this->log .= "OOPS: You need to pass a URL!<br />";
  69. return false;
  70. }
  71. $this->url = $url;
  72. $this->ttl = $ttl;
  73. $this->name = $name;
  74. $need_to_save = false;
  75. if ($this->ttl == "0") {
  76. if (!$fh = $this->getFromUrl($url, $user, $pwd, $verb)) {
  77. return false;
  78. }
  79. } else {
  80. if (strlen(trim($this->name)) == 0) { $this->name = MD5($url); }
  81. $this->filename = $this->dir."http_".$this->name;
  82. $this->log .= "Filename: ".$this->filename."<br />";
  83. $this->getFile_ts();
  84. if ($this->ttl == "daily") {
  85. if (date('Y-m-d',$this->data_ts) != date('Y-m-d',time())) {
  86. $this->log .= "cache has expired<br />";
  87. if (!$fh = $this->getFromUrl($url, $user, $pwd, $verb)) {
  88. return false;
  89. }
  90. $need_to_save = true;
  91. if ($this->getFromUrl()) { return $this->saveToCache(); }
  92. } else {
  93. if (!$fh = $this->getFromCache()) {
  94. return false;
  95. }
  96. }
  97. } else {
  98. if ((time() - $this->data_ts) >= $this->ttl) {
  99. $this->log .= "cache has expired<br />";
  100. if (!$fh = $this->getFromUrl($url, $user, $pwd)) {
  101. return false;
  102. }
  103. $need_to_save = true;
  104. } else {
  105. if (!$fh = $this->getFromCache()) {
  106. return false;
  107. }
  108. }
  109. }
  110. }
  111.  
  112. /*
  113.   Get response header.
  114.   */
  115. $this->header = fgets($fh, 1024);
  116. $this->status = substr($this->header,9,3);
  117. while ((trim($line = fgets($fh, 1024)) != "") && (!feof($fh))) {
  118. $this->header .= $line;
  119. if ($this->status=="401" and strpos($line,"WWW-Authenticate: Basic realm=\"")===0) {
  120. fclose($fh);
  121. $this->log .= "Could not authenticate<br />\n";
  122. return FALSE;
  123. }
  124. }
  125.  
  126. /*
  127.   Get response body.
  128.   */
  129. while (!feof($fh)) {
  130. $this->body .= fgets($fh, 1024);
  131. }
  132. fclose($fh);
  133. if ($need_to_save) { $this->saveToCache(); }
  134. return $this->status;
  135. }
  136.  
  137. /*
  138.   PRIVATE getFromUrl() method to scrape content from url.
  139.   */
  140. function getFromUrl($url, $user="", $pwd="", $verb="GET") {
  141. $this->log .= "getFromUrl() called<br />";
  142. preg_match("~([a-z]*://)?([^:^/]*)(:([0-9]{1,5}))?(/.*)?~i", $url, $parts);
  143. $protocol = $parts[1];
  144. $server = $parts[2];
  145. $port = $parts[4];
  146. $path = $parts[5];
  147. if ($port == "") {
  148. if (strtolower($protocol) == "https://") {
  149. $port = "443";
  150. } else {
  151. $port = "80";
  152. }
  153. }
  154.  
  155. if ($path == "") { $path = "/"; }
  156.  
  157. if (!$sock = @fsockopen(((strtolower($protocol) == "https://")?"ssl://":"").$server, $port, $errno, $errstr, $this->connect_timeout)) {
  158. $this->log .= "Could not open connection. Error "
  159. .$errno.": ".$errstr."<br />\n";
  160. return false;
  161. }
  162.  
  163. $this->headers["Host"] = $server.":".$port;
  164.  
  165. if ($user != "" && $pwd != "") {
  166. $this->log .= "Authentication will be attempted<br />\n";
  167. $this->headers["Authorization"] = "Basic ".base64_encode($user.":".$pwd);
  168. }
  169.  
  170. if (count($this->postvars) > 0) {
  171. $this->log .= "Variables will be POSTed<br />\n";
  172. $request = "POST ".$path." HTTP/1.0\r\n";
  173. $post_string = "";
  174. foreach ($this->postvars as $key=>$value) {
  175. $post_string .= "&".urlencode($key)."=".urlencode($value);
  176. }
  177. $post_string = substr($post_string,1);
  178. $this->headers["Content-Type"] = "application/x-www-form-urlencoded";
  179. $this->headers["Content-Length"] = strlen($post_string);
  180. } elseif (strlen($this->xmlrequest) > 0) {
  181. $this->log .= "XML request will be sent<br />\n";
  182. $request = $verb." ".$path." HTTP/1.0\r\n";
  183. $this->headers["Content-Length"] = strlen($this->xmlrequest);
  184. } else {
  185. $request = $verb." ".$path." HTTP/1.0\r\n";
  186. }
  187.  
  188. #echo "<br />request: ".$request;
  189.  
  190.  
  191. if (fwrite($sock, $request) === FALSE) {
  192. fclose($sock);
  193. $this->log .= "Error writing request type to socket<br />\n";
  194. return false;
  195. }
  196.  
  197. foreach ($this->headers as $key=>$value) {
  198. if (fwrite($sock, $key.": ".$value."\r\n") === FALSE) {
  199. fclose($sock);
  200. $this->log .= "Error writing headers to socket<br />\n";
  201. return false;
  202. }
  203. }
  204.  
  205. if (fwrite($sock, "\r\n") === FALSE) {
  206. fclose($sock);
  207. $this->log .= "Error writing end-of-line to socket<br />\n";
  208. return false;
  209. }
  210.  
  211. #echo "<br />post_string: ".$post_string;
  212. if (count($this->postvars) > 0) {
  213. if (fwrite($sock, $post_string."\r\n") === FALSE) {
  214. fclose($sock);
  215. $this->log .= "Error writing POST string to socket<br />\n";
  216. return false;
  217. }
  218. } elseif (strlen($this->xmlrequest) > 0) {
  219. if (fwrite($sock, $this->xmlrequest."\r\n") === FALSE) {
  220. fclose($sock);
  221. $this->log .= "Error writing xml request string to socket<br />\n";
  222. return false;
  223. }
  224. }
  225.  
  226. return $sock;
  227. }
  228.  
  229. /*
  230.   PRIVATE clean() method to reset the instance back to mostly new state.
  231.   */
  232. function clean()
  233. {
  234. $this->status = "";
  235. $this->header = "";
  236. $this->body = "";
  237. $this->headers = array();
  238. $this->postvars = array();
  239. /*
  240.   Try to use user agent of the user making this request. If not available,
  241.   default to IE6.0 on WinXP, SP1.
  242.   */
  243. if (isset($_SERVER['HTTP_USER_AGENT'])) {
  244. $this->headers["User-Agent"] = $_SERVER['HTTP_USER_AGENT'];
  245. } else {
  246. $this->headers["User-Agent"] = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)";
  247. }
  248.  
  249. /*
  250.   Set referrer to the current script since in essence, it is the referring
  251.   page.
  252.   */
  253. if (substr($_SERVER['SERVER_PROTOCOL'],0,5) == "HTTPS") {
  254. $this->headers["Referer"] = "https://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
  255. } else {
  256. $this->headers["Referer"] = "http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'];
  257. }
  258. }
  259.  
  260. /*
  261.   PRIVATE getFromCache() method to retrieve content from cache file.
  262.   */
  263. function getFromCache() {
  264. $this->log .= "getFromCache() called<br />";
  265. //create file pointer
  266. if (!$fp=@fopen($this->filename,"r")) {
  267. $this->log .= "Could not open ".$this->filename."<br />";
  268. return false;
  269. }
  270. return $fp;
  271. }
  272.  
  273. /*
  274.   PRIVATE saveToCache() method to save content to cache file.
  275.   */
  276. function saveToCache() {
  277. $this->log .= "saveToCache() called<br />";
  278.  
  279. //create file pointer
  280. if (!$fp=@fopen($this->filename,"w")) {
  281. $this->log .= "Could not open ".$this->filename."<br />";
  282. return false;
  283. }
  284. //write to file
  285. if (!@fwrite($fp,$this->header."\r\n".$this->body)) {
  286. $this->log .= "Could not write to ".$this->filename."<br />";
  287. fclose($fp);
  288. return false;
  289. }
  290. //close file pointer
  291. fclose($fp);
  292. return true;
  293. }
  294.  
  295. /*
  296.   PRIVATE getFile_ts() method to get cache file modified date.
  297.   */
  298. function getFile_ts() {
  299. $this->log .= "getFile_ts() called<br />";
  300. if (!file_exists($this->filename)) {
  301. $this->data_ts = 0;
  302. $this->log .= $this->filename." does not exist<br />";
  303. return false;
  304. }
  305. $this->data_ts = filemtime($this->filename);
  306. return true;
  307. }
  308.  
  309. /*
  310.   Static method table_into_array()
  311.   Generic function to return data array from HTML table data
  312.   rawHTML: the page source
  313.   needle: optional string to start parsing source from
  314.   needle_within: 0 = needle is BEFORE table, 1 = needle is within table
  315.   allowed_tags: list of tags to NOT strip from data, e.g. "<a><b>"
  316.   */
  317. function table_into_array($rawHTML,$needle="",$needle_within=0,$allowed_tags="") {
  318. $upperHTML = strtoupper($rawHTML);
  319. $idx = 0;
  320. if (strlen($needle) > 0) {
  321. $needle = strtoupper($needle);
  322. $idx = strpos($upperHTML,$needle);
  323. if ($idx === false) { return false; }
  324. if ($needle_within == 1) {
  325. $cnt = 0;
  326. while(($cnt < 100) && (substr($upperHTML,$idx,6) != "<TABLE")) {
  327. $idx = strrpos(substr($upperHTML,0,$idx-1),"<");
  328. $cnt++;
  329. }
  330. }
  331. }
  332. $aryData = array();
  333. $rowIdx = 0;
  334. /* If this table has a header row, it may use TD or TH, so
  335.   check special for this first row. */
  336. $tmp = strpos($upperHTML,"<TR",$idx);
  337. if ($tmp === false) { return false; }
  338. $tmp2 = strpos($upperHTML,"</TR>",$tmp);
  339. if ($tmp2 === false) { return false; }
  340. $row = substr($rawHTML,$tmp,$tmp2-$tmp);
  341. $pattern = "/<TH>|<TH\ |<TD>|<TD\ /";
  342. preg_match($pattern,strtoupper($row),$matches);
  343. $hdrTag = $matches[0];
  344.  
  345. while ($tmp = strpos(strtoupper($row),$hdrTag) !== false) {
  346. $tmp = strpos(strtoupper($row),">",$tmp);
  347. if ($tmp === false) { return false; }
  348. $tmp++;
  349. $tmp2 = strpos(strtoupper($row),"</T");
  350. $aryData[$rowIdx][] = trim(strip_tags(substr($row,$tmp,$tmp2-$tmp),$allowed_tags));
  351. $row = substr($row,$tmp2+5);
  352. preg_match($pattern,strtoupper($row),$matches);
  353. $hdrTag = $matches[0];
  354. }
  355. $idx = strpos($upperHTML,"</TR>",$idx)+5;
  356. $rowIdx++;
  357.  
  358. /* Now parse the rest of the rows. */
  359. $tmp = strpos($upperHTML,"<TR",$idx);
  360. if ($tmp === false) { return false; }
  361. $tmp2 = strpos($upperHTML,"</TABLE>",$idx);
  362. if ($tmp2 === false) { return false; }
  363. $table = substr($rawHTML,$tmp,$tmp2-$tmp);
  364.  
  365. while ($tmp = strpos(strtoupper($table),"<TR") !== false) {
  366. $tmp2 = strpos(strtoupper($table),"</TR");
  367. if ($tmp2 === false) { return false; }
  368. $row = substr($table,$tmp,$tmp2-$tmp);
  369.  
  370. while ($tmp = strpos(strtoupper($row),"<TD") !== false) {
  371. $tmp = strpos(strtoupper($row),">",$tmp);
  372. if ($tmp === false) { return false; }
  373. $tmp++;
  374. $tmp2 = strpos(strtoupper($row),"</TD");
  375. $aryData[$rowIdx][] = trim(strip_tags(substr($row,$tmp,$tmp2-$tmp),$allowed_tags));
  376. $row = substr($row,$tmp2+5);
  377. }
  378. $table = substr($table,strpos(strtoupper($table),"</TR>")+5);
  379. $rowIdx++;
  380. }
  381. return $aryData;
  382. }
  383.  
  384. /*
  385.   Static method table_into_xml()
  386.   Generic function to return xml dataset from HTML table data
  387.   rawHTML: the page source
  388.   needle: optional string to start parsing source from
  389.   allowedTags: list of tags to NOT strip from data, e.g. "<a><b>"
  390.   */
  391. function table_into_xml($rawHTML,$needle="",$needle_within=0,$allowedTags="") {
  392. if (!$aryTable = http::table_into_array($rawHTML,$needle,$needle_within,$allowedTags)) { return false; }
  393. $xml = "<?xml version=\"1.0\" standalone=\"yes\" \?\>\n";
  394. $xml .= "<TABLE>\n";
  395. $rowIdx = 0;
  396. foreach ($aryTable as $row) {
  397. $xml .= "\t<ROW id=\"".$rowIdx."\">\n";
  398. $colIdx = 0;
  399. foreach ($row as $col) {
  400. $xml .= "\t\t<COL id=\"".$colIdx."\">".trim(utf8_encode(htmlspecialchars($col)))."</COL>\n";
  401. $colIdx++;
  402. }
  403. $xml .= "\t</ROW>\n";
  404. $rowIdx++;
  405. }
  406. $xml .= "</TABLE>";
  407. return $xml;
  408. }
  409. }
  410.  
  411. ?>
  412.  
  413. =====================================================
  414. example.php
  415. =====================================================
  416. <?php
  417. /*
  418. * example.php
  419. * class_http.php example usage
  420. * Author: Troy Wolf (troy@troywolf.com)
  421. * Comments: Please be a good neighbor when screen-scraping. Don't write code
  422.   that will needlessly make hits to third-party websites. Use
  423.   class_http's caching feature whenever possible. It is designed to
  424.   make you a good neighbor!
  425. */
  426.  
  427. /*
  428. Include the http class. Modify path according to where you put the class
  429. file.
  430. */
  431. require_once(dirname(__FILE__).'/class_http.php');
  432.  
  433. /* -----------------------------------------------------------------------------
  434. Example to screen-scrape the Google home page without caching.
  435. ----------------------------------------------------------------------------- */
  436. /* First, instantiate a new http object. */
  437. $h = new http();
  438.  
  439. /* Screen-scrape a url. */
  440. if (!$h->fetch("http://www.google.com")) {
  441. /*
  442.   The class has a 'log' property that contains a log of events. This log is
  443.   useful for testing and debugging.
  444.   */
  445. echo "<h2>There is a problem with the http request!</h2>";
  446. echo $h->log;
  447. exit();
  448. }
  449.  
  450. /* Echo out the body content fetched from the url. */
  451. echo $h->body;
  452.  
  453. /* If you just want to know the HTTP status code: */
  454. echo "Status: ".$h->status;
  455.  
  456. /* If you are interested in seeing all the response headers: */
  457. echo "<pre>".$h->header."</pre>";
  458.  
  459.  
  460. /* -----------------------------------------------------------------------------
  461. Example to screen-scrape the MSFT stock page at moneycentral.com WITH caching.
  462. ----------------------------------------------------------------------------- */
  463. /* First, instantiate a new http object. */
  464. $h = new http();
  465.  
  466. /*
  467. Set a TTL (Time-to-Live) in seconds. A value of 600 means your site will not hit
  468. the source site more than once every 10 minutes. This makes your page faster and
  469. makes you a better neighbor to the external site.
  470. */
  471. $h->ttl = 600;
  472.  
  473. /*
  474. You can give the request a 'name' which will be used in the cache file name.
  475. If you don't name the request, an MD5 hash of the url will be used.
  476. */
  477. #$h->name = "msft_quote";
  478.  
  479. /* Screen-scrape a url. */
  480. if (!$h->fetch("http://moneycentral.msn.com/detail/stock_quote?Symbol=MSFT")) {
  481. /*
  482.   The class has a 'log' property that contains a log of events. This log is
  483.   useful for testing and debugging.
  484.   */
  485. echo "<h2>There is a problem with the http request!</h2>";
  486. echo $h->log;
  487. exit();
  488. }
  489.  
  490. /* Echo out the body content fetched from the url. */
  491. echo $h->body;
  492.  
  493.  
  494. /* -----------------------------------------------------------------------------
  495. Example to extract a specific table of data out of scraped content. The class
  496. comes with 2 static methods you can use for this purpose.
  497.   table_into_array() will rip a single table into an array.
  498.   table_into_xml() will internally call table_into_array() then create an
  499.   XML document from the array. I thought this would be cool, but in practice,
  500.   I've never used this method since the array is so easy to work with.
  501.  
  502. This example builds on the previous example to extract the MSFT stats out
  503. of the body content. Read the comments in the class file to learn how to use
  504. this static method.
  505. ----------------------------------------------------------------------------- */
  506. $msft_stats = http::table_into_array($h->body, "Avg Daily Volume", 1, null);
  507.  
  508. /* Print out the array so you can see the stats data. */
  509. echo "<pre>";
  510. print_r($msft_stats);
  511. echo "</pre>";
  512.  
  513.  
  514. /* -----------------------------------------------------------------------------
  515. Scraping content that is username/password protected. The class can do basic
  516. authentication. Pass your username and password in like this:
  517. ----------------------------------------------------------------------------- */
  518. #$h->fetch("http://someprivatesite.net","MyUserName","MyPassword");
  519.  
  520.  
  521. /* -----------------------------------------------------------------------------
  522. If your need to access content on a port other than 80, just put the port in
  523. the URL in the standard way:
  524. ----------------------------------------------------------------------------- */
  525. #$h->fetch("http://somedomain.org:8088");
  526.  
  527.  
  528. /* -----------------------------------------------------------------------------
  529. Example of using the image_cache.php companion script to cache images. Why not
  530. just link directly to a neighbor's images? If your site has a lot of traffic,
  531. that's a lot of hits to your neighbor's site. So why not just copy their image
  532. to your own server? That's fine for images that do not change, but some sites
  533. create dynamic images such as stock charts that are generated new every minute.
  534. image_cache.php in conjunction with class_http.php makes it easy to directly
  535. link to third-party images and cache the image data for whatever ttl makes
  536. sense for your application.
  537.  
  538. In this example, we will cache the chart image found at this moneycentral page:
  539. http://moneycentral.msn.com/investor/charts/chartdl.asp?FC=1&Symbol=MSFT&CA=1&CB=1&CC=1&CD=1&CP=0&PT=5
  540. You have to look at the page source code to find the url to their image. Then
  541. you url encode their image url, and pass it as a parameter to image_cache.php.
  542. ----------------------------------------------------------------------------- */
  543. ?>
  544.  
  545. <img src="image_cache.php?ttl=60&url=http%3A%2F%2Fdata.moneycentral.msn.com%2Fscripts%2Fchrtsrv.dll%3FSymbol%3DMSFT%26C1%3D0%26C2%3D1%26C9%3D2%26CA%3D1%26CB%3D1%26CC%3D1%26CD%3D1%26CF%3D0%26EFR%3D236%26EFG%3D246%26EFB%3D254%26E1%3D0" width="448" height="300" alt="Chart Graphic" />
  546.  
  547. <?
  548.  
  549.  
  550. /*
  551. The class has a 'log' property that is very useful for testing and debugging.
  552. During development, I suggest you always print this out so you can see what is
  553. happening.
  554. */
  555. echo "<h3>http log</h3>";
  556. echo $h->log;
  557.  
  558. ?>
  559.  
  560.  
  561. =====================================================
  562. image_cache.php
  563. =====================================================
  564. <?php
  565. /*
  566. * Filename.......: image_cache.php
  567. * Author.........: Troy Wolf [troy@troywolf.com]
  568. * Last Modified..: Date: 2005/06/21 10:30:00
  569. * Description....: Companion script to clas_http.php. When used in conjunction
  570.   with class_http.php, can be used to "screen-scrape" images
  571.   and cache them locally for any number of seconds. You use
  572.   this script in-line within img tags like so:
  573. <img src="image_cache.php?ttl=300&url=http%3A%2F%2Fwww.somedomain.com%2Fsomeimage.gif" />
  574.   (You must url encode the url within the src attribute.)
  575. */
  576.  
  577. /*
  578. Include the http class. Modify path according to where you put the class
  579. file.
  580. */
  581. require_once(dirname(__FILE__).'/class_http.php');
  582.  
  583. $h = new http();
  584. $h->ttl = $_GET['ttl'];
  585. $h->fetch($_GET['url']);
  586. header("Content-Type: image/jpeg");
  587. echo $h->body;
  588. ?>