fix image text
[pear] / PEAR / REST.php
1 <?php
2 /**
3  * PEAR_REST
4  *
5  * PHP versions 4 and 5
6  *
7  * @category   pear
8  * @package    PEAR
9  * @author     Greg Beaver <cellog@php.net>
10  * @copyright  1997-2009 The Authors
11  * @license    http://opensource.org/licenses/bsd-license.php New BSD License
12  * @version    CVS: $Id: REST.php 313023 2011-07-06 19:17:11Z dufuz $
13  * @link       http://pear.php.net/package/PEAR
14  * @since      File available since Release 1.4.0a1
15  */
16
17 /**
18  * For downloading xml files
19  */
20 require_once 'PEAR.php';
21 require_once 'PEAR/XMLParser.php';
22
23 /**
24  * Intelligently retrieve data, following hyperlinks if necessary, and re-directing
25  * as well
26  * @category   pear
27  * @package    PEAR
28  * @author     Greg Beaver <cellog@php.net>
29  * @copyright  1997-2009 The Authors
30  * @license    http://opensource.org/licenses/bsd-license.php New BSD License
31  * @version    Release: 1.9.4
32  * @link       http://pear.php.net/package/PEAR
33  * @since      Class available since Release 1.4.0a1
34  */
35 class PEAR_REST
36 {
37     var $config;
38     var $_options;
39
40     function PEAR_REST(&$config, $options = array())
41     {
42         $this->config   = &$config;
43         $this->_options = $options;
44     }
45
46     /**
47      * Retrieve REST data, but always retrieve the local cache if it is available.
48      *
49      * This is useful for elements that should never change, such as information on a particular
50      * release
51      * @param string full URL to this resource
52      * @param array|false contents of the accept-encoding header
53      * @param boolean     if true, xml will be returned as a string, otherwise, xml will be
54      *                    parsed using PEAR_XMLParser
55      * @return string|array
56      */
57     function retrieveCacheFirst($url, $accept = false, $forcestring = false, $channel = false)
58     {
59         $cachefile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR .
60             md5($url) . 'rest.cachefile';
61
62         if (file_exists($cachefile)) {
63             return unserialize(implode('', file($cachefile)));
64         }
65
66         return $this->retrieveData($url, $accept, $forcestring, $channel);
67     }
68
69     /**
70      * Retrieve a remote REST resource
71      * @param string full URL to this resource
72      * @param array|false contents of the accept-encoding header
73      * @param boolean     if true, xml will be returned as a string, otherwise, xml will be
74      *                    parsed using PEAR_XMLParser
75      * @return string|array
76      */
77     function retrieveData($url, $accept = false, $forcestring = false, $channel = false)
78     {
79         $cacheId = $this->getCacheId($url);
80         if ($ret = $this->useLocalCache($url, $cacheId)) {
81             return $ret;
82         }
83
84         $file = $trieddownload = false;
85         if (!isset($this->_options['offline'])) {
86             $trieddownload = true;
87             $file = $this->downloadHttp($url, $cacheId ? $cacheId['lastChange'] : false, $accept, $channel);
88         }
89
90         if (PEAR::isError($file)) {
91             if ($file->getCode() !== -9276) {
92                 return $file;
93             }
94
95             $trieddownload = false;
96             $file = false; // use local copy if available on socket connect error
97         }
98
99         if (!$file) {
100             $ret = $this->getCache($url);
101             if (!PEAR::isError($ret) && $trieddownload) {
102                 // reset the age of the cache if the server says it was unmodified
103                 $result = $this->saveCache($url, $ret, null, true, $cacheId);
104                 if (PEAR::isError($result)) {
105                     return PEAR::raiseError($result->getMessage());
106                 }
107             }
108
109             return $ret;
110         }
111
112         if (is_array($file)) {
113             $headers      = $file[2];
114             $lastmodified = $file[1];
115             $content      = $file[0];
116         } else {
117             $headers      = array();
118             $lastmodified = false;
119             $content      = $file;
120         }
121
122         if ($forcestring) {
123             $result = $this->saveCache($url, $content, $lastmodified, false, $cacheId);
124             if (PEAR::isError($result)) {
125                 return PEAR::raiseError($result->getMessage());
126             }
127
128             return $content;
129         }
130
131         if (isset($headers['content-type'])) {
132             switch ($headers['content-type']) {
133                 case 'text/xml' :
134                 case 'application/xml' :
135                 case 'text/plain' :
136                     if ($headers['content-type'] === 'text/plain') {
137                         $check = substr($content, 0, 5);
138                         if ($check !== '<?xml') {
139                             break;
140                         }
141                     }
142
143                     $parser = new PEAR_XMLParser;
144                     PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
145                     $err = $parser->parse($content);
146                     PEAR::popErrorHandling();
147                     if (PEAR::isError($err)) {
148                         return PEAR::raiseError('Invalid xml downloaded from "' . $url . '": ' .
149                             $err->getMessage());
150                     }
151                     $content = $parser->getData();
152                 case 'text/html' :
153                 default :
154                     // use it as a string
155             }
156         } else {
157             // assume XML
158             $parser = new PEAR_XMLParser;
159             $parser->parse($content);
160             $content = $parser->getData();
161         }
162
163         $result = $this->saveCache($url, $content, $lastmodified, false, $cacheId);
164         if (PEAR::isError($result)) {
165             return PEAR::raiseError($result->getMessage());
166         }
167
168         return $content;
169     }
170
171     function useLocalCache($url, $cacheid = null)
172     {
173         if ($cacheid === null) {
174             $cacheidfile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR .
175                 md5($url) . 'rest.cacheid';
176             if (!file_exists($cacheidfile)) {
177                 return false;
178             }
179
180             $cacheid = unserialize(implode('', file($cacheidfile)));
181         }
182
183         $cachettl = $this->config->get('cache_ttl');
184         // If cache is newer than $cachettl seconds, we use the cache!
185         if (time() - $cacheid['age'] < $cachettl) {
186             return $this->getCache($url);
187         }
188
189         return false;
190     }
191
192     function getCacheId($url)
193     {
194         $cacheidfile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR .
195             md5($url) . 'rest.cacheid';
196
197         if (!file_exists($cacheidfile)) {
198             return false;
199         }
200
201         $ret = unserialize(implode('', file($cacheidfile)));
202         return $ret;
203     }
204
205     function getCache($url)
206     {
207         $cachefile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR .
208             md5($url) . 'rest.cachefile';
209
210         if (!file_exists($cachefile)) {
211             return PEAR::raiseError('No cached content available for "' . $url . '"');
212         }
213
214         return unserialize(implode('', file($cachefile)));
215     }
216
217     /**
218      * @param string full URL to REST resource
219      * @param string original contents of the REST resource
220      * @param array  HTTP Last-Modified and ETag headers
221      * @param bool   if true, then the cache id file should be regenerated to
222      *               trigger a new time-to-live value
223      */
224     function saveCache($url, $contents, $lastmodified, $nochange = false, $cacheid = null)
225     {
226         $cache_dir   = $this->config->get('cache_dir');
227         $d           = $cache_dir . DIRECTORY_SEPARATOR . md5($url);
228         $cacheidfile = $d . 'rest.cacheid';
229         $cachefile   = $d . 'rest.cachefile';
230
231         if (!is_dir($cache_dir)) {
232             if (System::mkdir(array('-p', $cache_dir)) === false) {
233               return PEAR::raiseError("The value of config option cache_dir ($cache_dir) is not a directory and attempts to create the directory failed.");
234             }
235         }
236
237         if ($cacheid === null && $nochange) {
238             $cacheid = unserialize(implode('', file($cacheidfile)));
239         }
240
241         $idData = serialize(array(
242             'age'        => time(),
243             'lastChange' => ($nochange ? $cacheid['lastChange'] : $lastmodified),
244         ));
245
246         $result = $this->saveCacheFile($cacheidfile, $idData);
247         if (PEAR::isError($result)) {
248             return $result;
249         } elseif ($nochange) {
250             return true;
251         }
252
253         $result = $this->saveCacheFile($cachefile, serialize($contents));
254         if (PEAR::isError($result)) {
255             if (file_exists($cacheidfile)) {
256               @unlink($cacheidfile);
257             }
258
259             return $result;
260         }
261
262         return true;
263     }
264
265     function saveCacheFile($file, $contents)
266     {
267         $len = strlen($contents);
268
269         $cachefile_fp = @fopen($file, 'xb'); // x is the O_CREAT|O_EXCL mode
270         if ($cachefile_fp !== false) { // create file
271             if (fwrite($cachefile_fp, $contents, $len) < $len) {
272                 fclose($cachefile_fp);
273                 return PEAR::raiseError("Could not write $file.");
274             }
275         } else { // update file
276             $cachefile_lstat = lstat($file);
277             $cachefile_fp = @fopen($file, 'wb');
278             if (!$cachefile_fp) {
279                 return PEAR::raiseError("Could not open $file for writing.");
280             }
281
282             $cachefile_fstat = fstat($cachefile_fp);
283             if (
284               $cachefile_lstat['mode'] == $cachefile_fstat['mode'] &&
285               $cachefile_lstat['ino']  == $cachefile_fstat['ino'] &&
286               $cachefile_lstat['dev']  == $cachefile_fstat['dev'] &&
287               $cachefile_fstat['nlink'] === 1
288             ) {
289                 if (fwrite($cachefile_fp, $contents, $len) < $len) {
290                     fclose($cachefile_fp);
291                     return PEAR::raiseError("Could not write $file.");
292                 }
293             } else {
294                 fclose($cachefile_fp);
295                 $link = function_exists('readlink') ? readlink($file) : $file;
296                 return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $file . ' as it is symlinked to ' . $link . ' - Possible symlink attack');
297             }
298         }
299
300         fclose($cachefile_fp);
301         return true;
302     }
303
304     /**
305      * Efficiently Download a file through HTTP.  Returns downloaded file as a string in-memory
306      * This is best used for small files
307      *
308      * If an HTTP proxy has been configured (http_proxy PEAR_Config
309      * setting), the proxy will be used.
310      *
311      * @param string  $url       the URL to download
312      * @param string  $save_dir  directory to save file in
313      * @param false|string|array $lastmodified header values to check against for caching
314      *                           use false to return the header values from this download
315      * @param false|array $accept Accept headers to send
316      * @return string|array  Returns the contents of the downloaded file or a PEAR
317      *                       error on failure.  If the error is caused by
318      *                       socket-related errors, the error object will
319      *                       have the fsockopen error code available through
320      *                       getCode().  If caching is requested, then return the header
321      *                       values.
322      *
323      * @access public
324      */
325     function downloadHttp($url, $lastmodified = null, $accept = false, $channel = false)
326     {
327         static $redirect = 0;
328         // always reset , so we are clean case of error
329         $wasredirect = $redirect;
330         $redirect = 0;
331
332         $info = parse_url($url);
333         if (!isset($info['scheme']) || !in_array($info['scheme'], array('http', 'https'))) {
334             return PEAR::raiseError('Cannot download non-http URL "' . $url . '"');
335         }
336
337         if (!isset($info['host'])) {
338             return PEAR::raiseError('Cannot download from non-URL "' . $url . '"');
339         }
340
341         $host   = isset($info['host']) ? $info['host'] : null;
342         $port   = isset($info['port']) ? $info['port'] : null;
343         $path   = isset($info['path']) ? $info['path'] : null;
344         $schema = (isset($info['scheme']) && $info['scheme'] == 'https') ? 'https' : 'http';
345
346         $proxy_host = $proxy_port = $proxy_user = $proxy_pass = '';
347         if ($this->config->get('http_proxy')&&
348               $proxy = parse_url($this->config->get('http_proxy'))
349         ) {
350             $proxy_host = isset($proxy['host']) ? $proxy['host'] : null;
351             if ($schema === 'https') {
352                 $proxy_host = 'ssl://' . $proxy_host;
353             }
354
355             $proxy_port   = isset($proxy['port']) ? $proxy['port'] : 8080;
356             $proxy_user   = isset($proxy['user']) ? urldecode($proxy['user']) : null;
357             $proxy_pass   = isset($proxy['pass']) ? urldecode($proxy['pass']) : null;
358             $proxy_schema = (isset($proxy['scheme']) && $proxy['scheme'] == 'https') ? 'https' : 'http';
359         }
360
361         if (empty($port)) {
362             $port = (isset($info['scheme']) && $info['scheme'] == 'https')  ? 443 : 80;
363         }
364
365         if (isset($proxy['host'])) {
366             $request = "GET $url HTTP/1.1\r\n";
367         } else {
368             $request = "GET $path HTTP/1.1\r\n";
369         }
370
371         $request .= "Host: $host\r\n";
372         $ifmodifiedsince = '';
373         if (is_array($lastmodified)) {
374             if (isset($lastmodified['Last-Modified'])) {
375                 $ifmodifiedsince = 'If-Modified-Since: ' . $lastmodified['Last-Modified'] . "\r\n";
376             }
377
378             if (isset($lastmodified['ETag'])) {
379                 $ifmodifiedsince .= "If-None-Match: $lastmodified[ETag]\r\n";
380             }
381         } else {
382             $ifmodifiedsince = ($lastmodified ? "If-Modified-Since: $lastmodified\r\n" : '');
383         }
384
385         $request .= $ifmodifiedsince .
386             "User-Agent: PEAR/1.9.4/PHP/" . PHP_VERSION . "\r\n";
387
388         $username = $this->config->get('username', null, $channel);
389         $password = $this->config->get('password', null, $channel);
390
391         if ($username && $password) {
392             $tmp = base64_encode("$username:$password");
393             $request .= "Authorization: Basic $tmp\r\n";
394         }
395
396         if ($proxy_host != '' && $proxy_user != '') {
397             $request .= 'Proxy-Authorization: Basic ' .
398                 base64_encode($proxy_user . ':' . $proxy_pass) . "\r\n";
399         }
400
401         if ($accept) {
402             $request .= 'Accept: ' . implode(', ', $accept) . "\r\n";
403         }
404
405         $request .= "Accept-Encoding:\r\n";
406         $request .= "Connection: close\r\n";
407         $request .= "\r\n";
408
409         if ($proxy_host != '') {
410             $fp = @fsockopen($proxy_host, $proxy_port, $errno, $errstr, 15);
411             if (!$fp) {
412                 return PEAR::raiseError("Connection to `$proxy_host:$proxy_port' failed: $errstr", -9276);
413             }
414         } else {
415             if ($schema === 'https') {
416                 $host = 'ssl://' . $host;
417             }
418
419             $fp = @fsockopen($host, $port, $errno, $errstr);
420             if (!$fp) {
421                 return PEAR::raiseError("Connection to `$host:$port' failed: $errstr", $errno);
422             }
423         }
424
425         fwrite($fp, $request);
426
427         $headers = array();
428         $reply   = 0;
429         while ($line = trim(fgets($fp, 1024))) {
430             if (preg_match('/^([^:]+):\s+(.*)\s*\\z/', $line, $matches)) {
431                 $headers[strtolower($matches[1])] = trim($matches[2]);
432             } elseif (preg_match('|^HTTP/1.[01] ([0-9]{3}) |', $line, $matches)) {
433                 $reply = (int)$matches[1];
434                 if ($reply == 304 && ($lastmodified || ($lastmodified === false))) {
435                     return false;
436                 }
437
438                 if (!in_array($reply, array(200, 301, 302, 303, 305, 307))) {
439                     return PEAR::raiseError("File $schema://$host:$port$path not valid (received: $line)");
440                 }
441             }
442         }
443
444         if ($reply != 200) {
445             if (!isset($headers['location'])) {
446                 return PEAR::raiseError("File $schema://$host:$port$path not valid (redirected but no location)");
447             }
448
449             if ($wasredirect > 4) {
450                 return PEAR::raiseError("File $schema://$host:$port$path not valid (redirection looped more than 5 times)");
451             }
452
453             $redirect = $wasredirect + 1;
454             return $this->downloadHttp($headers['location'], $lastmodified, $accept, $channel);
455         }
456
457         $length = isset($headers['content-length']) ? $headers['content-length'] : -1;
458
459         $data = '';
460         while ($chunk = @fread($fp, 8192)) {
461             $data .= $chunk;
462         }
463         fclose($fp);
464
465         if ($lastmodified === false || $lastmodified) {
466             if (isset($headers['etag'])) {
467                 $lastmodified = array('ETag' => $headers['etag']);
468             }
469
470             if (isset($headers['last-modified'])) {
471                 if (is_array($lastmodified)) {
472                     $lastmodified['Last-Modified'] = $headers['last-modified'];
473                 } else {
474                     $lastmodified = $headers['last-modified'];
475                 }
476             }
477
478             return array($data, $lastmodified, $headers);
479         }
480
481         return $data;
482     }
483 }