1 : <?php
2 : /**
3 : * Class representing a HTTP request message
4 : *
5 : * PHP version 5
6 : *
7 : * LICENSE:
8 : *
9 : * Copyright (c) 2008-2011, Alexey Borzov <avb@php.net>
10 : * All rights reserved.
11 : *
12 : * Redistribution and use in source and binary forms, with or without
13 : * modification, are permitted provided that the following conditions
14 : * are met:
15 : *
16 : * * Redistributions of source code must retain the above copyright
17 : * notice, this list of conditions and the following disclaimer.
18 : * * Redistributions in binary form must reproduce the above copyright
19 : * notice, this list of conditions and the following disclaimer in the
20 : * documentation and/or other materials provided with the distribution.
21 : * * The names of the authors may not be used to endorse or promote products
22 : * derived from this software without specific prior written permission.
23 : *
24 : * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
25 : * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
26 : * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
27 : * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
28 : * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
29 : * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
30 : * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
31 : * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
32 : * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
33 : * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
34 : * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 : *
36 : * @category HTTP
37 : * @package HTTP_Request2
38 : * @author Alexey Borzov <avb@php.net>
39 : * @license http://opensource.org/licenses/bsd-license.php New BSD License
40 : * @version SVN: $Id: Request2.php 315409 2011-08-24 07:29:23Z avb $
41 : * @link http://pear.php.net/package/HTTP_Request2
42 : */
43 :
44 : /**
45 : * A class representing an URL as per RFC 3986.
46 : */
47 : require_once 'Net/URL2.php';
48 :
49 : /**
50 : * Exception class for HTTP_Request2 package
51 : */
52 : require_once 'HTTP/Request2/Exception.php';
53 :
54 : /**
55 : * Class representing a HTTP request message
56 : *
57 : * @category HTTP
58 : * @package HTTP_Request2
59 : * @author Alexey Borzov <avb@php.net>
60 : * @version Release: 2.0.0
61 : * @link http://tools.ietf.org/html/rfc2616#section-5
62 : */
63 : class HTTP_Request2 implements SplSubject
64 : {
65 : /**#@+
66 : * Constants for HTTP request methods
67 : *
68 : * @link http://tools.ietf.org/html/rfc2616#section-5.1.1
69 : */
70 : const METHOD_OPTIONS = 'OPTIONS';
71 : const METHOD_GET = 'GET';
72 : const METHOD_HEAD = 'HEAD';
73 : const METHOD_POST = 'POST';
74 : const METHOD_PUT = 'PUT';
75 : const METHOD_DELETE = 'DELETE';
76 : const METHOD_TRACE = 'TRACE';
77 : const METHOD_CONNECT = 'CONNECT';
78 : /**#@-*/
79 :
80 : /**#@+
81 : * Constants for HTTP authentication schemes
82 : *
83 : * @link http://tools.ietf.org/html/rfc2617
84 : */
85 : const AUTH_BASIC = 'basic';
86 : const AUTH_DIGEST = 'digest';
87 : /**#@-*/
88 :
89 : /**
90 : * Regular expression used to check for invalid symbols in RFC 2616 tokens
91 : * @link http://pear.php.net/bugs/bug.php?id=15630
92 : */
93 : const REGEXP_INVALID_TOKEN = '![\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]!';
94 :
95 : /**
96 : * Regular expression used to check for invalid symbols in cookie strings
97 : * @link http://pear.php.net/bugs/bug.php?id=15630
98 : * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html
99 : */
100 : const REGEXP_INVALID_COOKIE = '/[\s,;]/';
101 :
102 : /**
103 : * Fileinfo magic database resource
104 : * @var resource
105 : * @see detectMimeType()
106 : */
107 : private static $_fileinfoDb;
108 :
109 : /**
110 : * Observers attached to the request (instances of SplObserver)
111 : * @var array
112 : */
113 : protected $observers = array();
114 :
115 : /**
116 : * Request URL
117 : * @var Net_URL2
118 : */
119 : protected $url;
120 :
121 : /**
122 : * Request method
123 : * @var string
124 : */
125 : protected $method = self::METHOD_GET;
126 :
127 : /**
128 : * Authentication data
129 : * @var array
130 : * @see getAuth()
131 : */
132 : protected $auth;
133 :
134 : /**
135 : * Request headers
136 : * @var array
137 : */
138 : protected $headers = array();
139 :
140 : /**
141 : * Configuration parameters
142 : * @var array
143 : * @see setConfig()
144 : */
145 : protected $config = array(
146 : 'adapter' => 'HTTP_Request2_Adapter_Socket',
147 : 'connect_timeout' => 10,
148 : 'timeout' => 0,
149 : 'use_brackets' => true,
150 : 'protocol_version' => '1.1',
151 : 'buffer_size' => 16384,
152 : 'store_body' => true,
153 :
154 : 'proxy_host' => '',
155 : 'proxy_port' => '',
156 : 'proxy_user' => '',
157 : 'proxy_password' => '',
158 : 'proxy_auth_scheme' => self::AUTH_BASIC,
159 :
160 : 'ssl_verify_peer' => true,
161 : 'ssl_verify_host' => true,
162 : 'ssl_cafile' => null,
163 : 'ssl_capath' => null,
164 : 'ssl_local_cert' => null,
165 : 'ssl_passphrase' => null,
166 :
167 : 'digest_compat_ie' => false,
168 :
169 : 'follow_redirects' => false,
170 : 'max_redirects' => 5,
171 : 'strict_redirects' => false
172 : );
173 :
174 : /**
175 : * Last event in request / response handling, intended for observers
176 : * @var array
177 : * @see getLastEvent()
178 : */
179 : protected $lastEvent = array(
180 : 'name' => 'start',
181 : 'data' => null
182 : );
183 :
184 : /**
185 : * Request body
186 : * @var string|resource
187 : * @see setBody()
188 : */
189 : protected $body = '';
190 :
191 : /**
192 : * Array of POST parameters
193 : * @var array
194 : */
195 : protected $postParams = array();
196 :
197 : /**
198 : * Array of file uploads (for multipart/form-data POST requests)
199 : * @var array
200 : */
201 : protected $uploads = array();
202 :
203 : /**
204 : * Adapter used to perform actual HTTP request
205 : * @var HTTP_Request2_Adapter
206 : */
207 : protected $adapter;
208 :
209 : /**
210 : * Cookie jar to persist cookies between requests
211 : * @var HTTP_Request2_CookieJar
212 : */
213 : protected $cookieJar = null;
214 :
215 : /**
216 : * Constructor. Can set request URL, method and configuration array.
217 : *
218 : * Also sets a default value for User-Agent header.
219 : *
220 : * @param string|Net_Url2 Request URL
221 : * @param string Request method
222 : * @param array Configuration for this Request instance
223 : */
224 : public function __construct($url = null, $method = self::METHOD_GET, array $config = array())
225 : {
226 10 : $this->setConfig($config);
227 10 : if (!empty($url)) {
228 0 : $this->setUrl($url);
229 0 : }
230 10 : if (!empty($method)) {
231 10 : $this->setMethod($method);
232 10 : }
233 10 : $this->setHeader('user-agent', 'HTTP_Request2/2.0.0 ' .
234 10 : '(http://pear.php.net/package/http_request2) ' .
235 10 : 'PHP/' . phpversion());
236 10 : }
237 :
238 : /**
239 : * Sets the URL for this request
240 : *
241 : * If the URL has userinfo part (username & password) these will be removed
242 : * and converted to auth data. If the URL does not have a path component,
243 : * that will be set to '/'.
244 : *
245 : * @param string|Net_URL2 Request URL
246 : * @return HTTP_Request2
247 : * @throws HTTP_Request2_LogicException
248 : */
249 : public function setUrl($url)
250 : {
251 10 : if (is_string($url)) {
252 10 : $url = new Net_URL2(
253 10 : $url, array(Net_URL2::OPTION_USE_BRACKETS => $this->config['use_brackets'])
254 10 : );
255 10 : }
256 10 : if (!$url instanceof Net_URL2) {
257 0 : throw new HTTP_Request2_LogicException(
258 0 : 'Parameter is not a valid HTTP URL',
259 : HTTP_Request2_Exception::INVALID_ARGUMENT
260 0 : );
261 : }
262 : // URL contains username / password?
263 10 : if ($url->getUserinfo()) {
264 0 : $username = $url->getUser();
265 0 : $password = $url->getPassword();
266 0 : $this->setAuth(rawurldecode($username), $password? rawurldecode($password): '');
267 0 : $url->setUserinfo('');
268 0 : }
269 10 : if ('' == $url->getPath()) {
270 0 : $url->setPath('/');
271 0 : }
272 10 : $this->url = $url;
273 :
274 10 : return $this;
275 : }
276 :
277 : /**
278 : * Returns the request URL
279 : *
280 : * @return Net_URL2
281 : */
282 : public function getUrl()
283 : {
284 0 : return $this->url;
285 : }
286 :
287 : /**
288 : * Sets the request method
289 : *
290 : * @param string
291 : * @return HTTP_Request2
292 : * @throws HTTP_Request2_LogicException if the method name is invalid
293 : */
294 : public function setMethod($method)
295 : {
296 : // Method name should be a token: http://tools.ietf.org/html/rfc2616#section-5.1.1
297 10 : if (preg_match(self::REGEXP_INVALID_TOKEN, $method)) {
298 0 : throw new HTTP_Request2_LogicException(
299 0 : "Invalid request method '{$method}'",
300 : HTTP_Request2_Exception::INVALID_ARGUMENT
301 0 : );
302 : }
303 10 : $this->method = $method;
304 :
305 10 : return $this;
306 : }
307 :
308 : /**
309 : * Returns the request method
310 : *
311 : * @return string
312 : */
313 : public function getMethod()
314 : {
315 0 : return $this->method;
316 : }
317 :
318 : /**
319 : * Sets the configuration parameter(s)
320 : *
321 : * The following parameters are available:
322 : * <ul>
323 : * <li> 'adapter' - adapter to use (string)</li>
324 : * <li> 'connect_timeout' - Connection timeout in seconds (integer)</li>
325 : * <li> 'timeout' - Total number of seconds a request can take.
326 : * Use 0 for no limit, should be greater than
327 : * 'connect_timeout' if set (integer)</li>
328 : * <li> 'use_brackets' - Whether to append [] to array variable names (bool)</li>
329 : * <li> 'protocol_version' - HTTP Version to use, '1.0' or '1.1' (string)</li>
330 : * <li> 'buffer_size' - Buffer size to use for reading and writing (int)</li>
331 : * <li> 'store_body' - Whether to store response body in response object.
332 : * Set to false if receiving a huge response and
333 : * using an Observer to save it (boolean)</li>
334 : * <li> 'proxy_host' - Proxy server host (string)</li>
335 : * <li> 'proxy_port' - Proxy server port (integer)</li>
336 : * <li> 'proxy_user' - Proxy auth username (string)</li>
337 : * <li> 'proxy_password' - Proxy auth password (string)</li>
338 : * <li> 'proxy_auth_scheme' - Proxy auth scheme, one of HTTP_Request2::AUTH_* constants (string)</li>
339 : * <li> 'ssl_verify_peer' - Whether to verify peer's SSL certificate (bool)</li>
340 : * <li> 'ssl_verify_host' - Whether to check that Common Name in SSL
341 : * certificate matches host name (bool)</li>
342 : * <li> 'ssl_cafile' - Cerificate Authority file to verify the peer
343 : * with (use with 'ssl_verify_peer') (string)</li>
344 : * <li> 'ssl_capath' - Directory holding multiple Certificate
345 : * Authority files (string)</li>
346 : * <li> 'ssl_local_cert' - Name of a file containing local cerificate (string)</li>
347 : * <li> 'ssl_passphrase' - Passphrase with which local certificate
348 : * was encoded (string)</li>
349 : * <li> 'digest_compat_ie' - Whether to imitate behaviour of MSIE 5 and 6
350 : * in using URL without query string in digest
351 : * authentication (boolean)</li>
352 : * <li> 'follow_redirects' - Whether to automatically follow HTTP Redirects (boolean)</li>
353 : * <li> 'max_redirects' - Maximum number of redirects to follow (integer)</li>
354 : * <li> 'strict_redirects' - Whether to keep request method on redirects via status 301 and
355 : * 302 (true, needed for compatibility with RFC 2616)
356 : * or switch to GET (false, needed for compatibility with most
357 : * browsers) (boolean)</li>
358 : * </ul>
359 : *
360 : * @param string|array configuration parameter name or array
361 : * ('parameter name' => 'parameter value')
362 : * @param mixed parameter value if $nameOrConfig is not an array
363 : * @return HTTP_Request2
364 : * @throws HTTP_Request2_LogicException If the parameter is unknown
365 : */
366 : public function setConfig($nameOrConfig, $value = null)
367 : {
368 10 : if (is_array($nameOrConfig)) {
369 10 : foreach ($nameOrConfig as $name => $value) {
370 0 : $this->setConfig($name, $value);
371 10 : }
372 :
373 10 : } else {
374 0 : if (!array_key_exists($nameOrConfig, $this->config)) {
375 0 : throw new HTTP_Request2_LogicException(
376 0 : "Unknown configuration parameter '{$nameOrConfig}'",
377 : HTTP_Request2_Exception::INVALID_ARGUMENT
378 0 : );
379 : }
380 0 : $this->config[$nameOrConfig] = $value;
381 : }
382 :
383 10 : return $this;
384 : }
385 :
386 : /**
387 : * Returns the value(s) of the configuration parameter(s)
388 : *
389 : * @param string parameter name
390 : * @return mixed value of $name parameter, array of all configuration
391 : * parameters if $name is not given
392 : * @throws HTTP_Request2_LogicException If the parameter is unknown
393 : */
394 : public function getConfig($name = null)
395 : {
396 0 : if (null === $name) {
397 0 : return $this->config;
398 0 : } elseif (!array_key_exists($name, $this->config)) {
399 0 : throw new HTTP_Request2_LogicException(
400 0 : "Unknown configuration parameter '{$name}'",
401 : HTTP_Request2_Exception::INVALID_ARGUMENT
402 0 : );
403 : }
404 0 : return $this->config[$name];
405 : }
406 :
407 : /**
408 : * Sets the autentification data
409 : *
410 : * @param string user name
411 : * @param string password
412 : * @param string authentication scheme
413 : * @return HTTP_Request2
414 : */
415 : public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC)
416 : {
417 0 : if (empty($user)) {
418 0 : $this->auth = null;
419 0 : } else {
420 0 : $this->auth = array(
421 0 : 'user' => (string)$user,
422 0 : 'password' => (string)$password,
423 : 'scheme' => $scheme
424 0 : );
425 : }
426 :
427 0 : return $this;
428 : }
429 :
430 : /**
431 : * Returns the authentication data
432 : *
433 : * The array has the keys 'user', 'password' and 'scheme', where 'scheme'
434 : * is one of the HTTP_Request2::AUTH_* constants.
435 : *
436 : * @return array
437 : */
438 : public function getAuth()
439 : {
440 0 : return $this->auth;
441 : }
442 :
443 : /**
444 : * Sets request header(s)
445 : *
446 : * The first parameter may be either a full header string 'header: value' or
447 : * header name. In the former case $value parameter is ignored, in the latter
448 : * the header's value will either be set to $value or the header will be
449 : * removed if $value is null. The first parameter can also be an array of
450 : * headers, in that case method will be called recursively.
451 : *
452 : * Note that headers are treated case insensitively as per RFC 2616.
453 : *
454 : * <code>
455 : * $req->setHeader('Foo: Bar'); // sets the value of 'Foo' header to 'Bar'
456 : * $req->setHeader('FoO', 'Baz'); // sets the value of 'Foo' header to 'Baz'
457 : * $req->setHeader(array('foo' => 'Quux')); // sets the value of 'Foo' header to 'Quux'
458 : * $req->setHeader('FOO'); // removes 'Foo' header from request
459 : * </code>
460 : *
461 : * @param string|array header name, header string ('Header: value')
462 : * or an array of headers
463 : * @param string|array|null header value if $name is not an array,
464 : * header will be removed if value is null
465 : * @param bool whether to replace previous header with the
466 : * same name or append to its value
467 : * @return HTTP_Request2
468 : * @throws HTTP_Request2_LogicException
469 : */
470 : public function setHeader($name, $value = null, $replace = true)
471 : {
472 10 : if (is_array($name)) {
473 0 : foreach ($name as $k => $v) {
474 0 : if (is_string($k)) {
475 0 : $this->setHeader($k, $v, $replace);
476 0 : } else {
477 0 : $this->setHeader($v, null, $replace);
478 : }
479 0 : }
480 0 : } else {
481 10 : if (null === $value && strpos($name, ':')) {
482 0 : list($name, $value) = array_map('trim', explode(':', $name, 2));
483 0 : }
484 : // Header name should be a token: http://tools.ietf.org/html/rfc2616#section-4.2
485 10 : if (preg_match(self::REGEXP_INVALID_TOKEN, $name)) {
486 0 : throw new HTTP_Request2_LogicException(
487 0 : "Invalid header name '{$name}'",
488 : HTTP_Request2_Exception::INVALID_ARGUMENT
489 0 : );
490 : }
491 : // Header names are case insensitive anyway
492 10 : $name = strtolower($name);
493 10 : if (null === $value) {
494 0 : unset($this->headers[$name]);
495 :
496 0 : } else {
497 10 : if (is_array($value)) {
498 0 : $value = implode(', ', array_map('trim', $value));
499 10 : } elseif (is_string($value)) {
500 10 : $value = trim($value);
501 10 : }
502 10 : if (!isset($this->headers[$name]) || $replace) {
503 10 : $this->headers[$name] = $value;
504 10 : } else {
505 0 : $this->headers[$name] .= ', ' . $value;
506 : }
507 : }
508 : }
509 :
510 10 : return $this;
511 : }
512 :
513 : /**
514 : * Returns the request headers
515 : *
516 : * The array is of the form ('header name' => 'header value'), header names
517 : * are lowercased
518 : *
519 : * @return array
520 : */
521 : public function getHeaders()
522 : {
523 0 : return $this->headers;
524 : }
525 :
526 : /**
527 : * Adds a cookie to the request
528 : *
529 : * If the request does not have a CookieJar object set, this method simply
530 : * appends a cookie to "Cookie:" header.
531 : *
532 : * If a CookieJar object is available, the cookie is stored in that object.
533 : * Data from request URL will be used for setting its 'domain' and 'path'
534 : * parameters, 'expires' and 'secure' will be set to null and false,
535 : * respectively. If you need further control, use CookieJar's methods.
536 : *
537 : * @param string cookie name
538 : * @param string cookie value
539 : * @return HTTP_Request2
540 : * @throws HTTP_Request2_LogicException
541 : * @see setCookieJar()
542 : */
543 : public function addCookie($name, $value)
544 : {
545 0 : if (!empty($this->cookieJar)) {
546 0 : $this->cookieJar->store(array('name' => $name, 'value' => $value),
547 0 : $this->url);
548 :
549 0 : } else {
550 0 : $cookie = $name . '=' . $value;
551 0 : if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) {
552 0 : throw new HTTP_Request2_LogicException(
553 0 : "Invalid cookie: '{$cookie}'",
554 : HTTP_Request2_Exception::INVALID_ARGUMENT
555 0 : );
556 : }
557 0 : $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; ';
558 0 : $this->setHeader('cookie', $cookies . $cookie);
559 : }
560 :
561 0 : return $this;
562 : }
563 :
564 : /**
565 : * Sets the request body
566 : *
567 : * If you provide file pointer rather than file name, it should support
568 : * fstat() and rewind() operations.
569 : *
570 : * @param string|resource|HTTP_Request2_MultipartBody Either a string
571 : * with the body or filename containing body or pointer to
572 : * an open file or object with multipart body data
573 : * @param bool Whether first parameter is a filename
574 : * @return HTTP_Request2
575 : * @throws HTTP_Request2_LogicException
576 : */
577 : public function setBody($body, $isFilename = false)
578 : {
579 0 : if (!$isFilename && !is_resource($body)) {
580 0 : if (!$body instanceof HTTP_Request2_MultipartBody) {
581 0 : $this->body = (string)$body;
582 0 : } else {
583 0 : $this->body = $body;
584 : }
585 0 : } else {
586 0 : $fileData = $this->fopenWrapper($body, empty($this->headers['content-type']));
587 0 : $this->body = $fileData['fp'];
588 0 : if (empty($this->headers['content-type'])) {
589 0 : $this->setHeader('content-type', $fileData['type']);
590 0 : }
591 : }
592 0 : $this->postParams = $this->uploads = array();
593 :
594 0 : return $this;
595 : }
596 :
597 : /**
598 : * Returns the request body
599 : *
600 : * @return string|resource|HTTP_Request2_MultipartBody
601 : */
602 : public function getBody()
603 : {
604 0 : if (self::METHOD_POST == $this->method &&
605 0 : (!empty($this->postParams) || !empty($this->uploads))
606 0 : ) {
607 0 : if (0 === strpos($this->headers['content-type'], 'application/x-www-form-urlencoded')) {
608 0 : $body = http_build_query($this->postParams, '', '&');
609 0 : if (!$this->getConfig('use_brackets')) {
610 0 : $body = preg_replace('/%5B\d+%5D=/', '=', $body);
611 0 : }
612 : // support RFC 3986 by not encoding '~' symbol (request #15368)
613 0 : return str_replace('%7E', '~', $body);
614 :
615 0 : } elseif (0 === strpos($this->headers['content-type'], 'multipart/form-data')) {
616 0 : require_once 'HTTP/Request2/MultipartBody.php';
617 0 : return new HTTP_Request2_MultipartBody(
618 0 : $this->postParams, $this->uploads, $this->getConfig('use_brackets')
619 0 : );
620 : }
621 0 : }
622 0 : return $this->body;
623 : }
624 :
625 : /**
626 : * Adds a file to form-based file upload
627 : *
628 : * Used to emulate file upload via a HTML form. The method also sets
629 : * Content-Type of HTTP request to 'multipart/form-data'.
630 : *
631 : * If you just want to send the contents of a file as the body of HTTP
632 : * request you should use setBody() method.
633 : *
634 : * If you provide file pointers rather than file names, they should support
635 : * fstat() and rewind() operations.
636 : *
637 : * @param string name of file-upload field
638 : * @param string|resource|array full name of local file, pointer to
639 : * open file or an array of files
640 : * @param string filename to send in the request
641 : * @param string content-type of file being uploaded
642 : * @return HTTP_Request2
643 : * @throws HTTP_Request2_LogicException
644 : */
645 : public function addUpload($fieldName, $filename, $sendFilename = null,
646 : $contentType = null)
647 : {
648 0 : if (!is_array($filename)) {
649 0 : $fileData = $this->fopenWrapper($filename, empty($contentType));
650 0 : $this->uploads[$fieldName] = array(
651 0 : 'fp' => $fileData['fp'],
652 0 : 'filename' => !empty($sendFilename)? $sendFilename
653 0 : :(is_string($filename)? basename($filename): 'anonymous.blob') ,
654 0 : 'size' => $fileData['size'],
655 0 : 'type' => empty($contentType)? $fileData['type']: $contentType
656 0 : );
657 0 : } else {
658 0 : $fps = $names = $sizes = $types = array();
659 0 : foreach ($filename as $f) {
660 0 : if (!is_array($f)) {
661 0 : $f = array($f);
662 0 : }
663 0 : $fileData = $this->fopenWrapper($f[0], empty($f[2]));
664 0 : $fps[] = $fileData['fp'];
665 0 : $names[] = !empty($f[1])? $f[1]
666 0 : :(is_string($f[0])? basename($f[0]): 'anonymous.blob');
667 0 : $sizes[] = $fileData['size'];
668 0 : $types[] = empty($f[2])? $fileData['type']: $f[2];
669 0 : }
670 0 : $this->uploads[$fieldName] = array(
671 0 : 'fp' => $fps, 'filename' => $names, 'size' => $sizes, 'type' => $types
672 0 : );
673 : }
674 0 : if (empty($this->headers['content-type']) ||
675 0 : 'application/x-www-form-urlencoded' == $this->headers['content-type']
676 0 : ) {
677 0 : $this->setHeader('content-type', 'multipart/form-data');
678 0 : }
679 :
680 0 : return $this;
681 : }
682 :
683 : /**
684 : * Adds POST parameter(s) to the request.
685 : *
686 : * @param string|array parameter name or array ('name' => 'value')
687 : * @param mixed parameter value (can be an array)
688 : * @return HTTP_Request2
689 : */
690 : public function addPostParameter($name, $value = null)
691 : {
692 0 : if (!is_array($name)) {
693 0 : $this->postParams[$name] = $value;
694 0 : } else {
695 0 : foreach ($name as $k => $v) {
696 0 : $this->addPostParameter($k, $v);
697 0 : }
698 : }
699 0 : if (empty($this->headers['content-type'])) {
700 0 : $this->setHeader('content-type', 'application/x-www-form-urlencoded');
701 0 : }
702 :
703 0 : return $this;
704 : }
705 :
706 : /**
707 : * Attaches a new observer
708 : *
709 : * @param SplObserver
710 : */
711 : public function attach(SplObserver $observer)
712 : {
713 0 : foreach ($this->observers as $attached) {
714 0 : if ($attached === $observer) {
715 0 : return;
716 : }
717 0 : }
718 0 : $this->observers[] = $observer;
719 0 : }
720 :
721 : /**
722 : * Detaches an existing observer
723 : *
724 : * @param SplObserver
725 : */
726 : public function detach(SplObserver $observer)
727 : {
728 0 : foreach ($this->observers as $key => $attached) {
729 0 : if ($attached === $observer) {
730 0 : unset($this->observers[$key]);
731 0 : return;
732 : }
733 0 : }
734 0 : }
735 :
736 : /**
737 : * Notifies all observers
738 : */
739 : public function notify()
740 : {
741 0 : foreach ($this->observers as $observer) {
742 0 : $observer->update($this);
743 0 : }
744 0 : }
745 :
746 : /**
747 : * Sets the last event
748 : *
749 : * Adapters should use this method to set the current state of the request
750 : * and notify the observers.
751 : *
752 : * @param string event name
753 : * @param mixed event data
754 : */
755 : public function setLastEvent($name, $data = null)
756 : {
757 0 : $this->lastEvent = array(
758 0 : 'name' => $name,
759 : 'data' => $data
760 0 : );
761 0 : $this->notify();
762 0 : }
763 :
764 : /**
765 : * Returns the last event
766 : *
767 : * Observers should use this method to access the last change in request.
768 : * The following event names are possible:
769 : * <ul>
770 : * <li>'connect' - after connection to remote server,
771 : * data is the destination (string)</li>
772 : * <li>'disconnect' - after disconnection from server</li>
773 : * <li>'sentHeaders' - after sending the request headers,
774 : * data is the headers sent (string)</li>
775 : * <li>'sentBodyPart' - after sending a part of the request body,
776 : * data is the length of that part (int)</li>
777 : * <li>'sentBody' - after sending the whole request body,
778 : * data is request body length (int)</li>
779 : * <li>'receivedHeaders' - after receiving the response headers,
780 : * data is HTTP_Request2_Response object</li>
781 : * <li>'receivedBodyPart' - after receiving a part of the response
782 : * body, data is that part (string)</li>
783 : * <li>'receivedEncodedBodyPart' - as 'receivedBodyPart', but data is still
784 : * encoded by Content-Encoding</li>
785 : * <li>'receivedBody' - after receiving the complete response
786 : * body, data is HTTP_Request2_Response object</li>
787 : * </ul>
788 : * Different adapters may not send all the event types. Mock adapter does
789 : * not send any events to the observers.
790 : *
791 : * @return array The array has two keys: 'name' and 'data'
792 : */
793 : public function getLastEvent()
794 : {
795 0 : return $this->lastEvent;
796 : }
797 :
798 : /**
799 : * Sets the adapter used to actually perform the request
800 : *
801 : * You can pass either an instance of a class implementing HTTP_Request2_Adapter
802 : * or a class name. The method will only try to include a file if the class
803 : * name starts with HTTP_Request2_Adapter_, it will also try to prepend this
804 : * prefix to the class name if it doesn't contain any underscores, so that
805 : * <code>
806 : * $request->setAdapter('curl');
807 : * </code>
808 : * will work.
809 : *
810 : * @param string|HTTP_Request2_Adapter
811 : * @return HTTP_Request2
812 : * @throws HTTP_Request2_LogicException
813 : */
814 : public function setAdapter($adapter)
815 : {
816 10 : if (is_string($adapter)) {
817 0 : if (!class_exists($adapter, false)) {
818 0 : if (false === strpos($adapter, '_')) {
819 0 : $adapter = 'HTTP_Request2_Adapter_' . ucfirst($adapter);
820 0 : }
821 0 : if (preg_match('/^HTTP_Request2_Adapter_([a-zA-Z0-9]+)$/', $adapter)) {
822 0 : include_once str_replace('_', DIRECTORY_SEPARATOR, $adapter) . '.php';
823 0 : }
824 0 : if (!class_exists($adapter, false)) {
825 0 : throw new HTTP_Request2_LogicException(
826 0 : "Class {$adapter} not found",
827 : HTTP_Request2_Exception::MISSING_VALUE
828 0 : );
829 : }
830 0 : }
831 0 : $adapter = new $adapter;
832 0 : }
833 10 : if (!$adapter instanceof HTTP_Request2_Adapter) {
834 0 : throw new HTTP_Request2_LogicException(
835 0 : 'Parameter is not a HTTP request adapter',
836 : HTTP_Request2_Exception::INVALID_ARGUMENT
837 0 : );
838 : }
839 10 : $this->adapter = $adapter;
840 :
841 10 : return $this;
842 : }
843 :
844 : /**
845 : * Sets the cookie jar
846 : *
847 : * A cookie jar is used to maintain cookies across HTTP requests and
848 : * responses. Cookies from jar will be automatically added to the request
849 : * headers based on request URL.
850 : *
851 : * @param HTTP_Request2_CookieJar|bool Existing CookieJar object, true to
852 : * create a new one, false to remove
853 : */
854 : public function setCookieJar($jar = true)
855 : {
856 0 : if (!class_exists('HTTP_Request2_CookieJar', false)) {
857 0 : require_once 'HTTP/Request2/CookieJar.php';
858 0 : }
859 :
860 0 : if ($jar instanceof HTTP_Request2_CookieJar) {
861 0 : $this->cookieJar = $jar;
862 0 : } elseif (true === $jar) {
863 0 : $this->cookieJar = new HTTP_Request2_CookieJar();
864 0 : } elseif (!$jar) {
865 0 : $this->cookieJar = null;
866 0 : } else {
867 0 : throw new HTTP_Request2_LogicException(
868 0 : 'Invalid parameter passed to setCookieJar()',
869 : HTTP_Request2_Exception::INVALID_ARGUMENT
870 0 : );
871 : }
872 :
873 0 : return $this;
874 : }
875 :
876 : /**
877 : * Returns current CookieJar object or null if none
878 : *
879 : * @return HTTP_Request2_CookieJar|null
880 : */
881 : public function getCookieJar()
882 : {
883 0 : return $this->cookieJar;
884 : }
885 :
886 : /**
887 : * Sends the request and returns the response
888 : *
889 : * @throws HTTP_Request2_Exception
890 : * @return HTTP_Request2_Response
891 : */
892 : public function send()
893 : {
894 : // Sanity check for URL
895 10 : if (!$this->url instanceof Net_URL2
896 10 : || !$this->url->isAbsolute()
897 10 : || !in_array(strtolower($this->url->getScheme()), array('https', 'http'))
898 10 : ) {
899 0 : throw new HTTP_Request2_LogicException(
900 : 'HTTP_Request2 needs an absolute HTTP(S) request URL, '
901 0 : . ($this->url instanceof Net_URL2
902 0 : ? "'" . $this->url->__toString() . "'" : 'none')
903 0 : . ' given',
904 : HTTP_Request2_Exception::INVALID_ARGUMENT
905 0 : );
906 : }
907 10 : if (empty($this->adapter)) {
908 0 : $this->setAdapter($this->getConfig('adapter'));
909 0 : }
910 : // magic_quotes_runtime may break file uploads and chunked response
911 : // processing; see bug #4543. Don't use ini_get() here; see bug #16440.
912 10 : if ($magicQuotes = get_magic_quotes_runtime()) {
913 0 : set_magic_quotes_runtime(false);
914 0 : }
915 : // force using single byte encoding if mbstring extension overloads
916 : // strlen() and substr(); see bug #1781, bug #10605
917 10 : if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) {
918 0 : $oldEncoding = mb_internal_encoding();
919 0 : mb_internal_encoding('iso-8859-1');
920 0 : }
921 :
922 : try {
923 10 : $response = $this->adapter->sendRequest($this);
924 10 : } catch (Exception $e) {
925 : }
926 : // cleanup in either case (poor man's "finally" clause)
927 10 : if ($magicQuotes) {
928 0 : set_magic_quotes_runtime(true);
929 0 : }
930 10 : if (!empty($oldEncoding)) {
931 0 : mb_internal_encoding($oldEncoding);
932 0 : }
933 : // rethrow the exception
934 10 : if (!empty($e)) {
935 0 : throw $e;
936 : }
937 10 : return $response;
938 : }
939 :
940 : /**
941 : * Wrapper around fopen()/fstat() used by setBody() and addUpload()
942 : *
943 : * @param string|resource file name or pointer to open file
944 : * @param bool whether to try autodetecting MIME type of file,
945 : * will only work if $file is a filename, not pointer
946 : * @return array array('fp' => file pointer, 'size' => file size, 'type' => MIME type)
947 : * @throws HTTP_Request2_LogicException
948 : */
949 : protected function fopenWrapper($file, $detectType = false)
950 : {
951 0 : if (!is_string($file) && !is_resource($file)) {
952 0 : throw new HTTP_Request2_LogicException(
953 0 : "Filename or file pointer resource expected",
954 : HTTP_Request2_Exception::INVALID_ARGUMENT
955 0 : );
956 : }
957 : $fileData = array(
958 0 : 'fp' => is_string($file)? null: $file,
959 0 : 'type' => 'application/octet-stream',
960 : 'size' => 0
961 0 : );
962 0 : if (is_string($file)) {
963 0 : $track = @ini_set('track_errors', 1);
964 0 : if (!($fileData['fp'] = @fopen($file, 'rb'))) {
965 0 : $e = new HTTP_Request2_LogicException(
966 0 : $php_errormsg, HTTP_Request2_Exception::READ_ERROR
967 0 : );
968 0 : }
969 0 : @ini_set('track_errors', $track);
970 0 : if (isset($e)) {
971 0 : throw $e;
972 : }
973 0 : if ($detectType) {
974 0 : $fileData['type'] = self::detectMimeType($file);
975 0 : }
976 0 : }
977 0 : if (!($stat = fstat($fileData['fp']))) {
978 0 : throw new HTTP_Request2_LogicException(
979 0 : "fstat() call failed", HTTP_Request2_Exception::READ_ERROR
980 0 : );
981 : }
982 0 : $fileData['size'] = $stat['size'];
983 :
984 0 : return $fileData;
985 : }
986 :
987 : /**
988 : * Tries to detect MIME type of a file
989 : *
990 : * The method will try to use fileinfo extension if it is available,
991 : * deprecated mime_content_type() function in the other case. If neither
992 : * works, default 'application/octet-stream' MIME type is returned
993 : *
994 : * @param string filename
995 : * @return string file MIME type
996 : */
997 : protected static function detectMimeType($filename)
998 : {
999 : // finfo extension from PECL available
1000 0 : if (function_exists('finfo_open')) {
1001 0 : if (!isset(self::$_fileinfoDb)) {
1002 0 : self::$_fileinfoDb = @finfo_open(FILEINFO_MIME);
1003 0 : }
1004 0 : if (self::$_fileinfoDb) {
1005 0 : $info = finfo_file(self::$_fileinfoDb, $filename);
1006 0 : }
1007 0 : }
1008 : // (deprecated) mime_content_type function available
1009 0 : if (empty($info) && function_exists('mime_content_type')) {
1010 0 : return mime_content_type($filename);
1011 : }
1012 0 : return empty($info)? 'application/octet-stream': $info;
1013 : }
1014 : }
|