Completed
Pull Request — master (#560)
by
unknown
06:51
created

Autodiscover::getAutodiscoverRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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