Completed
Pull Request — master (#560)
by
unknown
01:39
created

Autodiscover::readHeaders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
c 0
b 0
f 0
rs 9.9
cc 2
nc 2
nop 2
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
 * @todo This class is quite large; it should be refactored into smaller
36
 * classes.
37
 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
38
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
39
 * @SuppressWarnings(PHPMD.NPathComplexity)
40
 */
41
class Autodiscover
42
{
43
    /**
44
     * The path appended to the various schemes and hostnames used during
45
     * autodiscovery.
46
     *
47
     * @var string
48
     */
49
    const AUTODISCOVER_PATH = '/autodiscover/autodiscover.xml';
50
51
    /**
52
     * Server was discovered using the TLD method.
53
     *
54
     * @var integer
55
     */
56
    const AUTODISCOVERED_VIA_TLD = 10;
57
58
    /**
59
     * Server was discovered using the subdomain method.
60
     *
61
     * @var integer
62
     */
63
    const AUTODISCOVERED_VIA_SUBDOMAIN = 11;
64
65
    /**
66
     * Server was discovered using the unauthenticated GET method.
67
     *
68
     * @var integer
69
     */
70
    const AUTODISCOVERED_VIA_UNAUTHENTICATED_GET = 12;
71
72
    /**
73
     * Server was discovered using the DNS SRV redirect method.
74
     *
75
     * @var integer
76
     */
77
    const AUTODISCOVERED_VIA_SRV_RECORD = 13;
78
79
    /**
80
     * Server was discovered using the HTTP redirect method.
81
     *
82
     * @var integer
83
     *
84
     * @todo We do not currently support this.
85
     */
86
    const AUTODISCOVERED_VIA_RESPONSE_REDIRECT = 14;
87
88
    /**
89
     * The email address to attempt autodiscovery against.
90
     *
91
     * @var string
92
     */
93
    protected $email;
94
95
    /**
96
     * The password to present during autodiscovery.
97
     *
98
     * @var string
99
     */
100
    protected $password;
101
102
    /**
103
     * The Exchange username to use during authentication. If unspecified,
104
     * the provided email address will be used as the username.
105
     *
106
     * @var string
107
     */
108
    protected $username;
109
110
    /**
111
     * The top-level domain name, extracted from the provided email address.
112
     *
113
     * @var string
114
     */
115
    protected $tld;
116
117
    /**
118
     * The Autodiscover XML request. Since it's used repeatedly, it's cached
119
     * in this property to avoid redundant re-generation.
120
     *
121
     * @var string
122
     */
123
    protected $requestxml;
124
125
    /**
126
     * The Certificate Authority path. Should point to a directory containing
127
     * one or more certificates to use in SSL verification.
128
     *
129
     * @var string
130
     */
131
    protected $capath;
132
133
    /**
134
     * The path to a specific Certificate Authority file. Get one and use it
135
     * for full Autodiscovery compliance.
136
     *
137
     * @var string
138
     *
139
     * @link http://curl.haxx.se/ca/cacert.pem
140
     * @link http://curl.haxx.se/ca/
141
     */
142
    protected $cainfo;
143
144
    /**
145
     * Skip SSL verification. Bad idea, and violates the strict Autodiscover
146
     * protocol. But, here in case you have no other option.
147
     * Defaults to FALSE.
148
     *
149
     * @var boolean
150
     */
151
    protected $skip_ssl_verification = false;
152
153
    /**
154
     * The body of the last response.
155
     *
156
     * @var string
157
     */
158
    public $last_response;
159
160
    /**
161
     * An associative array of response headers that resulted from the
162
     * last request. Keys are lowercased for easy checking.
163
     *
164
     * @var array
165
     */
166
    public $last_response_headers;
167
168
    /**
169
     * The output of curl_info() relating to the most recent cURL request.
170
     *
171
     * @var array
172
     */
173
    public $last_info;
174
175
    /**
176
     * The cURL error code associated with the most recent cURL request.
177
     *
178
     * @var integer
179
     */
180
    public $last_curl_errno;
181
182
    /**
183
     * Human-readable description of the most recent cURL error.
184
     *
185
     * @var string
186
     */
187
    public $last_curl_error;
188
189
    /**
190
     * The value in seconds to use for Autodiscover host connection timeouts.
191
     * Default connection timeout is 2 seconds, so that unresponsive methods
192
     * can be bypassed quickly.
193
     *
194
     * @var integer
195
     */
196
    public $connection_timeout = 2;
197
198
    /**
199
     * Information about an Autodiscover Response containing an error will
200
     * be stored here.
201
     *
202
     * @var mixed
203
     */
204
    public $error = false;
205
206
    /**
207
     * Information about an Autodiscover Response with a redirect will be
208
     * retained here.
209
     *
210
     * @var mixed
211
     */
212
    public $redirect = false;
213
214
    /**
215
     * A successful, non-error and non-redirect parsed Autodiscover response
216
     * will be stored here.
217
     *
218
     * @var mixed
219
     */
220
    public $discovered = null;
221
222
    /**
223
     * Constructor for the EWSAutodiscover class.
224
     *
225
     * @param string $email
226
     * @param string $password
227
     * @param string $username
228
     *   If left blank, the email provided will be used.
229
     */
230
    public function __construct($email, $password, $username = null)
231
    {
232
        $this->email = $email;
233
        $this->password = $password;
234
235
        if ($username === null) {
236
            $username = $email;
237
        }
238
        $this->username = $username;
239
240
        $this->setTLD();
241
    }
242
243
    /**
244
     * Execute the full discovery chain of events in the correct sequence
245
     * until a valid response is received, or all methods have failed.
246
     *
247
     * @return integer
248
     *   One of the AUTODISCOVERED_VIA_* constants.
249
     *
250
     * @throws \RuntimeException
251
     *   When all autodiscovery methods fail.
252
     */
253
    public function discover()
254
    {
255
        $result = false;
256
        $actions = array(
257
            'tryTLD',
258
            'trySubdomain',
259
            'trySubdomainUnauthenticatedGet',
260
            'trySRVRecord'
261
        );
262
        $action = 0;
263
        $bailout = 10;
264
        $redirectCount = 0;
265
        $maxRedirects = 5;
266
267
        while (!$result && ($action < count($actions)) && $bailout--) {
268
            if (is_array($this->redirect) && $redirectCount < $maxRedirects) {
269
                $redirectCount++;
270
                if ($this->email != $this->redirect['redirectAddr']) {
271
                    $action = 0;
272
                    $this->email = $this->redirect['redirectAddr'];
273
                    $this->redirect = false;
274
                    $this->setTLD();
275
                }
276
                continue;
277
            }
278
            $result = $this->$actions[$action]();
279
            $action++;
280
        }
281
282
        if ($result === false) {
283
            throw new \RuntimeException('Autodiscovery failed.');
284
        }
285
286
        return $result;
287
    }
288
289
    /**
290
     * Return the settings discovered from the Autodiscover process.
291
     *
292
     * NULL indicates discovery has not completed (or been attempted)
293
     * FALSE indicates discovery was not successful. Check for errors
294
     *  or redirects.
295
     * An array will be returned with discovered settings on success.
296
     *
297
     * @return mixed
298
     */
299
    public function discoveredSettings()
300
    {
301
        return $this->discovered;
302
    }
303
304
    /**
305
     * Toggle skipping of SSL verification in cURL requests.
306
     *
307
     * @param boolean $skip
308
     *   Whether or not to skip SSL certificate verification.
309
     * @return self
310
     *
311
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
312
     */
313
    public function skipSSLVerification($skip = true)
314
    {
315
        $this->skip_ssl_verification = (bool) $skip;
316
317
        return $this;
318
    }
319
320
    /**
321
     * Parse the hex ServerVersion value and return a valid
322
     * Client::VERSION_* constant.
323
     *
324
     * @return string|boolean A known version constant, or FALSE if it could not
325
     * be determined.
326
     *
327
     * @link http://msdn.microsoft.com/en-us/library/bb204122(v=exchg.140).aspx
328
     * @link http://blogs.msdn.com/b/pcreehan/archive/2009/09/21/parsing-serverversion-when-an-int-is-really-5-ints.aspx
329
     * @link http://office.microsoft.com/en-us/outlook-help/determine-the-version-of-microsoft-exchange-server-my-account-connects-to-HA001191800.aspx
330
     *
331
     * @param string $version_hex
332
     *   Hexadecimal version string.
333
     */
334
    public function parseServerVersion($version_hex)
335
    {
336
        $svbinary = base_convert($version_hex, 16, 2);
337
        if (strlen($svbinary) == 31) {
338
            $svbinary = '0' . $svbinary;
339
        }
340
341
        $majorversion = (int) base_convert(substr($svbinary, 4, 6), 2, 10);
342
        $minorversion = (int) base_convert(substr($svbinary, 10, 6), 2, 10);
343
        $majorbuild = (int) base_convert(substr($svbinary, 17, 15), 2, 10);
344
345
        switch ($majorversion) {
346
            case 8:
347
                return $this->parseVersion2007($minorversion);
348
            case 14:
349
                return $this->parseVersion2010($minorversion);
350
            case 15:
351
                if ($minorversion == 0) {
352
                    return $this->parseVersion2013($majorbuild);
353
                }
354
355
                return $this->parseVersion2016();
356
        }
357
358
        // Guess we didn't find a known version.
359
        return false;
360
    }
361
362
    /**
363
     * Method to return a new Client object, auto-configured
364
     * with the proper hostname.
365
     *
366
     * @return mixed Client object on success, FALSE on failure.
367
     */
368
    public function newEWS()
369
    {
370
        // Discovery not yet attempted.
371
        if ($this->discovered === null) {
372
            $this->discover();
373
        }
374
375
        // Discovery not successful.
376
        if ($this->discovered === false) {
377
            return false;
378
        }
379
380
        $server = false;
381
        $version = null;
382
383
        // Pick out the host from the EXPR (Exchange RPC over HTTP).
384
        foreach ($this->discovered['Account']['Protocol'] as $protocol) {
385
            if (($protocol['Type'] == 'EXCH' || $protocol['Type'] == 'EXPR')
386
                && isset($protocol['ServerVersion'])) {
387
                if ($version === null) {
388
                    $sv = $this->parseServerVersion($protocol['ServerVersion']);
389
                    if ($sv !== false) {
390
                        $version = $sv;
391
                    }
392
                }
393
            }
394
395
            if ($protocol['Type'] == 'EXPR' && isset($protocol['Server'])) {
396
                $server = $protocol['Server'];
397
            }
398
        }
399
400
        if ($server) {
401
            if ($version === null) {
402
                // EWS class default.
403
                $version = Client::VERSION_2007;
404
            }
405
            return new Client(
406
                $server,
407
                (!empty($this->username) ? $this->username : $this->email),
408
                $this->password,
409
                $version
410
            );
411
        }
412
413
        return false;
414
    }
415
416
    /**
417
     * Static method may fail if there are issues surrounding SSL certificates.
418
     * In such cases, set up the object as needed, and then call newEWS().
419
     *
420
     * @param string $email
421
     * @param string $password
422
     * @param string $username
423
     *   If left blank, the email provided will be used.
424
     * @return mixed
425
     */
426
    public static function getEWS($email, $password, $username = null)
427
    {
428
        $auto = new Autodiscover($email, $password, $username);
429
        return $auto->newEWS();
430
    }
431
432
    /**
433
     * Perform an NTLM authenticated HTTPS POST to the top-level
434
     * domain of the email address.
435
     *
436
     * @return integer|boolean
437
     *   One of the AUTODISCOVERED_VIA_* constants or false on failure.
438
     */
439
    public function tryTLD()
440
    {
441
        $url = 'https://' . $this->tld . self::AUTODISCOVER_PATH;
442
        return ($this->tryViaUrl($url) ? self::AUTODISCOVERED_VIA_TLD : false);
443
    }
444
445
    /**
446
     * Perform an NTLM authenticated HTTPS POST to the 'autodiscover'
447
     * subdomain of the email address' TLD.
448
     *
449
     * @return integer|boolean
450
     *   One of the AUTODISCOVERED_VIA_* constants or false on failure.
451
     */
452
    public function trySubdomain()
453
    {
454
        $url = 'https://autodiscover.' . $this->tld . self::AUTODISCOVER_PATH;
455
        return ($this->tryViaUrl($url)
456
            ? self::AUTODISCOVERED_VIA_SUBDOMAIN
457
            : false);
458
    }
459
460
    /**
461
     * Perform an unauthenticated HTTP GET in an attempt to get redirected
462
     * via 302 to the correct location to perform the HTTPS POST.
463
     *
464
     * @return integer|boolean
465
     *   One of the AUTODISCOVERED_VIA_* constants or false on failure.
466
     */
467
    public function trySubdomainUnauthenticatedGet()
468
    {
469
        $this->reset();
470
        $url = 'http://autodiscover.' . $this->tld . self::AUTODISCOVER_PATH;
471
        $ch = curl_init();
472
        $opts = array(
473
            CURLOPT_URL                 => $url,
474
            CURLOPT_HTTPGET             => true,
475
            CURLOPT_RETURNTRANSFER      => true,
476
            CURLOPT_TIMEOUT             => 4,
477
            CURLOPT_CONNECTTIMEOUT      => $this->connection_timeout,
478
            CURLOPT_FOLLOWLOCATION      => false,
479
            CURLOPT_HEADER              => false,
480
            CURLOPT_HEADERFUNCTION      => array($this, 'readHeaders'),
481
            CURLOPT_HTTP200ALIASES      => array(301, 302),
482
            CURLOPT_IPRESOLVE           => CURL_IPRESOLVE_V4
483
        );
484
        curl_setopt_array($ch, $opts);
485
        $this->last_response    = curl_exec($ch);
486
        $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...
487
        $this->last_curl_errno  = curl_errno($ch);
488
        $this->last_curl_error  = curl_error($ch);
489
490
        if ($this->last_info['http_code'] == 302
491
            || $this->last_info['http_code'] == 301) {
492
            if ($this->tryViaUrl($this->last_response_headers['location'])) {
493
                return self::AUTODISCOVERED_VIA_UNAUTHENTICATED_GET;
494
            }
495
        }
496
497
        return false;
498
    }
499
500
    /**
501
     * Attempt to retrieve the autodiscover host from an SRV DNS record.
502
     *
503
     * @link http://support.microsoft.com/kb/940881
504
     *
505
     * @return integer|boolean
506
     *   The value of self::AUTODISCOVERED_VIA_SRV_RECORD or false.
507
     */
508
    public function trySRVRecord()
509
    {
510
        $srvhost = '_autodiscover._tcp.' . $this->tld;
511
        $lookup = dns_get_record($srvhost, DNS_SRV);
512
        if (sizeof($lookup) > 0) {
513
            $host = $lookup[0]['target'];
514
            $url = 'https://' . $host . self::AUTODISCOVER_PATH;
515
            if ($this->tryViaUrl($url)) {
516
                return self::AUTODISCOVERED_VIA_SRV_RECORD;
517
            }
518
        }
519
520
        return false;
521
    }
522
523
    /**
524
     * Set the path to the file to be used by CURLOPT_CAINFO.
525
     *
526
     * @param string $path
527
     *   Path to a certificate file such as cacert.pem
528
     * @return self
529
     */
530
    public function setCAInfo($path)
531
    {
532
        if (file_exists($path) && is_file($path)) {
533
            $this->cainfo = $path;
534
        }
535
536
        return $this;
537
    }
538
539
    /**
540
     * Set the path to the file to be used by CURLOPT_CAPATH.
541
     *
542
     * @param string $path
543
     *   Path to a directory containing one or more CA certificates.
544
     * @return self
545
     */
546
    public function setCAPath($path)
547
    {
548
        if (is_dir($path)) {
549
            $this->capath = $path;
550
        }
551
552
        return $this;
553
    }
554
555
    /**
556
     * Set a connection timeout for the POST methods.
557
     *
558
     * @param integer $seconds
559
     *   Seconds to wait for a connection.
560
     * @return self
561
     */
562
    public function setConnectionTimeout($seconds)
563
    {
564
        $this->connection_timeout = intval($seconds);
565
566
        return $this;
567
    }
568
569
    /**
570
     * Perform the NTLM authenticated post against one of the chosen
571
     * endpoints.
572
     *
573
     * @param string $url
574
     *   URL to try posting to.
575
     * @param integer $timeout
576
     *   Number of seconds before the request should timeout.
577
     * @return boolean
578
     */
579
    public function doNTLMPost($url, $timeout = 6)
580
    {
581
        $this->reset();
582
583
        $ch = curl_init();
584
        $opts = array(
585
            CURLOPT_URL             => $url,
586
            CURLOPT_HTTPAUTH        => CURLAUTH_BASIC | CURLAUTH_NTLM,
587
            CURLOPT_CUSTOMREQUEST   => 'POST',
588
            CURLOPT_POSTFIELDS      => $this->getAutoDiscoverRequest(),
589
            CURLOPT_RETURNTRANSFER  => true,
590
            CURLOPT_USERPWD         => $this->username . ':' . $this->password,
591
            CURLOPT_TIMEOUT         => $timeout,
592
            CURLOPT_CONNECTTIMEOUT  => $this->connection_timeout,
593
            CURLOPT_FOLLOWLOCATION  => true,
594
            CURLOPT_HEADER          => false,
595
            CURLOPT_HEADERFUNCTION  => array($this, 'readHeaders'),
596
            CURLOPT_IPRESOLVE       => CURL_IPRESOLVE_V4,
597
            CURLOPT_SSL_VERIFYPEER  => true,
598
            CURLOPT_SSL_VERIFYHOST  => 2,
599
        );
600
601
        // Set the appropriate content-type.
602
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml; charset=utf-8'));
603
604
        if (!empty($this->cainfo)) {
605
            $opts[CURLOPT_CAINFO] = $this->cainfo;
606
        }
607
608
        if (!empty($this->capath)) {
609
            $opts[CURLOPT_CAPATH] = $this->capath;
610
        }
611
612
        if ($this->skip_ssl_verification) {
613
            $opts[CURLOPT_SSL_VERIFYPEER] = false;
614
        }
615
616
        curl_setopt_array($ch, $opts);
617
        $this->last_response    = curl_exec($ch);
618
        $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...
619
        $this->last_curl_errno  = curl_errno($ch);
620
        $this->last_curl_error  = curl_error($ch);
621
622
        if ($this->last_curl_errno != CURLE_OK) {
623
            return false;
624
        }
625
626
        $discovered = $this->parseAutodiscoverResponse();
627
628
        return $discovered;
629
    }
630
631
    /**
632
     * Parse the Autoresponse Payload, particularly to determine if an
633
     * additional request is necessary.
634
     *
635
     * @return boolean|array FALSE if response isn't XML or parsed response
636
     *   array.
637
     */
638
    protected function parseAutodiscoverResponse()
639
    {
640
        // Content-type isn't trustworthy, unfortunately. Shame on Microsoft.
641
        if (substr($this->last_response, 0, 5) !== '<?xml') {
642
            return false;
643
        }
644
645
        $response = $this->responseToArray($this->last_response);
646
647
        if (isset($response['Error'])) {
648
            $this->error = $response['Error'];
649
            return false;
650
        }
651
652
        // Check the account action for redirect.
653
        switch ($response['Account']['Action']) {
654
            case 'redirectUrl':
655
                $this->redirect = array(
656
                    'redirectUrl' => $response['Account']['RedirectUrl']
657
                );
658
                return false;
659
            case 'redirectAddr':
660
                $this->redirect = array(
661
                    'redirectAddr' => $response['Account']['RedirectAddr']
662
                );
663
                return false;
664
            case 'settings':
665
            default:
666
                $this->discovered = $response;
667
                return true;
668
        }
669
    }
670
671
    /**
672
     * Set the top-level domain to be used with autodiscover attempts based
673
     * on the provided email address.
674
     *
675
     * @return boolean
676
     */
677
    protected function setTLD()
678
    {
679
        $pos = strpos($this->email, '@');
680
        if ($pos !== false) {
681
            $this->tld = trim(substr($this->email, $pos + 1));
682
            return true;
683
        }
684
685
        return false;
686
    }
687
688
    /**
689
     * Reset the response-related structures. Called before making a new
690
     * request.
691
     *
692
     * @return self
693
     */
694
    public function reset()
695
    {
696
        $this->last_response_headers = array();
697
        $this->last_info = array();
698
        $this->last_curl_errno = 0;
699
        $this->last_curl_error = '';
700
701
        return $this;
702
    }
703
704
    /**
705
     * Return the generated Autodiscover XML request body.
706
     *
707
     * @return string
708
     *
709
     * @suppress PhanTypeMismatchArgumentInternal
710
     */
711
    public function getAutodiscoverRequest()
712
    {
713
        if (!empty($this->requestxml)) {
714
            return $this->requestxml;
715
        }
716
717
        $xml = new \XMLWriter();
718
        $xml->openMemory();
719
        $xml->setIndent(true);
720
        $xml->startDocument('1.0', 'UTF-8');
721
        $xml->startElementNS(
722
            null,
723
            'Autodiscover',
724
            'http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006'
725
        );
726
727
        $xml->startElement('Request');
728
        $xml->writeElement('EMailAddress', $this->email);
729
        $xml->writeElement(
730
            'AcceptableResponseSchema',
731
            'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'
732
        );
733
        $xml->endElement();
734
        $xml->endElement();
735
736
        $this->requestxml = $xml->outputMemory();
737
        return $this->requestxml;
738
    }
739
740
    /**
741
     * Utility function to pick headers off of the incoming cURL response.
742
     * Used with CURLOPT_HEADERFUNCTION.
743
     *
744
     * @param resource $_ch
745
     *   cURL handle.
746
     * @param string $str
747
     *   Header string to read.
748
     * @return integer
749
     *   Bytes read.
750
     *
751
     * @todo Determine if we can remove $_ch here.
752
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
753
     */
754
    public function readHeaders($_ch, $str)
755
    {
756
        $pos = strpos($str, ':');
757
        if ($pos !== false) {
758
            $key = strtolower(substr($str, 0, $pos));
759
            $val = trim(substr($str, $pos + 1));
760
            $this->last_response_headers[$key] = $val;
761
        }
762
763
        return strlen($str);
764
    }
765
766
    /**
767
     * Utility function to parse XML payloads from the response into easier
768
     * to manage associative arrays.
769
     *
770
     * @param string $xml
771
     *   XML to parse.
772
     * @return array
773
     */
774
    public function responseToArray($xml)
775
    {
776
        $doc = new \DOMDocument();
777
        $doc->loadXML($xml);
778
        $out = $this->nodeToArray($doc->documentElement);
779
780
        return $out['Response'];
781
    }
782
783
    /**
784
     * Recursive method for parsing DOM nodes.
785
     *
786
     * @param \DOMElement $node
787
     *   DOMNode object.
788
     * @return mixed
789
     *
790
     * @link https://github.com/gaarf/XML-string-to-PHP-array
791
     *
792
     * @suppress PhanTypeMismatchArgument, PhanUndeclaredProperty
793
     */
794
    protected function nodeToArray($node)
795
    {
796
        $output = array();
797
        switch ($node->nodeType) {
798
            case XML_CDATA_SECTION_NODE:
799
            case XML_TEXT_NODE:
800
                $output = trim($node->textContent);
801
                break;
802
            case XML_ELEMENT_NODE:
803
                for ($i = 0, $m = $node->childNodes->length; $i < $m; $i++) {
804
                    $child = $node->childNodes->item($i);
805
                    $value = $this->nodeToArray($child);
806
                    if (isset($child->tagName)) {
807
                        $tag = $child->tagName;
808
                        if (!isset($output[$tag])) {
809
                            $output[$tag] = array();
810
                        }
811
                        $output[$tag][] = $value;
812
                    } elseif ($value || $value === '0') {
813
                        $output = (string) $value;
814
                    }
815
                }
816
817
                // Edge case of a node containing a text node, which also has
818
                // attributes. this way we'll retain text and attributes for
819
                // this node.
820
                if (is_string($output) && $node->attributes->length) {
821
                    $output = array('@text' => $output);
822
                }
823
824
                if (is_array($output)) {
825
                    if ($node->attributes->length) {
826
                        $attributes = array();
827
                        foreach ($node->attributes as $attrName => $attrNode) {
828
                            $attributes[$attrName] = (string) $attrNode->value;
829
                        }
830
                        $output['@attributes'] = $attributes;
831
                    }
832
                    foreach ($output as $tag => $value) {
833
                        if (is_array($value) && count($value) == 1 && $tag != '@attributes') {
834
                            $output[$tag] = $value[0];
835
                        }
836
                    }
837
                }
838
                break;
839
        }
840
841
        return $output;
842
    }
843
844
    /**
845
     * Parses the version of an Exchange 2007 server.
846
     *
847
     * @param integer $minorversion
848
     *   Minor server version.
849
     * @return string Server version.
850
     */
851 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...
852
    {
853
        switch ($minorversion) {
854
            case 0:
855
                return Client::VERSION_2007;
856
            case 1:
857
            case 2:
858
            case 3:
859
                return Client::VERSION_2007_SP1;
860
            default:
861
                return Client::VERSION_2007;
862
        }
863
    }
864
865
    /**
866
     * Parses the version of an Exchange 2010 server.
867
     *
868
     * @param integer $minorversion
869
     *   Minor server version.
870
     * @return string Server version.
871
     */
872 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...
873
    {
874
        switch ($minorversion) {
875
            case 0:
876
                return Client::VERSION_2010;
877
            case 1:
878
                return Client::VERSION_2010_SP1;
879
            case 2:
880
                return Client::VERSION_2010_SP2;
881
            default:
882
                return Client::VERSION_2010;
883
        }
884
    }
885
886
    /**
887
     * Parses the version of an Exchange 2013 server.
888
     *
889
     * @param integer $majorbuild
890
     *   Major build version.
891
     * @return string Server version.
892
     */
893
    protected function parseVersion2013($majorbuild)
894
    {
895
        return ($majorbuild == 847
896
            ? Client::VERSION_2013_SP1
897
            : Client::VERSION_2013);
898
    }
899
900
    /**
901
     * Parses the version of an Exchange 2016 server.
902
     *
903
     * @return string Server version.
904
     */
905
    protected function parseVersion2016()
906
    {
907
        return Client::VERSION_2016;
908
    }
909
910
    /**
911
     * Attempts an autodiscover via a URL.
912
     *
913
     * @param string $url
914
     *   Url to attempt an autodiscover.
915
     * @param integer $timeout
916
     *    Number of seconds before the request should timeout.
917
     * @return boolean
918
     */
919
    protected function tryViaUrl($url, $timeout = 6)
920
    {
921
        $result = $this->doNTLMPost($url, $timeout);
922
        return ($result ? true : false);
923
    }
924
}
925