Completed
Branch master (e874e7)
by smiley
05:21
created

Request::setOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
/**
3
 * Class Request
4
 *
5
 * @filesource   Request.php
6
 * @created      13.02.2016
7
 * @package      chillerlan\TinyCurl
8
 * @author       Smiley <[email protected]>
9
 * @copyright    2016 Smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\TinyCurl;
14
15
/**
16
 *
17
 */
18
class Request{
19
20
	/**
21
	 * The cURL connection
22
	 *
23
	 * @var resource
24
	 */
25
	protected $curl;
26
27
	/**
28
	 * @var \chillerlan\TinyCurl\RequestOptions
29
	 */
30
	protected $options;
31
32
	/**
33
	 * Request constructor.
34
	 *
35
	 * @param \chillerlan\TinyCurl\RequestOptions $options
0 ignored issues
show
Documentation introduced by
Should the type for parameter $options not be null|RequestOptions?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
36
	 */
37
	public function __construct(RequestOptions $options = null){
38
		$this->setOptions($options ?: new RequestOptions);
39
	}
40
41
	/**
42
	 * @param \chillerlan\TinyCurl\RequestOptions $options
43
	 *
44
	 * @return \chillerlan\TinyCurl\Request
45
	 */
46
	public function setOptions(RequestOptions $options):Request {
47
		$this->options = $options;
48
49
		return $this;
50
	}
51
52
	/**
53
	 * @param string $url
54
	 *
55
	 * @return \chillerlan\TinyCurl\Response
56
	 */
57
	protected function getResponse(string $url):Response {
58
		curl_setopt($this->curl, CURLOPT_URL, $url);
59
60
		return new Response($this->curl);
61
	}
62
63
	/**
64
	 * @return void
65
	 */
66
	protected function initCurl(){
67
		$this->curl = curl_init();
68
69
		curl_setopt_array($this->curl, [
70
			CURLOPT_HEADER         => false,
71
			CURLOPT_RETURNTRANSFER => true,
72
			CURLOPT_USERAGENT      => $this->options->user_agent,
73
			CURLOPT_PROTOCOLS      => CURLPROTO_HTTP|CURLPROTO_HTTPS,
74
			CURLOPT_SSL_VERIFYPEER => true,
75
			CURLOPT_SSL_VERIFYHOST => 2, // Support for value 1 removed in cURL 7.28.1
76
			CURLOPT_CAINFO         => is_file($this->options->ca_info) ? $this->options->ca_info : null,
77
			CURLOPT_TIMEOUT        => $this->options->timeout,
78
		]);
79
80
		curl_setopt_array($this->curl, $this->options->curl_options);
81
	}
82
83
	/**
84
	 * @param \chillerlan\TinyCurl\URL $url
85
	 * @param array                    $curl_options
86
	 *
87
	 * @return \chillerlan\TinyCurl\Response
88
	 * @throws \chillerlan\TinyCurl\RequestException
89
	 */
90
	public function fetch(URL $url, array $curl_options = []):Response {
91
		$this->initCurl();
92
93
		$headers = $this->normalizeHeaders($url->headers);
94
95
		if(in_array($url->method, ['POST', 'PUT'])){
96
97
			$curl_options += $url->method === 'PUT'
98
				? [CURLOPT_CUSTOMREQUEST => 'PUT']
99
				: [CURLOPT_POST => true];
100
101
			if(!isset($headers['Content-type']) && $url->method === 'POST' && is_array($url->body)){
102
				$headers += ['Content-type: application/x-www-form-urlencoded'];
103
				$body     = http_build_query($url->body, '', '&', PHP_QUERY_RFC1738);
104
			}
105
			else{
106
				$body = $url->body;
107
			}
108
109
			$curl_options += [CURLOPT_POSTFIELDS => $body];
110
		}
111
		else{
112
			$curl_options += [CURLOPT_CUSTOMREQUEST => $url->method];
113
		}
114
115
		$headers += [
116
			'Host: '.$url->host,
117
			'Connection: close',
118
		];
119
120
		if($this->options->max_redirects > 0){
121
			$curl_options += [
122
				CURLOPT_FOLLOWLOCATION => true,
123
				CURLOPT_MAXREDIRS      => $this->options->max_redirects,
124
			];
125
		}
126
127
		$curl_options += [CURLOPT_HTTPHEADER => $headers];
128
129
		curl_setopt_array($this->curl, $curl_options);
130
131
		return $this->getResponse((string)$url);
132
	}
133
134
	/**
135
	 * @param array $headers
136
	 *
137
	 * @return array
138
	 */
139
	public function normalizeHeaders(array $headers):array {
140
		$normalized_headers = [];
141
142
		foreach($headers as $key => $val){
143
144
			if(is_numeric($key)){
145
				$header = explode(':', $val, 2);
146
147
				if(count($header) === 2){
148
					$key = $header[0];
149
					$val = $header[1];
150
				}
151
				else{
152
					continue;
153
				}
154
			}
155
156
			$key = ucfirst(strtolower($key));
157
158
			$normalized_headers[$key] = trim($key).': '.trim($val);
159
		}
160
161
		return $normalized_headers;
162
	}
163
164
	/**
165
	 * @param string $url
166
	 *
167
	 * @return array<string>
168
	 */
169
	public function extractShortUrl(string $url):array {
170
		$urls = [$url];
171
172
		while($url = $this->extract($url)){
173
			$urls[] = $url;
174
		}
175
176
		return $urls;
177
	}
178
179
	/**
180
	 * @param string $url
181
	 *
182
	 * @return string
183
	 * @link http://www.internoetics.com/2012/11/12/resolve-short-urls-to-their-destination-url-php-api/
184
	 */
185
	protected function extract(string $url):string {
186
		$this->initCurl();
187
188
		curl_setopt_array($this->curl, [
189
			CURLOPT_FOLLOWLOCATION => false
190
		]);
191
192
		$response = $this->getResponse($url);
193
194
		$info    = $response->info;
195
		$headers = $response->headers;
196
197
		switch(true){
198
			// check curl_info()
199
			case in_array($info->http_code, range(300, 308), true) && isset($info->redirect_url) && !empty($info->redirect_url):
200
				return $info->redirect_url;
201
			// look for a location header
202
			case isset($headers->location) && !empty($headers->location):
203
				return $headers->location; // @codeCoverageIgnore
204
			default:
205
				return '';
206
		}
207
208
	}
209
210
}
211