Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
1 | <?php |
||
22 | trait HTTPOptionsTrait{ |
||
23 | |||
24 | /** |
||
25 | * @var string |
||
26 | */ |
||
27 | protected string $user_agent = 'chillerlanHttpInterface/2.0 +https://github.com/chillerlan/php-httpinterface'; |
||
|
|||
28 | |||
29 | /** |
||
30 | * options for each curl instance |
||
31 | * |
||
32 | * this array is being merged into the default options as the last thing before curl_exec(). |
||
33 | * none of the values (except existence of the CA file) will be checked - that's up to the implementation. |
||
34 | */ |
||
35 | protected array $curl_options = []; |
||
36 | |||
37 | /** |
||
38 | * CA Root Certificates for use with CURL/SSL (if not configured in php.ini or available in a default path) |
||
39 | * |
||
40 | * @link https://curl.haxx.se/docs/caextract.html |
||
41 | * @link https://curl.haxx.se/ca/cacert.pem |
||
42 | * @link https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt |
||
43 | */ |
||
44 | protected ?string $ca_info = null; |
||
45 | |||
46 | /** |
||
47 | * see CURLOPT_SSL_VERIFYPEER |
||
48 | * requires either HTTPOptions::$ca_info or a properly working system CA file |
||
49 | * |
||
50 | * @link https://php.net/manual/function.curl-setopt.php |
||
51 | */ |
||
52 | protected bool $ssl_verifypeer = true; |
||
53 | |||
54 | /** |
||
55 | * The CurlHandleInterface to use in CurlClient::sendRequest() |
||
56 | */ |
||
57 | protected string $curlHandle = CurlHandle::class; |
||
58 | |||
59 | /** |
||
60 | * options for the curl multi instance |
||
61 | * |
||
62 | * @link https://www.php.net/manual/function.curl-multi-setopt.php |
||
63 | */ |
||
64 | protected array $curl_multi_options = []; |
||
65 | |||
66 | /** |
||
67 | * maximum of concurrent requests for curl_multi |
||
68 | */ |
||
69 | protected int $windowSize = 5; |
||
70 | |||
71 | /** |
||
72 | * sleep timer (milliseconds) between each fired multi request on startup |
||
73 | */ |
||
74 | protected ?int $sleep = null; |
||
75 | |||
76 | /** |
||
77 | * |
||
78 | */ |
||
79 | protected int $timeout = 10; |
||
80 | |||
81 | /** |
||
82 | * |
||
83 | */ |
||
84 | protected int $retries = 3; |
||
85 | |||
86 | /** |
||
87 | * HTTPOptionsTrait constructor |
||
88 | * |
||
89 | * @throws \Psr\Http\Client\ClientExceptionInterface |
||
90 | */ |
||
91 | protected function HTTPOptionsTrait():void{ |
||
92 | |||
93 | if(!is_array($this->curl_options)){ |
||
94 | $this->curl_options = []; |
||
95 | } |
||
96 | |||
97 | if(!is_string($this->user_agent) || empty(trim($this->user_agent))){ |
||
98 | throw new ClientException('invalid user agent'); |
||
99 | } |
||
100 | |||
101 | $this->setCA(); |
||
102 | } |
||
103 | |||
104 | /** |
||
105 | * @return void |
||
106 | * @throws \Psr\Http\Client\ClientExceptionInterface |
||
107 | */ |
||
108 | protected function setCA():void{ |
||
109 | |||
110 | if(!$this->checkVerifyEnabled()){ |
||
111 | return; |
||
112 | } |
||
113 | |||
114 | // a path/dir/link to a CA bundle is given, let's check that |
||
115 | if(is_string($this->ca_info)){ |
||
116 | |||
117 | if($this->setCaBundle()){ |
||
118 | return; |
||
119 | } |
||
120 | |||
121 | throw new ClientException('invalid path to SSL CA bundle (HTTPOptions::$ca_info): '.$this->ca_info); |
||
122 | } |
||
123 | |||
124 | // we somehow landed here, so let's check if there's a CA bundle given via the cURL options |
||
125 | $ca = $this->curl_options[CURLOPT_CAPATH] ?? $this->curl_options[CURLOPT_CAINFO] ?? false; |
||
126 | |||
127 | if($ca){ |
||
128 | |||
129 | if($this->setCaBundleCurl($ca)){ |
||
130 | return; |
||
131 | } |
||
132 | |||
133 | throw new ClientException('invalid path to SSL CA bundle (CURLOPT_CAPATH/CURLOPT_CAINFO): '.$ca); |
||
134 | } |
||
135 | |||
136 | // check php.ini options - PHP should find the file by itself |
||
137 | if(file_exists(ini_get('curl.cainfo'))){ |
||
138 | return; // @codeCoverageIgnore |
||
139 | } |
||
140 | |||
141 | // this is getting weird. as a last resort, we're going to check some default paths for a CA bundle file |
||
142 | if($this->checkCaDefaultLocations()){ |
||
143 | return; |
||
144 | } |
||
145 | |||
146 | // @codeCoverageIgnoreStart |
||
147 | $msg = 'No system CA bundle could be found in any of the the common system locations. ' |
||
148 | .'In order to verify peer certificates, you will need to supply the path on disk to a certificate bundle via ' |
||
149 | .'HTTPOptions::$ca_info or HTTPOptions::$curl_options. If you do not need a specific certificate bundle, ' |
||
150 | .'then you can download a CA bundle over here: https://curl.haxx.se/docs/caextract.html. ' |
||
151 | .'Once you have a CA bundle available on disk, you can set the "curl.cainfo" php.ini setting to point ' |
||
152 | .'to the path of the file, allowing you to omit the $ca_info or $curl_options setting. ' |
||
153 | .'See http://curl.haxx.se/docs/sslcerts.html for more information.'; |
||
154 | |||
155 | throw new ClientException($msg); |
||
156 | // @codeCoverageIgnoreEnd |
||
157 | } |
||
158 | |||
159 | /** |
||
160 | * @return bool |
||
161 | */ |
||
162 | protected function checkCaDefaultLocations():bool{ |
||
163 | |||
164 | $cafiles = [ |
||
165 | // check other php.ini settings |
||
166 | ini_get('openssl.cafile'), |
||
167 | // Red Hat, CentOS, Fedora (provided by the ca-certificates package) |
||
168 | '/etc/pki/tls/certs/ca-bundle.crt', |
||
169 | // Ubuntu, Debian (provided by the ca-certificates package) |
||
170 | '/etc/ssl/certs/ca-certificates.crt', |
||
171 | // FreeBSD (provided by the ca_root_nss package) |
||
172 | '/usr/local/share/certs/ca-root-nss.crt', |
||
173 | // SLES 12 (provided by the ca-certificates package) |
||
174 | '/var/lib/ca-certificates/ca-bundle.pem', |
||
175 | // OS X provided by homebrew (using the default path) |
||
176 | '/usr/local/etc/openssl/cert.pem', |
||
177 | // Google app engine |
||
178 | '/etc/ca-certificates.crt', |
||
179 | // Windows? |
||
180 | // http://php.net/manual/en/function.curl-setopt.php#110457 |
||
181 | 'C:\\Windows\\system32\\curl-ca-bundle.crt', |
||
182 | 'C:\\Windows\\curl-ca-bundle.crt', |
||
183 | 'C:\\Windows\\system32\\cacert.pem', |
||
184 | 'C:\\Windows\\cacert.pem', |
||
185 | // working path |
||
186 | __DIR__.'/cacert.pem', |
||
187 | ]; |
||
188 | |||
189 | foreach($cafiles as $file){ |
||
190 | |||
191 | if(is_file($file) || (is_link($file) && is_file(readlink($file)))){ |
||
192 | $this->curl_options[CURLOPT_CAINFO] = $file; |
||
193 | $this->ca_info = $file; |
||
194 | |||
195 | return true; |
||
196 | } |
||
197 | |||
198 | } |
||
199 | |||
200 | return false; // @codeCoverageIgnore |
||
201 | } |
||
202 | |||
203 | /** |
||
204 | * @return bool |
||
205 | */ |
||
206 | protected function checkVerifyEnabled():bool{ |
||
207 | |||
208 | // disable verification if wanted so |
||
209 | if($this->ssl_verifypeer !== true || (isset($this->curl_options[CURLOPT_SSL_VERIFYPEER]) && !$this->curl_options[CURLOPT_SSL_VERIFYPEER])){ |
||
210 | unset($this->curl_options[CURLOPT_CAINFO], $this->curl_options[CURLOPT_CAPATH]); |
||
211 | |||
212 | $this->curl_options[CURLOPT_SSL_VERIFYHOST] = 0; |
||
213 | $this->curl_options[CURLOPT_SSL_VERIFYPEER] = false; |
||
214 | |||
215 | return false; |
||
216 | } |
||
217 | |||
218 | $this->curl_options[CURLOPT_SSL_VERIFYHOST] = 2; |
||
219 | $this->curl_options[CURLOPT_SSL_VERIFYPEER] = true; |
||
220 | |||
221 | return true; |
||
222 | } |
||
223 | |||
224 | /** |
||
225 | * @return bool |
||
226 | */ |
||
227 | protected function setCaBundle():bool{ |
||
228 | |||
229 | // if you - for whatever obscure reason - need to check Windows .lnk links, |
||
230 | // see http://php.net/manual/en/function.is-link.php#91249 |
||
231 | switch(true){ |
||
232 | case is_dir($this->ca_info): |
||
233 | case is_link($this->ca_info) && is_dir(readlink($this->ca_info)): // @codeCoverageIgnore |
||
234 | $this->curl_options[CURLOPT_CAPATH] = $this->ca_info; |
||
235 | unset($this->curl_options[CURLOPT_CAINFO]); |
||
236 | |||
237 | return true; |
||
238 | |||
239 | case is_file($this->ca_info): |
||
240 | case is_link($this->ca_info) && is_file(readlink($this->ca_info)): // @codeCoverageIgnore |
||
241 | $this->curl_options[CURLOPT_CAINFO] = $this->ca_info; |
||
242 | unset($this->curl_options[CURLOPT_CAPATH]); |
||
243 | |||
244 | return true; |
||
245 | } |
||
246 | |||
247 | return false; |
||
248 | } |
||
249 | |||
250 | /** |
||
251 | * @param string $ca |
||
252 | * |
||
253 | * @return bool |
||
254 | */ |
||
255 | protected function setCaBundleCurl(string $ca):bool{ |
||
256 | |||
257 | // just check if the file/path exists |
||
258 | switch(true){ |
||
259 | case is_dir($ca): |
||
260 | case is_link($ca) && is_dir(readlink($ca)): // @codeCoverageIgnore |
||
261 | unset($this->curl_options[CURLOPT_CAINFO]); |
||
262 | |||
263 | return true; |
||
264 | |||
265 | case is_file($ca): |
||
266 | case is_link($ca) && is_file(readlink($ca)): // @codeCoverageIgnore |
||
267 | |||
268 | return true; |
||
269 | } |
||
270 | |||
271 | return false; |
||
272 | } |
||
273 | |||
274 | } |
||
275 |