PageRenderTime 63ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 1ms

/symphony/lib/toolkit/class.gateway.php

https://github.com/nils-werner/symphony-2
PHP | 461 lines | 199 code | 86 blank | 176 comment | 41 complexity | e70d6a7c52c50624b86a0fd0ae274baa MD5 | raw file
  1. <?php
  2. /**
  3. * @package toolkit
  4. */
  5. /**
  6. * The Gateway class provides a standard way to interact with other pages.
  7. * By default it is essentially a wrapper for CURL, but if that is not available
  8. * it falls back to use sockets.
  9. * @example
  10. * `
  11. * require_once(TOOLKIT . '/class.gateway.php');
  12. * $ch = new Gateway;
  13. * $ch->init('http://www.example.com/');
  14. * $ch->setopt('POST', 1);
  15. * $ch->setopt('POSTFIELDS', array('fred' => 1, 'happy' => 'yes'));
  16. * print $ch->exec();
  17. * `
  18. */
  19. Class Gateway{
  20. /**
  21. * Constant used to explicitly bypass CURL and use Sockets to
  22. * complete the request.
  23. * @var string
  24. */
  25. const FORCE_SOCKET = 'socket';
  26. /**
  27. * An associative array of some common ports for HTTP, HTTPS
  28. * and FTP. Port cannot be null when using Sockets
  29. * @var array
  30. */
  31. private static $ports = array(
  32. 'http' => 80,
  33. 'https' => 443,
  34. 'ftp' => 21
  35. );
  36. /**
  37. * The URL for the request, as string. This may be a full URL including
  38. * any basic authentication. It will be parsed and applied to CURL using
  39. * the correct options.
  40. * @var string
  41. */
  42. private $_url = null;
  43. /**
  44. * The hostname of the request, as parsed by parse_url
  45. *
  46. * @link http://php.net/manual/en/function.parse-url.php
  47. * @var string
  48. */
  49. private $_host = null;
  50. /**
  51. * The protocol of the URL in the request, as parsed by parse_url
  52. * Defaults to http://
  53. *
  54. * @link http://php.net/manual/en/function.parse-url.php
  55. * @var string
  56. */
  57. private $_scheme = 'http://';
  58. /**
  59. * The port of the URL in the request, as parsed by parse_url
  60. *
  61. * @link http://php.net/manual/en/function.parse-url.php
  62. * @var integer
  63. */
  64. private $_port = null;
  65. /**
  66. * The path of the URL in the request, as parsed by parse_url
  67. *
  68. * @link http://php.net/manual/en/function.parse-url.php
  69. * @var string
  70. */
  71. private $_path = null;
  72. /**
  73. * The method to request the URL. By default, this is GET
  74. * @var string
  75. */
  76. private $_method = 'GET';
  77. /**
  78. * The content-type of the request, defaults to application/x-www-form-urlencoded
  79. * @var string
  80. */
  81. private $_content_type = 'application/x-www-form-urlencoded; charset=utf-8';
  82. /**
  83. * The user agent for the request, defaults to Symphony.
  84. * @var string
  85. */
  86. private $_agent = 'Symphony';
  87. /**
  88. * A URL encoded string of the `$_POST` fields, as built by
  89. * http_build_query()
  90. *
  91. * @link http://php.net/manual/en/function.http-build-query.php
  92. * @var string
  93. */
  94. private $_postfields = '';
  95. /**
  96. * Whether to the return the Header with the result of the request
  97. * @var boolean
  98. */
  99. private $_returnHeaders = false;
  100. /**
  101. * The timeout in seconds for the request, defaults to 4
  102. * @var integer
  103. */
  104. private $_timeout = 4;
  105. /**
  106. * An array of custom headers to pass with the request
  107. * @var integer
  108. */
  109. private $_headers = array();
  110. /**
  111. * An array of custom options for the CURL request, this
  112. * can be any option as listed on the PHP manual
  113. *
  114. * @link http://php.net/manual/en/function.curl-setopt.php
  115. * @var array
  116. */
  117. private $_custom_opt = array();
  118. /**
  119. * An array of information about the request after it has
  120. * been executed. At minimum, regardless of if CURL or Sockets
  121. * are used, the HTTP Code, URL and Content Type will be returned
  122. *
  123. * @link http://php.net/manual/en/function.curl-getinfo.php
  124. */
  125. private $_info_last = array();
  126. /**
  127. * Mimics curl_init in that a URL can be provided
  128. *
  129. * @param string $url
  130. * A full URL string to use for the request, this can include
  131. * basic authentication which will automatically set the
  132. * correct options for the CURL request. Defaults to null
  133. */
  134. public function init($url = null){
  135. if(!is_null($url)) {
  136. $this->setopt('URL', $url);
  137. }
  138. }
  139. /**
  140. * Checks to the see if CURL is available, if it isn't, false will
  141. * be returned, and sockets will be used
  142. *
  143. * @return boolean
  144. */
  145. public static function isCurlAvailable(){
  146. return function_exists('curl_init');
  147. }
  148. /**
  149. * Resets `$this->_postfields` variable to an empty string
  150. */
  151. public function flush(){
  152. $this->_postfields = '';
  153. }
  154. /**
  155. * A basic wrapper that simulates the curl_setopt function. Any
  156. * options that are not recognised by Symphony will fallback to
  157. * being added to the `$custom_opt` array. Any options in `$custom_opt`
  158. * will be applied on executed using curl_setopt. Custom options are not
  159. * available for Socket requests. The benefit of using this function is for
  160. * convienience as it performs some basic preprocessing for some options
  161. * such as 'URL', which will take a full formatted URL string and set any
  162. * authentication or SSL curl options automatically
  163. *
  164. * @link http://php.net/manual/en/function.curl-setopt.php
  165. * @param string $opt
  166. * A string representing a CURL constant. Symphony will intercept the
  167. * following, URL, POST, POSTFIELDS, USERAGENT, HTTPHEADER,
  168. * RETURNHEADERS, CONTENTTYPE and TIMEOUT. Any other values
  169. * will be saved in the `$custom_opt` array.
  170. * @param mixed $value
  171. * The value of the option, usually boolean or a string. Consult the
  172. * setopt documentation for more information.
  173. */
  174. public function setopt($opt, $value){
  175. switch($opt){
  176. case 'URL':
  177. $this->_url = $value;
  178. $url_parsed = parse_url($value);
  179. $this->_host = $url_parsed['host'];
  180. if(isset($url_parsed['scheme']) && strlen(trim($url_parsed['scheme'])) > 0){
  181. $this->_scheme = $url_parsed['scheme'];
  182. }
  183. if(isset($url_parsed['port'])){
  184. $this->_port = $url_parsed['port'];
  185. }
  186. if(isset($url_parsed['path'])){
  187. $this->_path = $url_parsed['path'];
  188. }
  189. if(isset($url_parsed['query'])){
  190. $this->_path .= '?' . $url_parsed['query'];
  191. }
  192. // Allow basic HTTP authentiction
  193. if(isset($url_parsed['user']) && isset($url_parsed['pass'])){
  194. $this->setopt(CURLOPT_USERPWD, sprintf('%s:%s', $url_parsed['user'], $url_parsed['pass']));
  195. $this->setopt(CURLOPT_HTTPAUTH, CURLAUTH_ANY);
  196. }
  197. // Better support for HTTPS requests
  198. if($url_parsed['scheme'] == 'https'){
  199. $this->setopt(CURLOPT_SSL_VERIFYPEER, false);
  200. }
  201. break;
  202. case 'POST':
  203. case 'GET':
  204. case 'PUT':
  205. case 'DELETE':
  206. $this->_method = ($value == 1 ? $opt : 'GET');
  207. break;
  208. case 'POSTFIELDS':
  209. if(is_array($value) && !empty($value)){
  210. $this->_postfields = http_build_query($value);
  211. }
  212. else {
  213. $this->_postfields = $value;
  214. }
  215. break;
  216. case 'USERAGENT':
  217. $this->_agent = $value;
  218. break;
  219. case 'HTTPHEADER':
  220. $this->_headers = $value;
  221. break;
  222. case 'RETURNHEADERS':
  223. $this->_returnHeaders = (intval($value) == 1 ? true : false);
  224. break;
  225. case 'CONTENTTYPE':
  226. $this->_content_type = $value;
  227. break;
  228. case 'TIMEOUT':
  229. $this->_timeout = max(1, intval($value));
  230. break;
  231. default:
  232. $this->_custom_opt[$opt] = $value;
  233. break;
  234. }
  235. }
  236. /**
  237. * Executes the request using Curl unless it is not available
  238. * or this function has explicitly been told not by providing
  239. * the `Gateway::FORCE_SOCKET` constant as a parameter. The function
  240. * will apply all the options set using `curl_setopt` before
  241. * executing the request. Information about the transfer is
  242. * available using the `getInfoLast()` function. Should Curl not be
  243. * available, this function will fallback to using Sockets with `fsockopen`
  244. *
  245. * @see toolkit.Gateway#getInfoLast()
  246. * @param string $force_connection_method
  247. * Only one valid parameter, `Gateway::FORCE_SOCKET`
  248. * @return string
  249. * The result of the transfer as a string. If any errors occur during
  250. * a socket request, false will be returned.
  251. */
  252. public function exec($force_connection_method = null){
  253. if($force_connection_method != self::FORCE_SOCKET && self::isCurlAvailable()){
  254. $ch = curl_init();
  255. curl_setopt($ch, CURLOPT_URL,
  256. sprintf("%s://%s%s%s", $this->_scheme, $this->_host, (!is_null($this->_port) ? ':' . $this->_port : null), $this->_path)
  257. );
  258. curl_setopt($ch, CURLOPT_HEADER, $this->_returnHeaders);
  259. curl_setopt($ch, CURLOPT_USERAGENT, $this->_agent);
  260. curl_setopt($ch, CURLOPT_PORT, $this->_port);
  261. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  262. curl_setopt($ch, CURLOPT_TIMEOUT, $this->_timeout);
  263. if(ini_get('safe_mode') == 0 && ini_get('open_basedir') == '') {
  264. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
  265. }
  266. switch($this->_method) {
  267. case 'POST':
  268. curl_setopt($ch, CURLOPT_POST, 1);
  269. curl_setopt($ch, CURLOPT_POSTFIELDS, $this->_postfields);
  270. break;
  271. case 'PUT':
  272. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
  273. curl_setopt($ch, CURLOPT_POSTFIELDS, $this->_postfields);
  274. $this->setopt('HTTPHEADER', array('Content-Length:' => strlen($this->_postfields)));
  275. break;
  276. case 'DELETE':
  277. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
  278. curl_setopt($ch, CURLOPT_POSTFIELDS, $this->_postfields);
  279. break;
  280. }
  281. if(is_array($this->_headers) && !empty($this->_headers)) {
  282. curl_setopt($ch, CURLOPT_HTTPHEADER, $this->_headers);
  283. }
  284. if(is_array($this->_custom_opt) && !empty($this->_custom_opt)){
  285. foreach($this->_custom_opt as $opt => $value){
  286. curl_setopt($ch, $opt, $value);
  287. }
  288. }
  289. // Grab the result
  290. $result = curl_exec($ch);
  291. $this->_info_last = curl_getinfo($ch);
  292. $this->_info_last['curl_error'] = curl_errno($ch);
  293. // Close the connection
  294. curl_close ($ch);
  295. return $result;
  296. }
  297. $start = precision_timer();
  298. if(is_null($this->_port)){
  299. $this->_port = (!is_null($this->_scheme) ? self::$ports[$this->_scheme] : 80);
  300. }
  301. // No CURL is available, use attempt to use normal sockets
  302. $handle = @fsockopen($this->_host, $this->_port, $errno, $errstr, $this->_timeout);
  303. if($handle === false) return false;
  304. $query = $this->_method . ' ' . $this->_path . ' HTTP/1.1' . PHP_EOL;
  305. $query .= 'Host: '.$this->_host . PHP_EOL;
  306. $query .= 'Content-type: '.$this->_content_type . PHP_EOL;
  307. $query .= 'User-Agent: '.$this->_agent . PHP_EOL;
  308. $query .= @implode(PHP_EOL, $this->_headers);
  309. $query .= 'Content-length: ' . strlen($this->_postfields) . PHP_EOL;
  310. $query .= 'Connection: close' . PHP_EOL . PHP_EOL;
  311. if(in_array($this->_method, array('PUT', 'POST', 'DELETE'))) $query .= $this->_postfields;
  312. // send request
  313. if(!@fwrite($handle, $query)) return false;
  314. stream_set_blocking($handle, false);
  315. stream_set_timeout($handle, $this->_timeout);
  316. $status = stream_get_meta_data($handle);
  317. $response = $dechunked = '';
  318. // get header
  319. while (!preg_match('/\\r\\n\\r\\n$/', $header) && !$status['timed_out']) {
  320. $header .= @fread($handle, 1);
  321. $status = stream_get_meta_data($handle);
  322. }
  323. $status = socket_get_status($handle);
  324. // Get rest of the page data
  325. while (!feof($handle) && !$status['timed_out']){
  326. $response .= fread($handle, 4096);
  327. $status = stream_get_meta_data($handle);
  328. }
  329. @fclose($handle);
  330. $end = precision_timer('stop', $start);
  331. if(preg_match('/Transfer\\-Encoding:\\s+chunked\\r\\n/', $header)){
  332. $fp = 0;
  333. do {
  334. $byte = '';
  335. $chunk_size = '';
  336. do {
  337. $chunk_size .= $byte;
  338. $byte = substr($response, $fp, 1); $fp++;
  339. } while ($byte != "\r" && $byte != "\\r");
  340. $chunk_size = hexdec($chunk_size); // convert to real number
  341. if($chunk_size == 0) break(1);
  342. $fp++;
  343. $dechunked .= substr($response, $fp, $chunk_size); $fp += $chunk_size;
  344. $fp += 2;
  345. } while(true);
  346. $response = $dechunked;
  347. }
  348. // Following code emulates part of the function curl_getinfo()
  349. preg_match('/Content-Type:\s*([^\r\n]+)/i', $header, $match);
  350. $content_type = $match[1];
  351. preg_match('/HTTP\/\d+.\d+\s+(\d+)/i', $header, $match);
  352. $status = $match[1];
  353. $this->_info_last = array(
  354. 'url' => $this->_url,
  355. 'content_type' => $content_type,
  356. 'http_code' => $status,
  357. 'total_time' => $end
  358. );
  359. return ($this->_returnHeaders ? $header : NULL) . $response;
  360. }
  361. /**
  362. * Returns some information about the last transfer, this
  363. * the same output array as expected when calling the
  364. * `curl_getinfo()` function. If Sockets were used to complete
  365. * the request instead of CURL, the resulting array will be
  366. * the HTTP Code, Content Type, URL and Total Time of the resulting
  367. * request
  368. *
  369. * @link http://php.net/manual/en/function.curl-getinfo.php
  370. * @return array
  371. */
  372. public function getInfoLast(){
  373. return $this->_info_last;
  374. }
  375. }