PageRenderTime 53ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/system/classes/remoterequest.php

https://github.com/HabariMag/habarimag-old
PHP | 469 lines | 282 code | 64 blank | 123 comment | 50 complexity | 5e1f192ad6ae15d4cd09fd569fd1d63b MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /**
  3. * @package Habari
  4. *
  5. */
  6. /**
  7. * Holds the basic RemoteRequest functionality.
  8. *
  9. * Interface for Request Processors. RemoteRequest uses a RequestProcessor to
  10. * do the actual work.
  11. *
  12. */
  13. abstract class RequestProcessor
  14. {
  15. protected $response_body = '';
  16. protected $response_headers = array();
  17. protected $executed = false;
  18. protected $can_followlocation = true;
  19. abstract public function execute( $method, $url, $headers, $body, $config );
  20. public function get_response_body ( ) {
  21. if ( !$this->executed ) {
  22. throw new Exception( _t( 'Unable to get response body. Request did not yet execute.' ) );
  23. }
  24. return $this->response_body;
  25. }
  26. public function get_response_headers ( ) {
  27. if ( !$this->executed ) {
  28. throw new Exception( _t( 'Unable to get response headers. Request did not yet execute.' ) );
  29. }
  30. return $this->response_headers;
  31. }
  32. }
  33. /**
  34. * Generic class to make outgoing HTTP requests.
  35. *
  36. */
  37. class RemoteRequest
  38. {
  39. private $method = 'GET';
  40. private $url;
  41. private $params = array();
  42. private $headers = array();
  43. private $postdata = array();
  44. private $files = array();
  45. private $body = '';
  46. private $processor = null;
  47. private $executed = false;
  48. private $response_body = '';
  49. private $response_headers = '';
  50. private $user_agent = 'Habari';
  51. // Array of adapter configuration options
  52. private $config = array(
  53. 'connect_timeout' => 30,
  54. 'timeout' => 180,
  55. 'buffer_size' => 16384,
  56. 'follow_redirects' => true,
  57. 'max_redirects' => 5,
  58. // These are configured in the main config file
  59. 'proxy' => array(
  60. 'server' => null,
  61. 'port' => null,
  62. 'username' => null,
  63. 'password' => null,
  64. 'auth_type' => 'basic',
  65. 'exceptions' => array(),
  66. 'type' => 'http', // http is the default, 'socks' for a SOCKS5 proxy
  67. ),
  68. // TODO: These don't apply to SocketRequestProcessor yet
  69. 'ssl' => array(
  70. 'verify_peer' => true,
  71. 'verify_host' => 2, // 1 = check CN of ssl cert, 2 = check and verify @see http://php.net/curl_setopt
  72. 'cafile' => null,
  73. 'capath' => null,
  74. 'local_cert' => null,
  75. 'passphrase' => null,
  76. ),
  77. );
  78. /**
  79. * @param string $url URL to request
  80. * @param string $method Request method to use (default 'GET')
  81. * @param int $timeuot Timeout in seconds (default 180)
  82. */
  83. public function __construct( $url, $method = 'GET', $timeout = 180 )
  84. {
  85. $this->method = strtoupper( $method );
  86. $this->url = $url;
  87. $this->set_timeout( $timeout );
  88. // load the proxy configuration, if it exists
  89. $default = new stdClass();
  90. $proxy = Config::get( 'proxy', $default );
  91. if ( isset( $proxy->server ) ) {
  92. $this->set_config( array( 'proxy' => (array)$proxy ) );
  93. }
  94. // populate the default proxy exceptions list, since we can't up there
  95. $this->config['proxy']['exceptions'] = array_merge( $this->config['proxy']['exceptions'], array(
  96. 'localhost',
  97. '127.0.0.1',
  98. '::1', // ipv6 localhost
  99. ) );
  100. // these next two could be duplicates of 'localhost' and 127.0.0.1 / ::1 if you're on localhost - that's ok
  101. if ( isset( $_SERVER['SERVER_NAME'] ) ) {
  102. $this->config['proxy']['exceptions'][] = $_SERVER['SERVER_NAME'];
  103. }
  104. if ( isset( $_SERVER['SERVER_ADDR'] ) ) {
  105. $this->config['proxy']['exceptions'][] = $_SERVER['SERVER_ADDR'];
  106. }
  107. $this->user_agent .= '/' . Version::HABARI_VERSION;
  108. $this->add_header( array( 'User-Agent' => $this->user_agent ) );
  109. // if they've manually specified that we should not use curl, use sockets instead
  110. if ( Config::get( 'remote_request_processor' ) == 'socket' ) {
  111. $this->processor = new SocketRequestProcessor();
  112. }
  113. else {
  114. // otherwise, see if we can use curl and fall back to sockets if not
  115. if ( function_exists( 'curl_init' )
  116. && ! ( ini_get( 'safe_mode' ) || ini_get( 'open_basedir' ) ) ) {
  117. $this->processor = new CURLRequestProcessor;
  118. }
  119. else {
  120. $this->processor = new SocketRequestProcessor;
  121. }
  122. }
  123. }
  124. /**
  125. * DO NOT USE THIS FUNCTION.
  126. * This function is only to be used by the test case for RemoteRequest!
  127. */
  128. public function __set_processor( $processor )
  129. {
  130. $this->processor = $processor;
  131. }
  132. /**
  133. * Set adapter configuration options
  134. *
  135. * @param mixed $config An array of options or a string name with a corresponding $value
  136. * @param mixed $value
  137. */
  138. public function set_config( $config, $value = null )
  139. {
  140. if ( is_array( $config ) ) {
  141. foreach ( $config as $name => $value ) {
  142. $this->set_config( $name, $value );
  143. }
  144. }
  145. else {
  146. if ( is_array( $value ) ) {
  147. $this->config[ $config ] = array_merge( $this->config[ $config ], $value );
  148. }
  149. else {
  150. $this->config[ $config ] = $value;
  151. }
  152. }
  153. }
  154. /**
  155. * Add a request header.
  156. * @param mixed $header The header to add, either as a string 'Name: Value' or an associative array 'name'=>'value'
  157. */
  158. public function add_header( $header )
  159. {
  160. if ( is_array( $header ) ) {
  161. $this->headers = array_merge( $this->headers, $header );
  162. }
  163. else {
  164. list( $k, $v )= explode( ': ', $header );
  165. $this->headers[$k] = $v;
  166. }
  167. }
  168. /**
  169. * Add a list of headers.
  170. * @param array $headers List of headers to add.
  171. */
  172. public function add_headers( $headers )
  173. {
  174. foreach ( $headers as $header ) {
  175. $this->add_header( $header );
  176. }
  177. }
  178. /**
  179. * Set the request body.
  180. * Only used with POST requests, will raise a warning if used with GET.
  181. * @param string $body The request body.
  182. */
  183. public function set_body( $body )
  184. {
  185. if ( $this->method == 'GET' ) {
  186. throw new Exception( _t( 'Trying to add a request body to a GET request.' ) );
  187. }
  188. $this->body = $body;
  189. }
  190. /**
  191. * Set the request query parameters (i.e., the URI's query string).
  192. * Will be merged with existing query info from the URL.
  193. * @param array $params
  194. */
  195. public function set_params( $params )
  196. {
  197. if ( ! is_array( $params ) )
  198. $params = parse_str( $params );
  199. $this->params = $params;
  200. }
  201. /**
  202. * Set the timeout.
  203. * @param int $timeout Timeout in seconds
  204. */
  205. public function set_timeout( $timeout )
  206. {
  207. $this->config['timeout'] = $timeout;
  208. return $this->config['timeout'];
  209. }
  210. /**
  211. * set postdata
  212. *
  213. * @access public
  214. * @param mixed $name
  215. * @param string $value
  216. */
  217. public function set_postdata( $name, $value = null )
  218. {
  219. if ( is_array( $name ) ) {
  220. $this->postdata = array_merge( $this->postdata, $name );
  221. }
  222. else {
  223. $this->postdata[$name] = $value;
  224. }
  225. }
  226. /**
  227. * set file
  228. *
  229. * @access public
  230. * @param string $name
  231. * @param string $filename
  232. * @param string $content_type
  233. */
  234. public function set_file( $name, $filename, $content_type = null, $override_filename = null )
  235. {
  236. if ( !file_exists( $filename ) ) {
  237. throw new Exception( _t( 'File %s not found.', array( $filename ) ) );
  238. }
  239. if ( empty( $content_type ) ) $content_type = 'application/octet-stream';
  240. $this->files[$name] = array( 'filename' => $filename, 'content_type' => $content_type, 'override_filename' => $override_filename );
  241. $this->headers['Content-Type'] = 'multipart/form-data';
  242. }
  243. /**
  244. * A little housekeeping.
  245. */
  246. private function prepare()
  247. {
  248. // remove anchors (#foo) from the URL
  249. $this->url = $this->strip_anchors( $this->url );
  250. // merge query params from the URL with params given
  251. $this->url = $this->merge_query_params( $this->url, $this->params );
  252. if ( $this->method === 'POST' ) {
  253. if ( !isset( $this->headers['Content-Type'] ) || ( $this->headers['Content-Type'] == 'application/x-www-form-urlencoded' ) ) {
  254. // TODO should raise a warning
  255. $this->add_header( array( 'Content-Type' => 'application/x-www-form-urlencoded' ) );
  256. if ( $this->body != '' && count( $this->postdata ) > 0 ) {
  257. $this->body .= '&';
  258. }
  259. $this->body .= http_build_query( $this->postdata, '', '&' );
  260. }
  261. elseif ( $this->headers['Content-Type'] == 'multipart/form-data' ) {
  262. $boundary = md5( Utils::nonce() );
  263. $this->headers['Content-Type'] .= '; boundary=' . $boundary;
  264. $parts = array();
  265. if ( $this->postdata && is_array( $this->postdata ) ) {
  266. reset( $this->postdata );
  267. while ( list( $name, $value ) = each( $this->postdata ) ) {
  268. $parts[] = "Content-Disposition: form-data; name=\"{$name}\"\r\n\r\n{$value}\r\n";
  269. }
  270. }
  271. if ( $this->files && is_array( $this->files ) ) {
  272. reset( $this->files );
  273. while ( list( $name, $fileinfo ) = each( $this->files ) ) {
  274. $filename = basename( $fileinfo['filename'] );
  275. if ( !empty( $fileinfo['override_filename'] ) ) {
  276. $filename = $fileinfo['override_filename'];
  277. }
  278. $part = "Content-Disposition: form-data; name=\"{$name}\"; filename=\"{$filename}\"\r\n";
  279. $part .= "Content-Type: {$fileinfo['content_type']}\r\n\r\n";
  280. $part .= file_get_contents( $fileinfo['filename'] ) . "\r\n";
  281. $parts[] = $part;
  282. }
  283. }
  284. if ( !empty( $parts ) ) {
  285. $this->body = "--{$boundary}\r\n" . join( "--{$boundary}\r\n", $parts ) . "--{$boundary}--\r\n";
  286. }
  287. }
  288. $this->add_header( array( 'Content-Length' => strlen( $this->body ) ) );
  289. }
  290. }
  291. /**
  292. * Actually execute the request.
  293. * On success, returns true and populates the response_body and response_headers fields.
  294. * On failure, throws Exception.
  295. *
  296. * @throws Exception
  297. */
  298. public function execute()
  299. {
  300. $this->prepare();
  301. $result = $this->processor->execute( $this->method, $this->url, $this->headers, $this->body, $this->config );
  302. if ( $result ) { // XXX exceptions?
  303. $this->response_headers = $this->processor->get_response_headers();
  304. $this->response_body = $this->processor->get_response_body();
  305. $this->executed = true;
  306. return true;
  307. }
  308. else {
  309. // processor->execute should throw an Exception which would bubble up
  310. $this->executed = false;
  311. return $result;
  312. }
  313. }
  314. public function executed()
  315. {
  316. return $this->executed;
  317. }
  318. /**
  319. * Return the response headers. Raises a warning and returns '' if the request wasn't executed yet.
  320. * @todo This should probably just call the selected processor's method, which throws its own error.
  321. */
  322. public function get_response_headers()
  323. {
  324. if ( !$this->executed ) {
  325. throw new Exception( _t( 'Unable to fetch response headers for a pending request.' ) );
  326. }
  327. return $this->response_headers;
  328. }
  329. /**
  330. * Return the response body. Raises a warning and returns '' if the request wasn't executed yet.
  331. * @todo This should probably just call the selected processor's method, which throws its own error.
  332. */
  333. public function get_response_body()
  334. {
  335. if ( !$this->executed ) {
  336. throw new Exception( _t( 'Unable to fetch response body for a pending request.' ) );
  337. }
  338. return $this->response_body;
  339. }
  340. /**
  341. * Remove anchors (#foo) from given URL.
  342. */
  343. private function strip_anchors( $url )
  344. {
  345. return preg_replace( '/(#.*?)?$/', '', $url );
  346. }
  347. /**
  348. * Call the filter hook.
  349. */
  350. private function __filter( $data, $url )
  351. {
  352. return Plugins::filter( 'remoterequest', $data, $url );
  353. }
  354. /**
  355. * Merge query params from the URL with given params.
  356. * @param string $url The URL
  357. * @param string $params An associative array of parameters.
  358. */
  359. private function merge_query_params( $url, $params )
  360. {
  361. $urlparts = InputFilter::parse_url( $url );
  362. if ( ! isset( $urlparts['query'] ) ) {
  363. $urlparts['query'] = '';
  364. }
  365. if ( ! is_array( $params ) ) {
  366. parse_str( $params, $params );
  367. }
  368. $urlparts['query'] = http_build_query( array_merge( Utils::get_params( $urlparts['query'] ), $params ), '', '&' );
  369. return InputFilter::glue_url( $urlparts );
  370. }
  371. /**
  372. * Static helper function to quickly fetch an URL, with semantics similar to
  373. * PHP's file_get_contents. Does not support
  374. *
  375. * Returns the content on success or false if an error occurred.
  376. *
  377. * @param string $url The URL to fetch
  378. * @param bool $use_include_path whether to search the PHP include path first (unsupported)
  379. * @param resource $context a stream context to use (unsupported)
  380. * @param int $offset how many bytes to skip from the beginning of the result
  381. * @param int $maxlen how many bytes to return
  382. * @return string description
  383. */
  384. public static function get_contents( $url, $use_include_path = false, $context = null, $offset = 0, $maxlen = -1 )
  385. {
  386. try {
  387. $rr = new RemoteRequest( $url );
  388. if ( $rr->execute() === true ) {
  389. return ( $maxlen != -1
  390. ? MultiByte::substr( $rr->get_response_body(), $offset, $maxlen )
  391. : MultiByte::substr( $rr->get_response_body(), $offset ) );
  392. }
  393. else {
  394. return false;
  395. }
  396. }
  397. catch ( Exception $e ) {
  398. // catch any exceptions to try and emulate file_get_contents() as closely as possible.
  399. // if you want more control over the errors, instantiate RemoteRequest manually
  400. return false;
  401. }
  402. }
  403. }
  404. class RemoteRequest_Timeout extends Exception { }
  405. ?>