Completed
Push — master ( 6bc3b4...34ca55 )
by Ralf
15s queued 11s
created

Request::__call()   B

Complexity

Conditions 7
Paths 15

Size

Total Lines 42
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 17
nc 15
nop 2
dl 0
loc 42
rs 8.8333
c 0
b 0
f 0
1
<?php
2
3
namespace Httpful;
4
5
use Httpful\Exception\ConnectionErrorException;
6
7
/**
8
 * Clean, simple class for sending HTTP requests
9
 * in PHP.
10
 *
11
 * There is an emphasis of readability without loosing concise
12
 * syntax.  As such, you will notice that the library lends
13
 * itself very nicely to "chaining".  You will see several "alias"
14
 * methods: more readable method definitions that wrap
15
 * their more concise counterparts.  You will also notice
16
 * no public constructor.  This two adds to the readability
17
 * and "chainabilty" of the library.
18
 *
19
 * @author Nate Good <[email protected]>
20
 */
21
class Request
22
{
23
24
    // Option constants
25
    const SERIALIZE_PAYLOAD_NEVER   = 0;
26
    const SERIALIZE_PAYLOAD_ALWAYS  = 1;
27
    const SERIALIZE_PAYLOAD_SMART   = 2;
28
29
    const MAX_REDIRECTS_DEFAULT     = 25;
30
31
    public $uri,
32
           $method                  = Http::GET,
33
           $headers                 = array(),
34
           $raw_headers             = '',
35
           $strict_ssl              = false,
36
           $content_type,
37
           $expected_type,
38
           $additional_curl_opts    = array(),
39
           $auto_parse              = true,
40
           $serialize_payload_method = self::SERIALIZE_PAYLOAD_SMART,
41
           $username,
42
           $password,
43
           $serialized_payload,
44
           $payload,
45
           $parse_callback,
46
           $error_callback,
47
           $follow_redirects        = false,
48
           $max_redirects           = self::MAX_REDIRECTS_DEFAULT,
49
           $payload_serializers     = array();
50
51
    // Options
52
    // private $_options = array(
53
    //     'serialize_payload_method' => self::SERIALIZE_PAYLOAD_SMART
54
    //     'auto_parse' => true
55
    // );
56
57
    // Curl Handle
58
    public $_ch,
59
           $_debug;
60
61
    // Template Request object
62
    private static $_template;
63
64
    /**
65
     * We made the constructor private to force the factory style.  This was
66
     * done to keep the syntax cleaner and better the support the idea of
67
     * "default templates".  Very basic and flexible as it is only intended
68
     * for internal use.
69
     * @param array $attrs hash of initial attribute values
70
     */
71
    private function __construct($attrs = null)
72
    {
73
        if (!is_array($attrs)) return;
74
        foreach ($attrs as $attr => $value) {
75
            $this->$attr = $value;
76
        }
77
    }
78
79
    // Defaults Management
80
81
    /**
82
     * Let's you configure default settings for this
83
     * class from a template Request object.  Simply construct a
84
     * Request object as much as you want to and then pass it to
85
     * this method.  It will then lock in those settings from
86
     * that template object.
87
     * The most common of which may be default mime
88
     * settings or strict ssl settings.
89
     * Again some slight memory overhead incurred here but in the grand
90
     * scheme of things as it typically only occurs once
91
     * @param Request $template
92
     */
93
    public static function ini(Request $template)
94
    {
95
        self::$_template = clone $template;
96
    }
97
98
    /**
99
     * Reset the default template back to the
100
     * library defaults.
101
     */
102
    public static function resetIni()
103
    {
104
        self::_initializeDefaults();
105
    }
106
107
    /**
108
     * Get default for a value based on the template object
109
     * @return mixed default value
110
     * @param string|null $attr Name of attribute (e.g. mime, headers)
111
     *    if null just return the whole template object;
112
     */
113
    public static function d($attr)
114
    {
115
        return isset($attr) ? self::$_template->$attr : self::$_template;
116
    }
117
118
    // Accessors
119
120
    /**
121
     * @return bool does the request have a timeout?
122
     */
123
    public function hasTimeout()
124
    {
125
        return isset($this->timeout);
126
    }
127
128
    /**
129
     * @return bool has the internal curl request been initialized?
130
     */
131
    public function hasBeenInitialized()
132
    {
133
        return isset($this->_ch);
134
    }
135
136
    /**
137
     * @return bool Is this request setup for basic auth?
138
     */
139
    public function hasBasicAuth()
140
    {
141
        return isset($this->password) && isset($this->username);
142
    }
143
144
    /**
145
     * @return bool Is this request setup for digest auth?
146
     */
147
    public function hasDigestAuth()
148
    {
149
        return isset($this->password) && isset($this->username) && $this->additional_curl_opts[CURLOPT_HTTPAUTH] == CURLAUTH_DIGEST;
150
    }
151
152
    /**
153
     * Specify a HTTP timeout
154
     * @return Request $this
155
     * @param float|int $timeout seconds to timeout the HTTP call
156
     */
157
    public function timeout($timeout)
158
    {
159
        $this->timeout = $timeout;
160
        return $this;
161
    }
162
163
    // alias timeout
164
    public function timeoutIn($seconds)
165
    {
166
        return $this->timeout($seconds);
167
    }
168
169
    /**
170
     * If the response is a 301 or 302 redirect, automatically
171
     * send off another request to that location
172
     * @return Request $this
173
     * @param bool|int $follow follow or not to follow or maximal number of redirects
174
     */
175
    public function followRedirects($follow = true)
176
    {
177
        $this->max_redirects = $follow === true ? self::MAX_REDIRECTS_DEFAULT : max(0, $follow);
178
        $this->follow_redirects = (bool) $follow;
179
        return $this;
180
    }
181
182
    /**
183
     * @return Request $this
184
     * @see Request::followRedirects()
185
     */
186
    public function doNotFollowRedirects()
187
    {
188
        return $this->followRedirects(false);
189
    }
190
191
    /**
192
     * Actually send off the request, and parse the response
193
     * @return string|associative array of parsed results
194
     * @throws ConnectionErrorException when unable to parse or communicate w server
195
     */
196
    public function send()
197
    {
198
        if (!$this->hasBeenInitialized())
199
            $this->_curlPrep();
200
201
        $result = curl_exec($this->_ch);
202
203
        if ($result === false) {
204
            if ($curlErrorNumber = curl_errno($this->_ch)) {
205
                $curlErrorString = curl_error($this->_ch);
206
                $this->_error($curlErrorString);
207
                throw new ConnectionErrorException('Unable to connect: ' . $curlErrorNumber . ' ' . $curlErrorString);
208
            }
209
210
            $this->_error('Unable to connect.');
211
            throw new ConnectionErrorException('Unable to connect.');
212
        }
213
214
        $info = curl_getinfo($this->_ch);
215
216
        // Remove the "HTTP/1.x 200 Connection established" string and any other headers added by proxy
217
        $proxy_regex = "/HTTP\/1\.[01] 200 Connection established.*?\r\n\r\n/s";
218
        if ($this->hasProxy() && preg_match($proxy_regex, $result)) {
219
            $result = preg_replace($proxy_regex, '', $result);
220
        }
221
222
        $response = explode("\r\n\r\n", $result, 2 + $info['redirect_count']);
223
224
        $body = array_pop($response);
225
        $headers = array_pop($response);
226
227
        curl_close($this->_ch);
228
229
        return new Response($body, $headers, $this, $info);
230
    }
231
    public function sendIt()
232
    {
233
        return $this->send();
234
    }
235
236
    // Setters
237
238
    /**
239
     * @return Request this
240
     * @param string $uri
241
     */
242
    public function uri($uri)
243
    {
244
        $this->uri = $uri;
245
        return $this;
246
    }
247
248
    /**
249
     * User Basic Auth.
250
     * Only use when over SSL/TSL/HTTPS.
251
     * @return Request this
252
     * @param string $username
253
     * @param string $password
254
     */
255
    public function basicAuth($username, $password)
256
    {
257
        $this->username = $username;
258
        $this->password = $password;
259
        return $this;
260
    }
261
    // @alias of basicAuth
262
    public function authenticateWith($username, $password)
263
    {
264
        return $this->basicAuth($username, $password);
265
    }
266
    // @alias of basicAuth
267
    public function authenticateWithBasic($username, $password)
268
    {
269
        return $this->basicAuth($username, $password);
270
    }
271
272
    /**
273
     * User Digest Auth.
274
     * @return Request this
275
     * @param string $username
276
     * @param string $password
277
     */
278
    public function digestAuth($username, $password)
279
    {
280
        $this->addOnCurlOption(CURLOPT_HTTPAUTH, CURLAUTH_DIGEST);
281
        return $this->basicAuth($username, $password);
282
    }
283
284
    // @alias of digestAuth
285
    public function authenticateWithDigest($username, $password)
286
    {
287
        return $this->digestAuth($username, $password);
288
    }
289
290
    /**
291
     * @return is this request setup for client side cert?
292
     */
293
    public function hasClientSideCert()
294
    {
295
        return isset($this->client_cert) && isset($this->client_key);
296
    }
297
298
    /**
299
     * Use Client Side Cert Authentication
300
     * @return Request $this
301
     * @param string $key file path to client key
302
     * @param string $cert file path to client cert
303
     * @param string $passphrase for client key
304
     * @param string $encoding default PEM
305
     */
306
    public function clientSideCert($cert, $key, $passphrase = null, $encoding = 'PEM')
307
    {
308
        $this->client_cert          = $cert;
309
        $this->client_key           = $key;
310
        $this->client_passphrase    = $passphrase;
311
        $this->client_encoding      = $encoding;
312
313
        return $this;
314
    }
315
    // @alias of basicAuth
316
    public function authenticateWithCert($cert, $key, $passphrase = null, $encoding = 'PEM')
317
    {
318
        return $this->clientSideCert($cert, $key, $passphrase, $encoding);
319
    }
320
321
    /**
322
     * Set the body of the request
323
     * @return Request this
324
     * @param mixed $payload
325
     * @param string $mimeType currently, sets the sends AND expects mime type although this
326
     *    behavior may change in the next minor release (as it is a potential breaking change).
327
     */
328
    public function body($payload, $mimeType = null)
329
    {
330
        $this->mime($mimeType);
331
        $this->payload = $payload;
332
        // Iserntentially don't call _serializePayload yet.  Wait until
333
        // we actually send off the request to convert payload to string.
334
        // At that time, the `serialized_payload` is set accordingly.
335
        return $this;
336
    }
337
338
    /**
339
     * Helper function to set the Content type and Expected as same in
340
     * one swoop
341
     * @return Request this
342
     * @param string $mime mime type to use for content type and expected return type
343
     */
344
    public function mime($mime)
345
    {
346
        if (empty($mime)) return $this;
347
        $this->content_type = $this->expected_type = Mime::getFullMime($mime);
348
        if ($this->isUpload()) {
349
            $this->neverSerializePayload();
350
        }
351
        return $this;
352
    }
353
    // @alias of mime
354
    public function sendsAndExpectsType($mime)
355
    {
356
        return $this->mime($mime);
357
    }
358
    // @alias of mime
359
    public function sendsAndExpects($mime)
360
    {
361
        return $this->mime($mime);
362
    }
363
364
    /**
365
     * Set the method.  Shouldn't be called often as the preferred syntax
366
     * for instantiation is the method specific factory methods.
367
     * @return Request this
368
     * @param string $method
369
     */
370
    public function method($method)
371
    {
372
        if (empty($method)) return $this;
373
        $this->method = $method;
374
        return $this;
375
    }
376
377
    /**
378
     * @return Request this
379
     * @param string $mime
380
     */
381
    public function expects($mime)
382
    {
383
        if (empty($mime)) return $this;
384
        $this->expected_type = Mime::getFullMime($mime);
385
        return $this;
386
    }
387
    // @alias of expects
388
    public function expectsType($mime)
389
    {
390
        return $this->expects($mime);
391
    }
392
393
    public function attach($files)
394
    {
395
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
396
        foreach ($files as $key => $file) {
397
            $mimeType = finfo_file($finfo, $file);
398
            if (function_exists('curl_file_create')) {
399
                $this->payload[$key] = curl_file_create($file, $mimeType);
400
            } else {
401
                $this->payload[$key] = '@' . $file . ';type=' . $mimeType;
402
            }
403
        }
404
        $this->sendsType(Mime::UPLOAD);
405
        return $this;
406
    }
407
408
    /**
409
     * @return Request this
410
     * @param string $mime
411
     */
412
    public function contentType($mime)
413
    {
414
        if (empty($mime)) return $this;
415
        $this->content_type  = Mime::getFullMime($mime);
416
        if ($this->isUpload()) {
417
            $this->neverSerializePayload();
418
        }
419
        return $this;
420
    }
421
    // @alias of contentType
422
    public function sends($mime)
423
    {
424
        return $this->contentType($mime);
425
    }
426
    // @alias of contentType
427
    public function sendsType($mime)
428
    {
429
        return $this->contentType($mime);
430
    }
431
432
    /**
433
     * Do we strictly enforce SSL verification?
434
     * @return Request this
435
     * @param bool $strict
436
     */
437
    public function strictSSL($strict)
438
    {
439
        $this->strict_ssl = $strict;
440
        return $this;
441
    }
442
    public function withoutStrictSSL()
443
    {
444
        return $this->strictSSL(false);
445
    }
446
    public function withStrictSSL()
447
    {
448
        return $this->strictSSL(true);
449
    }
450
451
    /**
452
     * Use proxy configuration
453
     * @return Request this
454
     * @param string $proxy_host Hostname or address of the proxy
455
     * @param number $proxy_port Port of the proxy. Default 80
456
     * @param string $auth_type Authentication type or null. Accepted values are CURLAUTH_BASIC, CURLAUTH_NTLM. Default null, no authentication
457
     * @param string $auth_username Authentication username. Default null
458
     * @param string $auth_password Authentication password. Default null
459
     */
460
    public function useProxy($proxy_host, $proxy_port = 80, $auth_type = null, $auth_username = null, $auth_password = null, $proxy_type = Proxy::HTTP)
461
    {
462
        $this->addOnCurlOption(CURLOPT_PROXY, "{$proxy_host}:{$proxy_port}");
463
        $this->addOnCurlOption(CURLOPT_PROXYTYPE, $proxy_type);
464
        if (in_array($auth_type, array(CURLAUTH_BASIC,CURLAUTH_NTLM))) {
465
            $this->addOnCurlOption(CURLOPT_PROXYAUTH, $auth_type)
466
                ->addOnCurlOption(CURLOPT_PROXYUSERPWD, "{$auth_username}:{$auth_password}");
467
        }
468
        return $this;
469
    }
470
471
    /**
472
     * Shortcut for useProxy to configure SOCKS 4 proxy
473
     * @see Request::useProxy
474
     * @return Request
475
     */
476
    public function useSocks4Proxy($proxy_host, $proxy_port = 80, $auth_type = null, $auth_username = null, $auth_password = null)
477
    {
478
        return $this->useProxy($proxy_host, $proxy_port, $auth_type, $auth_username, $auth_password, Proxy::SOCKS4);
479
    }
480
481
    /**
482
     * Shortcut for useProxy to configure SOCKS 5 proxy
483
     * @see Request::useProxy
484
     * @return Request
485
     */
486
    public function useSocks5Proxy($proxy_host, $proxy_port = 80, $auth_type = null, $auth_username = null, $auth_password = null)
487
    {
488
        return $this->useProxy($proxy_host, $proxy_port, $auth_type, $auth_username, $auth_password, Proxy::SOCKS5);
489
    }
490
491
    /**
492
     * @return is this request setup for using proxy?
493
     */
494
    public function hasProxy()
495
    {
496
        return isset($this->additional_curl_opts[CURLOPT_PROXY]) && is_string($this->additional_curl_opts[CURLOPT_PROXY]);
497
    }
498
499
    /**
500
     * Determine how/if we use the built in serialization by
501
     * setting the serialize_payload_method
502
     * The default (SERIALIZE_PAYLOAD_SMART) is...
503
     *  - if payload is not a scalar (object/array)
504
     *    use the appropriate serialize method according to
505
     *    the Content-Type of this request.
506
     *  - if the payload IS a scalar (int, float, string, bool)
507
     *    than just return it as is.
508
     * When this option is set SERIALIZE_PAYLOAD_ALWAYS,
509
     * it will always use the appropriate
510
     * serialize option regardless of whether payload is scalar or not
511
     * When this option is set SERIALIZE_PAYLOAD_NEVER,
512
     * it will never use any of the serialization methods.
513
     * Really the only use for this is if you want the serialize methods
514
     * to handle strings or not (e.g. Blah is not valid JSON, but "Blah"
515
     * is).  Forcing the serialization helps prevent that kind of error from
516
     * happening.
517
     * @return Request $this
518
     * @param int $mode
519
     */
520
    public function serializePayload($mode)
521
    {
522
        $this->serialize_payload_method = $mode;
523
        return $this;
524
    }
525
526
    /**
527
     * @see Request::serializePayload()
528
     * @return Request
529
     */
530
    public function neverSerializePayload()
531
    {
532
        return $this->serializePayload(self::SERIALIZE_PAYLOAD_NEVER);
533
    }
534
535
    /**
536
     * This method is the default behavior
537
     * @see Request::serializePayload()
538
     * @return Request
539
     */
540
    public function smartSerializePayload()
541
    {
542
        return $this->serializePayload(self::SERIALIZE_PAYLOAD_SMART);
543
    }
544
545
    /**
546
     * @see Request::serializePayload()
547
     * @return Request
548
     */
549
    public function alwaysSerializePayload()
550
    {
551
        return $this->serializePayload(self::SERIALIZE_PAYLOAD_ALWAYS);
552
    }
553
554
    /**
555
     * Add an additional header to the request
556
     * Can also use the cleaner syntax of
557
     * $Request->withMyHeaderName($my_value);
558
     * @see Request::__call()
559
     *
560
     * @return Request this
561
     * @param string $header_name
562
     * @param string $value
563
     */
564
    public function addHeader($header_name, $value)
565
    {
566
        $this->headers[$header_name] = $value;
567
        return $this;
568
    }
569
570
    /**
571
     * Add group of headers all at once.  Note: This is
572
     * here just as a convenience in very specific cases.
573
     * The preferred "readable" way would be to leverage
574
     * the support for custom header methods.
575
     * @return Request $this
576
     * @param array $headers
577
     */
578
    public function addHeaders(array $headers)
579
    {
580
        foreach ($headers as $header => $value) {
581
            $this->addHeader($header, $value);
582
        }
583
        return $this;
584
    }
585
586
    /**
587
     * @return Request
588
     * @param bool $auto_parse perform automatic "smart"
589
     *    parsing based on Content-Type or "expectedType"
590
     *    If not auto parsing, Response->body returns the body
591
     *    as a string.
592
     */
593
    public function autoParse($auto_parse = true)
594
    {
595
        $this->auto_parse = $auto_parse;
596
        return $this;
597
    }
598
599
    /**
600
     * @see Request::autoParse()
601
     * @return Request
602
     */
603
    public function withoutAutoParsing()
604
    {
605
        return $this->autoParse(false);
606
    }
607
608
    /**
609
     * @see Request::autoParse()
610
     * @return Request
611
     */
612
    public function withAutoParsing()
613
    {
614
        return $this->autoParse(true);
615
    }
616
617
    /**
618
     * Use a custom function to parse the response.
619
     * @return Request this
620
     * @param \Closure $callback Takes the raw body of
621
     *    the http response and returns a mixed
622
     */
623
    public function parseWith(\Closure $callback)
624
    {
625
        $this->parse_callback = $callback;
626
        return $this;
627
    }
628
629
    /**
630
     * @see Request::parseResponsesWith()
631
     * @return Request $this
632
     * @param \Closure $callback
633
     */
634
    public function parseResponsesWith(\Closure $callback)
635
    {
636
        return $this->parseWith($callback);
637
    }
638
639
    /**
640
     * Callback called to handle HTTP errors. When nothing is set, defaults
641
     * to logging via `error_log`
642
     * @return Request
643
     * @param \Closure $callback (string $error)
644
     */
645
    public function whenError(\Closure $callback)
646
    {
647
        $this->error_callback = $callback;
648
        return $this;
649
    }
650
651
    /**
652
     * Register a callback that will be used to serialize the payload
653
     * for a particular mime type.  When using "*" for the mime
654
     * type, it will use that parser for all responses regardless of the mime
655
     * type.  If a custom '*' and 'application/json' exist, the custom
656
     * 'application/json' would take precedence over the '*' callback.
657
     *
658
     * @return Request $this
659
     * @param string $mime mime type we're registering
660
     * @param Closure $callback takes one argument, $payload,
661
     *    which is the payload that we'll be
662
     */
663
    public function registerPayloadSerializer($mime, \Closure $callback)
664
    {
665
        $this->payload_serializers[Mime::getFullMime($mime)] = $callback;
666
        return $this;
667
    }
668
669
    /**
670
     * @see Request::registerPayloadSerializer()
671
     * @return Request $this
672
     * @param Closure $callback
673
     */
674
    public function serializePayloadWith(\Closure $callback)
675
    {
676
        return $this->registerPayloadSerializer('*', $callback);
677
    }
678
679
    /**
680
     * Magic method allows for neatly setting other headers in a
681
     * similar syntax as the other setters.  This method also allows
682
     * for the sends* syntax.
683
     * @return Request this
684
     * @param string $method "missing" method name called
685
     *    the method name called should be the name of the header that you
686
     *    are trying to set in camel case without dashes e.g. to set a
687
     *    header for Content-Type you would use contentType() or more commonly
688
     *    to add a custom header like X-My-Header, you would use xMyHeader().
689
     *    To promote readability, you can optionally prefix these methods with
690
     *    "with"  (e.g. withXMyHeader("blah") instead of xMyHeader("blah")).
691
     * @param array $args in this case, there should only ever be 1 argument provided
692
     *    and that argument should be a string value of the header we're setting
693
     */
694
    public function __call($method, $args)
695
    {
696
        // This method supports the sends* methods
697
        // like sendsJSON, sendsForm
698
        //!method_exists($this, $method) &&
699
        if (substr($method, 0, 5) === 'sends') {
700
            $mime = strtolower(substr($method, 5));
701
            if (Mime::supportsMimeType($mime)) {
702
                $this->sends(Mime::getFullMime($mime));
703
                return $this;
704
            }
705
            // else {
706
            //     throw new \Exception("Unsupported Content-Type $mime");
707
            // }
708
        }
709
        if (substr($method, 0, 7) === 'expects') {
710
            $mime = strtolower(substr($method, 7));
711
            if (Mime::supportsMimeType($mime)) {
712
                $this->expects(Mime::getFullMime($mime));
713
                return $this;
714
            }
715
            // else {
716
            //     throw new \Exception("Unsupported Content-Type $mime");
717
            // }
718
        }
719
720
        // This method also adds the custom header support as described in the
721
        // method comments
722
        if (count($args) === 0)
723
            return;
724
725
        // Strip the sugar.  If it leads with "with", strip.
726
        // This is okay because: No defined HTTP headers begin with with,
727
        // and if you are defining a custom header, the standard is to prefix it
728
        // with an "X-", so that should take care of any collisions.
729
        if (substr($method, 0, 4) === 'with')
730
            $method = substr($method, 4);
731
732
        // Precede upper case letters with dashes, uppercase the first letter of method
733
        $header = ucwords(implode('-', preg_split('/([A-Z][^A-Z]*)/', $method, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY)));
734
        $this->addHeader($header, $args[0]);
735
        return $this;
736
    }
737
738
    // Internal Functions
739
740
    /**
741
     * This is the default template to use if no
742
     * template has been provided.  The template
743
     * tells the class which default values to use.
744
     * While there is a slight overhead for object
745
     * creation once per execution (not once per
746
     * Request instantiation), it promotes readability
747
     * and flexibility within the class.
748
     */
749
    private static function _initializeDefaults()
750
    {
751
        // This is the only place you will
752
        // see this constructor syntax.  It
753
        // is only done here to prevent infinite
754
        // recusion.  Do not use this syntax elsewhere.
755
        // It goes against the whole readability
756
        // and transparency idea.
757
        self::$_template = new Request(array('method' => Http::GET));
758
759
        // This is more like it...
760
        self::$_template
761
            ->withoutStrictSSL();
762
    }
763
764
    /**
765
     * Set the defaults on a newly instantiated object
766
     * Doesn't copy variables prefixed with _
767
     * @return Request this
768
     */
769
    private function _setDefaults()
770
    {
771
        if (!isset(self::$_template))
772
            self::_initializeDefaults();
773
        foreach (self::$_template as $k=>$v) {
774
            if ($k[0] != '_')
775
                $this->$k = $v;
776
        }
777
        return $this;
778
    }
779
780
    private function _error($error)
781
    {
782
        // TODO add in support for various Loggers that follow
783
        // PSR 3 https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
784
        if (isset($this->error_callback)) {
785
            $this->error_callback->__invoke($error);
786
        } else {
787
            error_log($error);
788
        }
789
    }
790
791
    /**
792
     * Factory style constructor works nicer for chaining.  This
793
     * should also really only be used internally.  The Request::get,
794
     * Request::post syntax is preferred as it is more readable.
795
     * @return Request
796
     * @param string $method Http Method
797
     * @param string $mime Mime Type to Use
798
     */
799
    public static function init($method = null, $mime = null)
800
    {
801
        // Setup our handlers, can call it here as it's idempotent
802
        Bootstrap::init();
803
804
        // Setup the default template if need be
805
        if (!isset(self::$_template))
806
            self::_initializeDefaults();
807
808
        $request = new Request();
809
        return $request
810
               ->_setDefaults()
811
               ->method($method)
812
               ->sendsType($mime)
813
               ->expectsType($mime);
814
    }
815
816
    /**
817
     * Does the heavy lifting.  Uses de facto HTTP
818
     * library cURL to set up the HTTP request.
819
     * Note: It does NOT actually send the request
820
     * @return Request $this;
821
     */
822
    public function _curlPrep()
823
    {
824
        // Check for required stuff
825
        if (!isset($this->uri))
826
            throw new \Exception('Attempting to send a request before defining a URI endpoint.');
827
828
        $ch = curl_init($this->uri);
829
830
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
831
        if ($this->method === Http::HEAD) {
832
            curl_setopt($ch, CURLOPT_NOBODY, true);
833
        }
834
835
        if ($this->hasBasicAuth()) {
836
            curl_setopt($ch, CURLOPT_USERPWD, $this->username . ':' . $this->password);
837
        }
838
839
        if ($this->hasClientSideCert()) {
840
841
            if (!file_exists($this->client_key))
842
                throw new \Exception('Could not read Client Key');
843
844
            if (!file_exists($this->client_cert))
845
                throw new \Exception('Could not read Client Certificate');
846
847
            curl_setopt($ch, CURLOPT_SSLCERTTYPE,   $this->client_encoding);
848
            curl_setopt($ch, CURLOPT_SSLKEYTYPE,    $this->client_encoding);
849
            curl_setopt($ch, CURLOPT_SSLCERT,       $this->client_cert);
850
            curl_setopt($ch, CURLOPT_SSLKEY,        $this->client_key);
851
            curl_setopt($ch, CURLOPT_SSLKEYPASSWD,  $this->client_passphrase);
852
            // curl_setopt($ch, CURLOPT_SSLCERTPASSWD,  $this->client_cert_passphrase);
853
        }
854
855
        if ($this->hasTimeout()) {
856
            if (defined('CURLOPT_TIMEOUT_MS')) {
857
                curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->timeout * 1000);
858
            } else {
859
                curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout);
860
            }
861
        }
862
863
        if ($this->follow_redirects) {
864
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
865
            curl_setopt($ch, CURLOPT_MAXREDIRS, $this->max_redirects);
866
        }
867
868
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->strict_ssl);
869
        // zero is safe for all curl versions
870
        $verifyValue = $this->strict_ssl + 0;
871
        //Support for value 1 removed in cURL 7.28.1 value 2 valid in all versions
872
        if ($verifyValue > 0) $verifyValue++;
873
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verifyValue);
874
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
875
876
        // https://github.com/nategood/httpful/issues/84
877
        // set Content-Length to the size of the payload if present
878
        if (isset($this->payload)) {
879
            $this->serialized_payload = $this->_serializePayload($this->payload);
880
            curl_setopt($ch, CURLOPT_POSTFIELDS, $this->serialized_payload);
881
            if (!$this->isUpload()) {
882
                $this->headers['Content-Length'] =
883
                    $this->_determineLength($this->serialized_payload);
884
            }
885
        }
886
887
        $headers = array();
888
        // https://github.com/nategood/httpful/issues/37
889
        // Except header removes any HTTP 1.1 Continue from response headers
890
        $headers[] = 'Expect:';
891
892
        if (!isset($this->headers['User-Agent'])) {
893
            $headers[] = $this->buildUserAgent();
894
        }
895
896
        $headers[] = "Content-Type: {$this->content_type}";
897
898
        // allow custom Accept header if set
899
        if (!isset($this->headers['Accept'])) {
900
            // http://pretty-rfc.herokuapp.com/RFC2616#header.accept
901
            $accept = 'Accept: */*; q=0.5, text/plain; q=0.8, text/html;level=3;';
902
903
            if (!empty($this->expected_type)) {
904
                $accept .= "q=0.9, {$this->expected_type}";
905
            }
906
907
            $headers[] = $accept;
908
        }
909
910
        // Solve a bug on squid proxy, NONE/411 when miss content length
911
        if (!isset($this->headers['Content-Length']) && !$this->isUpload()) {
912
            $this->headers['Content-Length'] = 0;
913
        }
914
915
        foreach ($this->headers as $header => $value) {
916
            $headers[] = "$header: $value";
917
        }
918
919
        $url = \parse_url($this->uri);
920
        $path = (isset($url['path']) ? $url['path'] : '/').(isset($url['query']) ? '?'.$url['query'] : '');
921
        $this->raw_headers = "{$this->method} $path HTTP/1.1\r\n";
922
        $host = (isset($url['host']) ? $url['host'] : 'localhost').(isset($url['port']) ? ':'.$url['port'] : '');
923
        $this->raw_headers .= "Host: $host\r\n";
924
        $this->raw_headers .= \implode("\r\n", $headers);
925
        $this->raw_headers .= "\r\n";
926
927
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
928
929
        if ($this->_debug) {
930
            curl_setopt($ch, CURLOPT_VERBOSE, true);
931
        }
932
933
        curl_setopt($ch, CURLOPT_HEADER, 1);
934
935
        // If there are some additional curl opts that the user wants
936
        // to set, we can tack them in here
937
        foreach ($this->additional_curl_opts as $curlopt => $curlval) {
938
            curl_setopt($ch, $curlopt, $curlval);
939
        }
940
941
        $this->_ch = $ch;
942
943
        return $this;
944
    }
945
946
    /**
947
     * @return int length of payload in bytes
948
     * @param string $str payload
949
     */
950
    public function _determineLength($str)
951
    {
952
        if (function_exists('mb_strlen')) {
953
            return mb_strlen($str, '8bit');
954
        } else {
955
            return strlen($str);
956
        }
957
    }
958
959
    /**
960
     * @return bool
961
     */
962
    public function isUpload()
963
    {
964
        return Mime::UPLOAD == $this->content_type;
965
    }
966
967
    /**
968
     * @return string
969
     */
970
    public function buildUserAgent()
971
    {
972
        $user_agent = 'User-Agent: Httpful/' . Httpful::VERSION . ' (cURL/';
973
        $curl = \curl_version();
974
975
        if (isset($curl['version'])) {
976
            $user_agent .= $curl['version'];
977
        } else {
978
            $user_agent .= '?.?.?';
979
        }
980
981
        $user_agent .= ' PHP/'. PHP_VERSION . ' (' . PHP_OS . ')';
982
983
        if (isset($_SERVER['SERVER_SOFTWARE'])) {
984
            $user_agent .= ' ' . \preg_replace('~PHP/[\d\.]+~U', '',
985
                $_SERVER['SERVER_SOFTWARE']);
986
        } else {
987
            if (isset($_SERVER['TERM_PROGRAM'])) {
988
                $user_agent .= " {$_SERVER['TERM_PROGRAM']}";
989
            }
990
991
            if (isset($_SERVER['TERM_PROGRAM_VERSION'])) {
992
                $user_agent .= "/{$_SERVER['TERM_PROGRAM_VERSION']}";
993
            }
994
        }
995
996
        if (isset($_SERVER['HTTP_USER_AGENT'])) {
997
            $user_agent .= " {$_SERVER['HTTP_USER_AGENT']}";
998
        }
999
1000
        $user_agent .= ')';
1001
1002
        return $user_agent;
1003
    }
1004
1005
    /**
1006
     * Semi-reluctantly added this as a way to add in curl opts
1007
     * that are not otherwise accessible from the rest of the API.
1008
     * @return Request $this
1009
     * @param string $curlopt
1010
     * @param mixed $curloptval
1011
     */
1012
    public function addOnCurlOption($curlopt, $curloptval)
1013
    {
1014
        $this->additional_curl_opts[$curlopt] = $curloptval;
1015
        return $this;
1016
    }
1017
1018
    /**
1019
     * Turn payload from structured data into
1020
     * a string based on the current Mime type.
1021
     * This uses the auto_serialize option to determine
1022
     * it's course of action.  See serialize method for more.
1023
     * Renamed from _detectPayload to _serializePayload as of
1024
     * 2012-02-15.
1025
     *
1026
     * Added in support for custom payload serializers.
1027
     * The serialize_payload_method stuff still holds true though.
1028
     * @see Request::registerPayloadSerializer()
1029
     *
1030
     * @return string
1031
     * @param mixed $payload
1032
     */
1033
    private function _serializePayload($payload)
1034
    {
1035
        if (empty($payload) || $this->serialize_payload_method === self::SERIALIZE_PAYLOAD_NEVER)
1036
            return $payload;
1037
1038
        // When we are in "smart" mode, don't serialize strings/scalars, assume they are already serialized
1039
        if ($this->serialize_payload_method === self::SERIALIZE_PAYLOAD_SMART && is_scalar($payload))
1040
            return $payload;
1041
1042
        // Use a custom serializer if one is registered for this mime type
1043
        if (isset($this->payload_serializers['*']) || isset($this->payload_serializers[$this->content_type])) {
1044
            $key = isset($this->payload_serializers[$this->content_type]) ? $this->content_type : '*';
1045
            return call_user_func($this->payload_serializers[$key], $payload);
1046
        }
1047
1048
        return Httpful::get($this->content_type)->serialize($payload);
1049
    }
1050
1051
    /**
1052
     * HTTP Method Get
1053
     * @return Request
1054
     * @param string $uri optional uri to use
1055
     * @param string $mime expected
1056
     */
1057
    public static function get($uri, $mime = null)
1058
    {
1059
        return self::init(Http::GET)->uri($uri)->mime($mime);
1060
    }
1061
1062
1063
    /**
1064
     * Like Request:::get, except that it sends off the request as well
1065
     * returning a response
1066
     * @return Response
1067
     * @param string $uri optional uri to use
1068
     * @param string $mime expected
1069
     */
1070
    public static function getQuick($uri, $mime = null)
1071
    {
1072
        return self::get($uri, $mime)->send();
1073
    }
1074
1075
    /**
1076
     * HTTP Method Post
1077
     * @return Request
1078
     * @param string $uri optional uri to use
1079
     * @param string $payload data to send in body of request
1080
     * @param string $mime MIME to use for Content-Type
1081
     */
1082
    public static function post($uri, $payload = null, $mime = null)
1083
    {
1084
        return self::init(Http::POST)->uri($uri)->body($payload, $mime);
1085
    }
1086
1087
    /**
1088
     * HTTP Method Put
1089
     * @return Request
1090
     * @param string $uri optional uri to use
1091
     * @param string $payload data to send in body of request
1092
     * @param string $mime MIME to use for Content-Type
1093
     */
1094
    public static function put($uri, $payload = null, $mime = null)
1095
    {
1096
        return self::init(Http::PUT)->uri($uri)->body($payload, $mime);
1097
    }
1098
1099
    /**
1100
     * HTTP Method Patch
1101
     * @return Request
1102
     * @param string $uri optional uri to use
1103
     * @param string $payload data to send in body of request
1104
     * @param string $mime MIME to use for Content-Type
1105
     */
1106
    public static function patch($uri, $payload = null, $mime = null)
1107
    {
1108
        return self::init(Http::PATCH)->uri($uri)->body($payload, $mime);
1109
    }
1110
1111
    /**
1112
     * HTTP Method Delete
1113
     * @return Request
1114
     * @param string $uri optional uri to use
1115
     */
1116
    public static function delete($uri, $mime = null)
1117
    {
1118
        return self::init(Http::DELETE)->uri($uri)->mime($mime);
1119
    }
1120
1121
    /**
1122
     * HTTP Method Head
1123
     * @return Request
1124
     * @param string $uri optional uri to use
1125
     */
1126
    public static function head($uri)
1127
    {
1128
        return self::init(Http::HEAD)->uri($uri);
1129
    }
1130
1131
    /**
1132
     * HTTP Method Options
1133
     * @return Request
1134
     * @param string $uri optional uri to use
1135
     */
1136
    public static function options($uri)
1137
    {
1138
        return self::init(Http::OPTIONS)->uri($uri);
1139
    }
1140
}
1141