| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512 |
- <?php
- /**
- * @link http://www.yiiframework.com/
- * @copyright Copyright (c) 2008 Yii Software LLC
- * @license http://www.yiiframework.com/license/
- */
- namespace yii\httpclient;
- use yii\base\InvalidConfigException;
- use yii\helpers\ArrayHelper;
- use yii\helpers\FileHelper;
- /**
- * Request represents HTTP request.
- *
- * @property string $fullUrl Full target URL.
- * @property string $method Request method.
- * @property array $options Request options. This property is read-only.
- * @property string|array $url Target URL or URL parameters.
- *
- * @author Paul Klimov <klimov.paul@gmail.com>
- * @since 2.0
- */
- class Request extends Message
- {
- /**
- * @event RequestEvent an event raised right before sending request.
- */
- const EVENT_BEFORE_SEND = 'beforeSend';
- /**
- * @event RequestEvent an event raised right after request has been sent.
- */
- const EVENT_AFTER_SEND = 'afterSend';
- /**
- * @var string|array target URL.
- */
- private $_url;
- /**
- * @var string|null full target URL.
- */
- private $_fullUrl;
- /**
- * @var string request method.
- */
- private $_method = 'get';
- /**
- * @var array request options.
- */
- private $_options = [];
- /**
- * @var bool whether request object has been prepared for sending or not.
- * @see prepare()
- */
- private $isPrepared = false;
- /**
- * Sets target URL.
- * @param string|array $url use a string to represent a URL (e.g. `http://some-domain.com`, `item/list`),
- * or an array to represent a URL with query parameters (e.g. `['item/list', 'param1' => 'value1']`).
- * @return $this self reference.
- */
- public function setUrl($url)
- {
- $this->_url = $url;
- $this->_fullUrl = null;
- return $this;
- }
- /**
- * Returns target URL.
- * @return string|array target URL or URL parameters
- */
- public function getUrl()
- {
- return $this->_url;
- }
- /**
- * Sets full target URL.
- * This method can be use during request formatting and preparation.
- * Do not use it for the target URL specification, use [[setUrl()]] instead.
- * @param string $fullUrl full target URL.
- * @since 2.0.3
- */
- public function setFullUrl($fullUrl)
- {
- $this->_fullUrl = $fullUrl;
- }
- /**
- * Returns full target URL, including [[Client::baseUrl]] as a string.
- * @return string full target URL.
- */
- public function getFullUrl()
- {
- if ($this->_fullUrl === null) {
- $this->_fullUrl = $this->createFullUrl($this->getUrl());
- }
- return $this->_fullUrl;
- }
- /**
- * @param string $method request method
- * @return $this self reference.
- */
- public function setMethod($method)
- {
- $this->_method = $method;
- return $this;
- }
- /**
- * @return string request method
- */
- public function getMethod()
- {
- return $this->_method;
- }
- /**
- * Following options are supported:
- * - timeout: int, the maximum number of seconds to allow request to be executed.
- * - proxy: string, URI specifying address of proxy server. (e.g. tcp://proxy.example.com:5100).
- * - userAgent: string, the contents of the "User-Agent: " header to be used in a HTTP request.
- * - followLocation: bool, whether to follow any "Location: " header that the server sends as part of the HTTP header.
- * - maxRedirects: int, the max number of redirects to follow.
- * - protocolVersion: float|string, HTTP protocol version.
- * - sslVerifyPeer: bool, whether verification of the peer's certificate should be performed.
- * - sslCafile: string, location of Certificate Authority file on local filesystem which should be used with
- * the 'sslVerifyPeer' option to authenticate the identity of the remote peer.
- * - sslCapath: string, a directory that holds multiple CA certificates.
- *
- * You may set options using keys, which are specific to particular transport, like `[CURLOPT_VERBOSE => true]` in case
- * there is a necessity for it.
- *
- * @param array $options request options.
- * @return $this self reference.
- */
- public function setOptions(array $options)
- {
- $this->_options = $options;
- return $this;
- }
- /**
- * @return array request options.
- */
- public function getOptions()
- {
- return $this->_options;
- }
- /**
- * Adds more options to already defined ones.
- * Please refer to [[setOptions()]] on how to specify options.
- * @param array $options additional options
- * @return $this self reference.
- */
- public function addOptions(array $options)
- {
- // `array_merge()` will produce invalid result for cURL options,
- // while `ArrayHelper::merge()` is unable to override cURL options
- foreach ($options as $key => $value) {
- if (is_array($value) && isset($this->_options[$key])) {
- $value = ArrayHelper::merge($this->_options[$key], $value);
- }
- $this->_options[$key] = $value;
- }
- return $this;
- }
- /**
- * @inheritdoc
- */
- public function setData($data)
- {
- if ($this->isPrepared) {
- $this->setContent(null);
- $this->isPrepared = false;
- }
- return parent::setData($data);
- }
- /**
- * @inheritdoc
- */
- public function addData($data)
- {
- if ($this->isPrepared) {
- $this->setContent(null);
- $this->isPrepared = false;
- }
- return parent::addData($data);
- }
- /**
- * Adds a content part for multi-part content request.
- * @param string $name part (form input) name.
- * @param string $content content.
- * @param array $options content part options, valid options are:
- * - contentType - string, part content type
- * - fileName - string, name of the uploading file
- * - mimeType - string, part content type in case of file uploading
- * @return $this self reference.
- */
- public function addContent($name, $content, $options = [])
- {
- $multiPartContent = $this->getContent();
- if (!is_array($multiPartContent)) {
- $multiPartContent = [];
- }
- $options['content'] = $content;
- $multiPartContent[$name] = $options;
- $this->setContent($multiPartContent);
- return $this;
- }
- /**
- * Adds a file for upload as multi-part content.
- * @see addContent()
- * @param string $name part (form input) name
- * @param string $fileName full name of the source file.
- * @param array $options content part options, valid options are:
- * - fileName - string, base name of the uploading file, if not set it base name of the source file will be used.
- * - mimeType - string, file mime type, if not set it will be determine automatically from source file.
- * @return $this
- */
- public function addFile($name, $fileName, $options = [])
- {
- $content = file_get_contents($fileName);
- if (!isset($options['mimeType'])) {
- $options['mimeType'] = FileHelper::getMimeType($fileName);
- }
- if (!isset($options['fileName'])) {
- $options['fileName'] = basename($fileName);
- }
- return $this->addContent($name, $content, $options);
- }
- /**
- * Adds a string as a file upload.
- * @see addContent()
- * @param string $name part (form input) name
- * @param string $content file content.
- * @param array $options content part options, valid options are:
- * - fileName - string, base name of the uploading file.
- * - mimeType - string, file mime type, if not set it 'application/octet-stream' will be used.
- * @return $this
- */
- public function addFileContent($name, $content, $options = [])
- {
- if (!isset($options['mimeType'])) {
- $options['mimeType'] = 'application/octet-stream';
- }
- if (!isset($options['fileName'])) {
- $options['fileName'] = $name . '.dat';
- }
- return $this->addContent($name, $content, $options);
- }
- /**
- * Prepares this request instance for sending.
- * This method should be invoked by transport before sending a request.
- * Do not call this method unless you know what you are doing.
- * @return $this self reference.
- */
- public function prepare()
- {
- $content = $this->getContent();
- if ($content === null) {
- $this->getFormatter()->format($this);
- } elseif (is_array($content)) {
- $this->prepareMultiPartContent($content);
- }
- $this->isPrepared = true;
- return $this;
- }
- /**
- * Normalizes given URL value, filling it with actual string URL value.
- * @param array|string $url raw URL,
- * @return string full URL
- */
- private function createFullUrl($url)
- {
- if (is_array($url)) {
- $params = $url;
- if (isset($params[0])) {
- $url = (string)$params[0];
- unset($params[0]);
- } else {
- $url = '';
- }
- }
- if (!empty($this->client->baseUrl)) {
- if (empty($url)) {
- $url = $this->client->baseUrl;
- } elseif (!preg_match('/^https?:\\/\\//i', $url)) {
- $url = $this->client->baseUrl . '/' . $url;
- }
- }
- if (!empty($params)) {
- if (strpos($url, '?') === false) {
- $url .= '?';
- } else {
- $url .= '&';
- }
- $url .= http_build_query($params);
- }
- return $url;
- }
- /**
- * Prepares multi-part content.
- * @param array $content multi part content.
- * @see https://tools.ietf.org/html/rfc7578
- * @see https://tools.ietf.org/html/rfc2616#section-19.5.1 for the Content-Disposition header
- * @see https://tools.ietf.org/html/rfc6266 for more details on the Content-Disposition header
- */
- private function prepareMultiPartContent(array $content)
- {
- static $disallowedChars = ["\0", '"', "\r", "\n"];
- $contentParts = [];
- $data = $this->getData();
- if (!empty($data)) {
- foreach ($this->composeFormInputs($data) as $name => $value) {
- $name = str_replace($disallowedChars, '_', $name);
- $contentDisposition = 'Content-Disposition: form-data; name="' . $name . '"';
- $contentParts[] = implode("\r\n", [$contentDisposition, '', $value]);
- }
- }
- // process content parts :
- foreach ($content as $name => $contentParams) {
- $headers = [];
- $name = str_replace($disallowedChars, '_', $name);
- $contentDisposition = 'Content-Disposition: form-data; name="' . $name . '"';
- if (isset($contentParams['fileName'])) {
- $fileName = str_replace($disallowedChars, '_', $contentParams['fileName']);
- $contentDisposition .= '; filename="' . $fileName . '"';
- }
- $headers[] = $contentDisposition;
- if (isset($contentParams['contentType'])) {
- $headers[] = 'Content-Type: ' . $contentParams['contentType'];
- } elseif (isset($contentParams['mimeType'])) {
- $headers[] = 'Content-Type: ' . $contentParams['mimeType'];
- }
- $contentParts[] = implode("\r\n", [implode("\r\n", $headers), '', $contentParams['content']]);
- }
- // generate safe boundary :
- do {
- $boundary = '---------------------' . md5(mt_rand() . microtime());
- } while (preg_grep("/{$boundary}/", $contentParts));
- // add boundary for each part :
- array_walk($contentParts, function (&$part) use ($boundary) {
- $part = "--{$boundary}\r\n{$part}";
- });
- // add final boundary :
- $contentParts[] = "--{$boundary}--";
- $contentParts[] = '';
- $this->getHeaders()->set('content-type', "multipart/form-data; boundary={$boundary}");
- $this->setContent(implode("\r\n", $contentParts));
- }
- /**
- * Composes given data as form inputs submitted values, taking in account nested arrays.
- * Converts `['form' => ['name' => 'value']]` to `['form[name]' => 'value']`.
- * @param array $data
- * @param string $baseKey
- * @return array
- */
- private function composeFormInputs(array $data, $baseKey = '')
- {
- $result = [];
- foreach ($data as $key => $value) {
- if (!empty($baseKey)) {
- $key = $baseKey . '[' . $key . ']';
- }
- if (is_array($value)) {
- $result = array_merge($result, $this->composeFormInputs($value, $key));
- } else {
- $result[$key] = $value;
- }
- }
- return $result;
- }
- /**
- * @inheritdoc
- */
- public function composeHeaderLines()
- {
- $headers = parent::composeHeaderLines();
- if ($this->hasCookies()) {
- $headers[] = $this->composeCookieHeader();
- }
- return $headers;
- }
- /**
- * Sends this request.
- * @return Response response instance.
- */
- public function send()
- {
- return $this->client->send($this);
- }
- /**
- * This method is invoked right before this request is sent.
- * The method will invoke [[Client::beforeSend()]] and trigger the [[EVENT_BEFORE_SEND]] event.
- * @since 2.0.1
- */
- public function beforeSend()
- {
- $this->client->beforeSend($this);
- $event = new RequestEvent();
- $event->request = $this;
- $this->trigger(self::EVENT_BEFORE_SEND, $event);
- }
- /**
- * This method is invoked right after this request is sent.
- * The method will invoke [[Client::afterSend()]] and trigger the [[EVENT_AFTER_SEND]] event.
- * @param Response $response received response instance.
- * @since 2.0.1
- */
- public function afterSend($response)
- {
- $this->client->afterSend($this, $response);
- $event = new RequestEvent();
- $event->request = $this;
- $event->response = $response;
- $this->trigger(self::EVENT_AFTER_SEND, $event);
- }
- /**
- * @inheritdoc
- */
- public function toString()
- {
- if (!$this->isPrepared) {
- $this->prepare();
- }
- $result = strtoupper($this->getMethod()) . ' ' . $this->getFullUrl();
- $parentResult = parent::toString();
- if ($parentResult !== '') {
- $result .= "\n" . $parentResult;
- }
- return $result;
- }
- /**
- * @return string cookie header value.
- * @throws InvalidConfigException on invalid cookies.
- */
- private function composeCookieHeader()
- {
- $parts = [];
- foreach ($this->getCookies() as $cookie) {
- if (!$this->validateCookieValue($cookie->name)) {
- throw new InvalidConfigException("Cookie name '{$cookie->name}' is invalid");
- }
- if (!$this->validateCookieValue($cookie->value)) {
- throw new InvalidConfigException("Cookie '{$cookie->name}' value '{$cookie->value}' is invalid");
- }
- $parts[] = $cookie->name . '=' . $cookie->value;
- }
- return 'Cookie: ' . implode(';', $parts);
- }
- /**
- * Validates cookie name or value.
- * @param string $value cookie value.
- * @return bool whether value is valid
- * @since 2.0.4
- */
- private function validateCookieValue($value)
- {
- // Invalid are: control characters (0-31;127), space, tab and the following: ()<>@,;:\"/?={}'
- return !preg_match('/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', $value);
- }
- /**
- * @return FormatterInterface message formatter instance.
- */
- private function getFormatter()
- {
- return $this->client->getFormatter($this->getFormat());
- }
- }
|