Autodiscover   F
last analyzed

Complexity

Total Complexity 98

Size/Duplication

Total Lines 871
Duplicated Lines 2.76 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 98
lcom 1
cbo 1
dl 24
loc 871
c 0
b 0
f 0
rs 1.729

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 2
A discover() 0 22 5
A discoveredSettings() 0 4 1
A skipSSLVerification() 0 6 1
B parseServerVersion() 0 27 6
C newEWS() 0 47 14
A getEWS() 0 5 1
A tryTLD() 0 5 2
A trySubdomain() 0 7 2
A trySubdomainUnauthenticatedGet() 0 32 4
A trySRVRecord() 0 14 3
A setCAInfo() 0 8 3
A setCAPath() 0 8 2
A setConnectionTimeout() 0 6 1
B doNTLMPost() 0 51 5
B parseAutodiscoverResponse() 0 32 6
A setTLD() 0 10 2
A reset() 0 9 1
A getAutodiscoverRequest() 0 28 2
A readHeaders() 0 11 2
A responseToArray() 0 8 1
D nodeToArray() 0 49 18
A parseVersion2007() 12 13 5
A parseVersion2010() 12 13 4
A parseVersion2013() 0 6 2
A parseVersion2016() 0 4 1
A tryViaUrl() 0 5 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Autodiscover often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Autodiscover, and based on these observations, apply Extract Interface, too.

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