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