Completed
Push — master ( bbbb6c...5d9efc )
by James
03:31
created

Autodiscover::tryViaUrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 2
eloc 3
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
class Autodiscover
36
{
37
    /**
38
     * The path appended to the various schemes and hostnames used during
39
     * autodiscovery.
40
     *
41
     * @var string
42
     */
43
    const AUTODISCOVER_PATH = '/autodiscover/autodiscover.xml';
44
45
    /**
46
     * Server was discovered using the TLD method.
47
     *
48
     * @var integer
49
     */
50
    const AUTODISCOVERED_VIA_TLD = 10;
51
52
    /**
53
     * Server was discovered using the subdomain method.
54
     *
55
     * @var integer
56
     */
57
    const AUTODISCOVERED_VIA_SUBDOMAIN = 11;
58
59
    /**
60
     * Server was discovered using the unauthenticated GET method.
61
     *
62
     * @var integer
63
     */
64
    const AUTODISCOVERED_VIA_UNAUTHENTICATED_GET = 12;
65
66
    /**
67
     * Server was discovered using the DNS SRV redirect method.
68
     *
69
     * @var integer
70
     */
71
    const AUTODISCOVERED_VIA_SRV_RECORD = 13;
72
73
    /**
74
     * Server was discovered using the HTTP redirect method.
75
     *
76
     * @var integer
77
     *
78
     * @todo We do not currently support this.
79
     */
80
    const AUTODISCOVERED_VIA_RESPONSE_REDIRECT = 14;
81
82
    /**
83
     * The email address to attempt autodiscovery against.
84
     *
85
     * @var string
86
     */
87
    protected $email;
88
89
    /**
90
     * The password to present during autodiscovery.
91
     *
92
     * @var string
93
     */
94
    protected $password;
95
96
    /**
97
     * The Exchange username to use during authentication. If unspecified,
98
     * the provided email address will be used as the username.
99
     *
100
     * @var string
101
     */
102
    protected $username;
103
104
    /**
105
     * The top-level domain name, extracted from the provided email address.
106
     *
107
     * @var string
108
     */
109
    protected $tld;
110
111
    /**
112
     * The Autodiscover XML request. Since it's used repeatedly, it's cached
113
     * in this property to avoid redundant re-generation.
114
     *
115
     * @var string
116
     */
117
    protected $requestxml;
118
119
    /**
120
     * The Certificate Authority path. Should point to a directory containing
121
     * one or more certificates to use in SSL verification.
122
     *
123
     * @var string
124
     */
125
    protected $capath;
126
127
    /**
128
     * The path to a specific Certificate Authority file. Get one and use it
129
     * for full Autodiscovery compliance.
130
     *
131
     * @var string
132
     *
133
     * @link http://curl.haxx.se/ca/cacert.pem
134
     * @link http://curl.haxx.se/ca/
135
     */
136
    protected $cainfo;
137
138
    /**
139
     * Skip SSL verification. Bad idea, and violates the strict Autodiscover
140
     * protocol. But, here in case you have no other option.
141
     * Defaults to FALSE.
142
     *
143
     * @var boolean
144
     */
145
    protected $skip_ssl_verification = false;
146
147
    /**
148
     * The body of the last response.
149
     *
150
     * @var string
151
     */
152
    public $last_response;
153
154
    /**
155
     * An associative array of response headers that resulted from the
156
     * last request. Keys are lowercased for easy checking.
157
     *
158
     * @var array
159
     */
160
    public $last_response_headers;
161
162
    /**
163
     * The output of curl_info() relating to the most recent cURL request.
164
     *
165
     * @var array
166
     */
167
    public $last_info;
168
169
    /**
170
     * The cURL error code associated with the most recent cURL request.
171
     *
172
     * @var integer
173
     */
174
    public $last_curl_errno;
175
176
    /**
177
     * Human-readable description of the most recent cURL error.
178
     *
179
     * @var string
180
     */
181
    public $last_curl_error;
182
183
    /**
184
     * The value in seconds to use for Autodiscover host connection timeouts.
185
     * Default connection timeout is 2 seconds, so that unresponsive methods
186
     * can be bypassed quickly.
187
     *
188
     * @var integer
189
     */
190
    public $connection_timeout = 2;
191
192
    /**
193
     * Information about an Autodiscover Response containing an error will
194
     * be stored here.
195
     *
196
     * @var mixed
197
     */
198
    public $error = false;
199
200
    /**
201
     * Information about an Autodiscover Response with a redirect will be
202
     * retained here.
203
     *
204
     * @var mixed
205
     */
206
    public $redirect = false;
207
208
    /**
209
     * A successful, non-error and non-redirect parsed Autodiscover response
210
     * will be stored here.
211
     *
212
     * @var mixed
213
     */
214
    public $discovered = null;
215
216
    /**
217
     * Constructor for the EWSAutodiscover class.
218
     *
219
     * @param string $email
220
     * @param string $password
221
     * @param string $username If left blank, the email provided will be used.
222
     */
223
    public function __construct($email, $password, $username = null)
224
    {
225
        $this->email = $email;
226
        $this->password = $password;
227
        if ($username === null) {
228
            $this->username = $email;
229
        } else {
230
            $this->username = $username;
231
        }
232
233
        $this->setTLD();
234
    }
235
236
    /**
237
     * Execute the full discovery chain of events in the correct sequence
238
     * until a valid response is received, or all methods have failed.
239
     *
240
     * @return An AUTODISCOVERED_VIA_* constant or FALSE on failure.
241
     *
242
     * @todo Throw an exception on failure.
243
     */
244
    public function discover()
245
    {
246
        $result = $this->tryTLD();
247
248
        if ($result === false) {
249
            $result = $this->trySubdomain();
250
        }
251
252
        if ($result === false) {
253
            $result = $this->trySubdomainUnauthenticatedGet();
254
        }
255
256
        if ($result === false) {
257
            $result = $this->trySRVRecord();
258
        }
259
260
        return $result;
261
    }
262
263
    /**
264
     * Return the settings discovered from the Autodiscover process.
265
     *
266
     * NULL indicates discovery hasn't completed (or been attempted)
267
     * FALSE indicates discovery wasn't successful. Check for errors
268
     *  or redirects.
269
     * An array will be returned with discovered settings on success.
270
     *
271
     * @return mixed
272
     */
273
    public function discoveredSettings()
274
    {
275
        return $this->discovered;
276
    }
277
278
    /**
279
     * Toggle skipping of SSL verification in cURL requests.
280
     *
281
     * @param boolean $skip To skip, or not.
282
     * @return self
283
     */
284
    public function skipSSLVerification($skip = true)
285
    {
286
        $this->skip_ssl_verification = (bool) $skip;
287
288
        return $this;
289
    }
290
291
    /**
292
     * Parse the hex ServerVersion value and return a valid
293
     * Client::VERSION_* constant.
294
     *
295
     * @return string|boolean A known version constant, or FALSE if it could not
296
     * be determined.
297
     *
298
     * @link http://msdn.microsoft.com/en-us/library/bb204122(v=exchg.140).aspx
299
     * @link http://blogs.msdn.com/b/pcreehan/archive/2009/09/21/parsing-serverversion-when-an-int-is-really-5-ints.aspx
300
     * @link http://office.microsoft.com/en-us/outlook-help/determine-the-version-of-microsoft-exchange-server-my-account-connects-to-HA001191800.aspx
301
     *
302
     * @todo Update to include Exchange 2013 versions.
303
     */
304
    public function parseServerVersion($version_hex)
305
    {
306
        $svbinary = base_convert($version_hex, 16, 2);
307
        if (strlen($svbinary) == 31) {
308
            $svbinary = '0'.$svbinary;
309
        }
310
311
        $majorversion = base_convert(substr($svbinary, 4, 6), 2, 10);
312
        $minorversion = base_convert(substr($svbinary, 10, 6), 2, 10);
313
314
        if ($majorversion == 8) {
315 View Code Duplication
            switch ($minorversion) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
316
                case 0:
317
                    return Client::VERSION_2007;
318
                case 1:
319
                    return Client::VERSION_2007_SP1;
320
                case 2:
321
                    return Client::VERSION_2007_SP2;
322
                case 3:
323
                    return Client::VERSION_2007_SP3;
324
                default:
325
                    return Client::VERSION_2007;
326
            }
327
        } elseif ($majorversion == 14) {
328 View Code Duplication
            switch ($minorversion) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
329
                case 0:
330
                    return Client::VERSION_2010;
331
                case 1:
332
                    return Client::VERSION_2010_SP1;
333
                case 2:
334
                    return Client::VERSION_2010_SP2;
335
                default:
336
                    return Client::VERSION_2010;
337
            }
338
        }
339
340
        // Guess we didn't find a known version.
341
        return false;
342
    }
343
344
    /**
345
     * Method to return a new Client object, auto-configured
346
     * with the proper hostname.
347
     *
348
     * @return mixed Client object on success, FALSE on failure.
349
     */
350
    public function newEWS()
351
    {
352
        // Discovery not yet attempted.
353
        if ($this->discovered === null) {
354
            $this->discover();
355
        }
356
357
        // Discovery not successful.
358
        if ($this->discovered === false) {
359
            return false;
360
        }
361
362
        $server = false;
363
        $version = null;
364
365
        // Pick out the host from the EXPR (Exchange RPC over HTTP).
366
        foreach ($this->discovered['Account']['Protocol'] as $protocol) {
367
            if (
368
                ($protocol['Type'] == 'EXCH' || $protocol['Type'] == 'EXPR')
369
                && isset($protocol['ServerVersion'])
370
            ) {
371
                if ($version == null) {
372
                    $sv = $this->parseServerVersion($protocol['ServerVersion']);
373
                    if ($sv !== false) {
374
                        $version = $sv;
375
                    }
376
                }
377
            }
378
379
            if ($protocol['Type'] == 'EXPR' && isset($protocol['Server'])) {
380
                $server = $protocol['Server'];
381
            }
382
        }
383
384
        if ($server) {
385
            if ($version === null) {
386
                // EWS class default.
387
                $version = Client::VERSION_2007;
388
            }
389
            return new Client(
390
                $server,
391
                (!empty($this->username) ? $this->username : $this->email),
392
                $this->password,
393
                $version
394
            );
395
        }
396
397
        return false;
398
    }
399
400
    /**
401
     * Static method may fail if there are issues surrounding SSL certificates.
402
     * In such cases, set up the object as needed, and then call newEWS().
403
     *
404
     * @param string $email
405
     * @param string $password
406
     * @param string $username If left blank, the email provided will be used.
407
     * @return mixed
408
     */
409
    public static function getEWS($email, $password, $username = null)
410
    {
411
        $auto = new Autodiscover($email, $password, $username);
412
        return $auto->newEWS();
413
    }
414
415
    /**
416
     * Perform an NTLM authenticated HTTPS POST to the top-level
417
     * domain of the email address.
418
     *
419
     * @return An AUTODISCOVERED_VIA_* constant or FALSE on failure.
420
     */
421
    public function tryTLD()
422
    {
423
        $url = 'https://www.' . $this->tld . self::AUTODISCOVER_PATH;
424
        return ($this->tryViaUrl($url) ? self::AUTODISCOVERED_VIA_TLD : false);
425
    }
426
427
    /**
428
     * Perform an NTLM authenticated HTTPS POST to the 'autodiscover'
429
     * subdomain of the email address' TLD.
430
     *
431
     * @return An AUTODISCOVERED_VIA_* constant 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 An AUTODISCOVERED_VIA_* constant or FALSE on failure.
446
     */
447
    public function trySubdomainUnauthenticatedGet()
448
    {
449
        $this->reset();
450
        $url = 'http://autodiscover.'.$this->tld . self::AUTODISCOVER_PATH;
451
        $ch = curl_init();
452
        $opts = array(
453
            CURLOPT_URL                 => $url,
454
            CURLOPT_HTTPGET             => true,
455
            CURLOPT_RETURNTRANSFER      => true,
456
            CURLOPT_TIMEOUT             => 4,
457
            CURLOPT_CONNECTTIMEOUT      => $this->connection_timeout,
458
            CURLOPT_FOLLOWLOCATION      => false,
459
            CURLOPT_HEADER              => false,
460
            CURLOPT_HEADERFUNCTION      => array($this, 'readHeaders'),
461
            CURLOPT_HTTP200ALIASES      => array(301, 302),
462
            CURLOPT_IPRESOLVE           => CURL_IPRESOLVE_V4
463
        );
464
        curl_setopt_array($ch, $opts);
465
        $this->last_response    = curl_exec($ch);
466
        $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...
467
        $this->last_curl_errno  = curl_errno($ch);
468
        $this->last_curl_error  = curl_error($ch);
469
470
        if (
471
            $this->last_info['http_code'] == 302
472
            || $this->last_info['http_code'] == 301
473
        ) {
474
            if ($this->tryViaUrl($this->last_response_headers['location'])) {
475
                return self::AUTODISCOVERED_VIA_UNAUTHENTICATED_GET;
476
            }
477
        }
478
479
        return false;
480
    }
481
482
    /**
483
     * Attempt to retrieve the autodiscover host from an SRV DNS record.
484
     *
485
     * @link http://support.microsoft.com/kb/940881
486
     * @return self::AUTODISCOVERED_VIA_SRV_RECORD or false
0 ignored issues
show
Documentation introduced by
The doc-type self::AUTODISCOVERED_VIA_SRV_RECORD could not be parsed: Unknown type name "self::AUTODISCOVERED_VIA_SRV_RECORD" at position 0. (view supported doc-types)

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

Loading history...
487
     */
488
    public function trySRVRecord()
489
    {
490
        $srvhost = '_autodiscover._tcp.' . $this->tld;
491
        $lookup = dns_get_record($srvhost, DNS_SRV);
492
        if (sizeof($lookup) > 0) {
493
            $host = $lookup[0]['target'];
494
            $url = 'https://' . $host . self::AUTODISCOVER_PATH;
495
            if ($this->tryViaUrl($url)) {
496
                return self::AUTODISCOVERED_VIA_SRV_RECORD;
497
            }
498
        }
499
500
        return false;
501
    }
502
503
    /**
504
     * Set the path to the file to be used by CURLOPT_CAINFO.
505
     *
506
     * @param string $path Path to a certificate file such as cacert.pem
507
     * @return self
508
     */
509
    public function setCAInfo($path)
510
    {
511
        if (file_exists($path) && is_file($path)) {
512
            $this->cainfo = $path;
513
        }
514
515
        return $this;
516
    }
517
518
    /**
519
     * Set the path to the file to be used by CURLOPT_CAPATH.
520
     *
521
     * @param string $path Path to a directory containing one or more CA
522
     * certificates.
523
     * @return self
524
     */
525
    public function setCAPath($path)
526
    {
527
        if (is_dir($path)) {
528
            $this->capath = $path;
529
        }
530
531
        return $this;
532
    }
533
534
    /**
535
     * Set a connection timeout for the POST methods.
536
     *
537
     * @param integer $seconds Seconds to wait for a connection.
538
     * @return self
539
     */
540
    public function setConnectionTimeout($seconds)
541
    {
542
        $this->connection_timeout = intval($seconds);
543
544
        return $this;
545
    }
546
547
    /**
548
     * Perform the NTLM authenticated post against one of the chosen
549
     * endpoints.
550
     *
551
     * @param string $url URL to try posting to
552
     * @param integer $timeout Overall cURL timeout for this request
553
     * @return boolean
554
     */
555
    public function doNTLMPost($url, $timeout = 6)
556
    {
557
        $this->reset();
558
559
        $ch = curl_init();
560
        $opts = array(
561
            CURLOPT_URL             => $url,
562
            CURLOPT_HTTPAUTH        => CURLAUTH_NTLM,
563
            CURLOPT_CUSTOMREQUEST   => 'POST',
564
            CURLOPT_POSTFIELDS      => $this->getAutoDiscoverRequest(),
565
            CURLOPT_RETURNTRANSFER  => true,
566
            CURLOPT_USERPWD         => $this->username.':'.$this->password,
567
            CURLOPT_TIMEOUT         => $timeout,
568
            CURLOPT_CONNECTTIMEOUT  => $this->connection_timeout,
569
            CURLOPT_FOLLOWLOCATION  => true,
570
            CURLOPT_HEADER          => false,
571
            CURLOPT_HEADERFUNCTION  => array($this, 'readHeaders'),
572
            CURLOPT_IPRESOLVE       => CURL_IPRESOLVE_V4,
573
            CURLOPT_SSL_VERIFYPEER  => true,
574
            CURLOPT_SSL_VERIFYHOST  => true,
575
        );
576
577
        // Set the appropriate content-type.
578
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml; charset=utf-8'));
579
580
        if (! empty($this->cainfo)) {
581
            $opts[CURLOPT_CAINFO] = $this->cainfo;
582
        }
583
584
        if (! empty($this->capath)) {
585
            $opts[CURLOPT_CAPATH] = $this->capath;
586
        }
587
588
        if ($this->skip_ssl_verification) {
589
            $opts[CURLOPT_SSL_VERIFYPEER] = false;
590
            $opts[CURLOPT_SSL_VERIFYHOST] = false;
591
        }
592
593
        curl_setopt_array($ch, $opts);
594
        $this->last_response    = curl_exec($ch);
595
        $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...
596
        $this->last_curl_errno  = curl_errno($ch);
597
        $this->last_curl_error  = curl_error($ch);
598
599
        if ($this->last_curl_errno != CURLE_OK) {
600
            return false;
601
        }
602
603
        $discovered = $this->parseAutodiscoverResponse();
604
605
        return $discovered;
606
    }
607
608
    /**
609
     * Parse the Autoresponse Payload, particularly to determine if an
610
     * additional request is necessary.
611
     *
612
     * @return mixed FALSE if response isn't XML or parsed response array
613
     */
614
    protected function parseAutodiscoverResponse()
615
    {
616
        // Content-type isn't trustworthy, unfortunately. Shame on Microsoft.
617
        if (substr($this->last_response, 0, 5) !== '<?xml') {
618
            return false;
619
        }
620
621
        $response = $this->responseToArray($this->last_response);
622
623
        if (isset($response['Error'])) {
624
            $this->error = $response['Error'];
625
            return false;
626
        }
627
628
        // Check the account action for redirect.
629
        switch ($response['Account']['Action']) {
630
            case 'redirectUrl':
631
                $this->redirect = array(
632
                    'redirectUrl' => $response['Account']['redirectUrl']
633
                );
634
                return false;
635
            case 'redirectAddr':
636
                $this->redirect = array(
637
                    'redirectAddr' => $response['Account']['redirectAddr']
638
                );
639
                return false;
640
            case 'settings':
641
            default:
642
                $this->discovered = $response;
643
                return true;
644
        }
645
    }
646
647
    /**
648
     * Set the top-level domain to be used with autodiscover attempts based
649
     * on the provided email address.
650
     *
651
     * @return boolean
652
     */
653
    protected function setTLD()
654
    {
655
        $pos = strpos($this->email, '@');
656
        if ($pos !== false) {
657
            $this->tld = trim(substr($this->email, $pos+1));
658
            return true;
659
        }
660
661
        return false;
662
    }
663
664
    /**
665
     * Reset the response-related structures. Called before making a new
666
     * request.
667
     *
668
     * @return self
669
     */
670
    public function reset()
671
    {
672
        $this->last_response_headers = array();
673
        $this->last_info = array();
674
        $this->last_curl_errno = 0;
675
        $this->last_curl_error = '';
676
677
        return $this;
678
    }
679
680
    /**
681
     * Return the generated Autodiscover XML request body.
682
     *
683
     * @return string
684
     */
685
    public function getAutodiscoverRequest()
686
    {
687
        if (! empty($this->requestxml)) {
688
            return $this->requestxml;
689
        }
690
691
        $xml = new \XMLWriter();
692
        $xml->openMemory();
693
        $xml->setIndent(true);
694
        $xml->startDocument('1.0', 'UTF-8');
695
        $xml->startElementNS(
696
            null,
697
            'Autodiscover',
698
            'http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006'
699
        );
700
701
        $xml->startElement('Request');
702
        $xml->writeElement('EMailAddress', $this->email);
703
        $xml->writeElement(
704
            'AcceptableResponseSchema',
705
            'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'
706
        );
707
        $xml->endElement();
708
        $xml->endElement();
709
710
        $this->requestxml = $xml->outputMemory();
711
        return $this->requestxml;
712
    }
713
714
    /**
715
     * Utility function to pick headers off of the incoming cURL response.
716
     * Used with CURLOPT_HEADERFUNCTION.
717
     *
718
     * @param resource $_ch cURL handle
719
     * @param string $str Header string to read
720
     * @return integer Bytes read
721
     */
722
    public function readHeaders($_ch, $str)
723
    {
724
        $pos = strpos($str, ':');
725
        if ($pos !== false) {
726
            $key = strtolower(substr($str, 0, $pos));
727
            $val = trim(substr($str, $pos+1));
728
            $this->last_response_headers[$key] = $val;
729
        }
730
731
        return strlen($str);
732
    }
733
734
    /**
735
     * Utility function to parse XML payloads from the response into easier
736
     * to manage associative arrays.
737
     *
738
     * @param string $xml XML to parse
739
     * @return array
740
     */
741
    public function responseToArray($xml)
742
    {
743
        $doc = new \DOMDocument();
744
        $doc->loadXML($xml);
745
        $out = $this->nodeToArray($doc->documentElement);
746
747
        return $out['Response'];
748
    }
749
750
    /**
751
     * Recursive method for parsing DOM nodes.
752
     *
753
     * @link https://github.com/gaarf/XML-string-to-PHP-array
754
     * @param object $node DOMNode object
755
     * @return mixed
756
     */
757
    protected function nodeToArray($node)
758
    {
759
        $output = array();
760
        switch ($node->nodeType) {
761
            case XML_CDATA_SECTION_NODE:
762
            case XML_TEXT_NODE:
763
                $output = trim($node->textContent);
764
                break;
765
            case XML_ELEMENT_NODE:
766
                for ($i=0, $m = $node->childNodes->length; $i < $m; $i++) {
767
                    $child = $node->childNodes->item($i);
768
                    $v = $this->nodeToArray($child);
769
                    if (isset($child->tagName)) {
770
                        $t = $child->tagName;
771
                        if (!isset($output[$t])) {
772
                            $output[$t] = array();
773
                        }
774
                        $output[$t][] = $v;
775
                    } elseif ($v || $v === '0') {
776
                        $output = (string) $v;
777
                    }
778
                }
779
780
                // Edge case of a node containing a text node, which also has
781
                // attributes. this way we'll retain text and attributes for
782
                // this node.
783
                if (is_string($output) && $node->attributes->length) {
784
                    $output = array('@text' => $output);
785
                }
786
787
                if (is_array($output)) {
788
                    if ($node->attributes->length) {
789
                        $a = array();
790
                        foreach ($node->attributes as $attrName => $attrNode) {
791
                            $a[$attrName] = (string) $attrNode->value;
792
                        }
793
                        $output['@attributes'] = $a;
794
                    }
795
                    foreach ($output as $t => $v) {
796
                        if (is_array($v) && count($v)==1 && $t!='@attributes') {
797
                            $output[$t] = $v[0];
798
                        }
799
                    }
800
                }
801
                break;
802
        }
803
804
        return $output;
805
    }
806
807
    /**
808
     * Attempts an autodiscover via a URL.
809
     *
810
     * @param string $url Url to attempt an autodiscover.
811
     * @return boolean
812
     */
813
    protected function tryViaUrl($url, $timeout = 6)
814
    {
815
        $result = $this->doNTLMPost($url, $timeout);
816
        return ($result ? true : false);
817
    }
818
}
819