Completed
Push — master ( 2c4e30...665a4f )
by James
03:07
created

Autodiscover::trySubdomain()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 2
eloc 5
nc 2
nop 0
1
<?php
2
/**
3
 * Contains \jamesiarmes\PhpEws\Autodiscover.
4
 */
5
6
namespace jamesiarmes\PhpEws;
7
8
/**
9
 * Exchange Web Services Autodiscover implementation
10
 *
11
 * This class supports POX (Plain Old XML), which is deprecated but functional
12
 * in Exchange 2010. It may make sense for you to combine your Autodiscovery
13
 * efforts with a SOAP Autodiscover request as well.
14
 *
15
 * USAGE:
16
 *
17
 * (after any auto-loading class incantation)
18
 *
19
 * $ews = EWSAutodiscover::getEWS($email, $password);
20
 *
21
 * -- OR --
22
 *
23
 * If there are issues with your cURL installation that require you to specify
24
 * a path to a valid Certificate Authority, you can configure that manually.
25
 *
26
 * $auto = new EWSAutodiscover($email, $password);
27
 * $auto->setCAInfo('/path/to/your/cacert.pem');
28
 * $ews = $auto->newEWS();
29
 *
30
 * @link http://technet.microsoft.com/en-us/library/bb332063(EXCHG.80).aspx
31
 * @link https://www.testexchangeconnectivity.com/
32
 *
33
 * @package php-ews\AutoDiscovery
34
 */
35
class Autodiscover
36
{
37
    /**
38
     * The path appended to the various schemes and hostnames used during
39
     * autodiscovery.
40
     *
41
     * @var string
42
     */
43
    const AUTODISCOVER_PATH = '/autodiscover/autodiscover.xml';
44
45
    /**
46
     * Server was discovered using the TLD method.
47
     *
48
     * @var integer
49
     */
50
    const AUTODISCOVERED_VIA_TLD = 10;
51
52
    /**
53
     * Server was discovered using the subdomain method.
54
     *
55
     * @var integer
56
     */
57
    const AUTODISCOVERED_VIA_SUBDOMAIN = 11;
58
59
    /**
60
     * Server was discovered using the unauthenticated GET method.
61
     *
62
     * @var integer
63
     */
64
    const AUTODISCOVERED_VIA_UNAUTHENTICATED_GET = 12;
65
66
    /**
67
     * Server was discovered using the DNS SRV redirect method.
68
     *
69
     * @var integer
70
     */
71
    const AUTODISCOVERED_VIA_SRV_RECORD = 13;
72
73
    /**
74
     * Server was discovered using the HTTP redirect method.
75
     *
76
     * @var integer
77
     *
78
     * @todo We do not currently support this.
79
     */
80
    const AUTODISCOVERED_VIA_RESPONSE_REDIRECT = 14;
81
82
    /**
83
     * The email address to attempt autodiscovery against.
84
     *
85
     * @var string
86
     */
87
    protected $email;
88
89
    /**
90
     * The password to present during autodiscovery.
91
     *
92
     * @var string
93
     */
94
    protected $password;
95
96
    /**
97
     * The Exchange username to use during authentication. If unspecified,
98
     * the provided email address will be used as the username.
99
     *
100
     * @var string
101
     */
102
    protected $username;
103
104
    /**
105
     * The top-level domain name, extracted from the provided email address.
106
     *
107
     * @var string
108
     */
109
    protected $tld;
110
111
    /**
112
     * The Autodiscover XML request. Since it's used repeatedly, it's cached
113
     * in this property to avoid redundant re-generation.
114
     *
115
     * @var string
116
     */
117
    protected $requestxml;
118
119
    /**
120
     * The Certificate Authority path. Should point to a directory containing
121
     * one or more certificates to use in SSL verification.
122
     *
123
     * @var string
124
     */
125
    protected $capath;
126
127
    /**
128
     * The path to a specific Certificate Authority file. Get one and use it
129
     * for full Autodiscovery compliance.
130
     *
131
     * @var string
132
     *
133
     * @link http://curl.haxx.se/ca/cacert.pem
134
     * @link http://curl.haxx.se/ca/
135
     */
136
    protected $cainfo;
137
138
    /**
139
     * Skip SSL verification. Bad idea, and violates the strict Autodiscover
140
     * protocol. But, here in case you have no other option.
141
     * Defaults to FALSE.
142
     *
143
     * @var boolean
144
     */
145
    protected $skip_ssl_verification = false;
146
147
    /**
148
     * The body of the last response.
149
     *
150
     * @var string
151
     */
152
    public $last_response;
153
154
    /**
155
     * An associative array of response headers that resulted from the
156
     * last request. Keys are lowercased for easy checking.
157
     *
158
     * @var array
159
     */
160
    public $last_response_headers;
161
162
    /**
163
     * The output of curl_info() relating to the most recent cURL request.
164
     *
165
     * @var array
166
     */
167
    public $last_info;
168
169
    /**
170
     * The cURL error code associated with the most recent cURL request.
171
     *
172
     * @var integer
173
     */
174
    public $last_curl_errno;
175
176
    /**
177
     * Human-readable description of the most recent cURL error.
178
     *
179
     * @var string
180
     */
181
    public $last_curl_error;
182
183
    /**
184
     * The value in seconds to use for Autodiscover host connection timeouts.
185
     * Default connection timeout is 2 seconds, so that unresponsive methods
186
     * can be bypassed quickly.
187
     *
188
     * @var integer
189
     */
190
    public $connection_timeout = 2;
191
192
    /**
193
     * Information about an Autodiscover Response containing an error will
194
     * be stored here.
195
     *
196
     * @var mixed
197
     */
198
    public $error = false;
199
200
    /**
201
     * Information about an Autodiscover Response with a redirect will be
202
     * retained here.
203
     *
204
     * @var mixed
205
     */
206
    public $redirect = false;
207
208
    /**
209
     * A successful, non-error and non-redirect parsed Autodiscover response
210
     * will be stored here.
211
     *
212
     * @var mixed
213
     */
214
    public $discovered = null;
215
216
    /**
217
     * Constructor for the EWSAutodiscover class.
218
     *
219
     * @param string $email
220
     * @param string $password
221
     * @param string $username If left blank, the email provided will be used.
222
     */
223
    public function __construct($email, $password, $username = null)
224
    {
225
        $this->email = $email;
226
        $this->password = $password;
227
        if ($username === null) {
228
            $this->username = $email;
229
        } else {
230
            $this->username = $username;
231
        }
232
233
        $this->setTLD();
234
    }
235
236
    /**
237
     * Execute the full discovery chain of events in the correct sequence
238
     * until a valid response is received, or all methods have failed.
239
     *
240
     * @return An AUTODISCOVERED_VIA_* constant or FALSE on failure.
241
     *
242
     * @todo Throw an exception on failure.
243
     */
244
    public function discover()
245
    {
246
        $result = $this->tryTLD();
247
248
        if ($result === false) {
249
            $result = $this->trySubdomain();
250
        }
251
252
        if ($result === false) {
253
            $result = $this->trySubdomainUnauthenticatedGet();
254
        }
255
256
        if ($result === false) {
257
            $result = $this->trySRVRecord();
258
        }
259
260
        return $result;
261
    }
262
263
    /**
264
     * Return the settings discovered from the Autodiscover process.
265
     *
266
     * NULL indicates discovery hasn't completed (or been attempted)
267
     * FALSE indicates discovery wasn't successful. Check for errors
268
     *  or redirects.
269
     * An array will be returned with discovered settings on success.
270
     *
271
     * @return mixed
272
     */
273
    public function discoveredSettings()
274
    {
275
        return $this->discovered;
276
    }
277
278
    /**
279
     * Toggle skipping of SSL verification in cURL requests.
280
     *
281
     * @param boolean $skip To skip, or not.
282
     * @return self
283
     */
284
    public function skipSSLVerification($skip = true)
285
    {
286
        $this->skip_ssl_verification = (bool) $skip;
287
288
        return $this;
289
    }
290
291
    /**
292
     * Parse the hex ServerVersion value and return a valid
293
     * Client::VERSION_* constant.
294
     *
295
     * @return string|boolean A known version constant, or FALSE if it could not
296
     * be determined.
297
     *
298
     * @link http://msdn.microsoft.com/en-us/library/bb204122(v=exchg.140).aspx
299
     * @link http://blogs.msdn.com/b/pcreehan/archive/2009/09/21/parsing-serverversion-when-an-int-is-really-5-ints.aspx
300
     * @link http://office.microsoft.com/en-us/outlook-help/determine-the-version-of-microsoft-exchange-server-my-account-connects-to-HA001191800.aspx
301
     *
302
     * @todo Update to include Exchange 2013 versions.
303
     */
304
    public function parseServerVersion($version_hex)
305
    {
306
        $svbinary = base_convert($version_hex, 16, 2);
307
        if (strlen($svbinary) == 31) {
308
            $svbinary = '0' . $svbinary;
309
        }
310
311
        $majorversion = base_convert(substr($svbinary, 4, 6), 2, 10);
312
        $minorversion = base_convert(substr($svbinary, 10, 6), 2, 10);
313
        $majorbuild = base_convert(substr($svbinary, 17, 15), 2, 10);
314
315
        switch ($majorversion) {
316
            case 8:
317
                return $this->parseVersion2007($minorversion);
318
            case 14:
319
                return $this->parseVersion2010($minorversion);
320
            case 15;
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
321
                if ($minorversion == 0) {
322
                    return $this->parseVersion2013($minorversion, $majorbuild);
0 ignored issues
show
Unused Code introduced by
The call to Autodiscover::parseVersion2013() has too many arguments starting with $majorbuild.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
323
                }
324
325
                return $this->parseVersion2016($minorversion, $majorbuild);
0 ignored issues
show
Unused Code introduced by
The call to Autodiscover::parseVersion2016() has too many arguments starting with $majorbuild.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
326
        }
327
328
        // Guess we didn't find a known version.
329
        return false;
330
    }
331
332
    /**
333
     * Method to return a new Client object, auto-configured
334
     * with the proper hostname.
335
     *
336
     * @return mixed Client object on success, FALSE on failure.
337
     */
338
    public function newEWS()
339
    {
340
        // Discovery not yet attempted.
341
        if ($this->discovered === null) {
342
            $this->discover();
343
        }
344
345
        // Discovery not successful.
346
        if ($this->discovered === false) {
347
            return false;
348
        }
349
350
        $server = false;
351
        $version = null;
352
353
        // Pick out the host from the EXPR (Exchange RPC over HTTP).
354
        foreach ($this->discovered['Account']['Protocol'] as $protocol) {
355
            if (
356
                ($protocol['Type'] == 'EXCH' || $protocol['Type'] == 'EXPR')
357
                && isset($protocol['ServerVersion'])
358
            ) {
359
                if ($version == null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $version of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
360
                    $sv = $this->parseServerVersion($protocol['ServerVersion']);
361
                    if ($sv !== false) {
362
                        $version = $sv;
363
                    }
364
                }
365
            }
366
367
            if ($protocol['Type'] == 'EXPR' && isset($protocol['Server'])) {
368
                $server = $protocol['Server'];
369
            }
370
        }
371
372
        if ($server) {
373
            if ($version === null) {
374
                // EWS class default.
375
                $version = Client::VERSION_2007;
376
            }
377
            return new Client(
378
                $server,
379
                (!empty($this->username) ? $this->username : $this->email),
380
                $this->password,
381
                $version
382
            );
383
        }
384
385
        return false;
386
    }
387
388
    /**
389
     * Static method may fail if there are issues surrounding SSL certificates.
390
     * In such cases, set up the object as needed, and then call newEWS().
391
     *
392
     * @param string $email
393
     * @param string $password
394
     * @param string $username If left blank, the email provided will be used.
395
     * @return mixed
396
     */
397
    public static function getEWS($email, $password, $username = null)
398
    {
399
        $auto = new Autodiscover($email, $password, $username);
400
        return $auto->newEWS();
401
    }
402
403
    /**
404
     * Perform an NTLM authenticated HTTPS POST to the top-level
405
     * domain of the email address.
406
     *
407
     * @return An AUTODISCOVERED_VIA_* constant or FALSE on failure.
408
     */
409
    public function tryTLD()
410
    {
411
        $url = 'https://' . $this->tld . self::AUTODISCOVER_PATH;
412
        return ($this->tryViaUrl($url) ? self::AUTODISCOVERED_VIA_TLD : false);
413
    }
414
415
    /**
416
     * Perform an NTLM authenticated HTTPS POST to the 'autodiscover'
417
     * subdomain of the email address' TLD.
418
     *
419
     * @return An AUTODISCOVERED_VIA_* constant or FALSE on failure.
420
     */
421
    public function trySubdomain()
422
    {
423
        $url = 'https://autodiscover.' . $this->tld . self::AUTODISCOVER_PATH;
424
        return ($this->tryViaUrl($url)
425
            ? self::AUTODISCOVERED_VIA_SUBDOMAIN
426
            : false);
427
    }
428
429
    /**
430
     * Perform an unauthenticated HTTP GET in an attempt to get redirected
431
     * via 302 to the correct location to perform the HTTPS POST.
432
     *
433
     * @return An AUTODISCOVERED_VIA_* constant or FALSE on failure.
434
     */
435
    public function trySubdomainUnauthenticatedGet()
436
    {
437
        $this->reset();
438
        $url = 'http://autodiscover.' . $this->tld . self::AUTODISCOVER_PATH;
439
        $ch = curl_init();
440
        $opts = array(
441
            CURLOPT_URL                 => $url,
442
            CURLOPT_HTTPGET             => true,
443
            CURLOPT_RETURNTRANSFER      => true,
444
            CURLOPT_TIMEOUT             => 4,
445
            CURLOPT_CONNECTTIMEOUT      => $this->connection_timeout,
446
            CURLOPT_FOLLOWLOCATION      => false,
447
            CURLOPT_HEADER              => false,
448
            CURLOPT_HEADERFUNCTION      => array($this, 'readHeaders'),
449
            CURLOPT_HTTP200ALIASES      => array(301, 302),
450
            CURLOPT_IPRESOLVE           => CURL_IPRESOLVE_V4
451
        );
452
        curl_setopt_array($ch, $opts);
453
        $this->last_response    = curl_exec($ch);
454
        $this->last_info        = curl_getinfo($ch);
0 ignored issues
show
Documentation Bug introduced by
It seems like curl_getinfo($ch) of type * is incompatible with the declared type array of property $last_info.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
455
        $this->last_curl_errno  = curl_errno($ch);
456
        $this->last_curl_error  = curl_error($ch);
457
458
        if (
459
            $this->last_info['http_code'] == 302
460
            || $this->last_info['http_code'] == 301
461
        ) {
462
            if ($this->tryViaUrl($this->last_response_headers['location'])) {
463
                return self::AUTODISCOVERED_VIA_UNAUTHENTICATED_GET;
464
            }
465
        }
466
467
        return false;
468
    }
469
470
    /**
471
     * Attempt to retrieve the autodiscover host from an SRV DNS record.
472
     *
473
     * @link http://support.microsoft.com/kb/940881
474
     * @return self::AUTODISCOVERED_VIA_SRV_RECORD or false
0 ignored issues
show
Documentation introduced by
The doc-type self::AUTODISCOVERED_VIA_SRV_RECORD could not be parsed: Unknown type name "self::AUTODISCOVERED_VIA_SRV_RECORD" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
475
     */
476
    public function trySRVRecord()
477
    {
478
        $srvhost = '_autodiscover._tcp.' . $this->tld;
479
        $lookup = dns_get_record($srvhost, DNS_SRV);
480
        if (sizeof($lookup) > 0) {
481
            $host = $lookup[0]['target'];
482
            $url = 'https://' . $host . self::AUTODISCOVER_PATH;
483
            if ($this->tryViaUrl($url)) {
484
                return self::AUTODISCOVERED_VIA_SRV_RECORD;
485
            }
486
        }
487
488
        return false;
489
    }
490
491
    /**
492
     * Set the path to the file to be used by CURLOPT_CAINFO.
493
     *
494
     * @param string $path Path to a certificate file such as cacert.pem
495
     * @return self
496
     */
497
    public function setCAInfo($path)
498
    {
499
        if (file_exists($path) && is_file($path)) {
500
            $this->cainfo = $path;
501
        }
502
503
        return $this;
504
    }
505
506
    /**
507
     * Set the path to the file to be used by CURLOPT_CAPATH.
508
     *
509
     * @param string $path Path to a directory containing one or more CA
510
     * certificates.
511
     * @return self
512
     */
513
    public function setCAPath($path)
514
    {
515
        if (is_dir($path)) {
516
            $this->capath = $path;
517
        }
518
519
        return $this;
520
    }
521
522
    /**
523
     * Set a connection timeout for the POST methods.
524
     *
525
     * @param integer $seconds Seconds to wait for a connection.
526
     * @return self
527
     */
528
    public function setConnectionTimeout($seconds)
529
    {
530
        $this->connection_timeout = intval($seconds);
531
532
        return $this;
533
    }
534
535
    /**
536
     * Perform the NTLM authenticated post against one of the chosen
537
     * endpoints.
538
     *
539
     * @param string $url URL to try posting to
540
     * @param integer $timeout Overall cURL timeout for this request
541
     * @return boolean
542
     */
543
    public function doNTLMPost($url, $timeout = 6)
544
    {
545
        $this->reset();
546
547
        $ch = curl_init();
548
        $opts = array(
549
            CURLOPT_URL             => $url,
550
            CURLOPT_HTTPAUTH        => CURLAUTH_BASIC | CURLAUTH_NTLM,
551
            CURLOPT_CUSTOMREQUEST   => 'POST',
552
            CURLOPT_POSTFIELDS      => $this->getAutoDiscoverRequest(),
553
            CURLOPT_RETURNTRANSFER  => true,
554
            CURLOPT_USERPWD         => $this->username . ':' . $this->password,
555
            CURLOPT_TIMEOUT         => $timeout,
556
            CURLOPT_CONNECTTIMEOUT  => $this->connection_timeout,
557
            CURLOPT_FOLLOWLOCATION  => true,
558
            CURLOPT_HEADER          => false,
559
            CURLOPT_HEADERFUNCTION  => array($this, 'readHeaders'),
560
            CURLOPT_IPRESOLVE       => CURL_IPRESOLVE_V4,
561
            CURLOPT_SSL_VERIFYPEER  => true,
562
            CURLOPT_SSL_VERIFYHOST  => 2,
563
        );
564
565
        // Set the appropriate content-type.
566
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml; charset=utf-8'));
567
568
        if (!empty($this->cainfo)) {
569
            $opts[CURLOPT_CAINFO] = $this->cainfo;
570
        }
571
572
        if (!empty($this->capath)) {
573
            $opts[CURLOPT_CAPATH] = $this->capath;
574
        }
575
576
        if ($this->skip_ssl_verification) {
577
            $opts[CURLOPT_SSL_VERIFYPEER] = false;
578
        }
579
580
        curl_setopt_array($ch, $opts);
581
        $this->last_response    = curl_exec($ch);
582
        $this->last_info        = curl_getinfo($ch);
0 ignored issues
show
Documentation Bug introduced by
It seems like curl_getinfo($ch) of type * is incompatible with the declared type array of property $last_info.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
583
        $this->last_curl_errno  = curl_errno($ch);
584
        $this->last_curl_error  = curl_error($ch);
585
586
        if ($this->last_curl_errno != CURLE_OK) {
587
            return false;
588
        }
589
590
        $discovered = $this->parseAutodiscoverResponse();
591
592
        return $discovered;
593
    }
594
595
    /**
596
     * Parse the Autoresponse Payload, particularly to determine if an
597
     * additional request is necessary.
598
     *
599
     * @return boolean|array FALSE if response isn't XML or parsed response
600
     *   array.
601
     */
602
    protected function parseAutodiscoverResponse()
603
    {
604
        // Content-type isn't trustworthy, unfortunately. Shame on Microsoft.
605
        if (substr($this->last_response, 0, 5) !== '<?xml') {
606
            return false;
607
        }
608
609
        $response = $this->responseToArray($this->last_response);
610
611
        if (isset($response['Error'])) {
612
            $this->error = $response['Error'];
613
            return false;
614
        }
615
616
        // Check the account action for redirect.
617
        switch ($response['Account']['Action']) {
618
            case 'redirectUrl':
619
                $this->redirect = array(
620
                    'redirectUrl' => $response['Account']['redirectUrl']
621
                );
622
                return false;
623
            case 'redirectAddr':
624
                $this->redirect = array(
625
                    'redirectAddr' => $response['Account']['redirectAddr']
626
                );
627
                return false;
628
            case 'settings':
629
            default:
630
                $this->discovered = $response;
631
                return true;
632
        }
633
    }
634
635
    /**
636
     * Set the top-level domain to be used with autodiscover attempts based
637
     * on the provided email address.
638
     *
639
     * @return boolean
640
     */
641
    protected function setTLD()
642
    {
643
        $pos = strpos($this->email, '@');
644
        if ($pos !== false) {
645
            $this->tld = trim(substr($this->email, $pos + 1));
646
            return true;
647
        }
648
649
        return false;
650
    }
651
652
    /**
653
     * Reset the response-related structures. Called before making a new
654
     * request.
655
     *
656
     * @return self
657
     */
658
    public function reset()
659
    {
660
        $this->last_response_headers = array();
661
        $this->last_info = array();
662
        $this->last_curl_errno = 0;
663
        $this->last_curl_error = '';
664
665
        return $this;
666
    }
667
668
    /**
669
     * Return the generated Autodiscover XML request body.
670
     *
671
     * @return string
672
     */
673
    public function getAutodiscoverRequest()
674
    {
675
        if (!empty($this->requestxml)) {
676
            return $this->requestxml;
677
        }
678
679
        $xml = new \XMLWriter();
680
        $xml->openMemory();
681
        $xml->setIndent(true);
682
        $xml->startDocument('1.0', 'UTF-8');
683
        $xml->startElementNS(
684
            null,
685
            'Autodiscover',
686
            'http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006'
687
        );
688
689
        $xml->startElement('Request');
690
        $xml->writeElement('EMailAddress', $this->email);
691
        $xml->writeElement(
692
            'AcceptableResponseSchema',
693
            'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'
694
        );
695
        $xml->endElement();
696
        $xml->endElement();
697
698
        $this->requestxml = $xml->outputMemory();
699
        return $this->requestxml;
700
    }
701
702
    /**
703
     * Utility function to pick headers off of the incoming cURL response.
704
     * Used with CURLOPT_HEADERFUNCTION.
705
     *
706
     * @param resource $_ch cURL handle
707
     * @param string $str Header string to read
708
     * @return integer Bytes read
709
     */
710
    public function readHeaders($_ch, $str)
711
    {
712
        $pos = strpos($str, ':');
713
        if ($pos !== false) {
714
            $key = strtolower(substr($str, 0, $pos));
715
            $val = trim(substr($str, $pos + 1));
716
            $this->last_response_headers[$key] = $val;
717
        }
718
719
        return strlen($str);
720
    }
721
722
    /**
723
     * Utility function to parse XML payloads from the response into easier
724
     * to manage associative arrays.
725
     *
726
     * @param string $xml XML to parse
727
     * @return array
728
     */
729
    public function responseToArray($xml)
730
    {
731
        $doc = new \DOMDocument();
732
        $doc->loadXML($xml);
733
        $out = $this->nodeToArray($doc->documentElement);
734
735
        return $out['Response'];
736
    }
737
738
    /**
739
     * Recursive method for parsing DOM nodes.
740
     *
741
     * @param \DOMElement $node DOMNode object.
742
     * @return mixed
743
     *
744
     * @link https://github.com/gaarf/XML-string-to-PHP-array
745
     */
746
    protected function nodeToArray($node)
747
    {
748
        $output = array();
749
        switch ($node->nodeType) {
750
            case XML_CDATA_SECTION_NODE:
751
            case XML_TEXT_NODE:
752
                $output = trim($node->textContent);
753
                break;
754
            case XML_ELEMENT_NODE:
755
                for ($i = 0, $m = $node->childNodes->length; $i < $m; $i++) {
756
                    $child = $node->childNodes->item($i);
757
                    $v = $this->nodeToArray($child);
758
                    if (isset($child->tagName)) {
759
                        $t = $child->tagName;
760
                        if (!isset($output[$t])) {
761
                            $output[$t] = array();
762
                        }
763
                        $output[$t][] = $v;
764
                    } elseif ($v || $v === '0') {
765
                        $output = (string) $v;
766
                    }
767
                }
768
769
                // Edge case of a node containing a text node, which also has
770
                // attributes. this way we'll retain text and attributes for
771
                // this node.
772
                if (is_string($output) && $node->attributes->length) {
773
                    $output = array('@text' => $output);
774
                }
775
776
                if (is_array($output)) {
777
                    if ($node->attributes->length) {
778
                        $a = array();
779
                        foreach ($node->attributes as $attrName => $attrNode) {
780
                            $a[$attrName] = (string) $attrNode->value;
781
                        }
782
                        $output['@attributes'] = $a;
783
                    }
784
                    foreach ($output as $t => $v) {
785
                        if (is_array($v) && count($v) == 1 && $t != '@attributes') {
786
                            $output[$t] = $v[0];
787
                        }
788
                    }
789
                }
790
                break;
791
        }
792
793
        return $output;
794
    }
795
796
    /**
797
     * Parses the version of an Exchange 2007 server.
798
     *
799
     * @param integer $minorversion Minor server version.
800
     * @return string Server version.
801
     */
802 View Code Duplication
    protected function parseVersion2007($minorversion) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
803
        switch ($minorversion) {
804
            case 0:
805
                return Client::VERSION_2007;
806
            case 1:
807
                return Client::VERSION_2007_SP1;
808
            case 2:
809
                return Client::VERSION_2007_SP2;
810
            case 3:
811
                return Client::VERSION_2007_SP3;
812
            default:
813
                return Client::VERSION_2007;
814
        }
815
    }
816
817
    /**
818
     * Parses the version of an Exchange 2010 server.
819
     *
820
     * @param integer $minorversion Minor server version.
821
     * @return string Server version.
822
     */
823 View Code Duplication
    protected function parseVersion2010($minorversion) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
824
        switch ($minorversion) {
825
            case 0:
826
                return Client::VERSION_2010;
827
            case 1:
828
                return Client::VERSION_2010_SP1;
829
            case 2:
830
                return Client::VERSION_2010_SP2;
831
            default:
832
                return Client::VERSION_2010;
833
        }
834
    }
835
836
    /**
837
     * Parses the version of an Exchange 2013 server.
838
     *
839
     * @param integer $majorbuild Major build version.
840
     * @return string Server version.
841
     */
842
    protected function parseVersion2013($majorbuild) {
843
        return ($majorbuild == 847
844
            ? Client::VERSION_2013_SP1
845
            : Client::VERSION_2013);
846
    }
847
848
    /**
849
     * Parses the version of an Exchange 2016 server.
850
     *
851
     * @param integer $majorbuild Major build version.
852
     * @return string Server version.
853
     */
854
    protected function parseVersion2016($majorbuild) {
0 ignored issues
show
Unused Code introduced by
The parameter $majorbuild is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
855
        return Client::VERSION_2016;
856
    }
857
858
    /**
859
     * Attempts an autodiscover via a URL.
860
     *
861
     * @param string $url Url to attempt an autodiscover.
862
     * @return boolean
863
     */
864
    protected function tryViaUrl($url, $timeout = 6)
865
    {
866
        $result = $this->doNTLMPost($url, $timeout);
867
        return ($result ? true : false);
868
    }
869
}
870