RequestHelper::getHeader()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
rs 10
cc 1
nc 1
nop 1
1
<?php
2
/**
3
 * File containing the {@link RequestHelper} class.
4
 * @package Application Utils
5
 * @subpackage RequestHelper
6
 * @see RequestHelper
7
 */
8
9
namespace AppUtils;
10
11
use CurlHandle;
12
13
/**
14
 * Handles sending POST requests with file attachments and regular variables.
15
 * Creates the raw request headers required for the request and sends them
16
 * using file_get_contents with the according context parameters.
17
 *
18
 * @package Application Utils
19
 * @subpackage RequestHelper
20
 * @author Sebastian Mordziol <[email protected]>
21
 */
22
class RequestHelper
23
{
24
    public const FILETYPE_TEXT = 'text/plain';
25
    public const FILETYPE_XML = 'text/xml';
26
    public const FILETYPE_HTML = 'text/html';
27
28
    public const ENCODING_UTF8 = 'UTF-8';
29
30
    public const TRANSFER_ENCODING_BASE64 = 'BASE64';
31
    public const TRANSFER_ENCODING_8BIT = '8BIT';
32
    public const TRANSFER_ENCODING_BINARY = 'BINARY';
33
    
34
    public const ERROR_REQUEST_FAILED = 17902;
35
    public const ERROR_CURL_INIT_FAILED = 17903;
36
    public const ERROR_CANNOT_OPEN_LOGFILE = 17904;
37
38
    protected string $eol = "\r\n";
39
    protected string $mimeBoundary;
40
    protected string $destination;
41
    protected bool $verifySSL = true;
42
    protected RequestHelper_Boundaries $boundaries;
43
    protected ?RequestHelper_Response $response = null;
44
    protected int $timeout = 30; // seconds
45
    protected string $logfile = '';
46
    protected int $contentLength = 0;
47
48
    /**
49
    * @var array<string,string>
50
    */
51
    protected $headers = array();
52
53
   /**
54
    * @var resource|NULL
55
    */
56
    protected $logfilePointer;
57
    
58
   /**
59
    * Creates a new request helper to send POST data to the specified destination URL.
60
    * @param string $destinationURL
61
    */
62
    public function __construct(string $destinationURL)
63
    {
64
        $this->destination = $destinationURL;
65
        $this->mimeBoundary = str_repeat('-', 20).md5('request-helper-boundary');
66
        $this->boundaries = new RequestHelper_Boundaries($this);
67
    }
68
    
69
    public function getMimeBoundary() : string
70
    {
71
        return $this->mimeBoundary;
72
    }
73
    
74
    public function getMimeBody() : string
75
    {
76
        return $this->boundaries->render();
77
    }
78
    
79
    public function getEOL() : string
80
    {
81
        return $this->eol;
82
    }
83
    
84
   /**
85
    * Sets the timeout for the request, in seconds. If the request
86
    * takes longer, it will be cancelled and an exception triggered.
87
    * 
88
    * @param int $seconds
89
    * @return RequestHelper
90
    */
91
    public function setTimeout(int $seconds) : RequestHelper
92
    {
93
        $this->timeout = $seconds;
94
        
95
        return $this;
96
    }
97
    
98
    public function getTimeout() : int
99
    {
100
        return $this->timeout;
101
    }
102
    
103
   /**
104
    * Enables verbose logging of the CURL request, which
105
    * is then redirected to the target file.
106
    * 
107
    * @param string $targetFile
108
    * @return RequestHelper
109
    */
110
    public function enableLogging(string $targetFile) : RequestHelper
111
    {
112
        $this->logfile = $targetFile;
113
        
114
        return $this;
115
    }
116
117
   /**
118
    * Adds a file to be sent with the request.
119
    *
120
    * @param string $varName The variable name to send the file in
121
    * @param string $fileName The name of the file as it should be received at the destination
122
    * @param string $content The raw content of the file
123
    * @param string $contentType The content type, use the constants to specify this
124
    * @param string $encoding The encoding of the file, use the constants to specify this
125
    */
126
    public function addFile(string $varName, string $fileName, string $content, string $contentType = '', string $encoding = '') : RequestHelper
127
    {
128
        $this->boundaries->addFile($varName, $fileName, $content, $contentType, $encoding);
129
        
130
        return $this;
131
    }
132
133
    /**
134
     * Adds arbitrary content.
135
     *
136
     * @param string $varName The variable name to send the content in.
137
     * @param string $content
138
     * @param string $contentType
139
     * @return RequestHelper
140
     */
141
    public function addContent(string $varName, string $content, string $contentType) : RequestHelper
142
    {
143
        $this->boundaries->addContent($varName, $content, $contentType);
144
        
145
        return $this;
146
    }
147
148
    /**
149
     * Adds a variable to be sent with the request. If it
150
     * already exists, its value is overwritten.
151
     *
152
     * @param string $name
153
     * @param string $value
154
     * @return RequestHelper
155
     */
156
    public function addVariable(string $name, string $value) : RequestHelper
157
    {
158
        $this->boundaries->addVariable($name, $value);
159
        
160
        return $this;
161
    }
162
    
163
   /**
164
    * Sets an HTTP header to include in the request.
165
    * 
166
    * @param string $name
167
    * @param string $value
168
    * @return RequestHelper
169
    */
170
    public function setHeader(string $name, string $value) : RequestHelper
171
    {
172
        $this->headers[$name] = $value;
173
        
174
        return $this;
175
    }
176
    
177
   /**
178
    * Disables SSL certificate checking.
179
    * 
180
    * @return RequestHelper
181
    */
182
    public function disableSSLChecks() : RequestHelper
183
    {
184
        $this->verifySSL = false;
185
        return $this;
186
    }
187
   
188
   /**
189
    * Sends the POST request to the destination, and returns
190
    * the response text.
191
    *
192
    * The response object is stored internally, so after calling
193
    * this method it may be retrieved at any moment using the
194
    * {@link getResponse()} method.
195
    *
196
    * @return string
197
    * @see RequestHelper::getResponse()
198
    * @throws RequestHelper_Exception
199
    * 
200
    * @see RequestHelper::ERROR_REQUEST_FAILED
201
    */
202
    public function send() : string
203
    {
204
        $info = parseURL($this->destination);
205
        
206
        $ch = $this->configureCURL($info);
207
208
        $output = curl_exec($ch);
209
210
        if(isset($this->logfilePointer))
211
        {
212
            fclose($this->logfilePointer);
213
        }
214
        
215
        $info = curl_getinfo($ch);
216
        
217
        $this->response = new RequestHelper_Response($this, $info);
218
        
219
        // CURL will complain about an empty response when the 
220
        // server sends a 100-continue code. That should not be
221
        // regarded as an error.
222
        if($output === false && $this->response->getCode() !== 100)
223
        {
224
            $curlCode = curl_errno($ch);
225
            
226
            $this->response->setError(
227
                $curlCode,
228
                curl_error($ch).' | Explanation: '.curl_strerror($curlCode)
229
            );
230
        }
231
        else
232
        {
233
            $this->response->setBody((string)$output);
234
        }
235
        
236
        curl_close($ch);
237
        
238
        return $this->response->getResponseBody();
239
    }
240
    
241
   /**
242
    * Retrieves the request's body content. This is an alias
243
    * for {@see RequestHelper::getMimeBody()}.
244
    * 
245
    * @return string
246
    * @see RequestHelper::getMimeBody()
247
    */
248
    public function getBody() : string
249
    {
250
        return $this->getMimeBody();
251
    }
252
253
    /**
254
     * Creates a new CURL resource configured according to the
255
     * request's settings.
256
     *
257
     * @return resource|CurlHandle
258
     * @throws RequestHelper_Exception
259
     */
260
    public static function createCURL()
261
    {
262
        $ch = curl_init();
263
264
        if($ch instanceof CurlHandle || is_resource($ch))
265
        {
266
            return $ch;
267
        }
268
269
        throw new RequestHelper_Exception(
270
            'Could not initialize a new cURL instance.',
271
            sprintf(
272
                'Calling curl_init failed to return a valid resource or instance. Given: [%s].',
273
                parseVariable($ch)->enableType()->toString()
274
            ),
275
            self::ERROR_CURL_INIT_FAILED
276
        );
277
    }
278
279
   /**
280
    * Creates a new CURL resource configured according to the
281
    * request's settings.
282
    * 
283
    * @param URLInfo $url
284
    * @throws RequestHelper_Exception
285
    * @return resource
286
    */
287
    protected function configureCURL(URLInfo $url)
288
    {
289
        $ch = self::createCURL();
290
291
        $this->setHeader('Content-Length', (string)$this->boundaries->getContentLength());
292
        $this->setHeader('Content-Type', 'multipart/form-data; boundary=' . $this->mimeBoundary);
293
294
        curl_setopt($ch, CURLOPT_POST, true);
295
        curl_setopt($ch, CURLOPT_URL, $url->getNormalizedWithoutAuth());
296
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
297
        curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
298
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
299
        curl_setopt($ch, CURLOPT_HTTPHEADER, $this->renderHeaders());
300
        curl_setopt($ch, CURLOPT_POSTFIELDS, $this->boundaries->render());
301
        curl_setopt($ch, CURLOPT_FAILONERROR, true);
302
        
303
        $loggingEnabled = $this->configureLogging($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type CurlHandle; however, parameter $ch of AppUtils\RequestHelper::configureLogging() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

303
        $loggingEnabled = $this->configureLogging(/** @scrutinizer ignore-type */ $ch);
Loading history...
304
        
305
        if(!$loggingEnabled) 
306
        {
307
            curl_setopt($ch, CURLINFO_HEADER_OUT, true);
308
        }
309
        
310
        if($this->verifySSL)
311
        {
312
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
313
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
314
        }
315
        
316
        if($url->hasUsername())
317
        {
318
            curl_setopt($ch, CURLOPT_USERNAME, $url->getUsername());
319
            curl_setopt($ch, CURLOPT_PASSWORD, $url->getPassword());
320
        }
321
        
322
        return $ch;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ch also could return the type CurlHandle which is incompatible with the documented return type resource.
Loading history...
323
    }
324
325
    /**
326
     * @param resource $ch
327
     * @return bool Whether logging is enabled.
328
     * @throws RequestHelper_Exception
329
     */
330
    protected function configureLogging($ch) : bool
331
    {
332
        if(empty($this->logfile))
333
        {
334
            return false;
335
        }
336
        
337
        $res = fopen($this->logfile, 'wb+');
338
        
339
        if($res === false)
340
        {
341
            throw new RequestHelper_Exception(
342
                'Cannot open logfile for writing.',
343
                sprintf('Tried accessing the file at [%s].', $this->logfile),
344
                self::ERROR_CANNOT_OPEN_LOGFILE
345
            );
346
        }
347
        
348
        $this->logfilePointer = $res;
349
        
350
        curl_setopt($ch, CURLOPT_VERBOSE, true);
351
        curl_setopt($ch, CURLOPT_STDERR, $res);
352
        
353
        return true;
354
    }
355
356
   /**
357
    * Compiles the associative headers array into
358
    * the format understood by CURL, namely an indexed
359
    * array with one header string per entry.
360
    * 
361
    * @return string[]
362
    */
363
    protected function renderHeaders() : array
364
    {
365
        $result = array();
366
        
367
        $this->setHeader('Expect', '');
368
        
369
        foreach($this->headers as $name => $value) {
370
            $result[] = $name.': '.$value;
371
        }
372
        
373
        return $result;
374
    }
375
    
376
   /**
377
    * Retrieves the raw response header, in the form of an indexed
378
    * array containing all response header lines.
379
    * 
380
    * @return string[]
381
    */
382
    public function getResponseHeader() : array
383
    {
384
        $response = $this->getResponse();
385
        
386
        if($response !== null) {
387
            return $response->getHeaders();
388
        }
389
390
        return array();
391
    }
392
393
   /**
394
    * After calling the {@link send()} method, this may be used to
395
    * retrieve the response text from the POST request.
396
    *
397
    * @return RequestHelper_Response|NULL
398
    */
399
    public function getResponse() : ?RequestHelper_Response
400
    {
401
        return $this->response;
402
    }
403
    
404
   /**
405
    * Retrieves all headers set until now.
406
    * 
407
    * @return array<string,string>
408
    */
409
    public function getHeaders() : array
410
    {
411
        return $this->headers;
412
    }
413
    
414
   /**
415
    * Retrieves the value of a header by its name.
416
    * 
417
    * @param string $name
418
    * @return string The header value, or an empty string if not set.
419
    */
420
    public function getHeader(string $name) : string
421
    {
422
        return $this->headers[$name] ?? '';
423
    }
424
}
425