Curl.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. <?php
  2. /**
  3. * Adapter for HTTP_Request2 wrapping around cURL extension
  4. *
  5. * PHP version 5
  6. *
  7. * LICENSE
  8. *
  9. * This source file is subject to BSD 3-Clause License that is bundled
  10. * with this package in the file LICENSE and available at the URL
  11. * https://raw.github.com/pear/HTTP_Request2/trunk/docs/LICENSE
  12. *
  13. * @category HTTP
  14. * @package HTTP_Request2
  15. * @author Alexey Borzov <avb@php.net>
  16. * @copyright 2008-2016 Alexey Borzov <avb@php.net>
  17. * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
  18. * @link http://pear.php.net/package/HTTP_Request2
  19. */
  20. /**
  21. * Base class for HTTP_Request2 adapters
  22. */
  23. require_once 'HTTP/Request2/Adapter.php';
  24. /**
  25. * Adapter for HTTP_Request2 wrapping around cURL extension
  26. *
  27. * @category HTTP
  28. * @package HTTP_Request2
  29. * @author Alexey Borzov <avb@php.net>
  30. * @license http://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
  31. * @version Release: 2.3.0
  32. * @link http://pear.php.net/package/HTTP_Request2
  33. */
  34. class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter
  35. {
  36. /**
  37. * Mapping of header names to cURL options
  38. * @var array
  39. */
  40. protected static $headerMap = array(
  41. 'accept-encoding' => CURLOPT_ENCODING,
  42. 'cookie' => CURLOPT_COOKIE,
  43. 'referer' => CURLOPT_REFERER,
  44. 'user-agent' => CURLOPT_USERAGENT
  45. );
  46. /**
  47. * Mapping of SSL context options to cURL options
  48. * @var array
  49. */
  50. protected static $sslContextMap = array(
  51. 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER,
  52. 'ssl_cafile' => CURLOPT_CAINFO,
  53. 'ssl_capath' => CURLOPT_CAPATH,
  54. 'ssl_local_cert' => CURLOPT_SSLCERT,
  55. 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD
  56. );
  57. /**
  58. * Mapping of CURLE_* constants to Exception subclasses and error codes
  59. * @var array
  60. */
  61. protected static $errorMap = array(
  62. CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException',
  63. HTTP_Request2_Exception::NON_HTTP_REDIRECT),
  64. CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'),
  65. CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'),
  66. CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'),
  67. // error returned from write callback
  68. CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException',
  69. HTTP_Request2_Exception::NON_HTTP_REDIRECT),
  70. CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException',
  71. HTTP_Request2_Exception::TIMEOUT),
  72. CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'),
  73. CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'),
  74. CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException',
  75. HTTP_Request2_Exception::MISCONFIGURATION),
  76. CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException',
  77. HTTP_Request2_Exception::MISCONFIGURATION),
  78. CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException',
  79. HTTP_Request2_Exception::NON_HTTP_REDIRECT),
  80. CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException',
  81. HTTP_Request2_Exception::TOO_MANY_REDIRECTS),
  82. CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'),
  83. CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'),
  84. CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException',
  85. HTTP_Request2_Exception::MISCONFIGURATION),
  86. CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException',
  87. HTTP_Request2_Exception::MISCONFIGURATION),
  88. CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'),
  89. CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'),
  90. CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException',
  91. HTTP_Request2_Exception::INVALID_ARGUMENT),
  92. CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'),
  93. CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'),
  94. CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'),
  95. );
  96. /**
  97. * Response being received
  98. * @var HTTP_Request2_Response
  99. */
  100. protected $response;
  101. /**
  102. * Whether 'sentHeaders' event was sent to observers
  103. * @var boolean
  104. */
  105. protected $eventSentHeaders = false;
  106. /**
  107. * Whether 'receivedHeaders' event was sent to observers
  108. * @var boolean
  109. */
  110. protected $eventReceivedHeaders = false;
  111. /**
  112. * Whether 'sentBoody' event was sent to observers
  113. * @var boolean
  114. */
  115. protected $eventSentBody = false;
  116. /**
  117. * Position within request body
  118. * @var integer
  119. * @see callbackReadBody()
  120. */
  121. protected $position = 0;
  122. /**
  123. * Information about last transfer, as returned by curl_getinfo()
  124. * @var array
  125. */
  126. protected $lastInfo;
  127. /**
  128. * Creates a subclass of HTTP_Request2_Exception from curl error data
  129. *
  130. * @param resource $ch curl handle
  131. *
  132. * @return HTTP_Request2_Exception
  133. */
  134. protected static function wrapCurlError($ch)
  135. {
  136. $nativeCode = curl_errno($ch);
  137. $message = 'Curl error: ' . curl_error($ch);
  138. if (!isset(self::$errorMap[$nativeCode])) {
  139. return new HTTP_Request2_Exception($message, 0, $nativeCode);
  140. } else {
  141. $class = self::$errorMap[$nativeCode][0];
  142. $code = empty(self::$errorMap[$nativeCode][1])
  143. ? 0 : self::$errorMap[$nativeCode][1];
  144. return new $class($message, $code, $nativeCode);
  145. }
  146. }
  147. /**
  148. * Sends request to the remote server and returns its response
  149. *
  150. * @param HTTP_Request2 $request HTTP request message
  151. *
  152. * @return HTTP_Request2_Response
  153. * @throws HTTP_Request2_Exception
  154. */
  155. public function sendRequest(HTTP_Request2 $request)
  156. {
  157. if (!extension_loaded('curl')) {
  158. throw new HTTP_Request2_LogicException(
  159. 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION
  160. );
  161. }
  162. $this->request = $request;
  163. $this->response = null;
  164. $this->position = 0;
  165. $this->eventSentHeaders = false;
  166. $this->eventReceivedHeaders = false;
  167. $this->eventSentBody = false;
  168. try {
  169. if (false === curl_exec($ch = $this->createCurlHandle())) {
  170. $e = self::wrapCurlError($ch);
  171. }
  172. } catch (Exception $e) {
  173. }
  174. if (isset($ch)) {
  175. $this->lastInfo = curl_getinfo($ch);
  176. if (CURLE_OK !== curl_errno($ch)) {
  177. $this->request->setLastEvent('warning', curl_error($ch));
  178. }
  179. curl_close($ch);
  180. }
  181. $response = $this->response;
  182. unset($this->request, $this->requestBody, $this->response);
  183. if (!empty($e)) {
  184. throw $e;
  185. }
  186. if ($jar = $request->getCookieJar()) {
  187. $jar->addCookiesFromResponse($response);
  188. }
  189. if (0 < $this->lastInfo['size_download']) {
  190. $request->setLastEvent('receivedBody', $response);
  191. }
  192. return $response;
  193. }
  194. /**
  195. * Returns information about last transfer
  196. *
  197. * @return array associative array as returned by curl_getinfo()
  198. */
  199. public function getInfo()
  200. {
  201. return $this->lastInfo;
  202. }
  203. /**
  204. * Creates a new cURL handle and populates it with data from the request
  205. *
  206. * @return resource a cURL handle, as created by curl_init()
  207. * @throws HTTP_Request2_LogicException
  208. * @throws HTTP_Request2_NotImplementedException
  209. */
  210. protected function createCurlHandle()
  211. {
  212. $ch = curl_init();
  213. curl_setopt_array($ch, array(
  214. // setup write callbacks
  215. CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'),
  216. CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'),
  217. // buffer size
  218. CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'),
  219. // connection timeout
  220. CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'),
  221. // save full outgoing headers, in case someone is interested
  222. CURLINFO_HEADER_OUT => true,
  223. // request url
  224. CURLOPT_URL => $this->request->getUrl()->getUrl()
  225. ));
  226. // set up redirects
  227. if (!$this->request->getConfig('follow_redirects')) {
  228. curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
  229. } else {
  230. if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) {
  231. throw new HTTP_Request2_LogicException(
  232. 'Redirect support in curl is unavailable due to open_basedir or safe_mode setting',
  233. HTTP_Request2_Exception::MISCONFIGURATION
  234. );
  235. }
  236. curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects'));
  237. // limit redirects to http(s), works in 5.2.10+
  238. if (defined('CURLOPT_REDIR_PROTOCOLS')) {
  239. curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
  240. }
  241. // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571
  242. if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) {
  243. curl_setopt($ch, CURLOPT_POSTREDIR, 3);
  244. }
  245. }
  246. // set local IP via CURLOPT_INTERFACE (request #19515)
  247. if ($ip = $this->request->getConfig('local_ip')) {
  248. curl_setopt($ch, CURLOPT_INTERFACE, $ip);
  249. }
  250. // request timeout
  251. if ($timeout = $this->request->getConfig('timeout')) {
  252. curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
  253. }
  254. // set HTTP version
  255. switch ($this->request->getConfig('protocol_version')) {
  256. case '1.0':
  257. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
  258. break;
  259. case '1.1':
  260. curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  261. }
  262. // set request method
  263. switch ($this->request->getMethod()) {
  264. case HTTP_Request2::METHOD_GET:
  265. curl_setopt($ch, CURLOPT_HTTPGET, true);
  266. break;
  267. case HTTP_Request2::METHOD_POST:
  268. curl_setopt($ch, CURLOPT_POST, true);
  269. break;
  270. case HTTP_Request2::METHOD_HEAD:
  271. curl_setopt($ch, CURLOPT_NOBODY, true);
  272. break;
  273. case HTTP_Request2::METHOD_PUT:
  274. curl_setopt($ch, CURLOPT_UPLOAD, true);
  275. break;
  276. default:
  277. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod());
  278. }
  279. // set proxy, if needed
  280. if ($host = $this->request->getConfig('proxy_host')) {
  281. if (!($port = $this->request->getConfig('proxy_port'))) {
  282. throw new HTTP_Request2_LogicException(
  283. 'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE
  284. );
  285. }
  286. curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port);
  287. if ($user = $this->request->getConfig('proxy_user')) {
  288. curl_setopt(
  289. $ch, CURLOPT_PROXYUSERPWD,
  290. $user . ':' . $this->request->getConfig('proxy_password')
  291. );
  292. switch ($this->request->getConfig('proxy_auth_scheme')) {
  293. case HTTP_Request2::AUTH_BASIC:
  294. curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
  295. break;
  296. case HTTP_Request2::AUTH_DIGEST:
  297. curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST);
  298. }
  299. }
  300. if ($type = $this->request->getConfig('proxy_type')) {
  301. switch ($type) {
  302. case 'http':
  303. curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
  304. break;
  305. case 'socks5':
  306. curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
  307. break;
  308. default:
  309. throw new HTTP_Request2_NotImplementedException(
  310. "Proxy type '{$type}' is not supported"
  311. );
  312. }
  313. }
  314. }
  315. // set authentication data
  316. if ($auth = $this->request->getAuth()) {
  317. curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']);
  318. switch ($auth['scheme']) {
  319. case HTTP_Request2::AUTH_BASIC:
  320. curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
  321. break;
  322. case HTTP_Request2::AUTH_DIGEST:
  323. curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
  324. }
  325. }
  326. // set SSL options
  327. foreach ($this->request->getConfig() as $name => $value) {
  328. if ('ssl_verify_host' == $name && null !== $value) {
  329. curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0);
  330. } elseif (isset(self::$sslContextMap[$name]) && null !== $value) {
  331. curl_setopt($ch, self::$sslContextMap[$name], $value);
  332. }
  333. }
  334. $headers = $this->request->getHeaders();
  335. // make cURL automagically send proper header
  336. if (!isset($headers['accept-encoding'])) {
  337. $headers['accept-encoding'] = '';
  338. }
  339. if (($jar = $this->request->getCookieJar())
  340. && ($cookies = $jar->getMatching($this->request->getUrl(), true))
  341. ) {
  342. $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies;
  343. }
  344. // set headers having special cURL keys
  345. foreach (self::$headerMap as $name => $option) {
  346. if (isset($headers[$name])) {
  347. curl_setopt($ch, $option, $headers[$name]);
  348. unset($headers[$name]);
  349. }
  350. }
  351. $this->calculateRequestLength($headers);
  352. if (isset($headers['content-length']) || isset($headers['transfer-encoding'])) {
  353. $this->workaroundPhpBug47204($ch, $headers);
  354. }
  355. // set headers not having special keys
  356. $headersFmt = array();
  357. foreach ($headers as $name => $value) {
  358. $canonicalName = implode('-', array_map('ucfirst', explode('-', $name)));
  359. $headersFmt[] = $canonicalName . ': ' . $value;
  360. }
  361. curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt);
  362. return $ch;
  363. }
  364. /**
  365. * Workaround for PHP bug #47204 that prevents rewinding request body
  366. *
  367. * The workaround consists of reading the entire request body into memory
  368. * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large
  369. * file uploads, use Socket adapter instead.
  370. *
  371. * @param resource $ch cURL handle
  372. * @param array &$headers Request headers
  373. */
  374. protected function workaroundPhpBug47204($ch, &$headers)
  375. {
  376. // no redirects, no digest auth -> probably no rewind needed
  377. // also apply workaround only for POSTs, othrerwise we get
  378. // https://pear.php.net/bugs/bug.php?id=20440 for PUTs
  379. if (!$this->request->getConfig('follow_redirects')
  380. && (!($auth = $this->request->getAuth())
  381. || HTTP_Request2::AUTH_DIGEST != $auth['scheme'])
  382. || HTTP_Request2::METHOD_POST !== $this->request->getMethod()
  383. ) {
  384. curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody'));
  385. } else {
  386. // rewind may be needed, read the whole body into memory
  387. if ($this->requestBody instanceof HTTP_Request2_MultipartBody) {
  388. $this->requestBody = $this->requestBody->__toString();
  389. } elseif (is_resource($this->requestBody)) {
  390. $fp = $this->requestBody;
  391. $this->requestBody = '';
  392. while (!feof($fp)) {
  393. $this->requestBody .= fread($fp, 16384);
  394. }
  395. }
  396. // curl hangs up if content-length is present
  397. unset($headers['content-length']);
  398. curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody);
  399. }
  400. }
  401. /**
  402. * Callback function called by cURL for reading the request body
  403. *
  404. * @param resource $ch cURL handle
  405. * @param resource $fd file descriptor (not used)
  406. * @param integer $length maximum length of data to return
  407. *
  408. * @return string part of the request body, up to $length bytes
  409. */
  410. protected function callbackReadBody($ch, $fd, $length)
  411. {
  412. if (!$this->eventSentHeaders) {
  413. $this->request->setLastEvent(
  414. 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
  415. );
  416. $this->eventSentHeaders = true;
  417. }
  418. if (in_array($this->request->getMethod(), self::$bodyDisallowed)
  419. || 0 == $this->contentLength || $this->position >= $this->contentLength
  420. ) {
  421. return '';
  422. }
  423. if (is_string($this->requestBody)) {
  424. $string = substr($this->requestBody, $this->position, $length);
  425. } elseif (is_resource($this->requestBody)) {
  426. $string = fread($this->requestBody, $length);
  427. } else {
  428. $string = $this->requestBody->read($length);
  429. }
  430. $this->request->setLastEvent('sentBodyPart', strlen($string));
  431. $this->position += strlen($string);
  432. return $string;
  433. }
  434. /**
  435. * Callback function called by cURL for saving the response headers
  436. *
  437. * @param resource $ch cURL handle
  438. * @param string $string response header (with trailing CRLF)
  439. *
  440. * @return integer number of bytes saved
  441. * @see HTTP_Request2_Response::parseHeaderLine()
  442. */
  443. protected function callbackWriteHeader($ch, $string)
  444. {
  445. if (!$this->eventSentHeaders
  446. // we may receive a second set of headers if doing e.g. digest auth
  447. // but don't bother with 100-Continue responses (bug #15785)
  448. || $this->eventReceivedHeaders && $this->response->getStatus() >= 200
  449. ) {
  450. $this->request->setLastEvent(
  451. 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT)
  452. );
  453. }
  454. if (!$this->eventSentBody) {
  455. $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD);
  456. // if body wasn't read by the callback, send event with total body size
  457. if ($upload > $this->position) {
  458. $this->request->setLastEvent(
  459. 'sentBodyPart', $upload - $this->position
  460. );
  461. }
  462. if ($upload > 0) {
  463. $this->request->setLastEvent('sentBody', $upload);
  464. }
  465. }
  466. $this->eventSentHeaders = true;
  467. $this->eventSentBody = true;
  468. if ($this->eventReceivedHeaders || empty($this->response)) {
  469. $this->eventReceivedHeaders = false;
  470. $this->response = new HTTP_Request2_Response(
  471. $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL)
  472. );
  473. } else {
  474. $this->response->parseHeaderLine($string);
  475. if ('' == trim($string)) {
  476. // don't bother with 100-Continue responses (bug #15785)
  477. if (200 <= $this->response->getStatus()) {
  478. $this->request->setLastEvent('receivedHeaders', $this->response);
  479. }
  480. if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) {
  481. $redirectUrl = new Net_URL2($this->response->getHeader('location'));
  482. // for versions lower than 5.2.10, check the redirection URL protocol
  483. if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute()
  484. && !in_array($redirectUrl->getScheme(), array('http', 'https'))
  485. ) {
  486. return -1;
  487. }
  488. if ($jar = $this->request->getCookieJar()) {
  489. $jar->addCookiesFromResponse($this->response);
  490. if (!$redirectUrl->isAbsolute()) {
  491. $redirectUrl = $this->request->getUrl()->resolve($redirectUrl);
  492. }
  493. if ($cookies = $jar->getMatching($redirectUrl, true)) {
  494. curl_setopt($ch, CURLOPT_COOKIE, $cookies);
  495. }
  496. }
  497. }
  498. $this->eventReceivedHeaders = true;
  499. $this->eventSentBody = false;
  500. }
  501. }
  502. return strlen($string);
  503. }
  504. /**
  505. * Callback function called by cURL for saving the response body
  506. *
  507. * @param resource $ch cURL handle (not used)
  508. * @param string $string part of the response body
  509. *
  510. * @return integer number of bytes saved
  511. * @throws HTTP_Request2_MessageException
  512. * @see HTTP_Request2_Response::appendBody()
  513. */
  514. protected function callbackWriteBody($ch, $string)
  515. {
  516. // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if
  517. // response doesn't start with proper HTTP status line (see bug #15716)
  518. if (empty($this->response)) {
  519. throw new HTTP_Request2_MessageException(
  520. "Malformed response: {$string}",
  521. HTTP_Request2_Exception::MALFORMED_RESPONSE
  522. );
  523. }
  524. if ($this->request->getConfig('store_body')) {
  525. $this->response->appendBody($string);
  526. }
  527. $this->request->setLastEvent('receivedBodyPart', $string);
  528. return strlen($string);
  529. }
  530. }
  531. ?>