1<?php
2/**
3 * Session handler for persistent requests and default parameters
4 *
5 * @package Requests\SessionHandler
6 */
7
8namespace WpOrg\Requests;
9
10use WpOrg\Requests\Cookie\Jar;
11use WpOrg\Requests\Exception\InvalidArgument;
12use WpOrg\Requests\Iri;
13use WpOrg\Requests\Requests;
14use WpOrg\Requests\Utility\InputValidator;
15
16/**
17 * Session handler for persistent requests and default parameters
18 *
19 * Allows various options to be set as default values, and merges both the
20 * options and URL properties together. A base URL can be set for all requests,
21 * with all subrequests resolved from this. Base options can be set (including
22 * a shared cookie jar), then overridden for individual requests.
23 *
24 * @package Requests\SessionHandler
25 */
26class Session {
27 /**
28 * Base URL for requests
29 *
30 * URLs will be made absolute using this as the base
31 *
32 * @var string|null
33 */
34 public $url = null;
35
36 /**
37 * Base headers for requests
38 *
39 * @var array
40 */
41 public $headers = [];
42
43 /**
44 * Base data for requests
45 *
46 * If both the base data and the per-request data are arrays, the data will
47 * be merged before sending the request.
48 *
49 * @var array
50 */
51 public $data = [];
52
53 /**
54 * Base options for requests
55 *
56 * The base options are merged with the per-request data for each request.
57 * The only default option is a shared cookie jar between requests.
58 *
59 * Values here can also be set directly via properties on the Session
60 * object, e.g. `$session->useragent = 'X';`
61 *
62 * @var array
63 */
64 public $options = [];
65
66 /**
67 * Create a new session
68 *
69 * @param string|Stringable|null $url Base URL for requests
70 * @param array $headers Default headers for requests
71 * @param array $data Default data for requests
72 * @param array $options Default options for requests
73 *
74 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $url argument is not a string, Stringable or null.
75 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $headers argument is not an array.
76 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $data argument is not an array.
77 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
78 */
79 public function __construct($url = null, $headers = [], $data = [], $options = []) {
80 if ($url !== null && InputValidator::is_string_or_stringable($url) === false) {
81 throw InvalidArgument::create(1, '$url', 'string|Stringable|null', gettype($url));
82 }
83
84 if (is_array($headers) === false) {
85 throw InvalidArgument::create(2, '$headers', 'array', gettype($headers));
86 }
87
88 if (is_array($data) === false) {
89 throw InvalidArgument::create(3, '$data', 'array', gettype($data));
90 }
91
92 if (is_array($options) === false) {
93 throw InvalidArgument::create(4, '$options', 'array', gettype($options));
94 }
95
96 $this->url = $url;
97 $this->headers = $headers;
98 $this->data = $data;
99 $this->options = $options;
100
101 if (empty($this->options['cookies'])) {
102 $this->options['cookies'] = new Jar();
103 }
104 }
105
106 /**
107 * Get a property's value
108 *
109 * @param string $name Property name.
110 * @return mixed|null Property value, null if none found
111 */
112 public function __get($name) {
113 if (isset($this->options[$name])) {
114 return $this->options[$name];
115 }
116
117 return null;
118 }
119
120 /**
121 * Set a property's value
122 *
123 * @param string $name Property name.
124 * @param mixed $value Property value
125 */
126 public function __set($name, $value) {
127 $this->options[$name] = $value;
128 }
129
130 /**
131 * Remove a property's value
132 *
133 * @param string $name Property name.
134 */
135 public function __isset($name) {
136 return isset($this->options[$name]);
137 }
138
139 /**
140 * Remove a property's value
141 *
142 * @param string $name Property name.
143 */
144 public function __unset($name) {
145 unset($this->options[$name]);
146 }
147
148 /**#@+
149 * @see \WpOrg\Requests\Session::request()
150 * @param string $url
151 * @param array $headers
152 * @param array $options
153 * @return \WpOrg\Requests\Response
154 */
155 /**
156 * Send a GET request
157 */
158 public function get($url, $headers = [], $options = []) {
159 return $this->request($url, $headers, null, Requests::GET, $options);
160 }
161
162 /**
163 * Send a HEAD request
164 */
165 public function head($url, $headers = [], $options = []) {
166 return $this->request($url, $headers, null, Requests::HEAD, $options);
167 }
168
169 /**
170 * Send a DELETE request
171 */
172 public function delete($url, $headers = [], $options = []) {
173 return $this->request($url, $headers, null, Requests::DELETE, $options);
174 }
175 /**#@-*/
176
177 /**#@+
178 * @see \WpOrg\Requests\Session::request()
179 * @param string $url
180 * @param array $headers
181 * @param array $data
182 * @param array $options
183 * @return \WpOrg\Requests\Response
184 */
185 /**
186 * Send a POST request
187 */
188 public function post($url, $headers = [], $data = [], $options = []) {
189 return $this->request($url, $headers, $data, Requests::POST, $options);
190 }
191
192 /**
193 * Send a PUT request
194 */
195 public function put($url, $headers = [], $data = [], $options = []) {
196 return $this->request($url, $headers, $data, Requests::PUT, $options);
197 }
198
199 /**
200 * Send a PATCH request
201 *
202 * Note: Unlike {@see \WpOrg\Requests\Session::post()} and {@see \WpOrg\Requests\Session::put()},
203 * `$headers` is required, as the specification recommends that should send an ETag
204 *
205 * @link https://tools.ietf.org/html/rfc5789
206 */
207 public function patch($url, $headers, $data = [], $options = []) {
208 return $this->request($url, $headers, $data, Requests::PATCH, $options);
209 }
210 /**#@-*/
211
212 /**
213 * Main interface for HTTP requests
214 *
215 * This method initiates a request and sends it via a transport before
216 * parsing.
217 *
218 * @see \WpOrg\Requests\Requests::request()
219 *
220 * @param string $url URL to request
221 * @param array $headers Extra headers to send with the request
222 * @param array|null $data Data to send either as a query string for GET/HEAD requests, or in the body for POST requests
223 * @param string $type HTTP request type (use \WpOrg\Requests\Requests constants)
224 * @param array $options Options for the request (see {@see \WpOrg\Requests\Requests::request()})
225 * @return \WpOrg\Requests\Response
226 *
227 * @throws \WpOrg\Requests\Exception On invalid URLs (`nonhttp`)
228 */
229 public function request($url, $headers = [], $data = [], $type = Requests::GET, $options = []) {
230 $request = $this->merge_request(compact('url', 'headers', 'data', 'options'));
231
232 return Requests::request($request['url'], $request['headers'], $request['data'], $type, $request['options']);
233 }
234
235 /**
236 * Send multiple HTTP requests simultaneously
237 *
238 * @see \WpOrg\Requests\Requests::request_multiple()
239 *
240 * @param array $requests Requests data (see {@see \WpOrg\Requests\Requests::request_multiple()})
241 * @param array $options Global and default options (see {@see \WpOrg\Requests\Requests::request()})
242 * @return array Responses (either \WpOrg\Requests\Response or a \WpOrg\Requests\Exception object)
243 *
244 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $requests argument is not an array or iterable object with array access.
245 * @throws \WpOrg\Requests\Exception\InvalidArgument When the passed $options argument is not an array.
246 */
247 public function request_multiple($requests, $options = []) {
248 if (InputValidator::has_array_access($requests) === false || InputValidator::is_iterable($requests) === false) {
249 throw InvalidArgument::create(1, '$requests', 'array|ArrayAccess&Traversable', gettype($requests));
250 }
251
252 if (is_array($options) === false) {
253 throw InvalidArgument::create(2, '$options', 'array', gettype($options));
254 }
255
256 foreach ($requests as $key => $request) {
257 $requests[$key] = $this->merge_request($request, false);
258 }
259
260 $options = array_merge($this->options, $options);
261
262 // Disallow forcing the type, as that's a per request setting
263 unset($options['type']);
264
265 return Requests::request_multiple($requests, $options);
266 }
267
268 public function __wakeup() {
269 throw new \LogicException( __CLASS__ . ' should never be unserialized' );
270 }
271
272 /**
273 * Merge a request's data with the default data
274 *
275 * @param array $request Request data (same form as {@see \WpOrg\Requests\Session::request_multiple()})
276 * @param boolean $merge_options Should we merge options as well?
277 * @return array Request data
278 */
279 protected function merge_request($request, $merge_options = true) {
280 if ($this->url !== null) {
281 $request['url'] = Iri::absolutize($this->url, $request['url']);
282 $request['url'] = $request['url']->uri;
283 }
284
285 if (empty($request['headers'])) {
286 $request['headers'] = [];
287 }
288
289 $request['headers'] = array_merge($this->headers, $request['headers']);
290
291 if (empty($request['data'])) {
292 if (is_array($this->data)) {
293 $request['data'] = $this->data;
294 }
295 } elseif (is_array($request['data']) && is_array($this->data)) {
296 $request['data'] = array_merge($this->data, $request['data']);
297 }
298
299 if ($merge_options === true) {
300 $request['options'] = array_merge($this->options, $request['options']);
301
302 // Disallow forcing the type, as that's a per request setting
303 unset($request['options']['type']);
304 }
305
306 return $request;
307 }
308}
309