/** * Reads multipart data from sourceConnection and streams it to the * targetConnection.Returns the body of the request or the status code in * case there is no body. * * @param $method * @param $path * @param $streamEnd * @param array $requestHeaders * @return mixed|string * @throws \Exception * @throws \HTTPException */ protected function sendStream($method, $path, $streamEnd, $requestHeaders = array()) { $dataStream = $this->sourceConnection; // Read the json doc. Use _attachments field to find the total // Content-Length and create the request header with initial doc data. // At present CouchDB can't handle chunked data and needs // Content-Length header. $str = ''; $jsonFlag = 0; $attachmentCount = 0; $totalAttachmentLength = 0; $streamLine = $this->getNextLineFromSourceConnection(); while ($jsonFlag == 0 || $jsonFlag == 1 && trim($streamLine) == '') { $str .= $streamLine; if (strpos($streamLine, 'Content-Type: application/json') !== false) { $jsonFlag = 1; } $streamLine = $this->getNextLineFromSourceConnection(); } $docBoundaryLength = strlen(explode('=', $requestHeaders['Content-Type'], 2)[1]) + 2; $json = json_decode($streamLine, true); foreach ($json['_attachments'] as $docName => $metaData) { // Quotes and a "/r/n" $totalAttachmentLength += strlen('Content-Disposition: attachment; filename=') + strlen($docName) + 4; $totalAttachmentLength += strlen('Content-Type: ') + strlen($metaData['content_type']) + 2; $totalAttachmentLength += strlen('Content-Length: '); if (isset($metaData['encoding'])) { $totalAttachmentLength += $metaData['encoded_length'] + strlen($metaData['encoded_length']) + 2; $totalAttachmentLength += strlen('Content-Encoding: ') + strlen($metaData['encoding']) + 2; } else { $totalAttachmentLength += $metaData['length'] + strlen($metaData['length']) + 2; } $totalAttachmentLength += 2; $attachmentCount++; } // Add Content-Length to the headers. $requestHeaders['Content-Length'] = strlen($str) + strlen($streamLine) + $totalAttachmentLength + $attachmentCount * (2 + $docBoundaryLength) + $docBoundaryLength + 2; if ($this->targetConnection == null) { $this->targetConnection = $this->targetClient->getConnection($method, $path, null, $requestHeaders); } // Write the initial body data. fwrite($this->targetConnection, $str); // Write the rest of the data including attachments line by line or in // chunks. while (!feof($dataStream) && ($streamEnd === null || strpos($streamLine, $streamEnd) === false)) { $totalSent = 0; $length = strlen($streamLine); while ($totalSent != $length) { $sent = fwrite($this->targetConnection, substr($streamLine, $totalSent)); if ($sent === false) { throw new \HTTPException('Stream write error.'); } else { $totalSent += $sent; } } // Use maxLength while reading the data as there may be no newlines // in the binary and compressed attachments, or the lines may be // very long. $streamLine = $this->getNextLineFromSourceConnection(100000); } // Read response headers $rawHeaders = ''; $headers = array('connection' => $this->targetClient->getOptions()['keep-alive'] ? 'Keep-Alive' : 'Close'); // Remove leading newlines, should not occur at all, actually. while (($line = fgets($this->targetConnection)) !== false && ($lineContent = rtrim($line)) === '') { } // Throw exception, if connection has been aborted by the server, and // leave handling to the user for now. if ($line === false) { // sendStream can't be called in recursion as the source stream can be // read only once. $error = error_get_last(); throw HTTPException::connectionFailure($this->targetClient->getOptions()['ip'], $this->targetClient->getOptions()['port'], $error['message'], 0); } do { // Also store raw headers for later logging $rawHeaders .= $lineContent . "\n"; // Extract header values if (preg_match('(^HTTP/(?P<version>\\d+\\.\\d+)\\s+(?P<status>\\d+))S', $lineContent, $match)) { $headers['version'] = $match['version']; $headers['status'] = (int) $match['status']; } else { list($key, $value) = explode(':', $lineContent, 2); $headers[strtolower($key)] = ltrim($value); } } while (($line = fgets($this->targetConnection)) !== false && ($lineContent = rtrim($line)) !== ''); // Read response body $body = ''; // HTTP 1.1 supports chunked transfer encoding, if the according // header is not set, just read the specified amount of bytes. $bytesToRead = (int) (isset($headers['content-length']) ? $headers['content-length'] : 0); // Read body only as specified by chunk sizes, everything else // are just footnotes, which are not relevant for us. while ($bytesToRead > 0) { $body .= $read = fgets($this->targetConnection, $bytesToRead + 1); $bytesToRead -= strlen($read); } // Reset the connection if the server asks for it. if ($headers['connection'] !== 'Keep-Alive') { fclose($this->targetConnection); $this->targetConnection = null; } // Handle some response state as special cases switch ($headers['status']) { case 301: case 302: case 303: case 307: // Temporary redirect. // sendStream can't be called in recursion as the source stream can be // read only once. throw HTTPException::fromResponse($path, new Response($headers['status'], $headers, $body)); } return $body != '' ? json_decode($body, true) : array("status" => $headers['status']); }