Client.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. <?php
  2. /**
  3. * Request Client. Processes a [Request] and handles [HTTP_Caching] if
  4. * available. Will usually return a [Response] object as a result of the
  5. * request unless an unexpected error occurs.
  6. *
  7. * @package KO7
  8. * @category Base
  9. *
  10. * @copyright (c) 2007-2016 Kohana Team
  11. * @copyright (c) since 2016 Koseven Team
  12. * @license https://koseven.dev/LICENSE
  13. * @since 3.1.0
  14. */
  15. abstract class KO7_Request_Client {
  16. /**
  17. * @var Cache Caching library for request caching
  18. */
  19. protected $_cache;
  20. /**
  21. * @var bool Should redirects be followed?
  22. */
  23. protected $_follow = FALSE;
  24. /**
  25. * @var array Headers to preserve when following a redirect
  26. */
  27. protected $_follow_headers = ['authorization'];
  28. /**
  29. * @var bool Follow 302 redirect with original request method?
  30. */
  31. protected $_strict_redirect = TRUE;
  32. /**
  33. * @var array Callbacks to use when response contains given headers
  34. */
  35. protected $_header_callbacks = [
  36. 'Location' => 'Request_Client::on_header_location'
  37. ];
  38. /**
  39. * @var int Maximum number of requests that header callbacks can trigger before the request is aborted
  40. */
  41. protected $_max_callback_depth = 5;
  42. /**
  43. * @var int Tracks the callback depth of the currently executing request
  44. */
  45. protected $_callback_depth = 1;
  46. /**
  47. * @var array Arbitrary parameters that are shared with header callbacks through their Request_Client object
  48. */
  49. protected $_callback_params = [];
  50. /**
  51. * Creates a new `Request_Client` object,
  52. * allows for dependency injection.
  53. *
  54. * @param array $params Params
  55. */
  56. public function __construct(array $params = [])
  57. {
  58. foreach ($params as $key => $value)
  59. {
  60. if (method_exists($this, $key))
  61. {
  62. $this->$key($value);
  63. }
  64. }
  65. }
  66. /**
  67. * Processes the request, executing the controller action that handles this
  68. * request, determined by the [Route].
  69. *
  70. * 1. Before the controller action is called, the [Controller::before] method
  71. * will be called.
  72. * 2. Next the controller action will be called.
  73. * 3. After the controller action is called, the [Controller::after] method
  74. * will be called.
  75. *
  76. * By default, the output from the controller is captured and returned, and
  77. * no headers are sent.
  78. *
  79. * $request->execute();
  80. *
  81. * @param Request $request
  82. * @param Response $response
  83. * @return Response
  84. * @throws KO7_Exception
  85. * @uses [KO7::$profiling]
  86. * @uses [Profiler]
  87. */
  88. public function execute(Request $request)
  89. {
  90. // Prevent too much recursion of header callback requests
  91. if ($this->callback_depth() > $this->max_callback_depth())
  92. throw new Request_Client_Recursion_Exception(
  93. "Could not execute request to :uri - too many recursions after :depth requests",
  94. [
  95. ':uri' => $request->uri(),
  96. ':depth' => $this->callback_depth() - 1,
  97. ]);
  98. // Execute the request and pass the currently used protocol
  99. $orig_response = $response = Response::factory(['_protocol' => $request->protocol()]);
  100. if (($cache = $this->cache()) instanceof HTTP_Cache)
  101. return $cache->execute($this, $request, $response);
  102. $response = $this->execute_request($request, $response);
  103. // Execute response callbacks
  104. foreach ($this->header_callbacks() as $header => $callback)
  105. {
  106. if ($response->headers($header))
  107. {
  108. $cb_result = call_user_func($callback, $request, $response, $this);
  109. if ($cb_result instanceof Request)
  110. {
  111. // If the callback returns a request, automatically assign client params
  112. $this->assign_client_properties($cb_result->client());
  113. $cb_result->client()->callback_depth($this->callback_depth() + 1);
  114. // Execute the request
  115. $response = $cb_result->execute();
  116. }
  117. elseif ($cb_result instanceof Response)
  118. {
  119. // Assign the returned response
  120. $response = $cb_result;
  121. }
  122. // If the callback has created a new response, do not process any further
  123. if ($response !== $orig_response)
  124. break;
  125. }
  126. }
  127. return $response;
  128. }
  129. /**
  130. * Processes the request passed to it and returns the response from
  131. * the URI resource identified.
  132. *
  133. * This method must be implemented by all clients.
  134. *
  135. * @param Request $request request to execute by client
  136. * @param Response $response
  137. * @return Response
  138. * @since 3.2.0
  139. */
  140. abstract public function execute_request(Request $request, Response $response);
  141. /**
  142. * Getter and setter for the internal caching engine,
  143. * used to cache responses if available and valid.
  144. *
  145. * @param HTTP_Cache $cache engine to use for caching
  146. * @return HTTP_Cache
  147. * @return Request_Client
  148. */
  149. public function cache(HTTP_Cache $cache = NULL)
  150. {
  151. if ($cache === NULL)
  152. return $this->_cache;
  153. $this->_cache = $cache;
  154. return $this;
  155. }
  156. /**
  157. * Getter and setter for the follow redirects
  158. * setting.
  159. *
  160. * @param bool $follow Boolean indicating if redirects should be followed
  161. * @return bool
  162. * @return Request_Client
  163. */
  164. public function follow($follow = NULL)
  165. {
  166. if ($follow === NULL)
  167. return $this->_follow;
  168. $this->_follow = $follow;
  169. return $this;
  170. }
  171. /**
  172. * Getter and setter for the follow redirects
  173. * headers array.
  174. *
  175. * @param array $follow_headers Array of headers to be re-used when following a Location header
  176. * @return array
  177. * @return Request_Client
  178. */
  179. public function follow_headers($follow_headers = NULL)
  180. {
  181. if ($follow_headers === NULL)
  182. return $this->_follow_headers;
  183. $this->_follow_headers = array_map('strtolower', $follow_headers);
  184. return $this;
  185. }
  186. /**
  187. * Getter and setter for the strict redirects setting
  188. *
  189. * [!!] HTTP/1.1 specifies that a 302 redirect should be followed using the
  190. * original request method. However, the vast majority of clients and servers
  191. * get this wrong, with 302 widely used for 'POST - 302 redirect - GET' patterns.
  192. * By default, KO7's client is fully compliant with the HTTP spec. Some
  193. * non-compliant third party sites may require that strict_redirect is set
  194. * FALSE to force the client to switch to GET following a 302 response.
  195. *
  196. * @param bool $strict_redirect Boolean indicating if 302 redirects should be followed with the original method
  197. * @return Request_Client
  198. */
  199. public function strict_redirect($strict_redirect = NULL)
  200. {
  201. if ($strict_redirect === NULL)
  202. return $this->_strict_redirect;
  203. $this->_strict_redirect = $strict_redirect;
  204. return $this;
  205. }
  206. /**
  207. * Getter and setter for the header callbacks array.
  208. *
  209. * Accepts an array with HTTP response headers as keys and a PHP callback
  210. * function as values. These callbacks will be triggered if a response contains
  211. * the given header and can either issue a subsequent request or manipulate
  212. * the response as required.
  213. *
  214. * By default, the [Request_Client::on_header_location] callback is assigned
  215. * to the Location header to support automatic redirect following.
  216. *
  217. * $client->header_callbacks(array(
  218. * 'Location' => 'Request_Client::on_header_location',
  219. * 'WWW-Authenticate' => function($request, $response, $client) {return $new_response;},
  220. * );
  221. *
  222. * @param array $header_callbacks Array of callbacks to trigger on presence of given headers
  223. * @return Request_Client
  224. */
  225. public function header_callbacks($header_callbacks = NULL)
  226. {
  227. if ($header_callbacks === NULL)
  228. return $this->_header_callbacks;
  229. $this->_header_callbacks = $header_callbacks;
  230. return $this;
  231. }
  232. /**
  233. * Getter and setter for the maximum callback depth property.
  234. *
  235. * This protects the main execution from recursive callback execution (eg
  236. * following infinite redirects, conflicts between callbacks causing loops
  237. * etc). Requests will only be allowed to nest to the level set by this
  238. * param before execution is aborted with a Request_Client_Recursion_Exception.
  239. *
  240. * @param int $depth Maximum number of callback requests to execute before aborting
  241. * @return Request_Client|int
  242. */
  243. public function max_callback_depth($depth = NULL)
  244. {
  245. if ($depth === NULL)
  246. return $this->_max_callback_depth;
  247. $this->_max_callback_depth = $depth;
  248. return $this;
  249. }
  250. /**
  251. * Getter/Setter for the callback depth property, which is used to track
  252. * how many recursions have been executed within the current request execution.
  253. *
  254. * @param int $depth Current recursion depth
  255. * @return Request_Client|int
  256. */
  257. public function callback_depth($depth = NULL)
  258. {
  259. if ($depth === NULL)
  260. return $this->_callback_depth;
  261. $this->_callback_depth = $depth;
  262. return $this;
  263. }
  264. /**
  265. * Getter/Setter for the callback_params array, which allows additional
  266. * application-specific parameters to be shared with callbacks.
  267. *
  268. * As with other KO7 setter/getters, usage is:
  269. *
  270. * // Set full array
  271. * $client->callback_params(array('foo'=>'bar'));
  272. *
  273. * // Set single key
  274. * $client->callback_params('foo','bar');
  275. *
  276. * // Get full array
  277. * $params = $client->callback_params();
  278. *
  279. * // Get single key
  280. * $foo = $client->callback_params('foo');
  281. *
  282. * @param string|array $param
  283. * @param mixed $value
  284. * @return Request_Client|mixed
  285. */
  286. public function callback_params($param = NULL, $value = NULL)
  287. {
  288. // Getter for full array
  289. if ($param === NULL)
  290. return $this->_callback_params;
  291. // Setter for full array
  292. if (is_array($param))
  293. {
  294. $this->_callback_params = $param;
  295. return $this;
  296. }
  297. // Getter for single value
  298. elseif ($value === NULL)
  299. {
  300. return Arr::get($this->_callback_params, $param);
  301. }
  302. // Setter for single value
  303. else
  304. {
  305. $this->_callback_params[$param] = $value;
  306. return $this;
  307. }
  308. }
  309. /**
  310. * Assigns the properties of the current Request_Client to another
  311. * Request_Client instance - used when setting up a subsequent request.
  312. *
  313. * @param Request_Client $client
  314. */
  315. public function assign_client_properties(Request_Client $client)
  316. {
  317. $client->cache($this->cache());
  318. $client->follow($this->follow());
  319. $client->follow_headers($this->follow_headers());
  320. $client->header_callbacks($this->header_callbacks());
  321. $client->max_callback_depth($this->max_callback_depth());
  322. $client->callback_params($this->callback_params());
  323. }
  324. /**
  325. * The default handler for following redirects, triggered by the presence of
  326. * a Location header in the response.
  327. *
  328. * The client's follow property must be set TRUE and the HTTP response status
  329. * one of 201, 301, 302, 303 or 307 for the redirect to be followed.
  330. *
  331. * @param Request $request
  332. * @param Response $response
  333. * @param Request_Client $client
  334. */
  335. public static function on_header_location(Request $request, Response $response, Request_Client $client)
  336. {
  337. // Do we need to follow a Location header ?
  338. if ($client->follow() AND in_array($response->status(), [201, 301, 302, 303, 307]))
  339. {
  340. // Figure out which method to use for the follow request
  341. switch ($response->status())
  342. {
  343. default:
  344. case 301:
  345. case 307:
  346. $follow_method = $request->method();
  347. break;
  348. case 201:
  349. case 303:
  350. $follow_method = Request::GET;
  351. break;
  352. case 302:
  353. // Cater for sites with broken HTTP redirect implementations
  354. if ($client->strict_redirect())
  355. {
  356. $follow_method = $request->method();
  357. }
  358. else
  359. {
  360. $follow_method = Request::GET;
  361. }
  362. break;
  363. }
  364. // Prepare the additional request, copying any follow_headers that were present on the original request
  365. $orig_headers = $request->headers()->getArrayCopy();
  366. $follow_header_keys = array_intersect(array_keys($orig_headers), $client->follow_headers());
  367. $follow_headers = Arr::extract($orig_headers, $follow_header_keys);
  368. $follow_request = Request::factory($response->headers('Location'))
  369. ->method($follow_method)
  370. ->headers($follow_headers);
  371. if ($follow_method !== Request::GET)
  372. {
  373. $follow_request->body($request->body());
  374. }
  375. return $follow_request;
  376. }
  377. return NULL;
  378. }
  379. }