Completed
Push — master ( f0c41e...5d228f )
by James
03:53
created

Autodiscover::discover()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 22
rs 8.6737
cc 5
eloc 11
nc 16
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
222
     *   If left blank, the email provided will be used.
223
     */
224
    public function __construct($email, $password, $username = null)
225
    {
226
        $this->email = $email;
227
        $this->password = $password;
228
        if ($username === null) {
229
            $this->username = $email;
230
        } else {
231
            $this->username = $username;
232
        }
233
234
        $this->setTLD();
235
    }
236
237
    /**
238
     * Execute the full discovery chain of events in the correct sequence
239
     * until a valid response is received, or all methods have failed.
240
     *
241
     * @return integer
242
     *   One of the AUTODISCOVERED_VIA_* constants.
243
     *
244
     * @throws \RuntimeException
245
     *   When all autodiscovery methods fail.
246
     */
247
    public function discover()
248
    {
249
        $result = $this->tryTLD();
250
251
        if ($result === false) {
252
            $result = $this->trySubdomain();
253
        }
254
255
        if ($result === false) {
256
            $result = $this->trySubdomainUnauthenticatedGet();
257
        }
258
259
        if ($result === false) {
260
            $result = $this->trySRVRecord();
261
        }
262
263
        if ($result === false) {
264
            throw new \RuntimeException('Autodiscovery failed.');
265
        }
266
267
        return $result;
268
    }
269
270
    /**
271
     * Return the settings discovered from the Autodiscover process.
272
     *
273
     * NULL indicates discovery has not completed (or been attempted)
274
     * FALSE indicates discovery was not successful. Check for errors
275
     *  or redirects.
276
     * An array will be returned with discovered settings on success.
277
     *
278
     * @return mixed
279
     */
280
    public function discoveredSettings()
281
    {
282
        return $this->discovered;
283
    }
284
285
    /**
286
     * Toggle skipping of SSL verification in cURL requests.
287
     *
288
     * @param boolean $skip
289
     *   Whether or not to skip SSL certificate verification.
290
     * @return self
291
     */
292
    public function skipSSLVerification($skip = true)
293
    {
294
        $this->skip_ssl_verification = (bool) $skip;
295
296
        return $this;
297
    }
298
299
    /**
300
     * Parse the hex ServerVersion value and return a valid
301
     * Client::VERSION_* constant.
302
     *
303
     * @return string|boolean A known version constant, or FALSE if it could not
304
     * be determined.
305
     *
306
     * @link http://msdn.microsoft.com/en-us/library/bb204122(v=exchg.140).aspx
307
     * @link http://blogs.msdn.com/b/pcreehan/archive/2009/09/21/parsing-serverversion-when-an-int-is-really-5-ints.aspx
308
     * @link http://office.microsoft.com/en-us/outlook-help/determine-the-version-of-microsoft-exchange-server-my-account-connects-to-HA001191800.aspx
309
     *
310
     * @param string $version_hex
311
     *   Hexadecimal version string.
312
     */
313
    public function parseServerVersion($version_hex)
314
    {
315
        $svbinary = base_convert($version_hex, 16, 2);
316
        if (strlen($svbinary) == 31) {
317
            $svbinary = '0' . $svbinary;
318
        }
319
320
        $majorversion = base_convert(substr($svbinary, 4, 6), 2, 10);
321
        $minorversion = base_convert(substr($svbinary, 10, 6), 2, 10);
322
        $majorbuild = base_convert(substr($svbinary, 17, 15), 2, 10);
323
324
        switch ($majorversion) {
325
            case 8:
326
                return $this->parseVersion2007($minorversion);
327
            case 14:
328
                return $this->parseVersion2010($minorversion);
329
            case 15:
330
                if ($minorversion == 0) {
331
                    return $this->parseVersion2013($majorbuild);
332
                }
333
334
                return $this->parseVersion2016();
335
        }
336
337
        // Guess we didn't find a known version.
338
        return false;
339
    }
340
341
    /**
342
     * Method to return a new Client object, auto-configured
343
     * with the proper hostname.
344
     *
345
     * @return mixed Client object on success, FALSE on failure.
346
     */
347
    public function newEWS()
348
    {
349
        // Discovery not yet attempted.
350
        if ($this->discovered === null) {
351
            $this->discover();
352
        }
353
354
        // Discovery not successful.
355
        if ($this->discovered === false) {
356
            return false;
357
        }
358
359
        $server = false;
360
        $version = null;
361
362
        // Pick out the host from the EXPR (Exchange RPC over HTTP).
363
        foreach ($this->discovered['Account']['Protocol'] as $protocol) {
364
            if (
365
                ($protocol['Type'] == 'EXCH' || $protocol['Type'] == 'EXPR')
366
                && isset($protocol['ServerVersion'])
367
            ) {
368
                if ($version === null) {
369
                    $sv = $this->parseServerVersion($protocol['ServerVersion']);
370
                    if ($sv !== false) {
371
                        $version = $sv;
372
                    }
373
                }
374
            }
375
376
            if ($protocol['Type'] == 'EXPR' && isset($protocol['Server'])) {
377
                $server = $protocol['Server'];
378
            }
379
        }
380
381
        if ($server) {
382
            if ($version === null) {
383
                // EWS class default.
384
                $version = Client::VERSION_2007;
385
            }
386
            return new Client(
387
                $server,
388
                (!empty($this->username) ? $this->username : $this->email),
389
                $this->password,
390
                $version
391
            );
392
        }
393
394
        return false;
395
    }
396
397
    /**
398
     * Static method may fail if there are issues surrounding SSL certificates.
399
     * In such cases, set up the object as needed, and then call newEWS().
400
     *
401
     * @param string $email
402
     * @param string $password
403
     * @param string $username
404
     *   If left blank, the email provided will be used.
405
     * @return mixed
406
     */
407
    public static function getEWS($email, $password, $username = null)
408
    {
409
        $auto = new Autodiscover($email, $password, $username);
410
        return $auto->newEWS();
411
    }
412
413
    /**
414
     * Perform an NTLM authenticated HTTPS POST to the top-level
415
     * domain of the email address.
416
     *
417
     * @return integer|boolean
418
     *   One of the AUTODISCOVERED_VIA_* constants or false on failure.
419
     */
420
    public function tryTLD()
421
    {
422
        $url = 'https://' . $this->tld . self::AUTODISCOVER_PATH;
423
        return ($this->tryViaUrl($url) ? self::AUTODISCOVERED_VIA_TLD : false);
424
    }
425
426
    /**
427
     * Perform an NTLM authenticated HTTPS POST to the 'autodiscover'
428
     * subdomain of the email address' TLD.
429
     *
430
     * @return integer|boolean
431
     *   One of the AUTODISCOVERED_VIA_* constants or false on failure.
432
     */
433
    public function trySubdomain()
434
    {
435
        $url = 'https://autodiscover.' . $this->tld . self::AUTODISCOVER_PATH;
436
        return ($this->tryViaUrl($url)
437
            ? self::AUTODISCOVERED_VIA_SUBDOMAIN
438
            : false);
439
    }
440
441
    /**
442
     * Perform an unauthenticated HTTP GET in an attempt to get redirected
443
     * via 302 to the correct location to perform the HTTPS POST.
444
     *
445
     * @return integer|boolean
446
     *   One of the AUTODISCOVERED_VIA_* constants or false on failure.
447
     */
448
    public function trySubdomainUnauthenticatedGet()
449
    {
450
        $this->reset();
451
        $url = 'http://autodiscover.' . $this->tld . self::AUTODISCOVER_PATH;
452
        $ch = curl_init();
453
        $opts = array(
454
            CURLOPT_URL                 => $url,
455
            CURLOPT_HTTPGET             => true,
456
            CURLOPT_RETURNTRANSFER      => true,
457
            CURLOPT_TIMEOUT             => 4,
458
            CURLOPT_CONNECTTIMEOUT      => $this->connection_timeout,
459
            CURLOPT_FOLLOWLOCATION      => false,
460
            CURLOPT_HEADER              => false,
461
            CURLOPT_HEADERFUNCTION      => array($this, 'readHeaders'),
462
            CURLOPT_HTTP200ALIASES      => array(301, 302),
463
            CURLOPT_IPRESOLVE           => CURL_IPRESOLVE_V4
464
        );
465
        curl_setopt_array($ch, $opts);
466
        $this->last_response    = curl_exec($ch);
467
        $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...
468
        $this->last_curl_errno  = curl_errno($ch);
469
        $this->last_curl_error  = curl_error($ch);
470
471
        if (
472
            $this->last_info['http_code'] == 302
473
            || $this->last_info['http_code'] == 301
474
        ) {
475
            if ($this->tryViaUrl($this->last_response_headers['location'])) {
476
                return self::AUTODISCOVERED_VIA_UNAUTHENTICATED_GET;
477
            }
478
        }
479
480
        return false;
481
    }
482
483
    /**
484
     * Attempt to retrieve the autodiscover host from an SRV DNS record.
485
     *
486
     * @link http://support.microsoft.com/kb/940881
487
     *
488
     * @return integer|boolean
489
     *   The value of self::AUTODISCOVERED_VIA_SRV_RECORD or false.
490
     */
491
    public function trySRVRecord()
492
    {
493
        $srvhost = '_autodiscover._tcp.' . $this->tld;
494
        $lookup = dns_get_record($srvhost, DNS_SRV);
495
        if (sizeof($lookup) > 0) {
496
            $host = $lookup[0]['target'];
497
            $url = 'https://' . $host . self::AUTODISCOVER_PATH;
498
            if ($this->tryViaUrl($url)) {
499
                return self::AUTODISCOVERED_VIA_SRV_RECORD;
500
            }
501
        }
502
503
        return false;
504
    }
505
506
    /**
507
     * Set the path to the file to be used by CURLOPT_CAINFO.
508
     *
509
     * @param string $path
510
     *   Path to a certificate file such as cacert.pem
511
     * @return self
512
     */
513
    public function setCAInfo($path)
514
    {
515
        if (file_exists($path) && is_file($path)) {
516
            $this->cainfo = $path;
517
        }
518
519
        return $this;
520
    }
521
522
    /**
523
     * Set the path to the file to be used by CURLOPT_CAPATH.
524
     *
525
     * @param string $path
526
     *   Path to a directory containing one or more CA certificates.
527
     * @return self
528
     */
529
    public function setCAPath($path)
530
    {
531
        if (is_dir($path)) {
532
            $this->capath = $path;
533
        }
534
535
        return $this;
536
    }
537
538
    /**
539
     * Set a connection timeout for the POST methods.
540
     *
541
     * @param integer $seconds
542
     *   Seconds to wait for a connection.
543
     * @return self
544
     */
545
    public function setConnectionTimeout($seconds)
546
    {
547
        $this->connection_timeout = intval($seconds);
548
549
        return $this;
550
    }
551
552
    /**
553
     * Perform the NTLM authenticated post against one of the chosen
554
     * endpoints.
555
     *
556
     * @param string $url
557
     *   URL to try posting to.
558
     * @param integer $timeout
559
     *   Number of seconds before the request should timeout.
560
     * @return boolean
561
     */
562
    public function doNTLMPost($url, $timeout = 6)
563
    {
564
        $this->reset();
565
566
        $ch = curl_init();
567
        $opts = array(
568
            CURLOPT_URL             => $url,
569
            CURLOPT_HTTPAUTH        => CURLAUTH_BASIC | CURLAUTH_NTLM,
570
            CURLOPT_CUSTOMREQUEST   => 'POST',
571
            CURLOPT_POSTFIELDS      => $this->getAutoDiscoverRequest(),
572
            CURLOPT_RETURNTRANSFER  => true,
573
            CURLOPT_USERPWD         => $this->username . ':' . $this->password,
574
            CURLOPT_TIMEOUT         => $timeout,
575
            CURLOPT_CONNECTTIMEOUT  => $this->connection_timeout,
576
            CURLOPT_FOLLOWLOCATION  => true,
577
            CURLOPT_HEADER          => false,
578
            CURLOPT_HEADERFUNCTION  => array($this, 'readHeaders'),
579
            CURLOPT_IPRESOLVE       => CURL_IPRESOLVE_V4,
580
            CURLOPT_SSL_VERIFYPEER  => true,
581
            CURLOPT_SSL_VERIFYHOST  => 2,
582
        );
583
584
        // Set the appropriate content-type.
585
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml; charset=utf-8'));
586
587
        if (!empty($this->cainfo)) {
588
            $opts[CURLOPT_CAINFO] = $this->cainfo;
589
        }
590
591
        if (!empty($this->capath)) {
592
            $opts[CURLOPT_CAPATH] = $this->capath;
593
        }
594
595
        if ($this->skip_ssl_verification) {
596
            $opts[CURLOPT_SSL_VERIFYPEER] = false;
597
        }
598
599
        curl_setopt_array($ch, $opts);
600
        $this->last_response    = curl_exec($ch);
601
        $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...
602
        $this->last_curl_errno  = curl_errno($ch);
603
        $this->last_curl_error  = curl_error($ch);
604
605
        if ($this->last_curl_errno != CURLE_OK) {
606
            return false;
607
        }
608
609
        $discovered = $this->parseAutodiscoverResponse();
610
611
        return $discovered;
612
    }
613
614
    /**
615
     * Parse the Autoresponse Payload, particularly to determine if an
616
     * additional request is necessary.
617
     *
618
     * @return boolean|array FALSE if response isn't XML or parsed response
619
     *   array.
620
     */
621
    protected function parseAutodiscoverResponse()
622
    {
623
        // Content-type isn't trustworthy, unfortunately. Shame on Microsoft.
624
        if (substr($this->last_response, 0, 5) !== '<?xml') {
625
            return false;
626
        }
627
628
        $response = $this->responseToArray($this->last_response);
629
630
        if (isset($response['Error'])) {
631
            $this->error = $response['Error'];
632
            return false;
633
        }
634
635
        // Check the account action for redirect.
636
        switch ($response['Account']['Action']) {
637
            case 'redirectUrl':
638
                $this->redirect = array(
639
                    'redirectUrl' => $response['Account']['redirectUrl']
640
                );
641
                return false;
642
            case 'redirectAddr':
643
                $this->redirect = array(
644
                    'redirectAddr' => $response['Account']['redirectAddr']
645
                );
646
                return false;
647
            case 'settings':
648
            default:
649
                $this->discovered = $response;
650
                return true;
651
        }
652
    }
653
654
    /**
655
     * Set the top-level domain to be used with autodiscover attempts based
656
     * on the provided email address.
657
     *
658
     * @return boolean
659
     */
660
    protected function setTLD()
661
    {
662
        $pos = strpos($this->email, '@');
663
        if ($pos !== false) {
664
            $this->tld = trim(substr($this->email, $pos + 1));
665
            return true;
666
        }
667
668
        return false;
669
    }
670
671
    /**
672
     * Reset the response-related structures. Called before making a new
673
     * request.
674
     *
675
     * @return self
676
     */
677
    public function reset()
678
    {
679
        $this->last_response_headers = array();
680
        $this->last_info = array();
681
        $this->last_curl_errno = 0;
682
        $this->last_curl_error = '';
683
684
        return $this;
685
    }
686
687
    /**
688
     * Return the generated Autodiscover XML request body.
689
     *
690
     * @return string
691
     */
692
    public function getAutodiscoverRequest()
693
    {
694
        if (!empty($this->requestxml)) {
695
            return $this->requestxml;
696
        }
697
698
        $xml = new \XMLWriter();
699
        $xml->openMemory();
700
        $xml->setIndent(true);
701
        $xml->startDocument('1.0', 'UTF-8');
702
        $xml->startElementNS(
703
            null,
704
            'Autodiscover',
705
            'http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006'
706
        );
707
708
        $xml->startElement('Request');
709
        $xml->writeElement('EMailAddress', $this->email);
710
        $xml->writeElement(
711
            'AcceptableResponseSchema',
712
            'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'
713
        );
714
        $xml->endElement();
715
        $xml->endElement();
716
717
        $this->requestxml = $xml->outputMemory();
718
        return $this->requestxml;
719
    }
720
721
    /**
722
     * Utility function to pick headers off of the incoming cURL response.
723
     * Used with CURLOPT_HEADERFUNCTION.
724
     *
725
     * @param resource $_ch
726
     *   cURL handle.
727
     * @param string $str
728
     *   Header string to read.
729
     * @return integer
730
     *   Bytes read.
731
     */
732
    public function readHeaders($_ch, $str)
733
    {
734
        $pos = strpos($str, ':');
735
        if ($pos !== false) {
736
            $key = strtolower(substr($str, 0, $pos));
737
            $val = trim(substr($str, $pos + 1));
738
            $this->last_response_headers[$key] = $val;
739
        }
740
741
        return strlen($str);
742
    }
743
744
    /**
745
     * Utility function to parse XML payloads from the response into easier
746
     * to manage associative arrays.
747
     *
748
     * @param string $xml
749
     *   XML to parse.
750
     * @return array
751
     */
752
    public function responseToArray($xml)
753
    {
754
        $doc = new \DOMDocument();
755
        $doc->loadXML($xml);
756
        $out = $this->nodeToArray($doc->documentElement);
757
758
        return $out['Response'];
759
    }
760
761
    /**
762
     * Recursive method for parsing DOM nodes.
763
     *
764
     * @param \DOMElement $node
765
     *   DOMNode object.
766
     * @return mixed
767
     *
768
     * @link https://github.com/gaarf/XML-string-to-PHP-array
769
     */
770
    protected function nodeToArray($node)
771
    {
772
        $output = array();
773
        switch ($node->nodeType) {
774
            case XML_CDATA_SECTION_NODE:
775
            case XML_TEXT_NODE:
776
                $output = trim($node->textContent);
777
                break;
778
            case XML_ELEMENT_NODE:
779
                for ($i = 0, $m = $node->childNodes->length; $i < $m; $i++) {
780
                    $child = $node->childNodes->item($i);
781
                    $v = $this->nodeToArray($child);
782
                    if (isset($child->tagName)) {
783
                        $t = $child->tagName;
784
                        if (!isset($output[$t])) {
785
                            $output[$t] = array();
786
                        }
787
                        $output[$t][] = $v;
788
                    } elseif ($v || $v === '0') {
789
                        $output = (string) $v;
790
                    }
791
                }
792
793
                // Edge case of a node containing a text node, which also has
794
                // attributes. this way we'll retain text and attributes for
795
                // this node.
796
                if (is_string($output) && $node->attributes->length) {
797
                    $output = array('@text' => $output);
798
                }
799
800
                if (is_array($output)) {
801
                    if ($node->attributes->length) {
802
                        $a = array();
803
                        foreach ($node->attributes as $attrName => $attrNode) {
804
                            $a[$attrName] = (string) $attrNode->value;
805
                        }
806
                        $output['@attributes'] = $a;
807
                    }
808
                    foreach ($output as $t => $v) {
809
                        if (is_array($v) && count($v) == 1 && $t != '@attributes') {
810
                            $output[$t] = $v[0];
811
                        }
812
                    }
813
                }
814
                break;
815
        }
816
817
        return $output;
818
    }
819
820
    /**
821
     * Parses the version of an Exchange 2007 server.
822
     *
823
     * @param integer $minorversion
824
     *   Minor server version.
825
     * @return string Server version.
826
     */
827
    protected function parseVersion2007($minorversion) {
828
        switch ($minorversion) {
829
            case 0:
830
                return Client::VERSION_2007;
831
            case 1:
832
            case 2:
833
            case 3:
834
                return Client::VERSION_2007_SP1;
835
            default:
836
                return Client::VERSION_2007;
837
        }
838
    }
839
840
    /**
841
     * Parses the version of an Exchange 2010 server.
842
     *
843
     * @param integer $minorversion
844
     *   Minor server version.
845
     * @return string Server version.
846
     */
847
    protected function parseVersion2010($minorversion) {
848
        switch ($minorversion) {
849
            case 0:
850
                return Client::VERSION_2010;
851
            case 1:
852
                return Client::VERSION_2010_SP1;
853
            case 2:
854
                return Client::VERSION_2010_SP2;
855
            default:
856
                return Client::VERSION_2010;
857
        }
858
    }
859
860
    /**
861
     * Parses the version of an Exchange 2013 server.
862
     *
863
     * @param integer $majorbuild
864
     *   Major build version.
865
     * @return string Server version.
866
     */
867
    protected function parseVersion2013($majorbuild) {
868
        return ($majorbuild == 847
869
            ? Client::VERSION_2013_SP1
870
            : Client::VERSION_2013);
871
    }
872
873
    /**
874
     * Parses the version of an Exchange 2016 server.
875
     *
876
     * @return string Server version.
877
     */
878
    protected function parseVersion2016() {
879
        return Client::VERSION_2016;
880
    }
881
882
    /**
883
     * Attempts an autodiscover via a URL.
884
     *
885
     * @param string $url
886
     *   Url to attempt an autodiscover.
887
     * @param integer $timeout
888
     *    Number of seconds before the request should timeout.
889
     * @return boolean
890
     */
891
    protected function tryViaUrl($url, $timeout = 6)
892
    {
893
        $result = $this->doNTLMPost($url, $timeout);
894
        return ($result ? true : false);
895
    }
896
}
897