Request.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\httpclient;
  8. use yii\base\InvalidConfigException;
  9. use yii\helpers\ArrayHelper;
  10. use yii\helpers\FileHelper;
  11. /**
  12. * Request represents HTTP request.
  13. *
  14. * @property string $fullUrl Full target URL.
  15. * @property string $method Request method.
  16. * @property array $options Request options. This property is read-only.
  17. * @property string|array $url Target URL or URL parameters.
  18. *
  19. * @author Paul Klimov <klimov.paul@gmail.com>
  20. * @since 2.0
  21. */
  22. class Request extends Message
  23. {
  24. /**
  25. * @event RequestEvent an event raised right before sending request.
  26. */
  27. const EVENT_BEFORE_SEND = 'beforeSend';
  28. /**
  29. * @event RequestEvent an event raised right after request has been sent.
  30. */
  31. const EVENT_AFTER_SEND = 'afterSend';
  32. /**
  33. * @var string|array target URL.
  34. */
  35. private $_url;
  36. /**
  37. * @var string|null full target URL.
  38. */
  39. private $_fullUrl;
  40. /**
  41. * @var string request method.
  42. */
  43. private $_method = 'get';
  44. /**
  45. * @var array request options.
  46. */
  47. private $_options = [];
  48. /**
  49. * @var bool whether request object has been prepared for sending or not.
  50. * @see prepare()
  51. */
  52. private $isPrepared = false;
  53. /**
  54. * Sets target URL.
  55. * @param string|array $url use a string to represent a URL (e.g. `http://some-domain.com`, `item/list`),
  56. * or an array to represent a URL with query parameters (e.g. `['item/list', 'param1' => 'value1']`).
  57. * @return $this self reference.
  58. */
  59. public function setUrl($url)
  60. {
  61. $this->_url = $url;
  62. $this->_fullUrl = null;
  63. return $this;
  64. }
  65. /**
  66. * Returns target URL.
  67. * @return string|array target URL or URL parameters
  68. */
  69. public function getUrl()
  70. {
  71. return $this->_url;
  72. }
  73. /**
  74. * Sets full target URL.
  75. * This method can be use during request formatting and preparation.
  76. * Do not use it for the target URL specification, use [[setUrl()]] instead.
  77. * @param string $fullUrl full target URL.
  78. * @since 2.0.3
  79. */
  80. public function setFullUrl($fullUrl)
  81. {
  82. $this->_fullUrl = $fullUrl;
  83. }
  84. /**
  85. * Returns full target URL, including [[Client::baseUrl]] as a string.
  86. * @return string full target URL.
  87. */
  88. public function getFullUrl()
  89. {
  90. if ($this->_fullUrl === null) {
  91. $this->_fullUrl = $this->createFullUrl($this->getUrl());
  92. }
  93. return $this->_fullUrl;
  94. }
  95. /**
  96. * @param string $method request method
  97. * @return $this self reference.
  98. */
  99. public function setMethod($method)
  100. {
  101. $this->_method = $method;
  102. return $this;
  103. }
  104. /**
  105. * @return string request method
  106. */
  107. public function getMethod()
  108. {
  109. return $this->_method;
  110. }
  111. /**
  112. * Following options are supported:
  113. * - timeout: int, the maximum number of seconds to allow request to be executed.
  114. * - proxy: string, URI specifying address of proxy server. (e.g. tcp://proxy.example.com:5100).
  115. * - userAgent: string, the contents of the "User-Agent: " header to be used in a HTTP request.
  116. * - followLocation: bool, whether to follow any "Location: " header that the server sends as part of the HTTP header.
  117. * - maxRedirects: int, the max number of redirects to follow.
  118. * - protocolVersion: float|string, HTTP protocol version.
  119. * - sslVerifyPeer: bool, whether verification of the peer's certificate should be performed.
  120. * - sslCafile: string, location of Certificate Authority file on local filesystem which should be used with
  121. * the 'sslVerifyPeer' option to authenticate the identity of the remote peer.
  122. * - sslCapath: string, a directory that holds multiple CA certificates.
  123. *
  124. * You may set options using keys, which are specific to particular transport, like `[CURLOPT_VERBOSE => true]` in case
  125. * there is a necessity for it.
  126. *
  127. * @param array $options request options.
  128. * @return $this self reference.
  129. */
  130. public function setOptions(array $options)
  131. {
  132. $this->_options = $options;
  133. return $this;
  134. }
  135. /**
  136. * @return array request options.
  137. */
  138. public function getOptions()
  139. {
  140. return $this->_options;
  141. }
  142. /**
  143. * Adds more options to already defined ones.
  144. * Please refer to [[setOptions()]] on how to specify options.
  145. * @param array $options additional options
  146. * @return $this self reference.
  147. */
  148. public function addOptions(array $options)
  149. {
  150. // `array_merge()` will produce invalid result for cURL options,
  151. // while `ArrayHelper::merge()` is unable to override cURL options
  152. foreach ($options as $key => $value) {
  153. if (is_array($value) && isset($this->_options[$key])) {
  154. $value = ArrayHelper::merge($this->_options[$key], $value);
  155. }
  156. $this->_options[$key] = $value;
  157. }
  158. return $this;
  159. }
  160. /**
  161. * @inheritdoc
  162. */
  163. public function setData($data)
  164. {
  165. if ($this->isPrepared) {
  166. $this->setContent(null);
  167. $this->isPrepared = false;
  168. }
  169. return parent::setData($data);
  170. }
  171. /**
  172. * @inheritdoc
  173. */
  174. public function addData($data)
  175. {
  176. if ($this->isPrepared) {
  177. $this->setContent(null);
  178. $this->isPrepared = false;
  179. }
  180. return parent::addData($data);
  181. }
  182. /**
  183. * Adds a content part for multi-part content request.
  184. * @param string $name part (form input) name.
  185. * @param string $content content.
  186. * @param array $options content part options, valid options are:
  187. * - contentType - string, part content type
  188. * - fileName - string, name of the uploading file
  189. * - mimeType - string, part content type in case of file uploading
  190. * @return $this self reference.
  191. */
  192. public function addContent($name, $content, $options = [])
  193. {
  194. $multiPartContent = $this->getContent();
  195. if (!is_array($multiPartContent)) {
  196. $multiPartContent = [];
  197. }
  198. $options['content'] = $content;
  199. $multiPartContent[$name] = $options;
  200. $this->setContent($multiPartContent);
  201. return $this;
  202. }
  203. /**
  204. * Adds a file for upload as multi-part content.
  205. * @see addContent()
  206. * @param string $name part (form input) name
  207. * @param string $fileName full name of the source file.
  208. * @param array $options content part options, valid options are:
  209. * - fileName - string, base name of the uploading file, if not set it base name of the source file will be used.
  210. * - mimeType - string, file mime type, if not set it will be determine automatically from source file.
  211. * @return $this
  212. */
  213. public function addFile($name, $fileName, $options = [])
  214. {
  215. $content = file_get_contents($fileName);
  216. if (!isset($options['mimeType'])) {
  217. $options['mimeType'] = FileHelper::getMimeType($fileName);
  218. }
  219. if (!isset($options['fileName'])) {
  220. $options['fileName'] = basename($fileName);
  221. }
  222. return $this->addContent($name, $content, $options);
  223. }
  224. /**
  225. * Adds a string as a file upload.
  226. * @see addContent()
  227. * @param string $name part (form input) name
  228. * @param string $content file content.
  229. * @param array $options content part options, valid options are:
  230. * - fileName - string, base name of the uploading file.
  231. * - mimeType - string, file mime type, if not set it 'application/octet-stream' will be used.
  232. * @return $this
  233. */
  234. public function addFileContent($name, $content, $options = [])
  235. {
  236. if (!isset($options['mimeType'])) {
  237. $options['mimeType'] = 'application/octet-stream';
  238. }
  239. if (!isset($options['fileName'])) {
  240. $options['fileName'] = $name . '.dat';
  241. }
  242. return $this->addContent($name, $content, $options);
  243. }
  244. /**
  245. * Prepares this request instance for sending.
  246. * This method should be invoked by transport before sending a request.
  247. * Do not call this method unless you know what you are doing.
  248. * @return $this self reference.
  249. */
  250. public function prepare()
  251. {
  252. $content = $this->getContent();
  253. if ($content === null) {
  254. $this->getFormatter()->format($this);
  255. } elseif (is_array($content)) {
  256. $this->prepareMultiPartContent($content);
  257. }
  258. $this->isPrepared = true;
  259. return $this;
  260. }
  261. /**
  262. * Normalizes given URL value, filling it with actual string URL value.
  263. * @param array|string $url raw URL,
  264. * @return string full URL
  265. */
  266. private function createFullUrl($url)
  267. {
  268. if (is_array($url)) {
  269. $params = $url;
  270. if (isset($params[0])) {
  271. $url = (string)$params[0];
  272. unset($params[0]);
  273. } else {
  274. $url = '';
  275. }
  276. }
  277. if (!empty($this->client->baseUrl)) {
  278. if (empty($url)) {
  279. $url = $this->client->baseUrl;
  280. } elseif (!preg_match('/^https?:\\/\\//i', $url)) {
  281. $url = $this->client->baseUrl . '/' . $url;
  282. }
  283. }
  284. if (!empty($params)) {
  285. if (strpos($url, '?') === false) {
  286. $url .= '?';
  287. } else {
  288. $url .= '&';
  289. }
  290. $url .= http_build_query($params);
  291. }
  292. return $url;
  293. }
  294. /**
  295. * Prepares multi-part content.
  296. * @param array $content multi part content.
  297. * @see https://tools.ietf.org/html/rfc7578
  298. * @see https://tools.ietf.org/html/rfc2616#section-19.5.1 for the Content-Disposition header
  299. * @see https://tools.ietf.org/html/rfc6266 for more details on the Content-Disposition header
  300. */
  301. private function prepareMultiPartContent(array $content)
  302. {
  303. static $disallowedChars = ["\0", '"', "\r", "\n"];
  304. $contentParts = [];
  305. $data = $this->getData();
  306. if (!empty($data)) {
  307. foreach ($this->composeFormInputs($data) as $name => $value) {
  308. $name = str_replace($disallowedChars, '_', $name);
  309. $contentDisposition = 'Content-Disposition: form-data; name="' . $name . '"';
  310. $contentParts[] = implode("\r\n", [$contentDisposition, '', $value]);
  311. }
  312. }
  313. // process content parts :
  314. foreach ($content as $name => $contentParams) {
  315. $headers = [];
  316. $name = str_replace($disallowedChars, '_', $name);
  317. $contentDisposition = 'Content-Disposition: form-data; name="' . $name . '"';
  318. if (isset($contentParams['fileName'])) {
  319. $fileName = str_replace($disallowedChars, '_', $contentParams['fileName']);
  320. $contentDisposition .= '; filename="' . $fileName . '"';
  321. }
  322. $headers[] = $contentDisposition;
  323. if (isset($contentParams['contentType'])) {
  324. $headers[] = 'Content-Type: ' . $contentParams['contentType'];
  325. } elseif (isset($contentParams['mimeType'])) {
  326. $headers[] = 'Content-Type: ' . $contentParams['mimeType'];
  327. }
  328. $contentParts[] = implode("\r\n", [implode("\r\n", $headers), '', $contentParams['content']]);
  329. }
  330. // generate safe boundary :
  331. do {
  332. $boundary = '---------------------' . md5(mt_rand() . microtime());
  333. } while (preg_grep("/{$boundary}/", $contentParts));
  334. // add boundary for each part :
  335. array_walk($contentParts, function (&$part) use ($boundary) {
  336. $part = "--{$boundary}\r\n{$part}";
  337. });
  338. // add final boundary :
  339. $contentParts[] = "--{$boundary}--";
  340. $contentParts[] = '';
  341. $this->getHeaders()->set('content-type', "multipart/form-data; boundary={$boundary}");
  342. $this->setContent(implode("\r\n", $contentParts));
  343. }
  344. /**
  345. * Composes given data as form inputs submitted values, taking in account nested arrays.
  346. * Converts `['form' => ['name' => 'value']]` to `['form[name]' => 'value']`.
  347. * @param array $data
  348. * @param string $baseKey
  349. * @return array
  350. */
  351. private function composeFormInputs(array $data, $baseKey = '')
  352. {
  353. $result = [];
  354. foreach ($data as $key => $value) {
  355. if (!empty($baseKey)) {
  356. $key = $baseKey . '[' . $key . ']';
  357. }
  358. if (is_array($value)) {
  359. $result = array_merge($result, $this->composeFormInputs($value, $key));
  360. } else {
  361. $result[$key] = $value;
  362. }
  363. }
  364. return $result;
  365. }
  366. /**
  367. * @inheritdoc
  368. */
  369. public function composeHeaderLines()
  370. {
  371. $headers = parent::composeHeaderLines();
  372. if ($this->hasCookies()) {
  373. $headers[] = $this->composeCookieHeader();
  374. }
  375. return $headers;
  376. }
  377. /**
  378. * Sends this request.
  379. * @return Response response instance.
  380. */
  381. public function send()
  382. {
  383. return $this->client->send($this);
  384. }
  385. /**
  386. * This method is invoked right before this request is sent.
  387. * The method will invoke [[Client::beforeSend()]] and trigger the [[EVENT_BEFORE_SEND]] event.
  388. * @since 2.0.1
  389. */
  390. public function beforeSend()
  391. {
  392. $this->client->beforeSend($this);
  393. $event = new RequestEvent();
  394. $event->request = $this;
  395. $this->trigger(self::EVENT_BEFORE_SEND, $event);
  396. }
  397. /**
  398. * This method is invoked right after this request is sent.
  399. * The method will invoke [[Client::afterSend()]] and trigger the [[EVENT_AFTER_SEND]] event.
  400. * @param Response $response received response instance.
  401. * @since 2.0.1
  402. */
  403. public function afterSend($response)
  404. {
  405. $this->client->afterSend($this, $response);
  406. $event = new RequestEvent();
  407. $event->request = $this;
  408. $event->response = $response;
  409. $this->trigger(self::EVENT_AFTER_SEND, $event);
  410. }
  411. /**
  412. * @inheritdoc
  413. */
  414. public function toString()
  415. {
  416. if (!$this->isPrepared) {
  417. $this->prepare();
  418. }
  419. $result = strtoupper($this->getMethod()) . ' ' . $this->getFullUrl();
  420. $parentResult = parent::toString();
  421. if ($parentResult !== '') {
  422. $result .= "\n" . $parentResult;
  423. }
  424. return $result;
  425. }
  426. /**
  427. * @return string cookie header value.
  428. * @throws InvalidConfigException on invalid cookies.
  429. */
  430. private function composeCookieHeader()
  431. {
  432. $parts = [];
  433. foreach ($this->getCookies() as $cookie) {
  434. if (!$this->validateCookieValue($cookie->name)) {
  435. throw new InvalidConfigException("Cookie name '{$cookie->name}' is invalid");
  436. }
  437. if (!$this->validateCookieValue($cookie->value)) {
  438. throw new InvalidConfigException("Cookie '{$cookie->name}' value '{$cookie->value}' is invalid");
  439. }
  440. $parts[] = $cookie->name . '=' . $cookie->value;
  441. }
  442. return 'Cookie: ' . implode(';', $parts);
  443. }
  444. /**
  445. * Validates cookie name or value.
  446. * @param string $value cookie value.
  447. * @return bool whether value is valid
  448. * @since 2.0.4
  449. */
  450. private function validateCookieValue($value)
  451. {
  452. // Invalid are: control characters (0-31;127), space, tab and the following: ()<>@,;:\"/?={}'
  453. return !preg_match('/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', $value);
  454. }
  455. /**
  456. * @return FormatterInterface message formatter instance.
  457. */
  458. private function getFormatter()
  459. {
  460. return $this->client->getFormatter($this->getFormat());
  461. }
  462. }