AutodiscoveryManager::doNTLMPost()   B
last analyzed

Complexity

Conditions 5
Paths 16

Size

Total Lines 52
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 52
rs 8.6868
cc 5
eloc 35
nc 16
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
namespace PhpEws;
3
4
/**
5
 * Exchange Web Services Autodiscovery implementation
6
 *
7
 * This class supports POX (Plain Old XML), which is deprecated but functional
8
 * in Exchange 2010. It may make sense for you to combine your Autodiscovery 
9
 * efforts with a SOAP Autodiscover request as well.
10
 *
11
 * USAGE:
12
 *
13
 * (after any auto-loading class incantation)
14
 *
15
 * $ews = Autodiscovery::getConnection($email, $password);
16
 *
17
 * -- OR --
18
 *
19
 * If there are issues with your cURL installation that require you to specify
20
 * a path to a valid Certificate Authority, you can configure that manually.
21
 *
22
 * $auto = new self($email, $password);
23
 * $auto->setCAInfo('/path/to/your/cacert.pem');
24
 * $ews = $auto->createNewConnection();
25
 *
26
 * @link http://technet.microsoft.com/en-us/library/bb332063(EXCHG.80).aspx
27
 * @link https://www.testexchangeconnectivity.com/
28
 */
29
class AutodiscoveryManager
30
{
31
    /**
32
     * The path appended to the various schemes and hostnames used during
33
     * autodiscovery.
34
     *
35
     * @var string
36
     */
37
    const AUTODISCOVER_PATH = '/autodiscover/autodiscover.xml';
38
39
    /**
40
     * Server was discovered using the TLD method.
41
     *
42
     * @var integer
43
     */
44
    const AUTODISCOVERED_VIA_TLD = 10;
45
46
    /**
47
     * Server was discovered using the subdomain method.
48
     *
49
     * @var integer
50
     */
51
    const AUTODISCOVERED_VIA_SUBDOMAIN = 11;
52
53
    /**
54
     * Server was discovered using the unauthenticated GET method.
55
     *
56
     * @var integer
57
     */
58
    const AUTODISCOVERED_VIA_UNAUTHENTICATED_GET = 12;
59
60
    /**
61
     * Server was discovered using the DNS SRV redirect method.
62
     *
63
     * @var integer
64
     */
65
    const AUTODISCOVERED_VIA_SRV_RECORD = 13;
66
67
    /**
68
     * Server was discovered using the HTTP redirect method.
69
     *
70
     * @var integer
71
     *
72
     * @todo We do not currently support this.
73
     */
74
    const AUTODISCOVERED_VIA_RESPONSE_REDIRECT = 14;
75
76
    /**
77
     * The email address to attempt autodiscovery against.
78
     *
79
     * @var string
80
     */
81
    protected $email;
82
83
    /**
84
     * The password to present during autodiscovery.
85
     *
86
     * @var string
87
     */
88
    protected $password;
89
90
    /**
91
     * The Exchange username to use during authentication. If unspecified,
92
     * the provided email address will be used as the username.
93
     *
94
     * @var string
95
     */
96
    protected $username;
97
98
    /**
99
     * The top-level domain name, extracted from the provided email address.
100
     *
101
     * @var string
102
     */
103
    protected $tld;
104
105
    /**
106
     * The Autodiscover XML request. Since it's used repeatedly, it's cached
107
     * in this property to avoid redundant re-generation.
108
     *
109
     * @var string
110
     */
111
    protected $requestxml;
112
113
    /**
114
     * The Certificate Authority path. Should point to a directory containing
115
     * one or more certificates to use in SSL verification.
116
     *
117
     * @var string
118
     */
119
    protected $capath;
120
121
    /**
122
     * The path to a specific Certificate Authority file. Get one and use it
123
     * for full Autodiscovery compliance.
124
     *
125
     * @var string
126
     *
127
     * @link http://curl.haxx.se/ca/cacert.pem
128
     * @link http://curl.haxx.se/ca/
129
     */
130
    protected $cainfo;
131
132
    /**
133
     * Skip SSL verification. Bad idea, and violates the strict Autodiscover
134
     * protocol. But, here in case you have no other option. 
135
     * Defaults to FALSE.
136
     *
137
     * @var boolean
138
     */
139
    protected $skip_ssl_verification = false;
140
141
    /**
142
     * An associative array of response headers that resulted from the 
143
     * last request. Keys are lowercased for easy checking.
144
     *
145
     * @var array
146
     */
147
    public $last_response_headers;
148
149
    /**
150
     * The output of curl_info() relating to the most recent cURL request.
151
     *
152
     * @var array
153
     */
154
    public $last_info;
155
156
    /**
157
     * The cURL error code associated with the most recent cURL request.
158
     *
159
     * @var integer
160
     */
161
    public $last_curl_errno;
162
163
    /**
164
     * Human-readable description of the most recent cURL error. 
165
     *
166
     * @var string
167
     */
168
    public $last_curl_error;
169
170
    /**
171
     * The value in seconds to use for Autodiscover host connection timeouts.
172
     * Default connection timeout is 2 seconds, so that unresponsive methods
173
     * can be bypassed quickly.
174
     *
175
     * @var integer
176
     */
177
    public $connection_timeout = 2;
178
179
    /**
180
     * Information about an Autodiscover Response containing an error will 
181
     * be stored here.
182
     *
183
     * @var mixed
184
     */
185
    public $error = false;
186
187
    /**
188
     * Information about an Autodiscover Response with a redirect will be 
189
     * retained here.
190
     *
191
     * @var mixed
192
     */
193
    public $redirect = false;
194
195
    /**
196
     * A successful, non-error and non-redirect parsed Autodiscover response
197
     * will be stored here.
198
     *
199
     * @var mixed
200
     */
201
    public $discovered = null;
202
203
    /**
204
     * Constructor for the Autodiscovery class.
205
     *
206
     * @param string $email
207
     * @param string $password
208
     * @param string $username If left blank, the email provided will be used.
209
     */
210
    public function __construct($email, $password, $username = null)
211
    {
212
        $this->email = $email;
213
        $this->password = $password;
214
        if ($username === null) {
215
            $this->username = $email;
216
        } else {
217
            $this->username = $username;
218
        }
219
220
        $this->setTLD();
221
    }
222
223
    /**
224
     * Execute the full discovery chain of events in the correct sequence
225
     * until a valid response is received, or all methods have failed.
226
     *
227
     * @return An AUTODISCOVERED_VIA_* constant or FALSE on failure.
228
     */
229
    public function discover()
230
    {
231
        $result = $this->tryTLD();
232
233
        if ($result === false) {
234
            $result = $this->trySubdomain();
235
        }
236
237
        if ($result === false) {
238
            $result = $this->trySubdomainUnauthenticatedGet();
239
        }
240
241
        if ($result === false) {
242
            $result = $this->trySRVRecord();
243
        }
244
245
        return $result;
246
    }
247
248
    /**
249
     * Return the settings discovered from the Autodiscover process.
250
     *
251
     * NULL indicates discovery hasn't completed (or been attempted)
252
     * FALSE indicates discovery wasn't successful. Check for errors 
253
     *  or redirects.
254
     * An array will be returned with discovered settings on success.
255
     *
256
     * @return mixed
257
     */
258
    public function discoveredSettings()
259
    {
260
        return $this->discovered;
261
    }
262
263
    /**
264
     * Toggle skipping of SSL verification in cURL requests.
265
     *
266
     * @param boolean $skip To skip, or not.
267
     * @return AutodiscoveryManager
268
     */
269
    public function skipSSLVerification($skip = true)
270
    {
271
        $this->skip_ssl_verification = (bool) $skip;
272
273
        return $this;
274
    }
275
276
    /**
277
     * Parse the hex ServerVersion value and return a valid
278
     * \PhpEws\EwsConnection::VERSION_* constant.
279
     *
280
     * @return string|boolean A known version constant, or FALSE if it could not
281
     * be determined.
282
     *
283
     * @link http://msdn.microsoft.com/en-us/library/bb204122(v=exchg.140).aspx
284
     * @link http://blogs.msdn.com/b/pcreehan/archive/2009/09/21/parsing-serverversion-when-an-int-is-really-5-ints.aspx
285
     * @link http://office.microsoft.com/en-us/outlook-help/determine-the-version-of-microsoft-exchange-server-my-account-connects-to-HA001191800.aspx
286
     */
287
    public function parseServerVersion($version_hex)
288
    {
289
        $svbinary = base_convert($version_hex, 16, 2);
290
        if (strlen($svbinary) == 31) {
291
            $svbinary = '0'.$svbinary;
292
        }
293
294
        $majorversion = base_convert(substr($svbinary, 4, 6), 2, 10);
295
        $minorversion = base_convert(substr($svbinary, 10, 6), 2, 10);
296
        $buildversion = base_convert(substr($svbinary, 17, 15), 2, 10);
0 ignored issues
show
Unused Code introduced by
$buildversion is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
297
298
        if ($majorversion == 8) {
299 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...
300
                case 0:
301
                    return EwsConnection::VERSION_2007;
302
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
303
                case 1:
304
                    return EwsConnection::VERSION_2007_SP1;
305
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
306
                case 2:
307
                    return EwsConnection::VERSION_2007_SP2;
308
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
309
                case 3:
310
                    return EwsConnection::VERSION_2007_SP3;
311
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
312
                default:
313
                    return EwsConnection::VERSION_2007;
314
            }
315
        } elseif ($majorversion == 14) {
316 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...
317
                case 0:
318
                    return EwsConnection::VERSION_2010;
319
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
320
                case 1:
321
                    return EwsConnection::VERSION_2010_SP1;
322
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
323
                case 2:
324
                    return EwsConnection::VERSION_2010_SP2;
325
                    break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
326
                default:
327
                    return EwsConnection::VERSION_2010;
328
            }
329
        }
330
331
        // Guess we didn't find a known version.
332
        return false;
333
    }
334
335
    /**
336
     * Method to return a new EwsConnection object, auto-configured
337
     * with the proper hostname.
338
     *
339
     * @return EwsConnection|false EwsConnection on success, false on failure.
340
     */
341
    public function createNewConnection()
342
    {
343
        // Discovery not yet attempted.
344
        if ($this->discovered === null) {
345
            $this->discover();
346
        }
347
348
        // Discovery not successful.
349
        if ($this->discovered === false) {
350
            return false;
351
        }
352
353
        $server = false;
354
        $version = null;
355
356
        // Pick out the host from the EXPR (Exchange RPC over HTTP).
357
        foreach ($this->discovered['Account']['Protocol'] as $protocol) {
358
            if (
359
                ($protocol['Type'] == 'EXCH' || $protocol['Type'] == 'EXPR')
360
                && isset($protocol['ServerVersion'])
361
            ) {
362
                if ($version == null) {
363
                    $sv = $this->parseServerVersion($protocol['ServerVersion']);
364
                    if ($sv !== false) {
365
                        $version = $sv;
366
                    }
367
                }
368
            }
369
370
            if ($protocol['Type'] == 'EXPR' && isset($protocol['Server'])) {
371
                $server = $protocol['Server'];
372
            }
373
        }
374
375
        if ($server) {
376
            if ($version === null) {
377
                // EWS class default.
378
                $version = EwsConnection::VERSION_2007;
379
            }
380
            return new EwsConnection(
381
                $server,
382
                $this->email,
383
                $this->password,
384
                $version
385
            );
386
        }
387
388
        return false;
389
    }
390
391
    /**
392
     * Static method may fail if there are issues surrounding SSL certificates.
393
     * In such cases, set up the object as needed, and then call newEWS().
394
     *
395
     * @param string $email
396
     * @param string $password
397
     * @param string $username If left blank, the email provided will be used.
398
     *
399
     * @return EwsConnection
400
     */
401
    public static function getConnection($email, $password, $username = null)
402
    {
403
        $auto = new self($email, $password, $username);
404
405
        return $auto->createNewConnection();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $auto->createNewConnection(); of type false|PhpEws\EwsConnection adds false to the return on line 405 which is incompatible with the return type documented by PhpEws\AutodiscoveryManager::getConnection of type PhpEws\EwsConnection. It seems like you forgot to handle an error condition.
Loading history...
406
    }
407
408
    /** 
409
     * Perform an NTLM authenticated HTTPS POST to the top-level 
410
     * domain of the email address. 
411
     *
412
     * @return An AUTODISCOVERED_VIA_* constant or FALSE on failure.
413
     */
414 View Code Duplication
    public function tryTLD()
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...
415
    {
416
        $url = 'https://www.'.$this->tld . self::AUTODISCOVER_PATH;
417
        $result = $this->doNTLMPost($url, 5);
418
        if ($result) {
419
            return self::AUTODISCOVERED_VIA_TLD;
420
        }
421
422
        return false;
423
    }
424
425
    /**
426
     * Perform an NTLM authenticated HTTPS POST to the 'autodiscover' 
427
     * subdomain of the email address' TLD.
428
     *
429
     * @return An AUTODISCOVERED_VIA_* constant or FALSE on failure.
430
     */
431 View Code Duplication
    public function trySubdomain()
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...
432
    {
433
        $url = 'https://autodiscover.'.$this->tld . self::AUTODISCOVER_PATH;
434
        $result = $this->doNTLMPost($url, 5);
435
        if ($result) {
436
            return self::AUTODISCOVERED_VIA_SUBDOMAIN;
437
        }
438
439
        return false;
440
    }
441
442
    /**
443
     * Perform an unauthenticated HTTP GET in an attempt to get redirected
444
     * via 302 to the correct location to perform the HTTPS POST.
445
     *
446
     * @return An AUTODISCOVERED_VIA_* constant or FALSE on failure.
447
     */
448
    public function trySubdomainUnauthenticatedGet()
449
    {
450
        $this->reset();
451
        $url = 'http://autodiscover.'.$this->tld . self::AUTODISCOVER_PATH;
452
        $ch = curl_init();
453
        $opts = array(
454
            CURLOPT_URL                 => $url,
455
            CURLOPT_HTTPGET             => true,
456
            CURLOPT_RETURNTRANSFER      => true,
457
            CURLOPT_TIMEOUT             => 4,
458
            CURLOPT_CONNECTTIMEOUT      => $this->connection_timeout,
459
            CURLOPT_FOLLOWLOCATION      => false,
460
            CURLOPT_HEADER              => false,
461
            CURLOPT_HEADERFUNCTION      => array($this, 'readHeaders'),
462
            CURLOPT_HTTP200ALIASES      => array(301, 302),
463
            CURLOPT_IPRESOLVE           => CURL_IPRESOLVE_V4
464
        );
465
        curl_setopt_array($ch, $opts);
466
        $this->last_response    = curl_exec($ch);
0 ignored issues
show
Bug introduced by
The property last_response does not seem to exist. Did you mean last_response_headers?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
467
        $this->last_info        = curl_getinfo($ch);
0 ignored issues
show
Documentation Bug introduced by
It seems like curl_getinfo($ch) of type * is incompatible with the declared type array of property $last_info.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
468
        $this->last_curl_errno  = curl_errno($ch);
469
        $this->last_curl_error  = curl_error($ch);
470
471
        if (
472
            $this->last_info['http_code'] == 302
473
            || $this->last_info['http_code'] == 301
474
        ) {
475
            // Do the NTLM POST to the redirect.
476
            $result = $this->doNTLMPost(
477
                $this->last_response_headers['location']
478
            );
479
480
            if ($result) {
481
                return self::AUTODISCOVERED_VIA_UNAUTHENTICATED_GET;
482
            }
483
        }
484
485
        return false;
486
    }
487
488
    /**
489
     * Attempt to retrieve the autodiscover host from an SRV DNS record.
490
     *
491
     * @link http://support.microsoft.com/kb/940881
492
     * @return AutodiscoveryManager::AUTODISCOVERED_VIA_SRV_RECORD or false
0 ignored issues
show
Documentation introduced by
The doc-type AutodiscoveryManager::AU...SCOVERED_VIA_SRV_RECORD could not be parsed: Unknown type name "AutodiscoveryManager::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...
493
     */
494
    public function trySRVRecord()
495
    {
496
        $srvhost = '_autodiscover._tcp.' . $this->tld;
497
        $lookup = dns_get_record($srvhost, DNS_SRV);
498
        if (sizeof($lookup) > 0) {
499
            $host = $lookup[0]['target'];
500
            $url = 'https://' . $host . self::AUTODISCOVER_PATH;
501
            $result = $this->doNTLMPost($url);
502
            if ($result) {
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 Path to a certificate file such as cacert.pem
514
     * @return AutodiscoveryManager
515
     */
516
    public function setCAInfo($path)
517
    {
518
        if (file_exists($path) && is_file($path)) {
519
            $this->cainfo = $path;
520
        }
521
522
        return $this;
523
    }
524
525
    /**
526
     * Set the path to the file to be used by CURLOPT_CAPATH.
527
     *
528
     * @param string $path Path to a directory containing one or more CA 
529
     * certificates.
530
     * @return AutodiscoveryManager
531
     */
532
    public function setCAPath($path)
533
    {
534
        if (is_dir($path)) {
535
            $this->capath = $path;
536
        }
537
538
        return $this;
539
    }
540
541
    /**
542
     * Set a connection timeout for the POST methods.
543
     *
544
     * @param integer $seconds Seconds to wait for a connection.
545
     * @return AutodiscoveryManager
546
     */
547
    public function setConnectionTimeout($seconds)
548
    {
549
        $this->connection_timeout = intval($seconds);
550
551
        return $this;
552
    }
553
554
    /**
555
     * Perform the NTLM authenticated post against one of the chosen
556
     * endpoints.
557
     *
558
     * @param string $url URL to try posting to
559
     * @param integer $timeout Overall cURL timeout for this request
560
     * @return boolean
561
     */
562
    public function doNTLMPost($url, $timeout = 6)
563
    {
564
        $this->reset();
565
566
        $ch = curl_init();
567
        $opts = array(
568
            CURLOPT_URL             => $url,
569
            CURLOPT_HTTPAUTH        => CURLAUTH_NTLM,
570
            CURLOPT_CUSTOMREQUEST   => 'POST',
571
            CURLOPT_POSTFIELDS      => $this->getAutoDiscoverRequest(),
572
            CURLOPT_RETURNTRANSFER  => true,
573
            CURLOPT_USERPWD         => $this->username.':'.$this->password,
574
            CURLOPT_TIMEOUT         => $timeout,
575
            CURLOPT_CONNECTTIMEOUT  => $this->connection_timeout,
576
            CURLOPT_FOLLOWLOCATION  => true,
577
            CURLOPT_HEADER          => false,
578
            CURLOPT_HEADERFUNCTION  => array($this, 'readHeaders'),
579
            CURLOPT_IPRESOLVE       => CURL_IPRESOLVE_V4,
580
            CURLOPT_SSL_VERIFYPEER  => true,
581
            CURLOPT_SSL_VERIFYHOST  => 2,
582
        );
583
584
        // Set the appropriate content-type.
585
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml; charset=utf-8'));
586
587
        if (! empty($this->cainfo)) {
588
            $opts[CURLOPT_CAINFO] = $this->cainfo;
589
        }
590
591
        if (! empty($this->capath)) {
592
            $opts[CURLOPT_CAPATH] = $this->capath;
593
        }
594
595
        if ($this->skip_ssl_verification) {
596
            $opts[CURLOPT_SSL_VERIFYPEER] = false;
597
            $opts[CURLOPT_SSL_VERIFYHOST] = false;
598
        }
599
600
        curl_setopt_array($ch, $opts);
601
        $this->last_response    = curl_exec($ch);
0 ignored issues
show
Bug introduced by
The property last_response does not seem to exist. Did you mean last_response_headers?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
602
        $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...
603
        $this->last_curl_errno  = curl_errno($ch);
604
        $this->last_curl_error  = curl_error($ch);
605
606
        if ($this->last_curl_errno != CURLE_OK) {
607
            return false;
608
        }
609
610
        $discovered = $this->parseAutodiscoverResponse();
611
612
        return $discovered;
613
    }
614
615
    /**
616
     * Parse the Autoresponse Payload, particularly to determine if an 
617
     * additional request is necessary.
618
     *
619
     * @return mixed FALSE if response isn't XML or parsed response array
620
     */
621
    protected function parseAutodiscoverResponse()
622
    {
623
        // Content-type isn't trustworthy, unfortunately. Shame on Microsoft.
624
        if (substr($this->last_response, 0, 5) !== '<?xml') {
0 ignored issues
show
Bug introduced by
The property last_response does not seem to exist. Did you mean last_response_headers?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
625
            return false;
626
        }
627
628
        $response = $this->responseToArray($this->last_response);
0 ignored issues
show
Bug introduced by
The property last_response does not seem to exist. Did you mean last_response_headers?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
629
630
        if (isset($response['Error'])) {
631
            $this->error = $response['Error'];
632
            return false;
633
        }
634
635
        // Check the account action for redirect.
636
        switch ($response['Account']['Action']) {
637
            case 'redirectUrl':
638
                $this->redirect = array(
639
                    'redirectUrl' => $response['Account']['redirectUrl']
640
                );
641
                return false;
642
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
643
            case 'redirectAddr':
644
                $this->redirect = array(
645
                    'redirectAddr' => $response['Account']['redirectAddr']
646
                );
647
                return false;
648
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
649
            case 'settings':
650
            default:
651
                $this->discovered = $response;
652
                return true;
653
        }
654
    }
655
656
    /**
657
     * Set the top-level domain to be used with autodiscover attempts based
658
     * on the provided email address.
659
     *
660
     * @return boolean
661
     */
662
    protected function setTLD()
663
    {
664
        $pos = strpos($this->email, '@');
665
        if ($pos !== false) {
666
            $this->tld = trim(substr($this->email, $pos+1));
667
            return true;
668
        }
669
670
        return false;
671
    }
672
673
    /**
674
     * Reset the response-related structures. Called before making a new 
675
     * request.
676
     *
677
     * @return AutodiscoveryManager
678
     */
679
    public function reset()
680
    {
681
        $this->last_response_headers = array();
682
        $this->last_info = array();
683
        $this->last_curl_errno = 0;
684
        $this->last_curl_error = '';
685
686
        return $this;
687
    }
688
689
    /**
690
     * Return the generated Autodiscover XML request body.
691
     *
692
     * @return string
693
     */
694
    public function getAutodiscoverRequest()
695
    {
696
        if (! empty($this->requestxml)) {
697
            return $this->requestxml;
698
        }
699
700
        $xml = new \XMLWriter();
701
        $xml->openMemory();
702
        $xml->setIndent(true);
703
        $xml->startDocument('1.0', 'UTF-8');
704
        $xml->startElementNS(
705
            null,
706
            'Autodiscover',
707
            'http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006'
708
        );
709
710
        $xml->startElement('Request');
711
        $xml->writeElement('EMailAddress', $this->email);
712
        $xml->writeElement(
713
            'AcceptableResponseSchema',
714
            'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a'
715
        );
716
        $xml->endElement();
717
        $xml->endElement();
718
719
        $this->requestxml = $xml->outputMemory();
720
        return $this->requestxml;
721
    }
722
723
    /**
724
     * Utility function to pick headers off of the incoming cURL response.
725
     * Used with CURLOPT_HEADERFUNCTION.
726
     *
727
     * @param resource $_ch cURL handle
728
     * @param string $str Header string to read
729
     * @return integer Bytes read
730
     */
731
    public function readHeaders($_ch, $str)
732
    {
733
        $pos = strpos($str, ':');
734
        if ($pos !== false) {
735
            $key = strtolower(substr($str, 0, $pos));
736
            $val = trim(substr($str, $pos+1));
737
            $this->last_response_headers[$key] = $val;
738
        }
739
740
        return strlen($str);
741
    }
742
743
    /**
744
     * Utility function to parse XML payloads from the response into easier
745
     * to manage associative arrays.
746
     *
747
     * @param string $xml XML to parse
748
     * @return array
749
     */
750
    public function responseToArray($xml)
751
    {
752
        $doc = new \DOMDocument();
753
        $doc->loadXML($xml);
754
        $out = $this->nodeToArray($doc->documentElement);
755
756
        return $out['Response'];
757
    }
758
759
    /**
760
     * Recursive method for parsing DOM nodes. 
761
     *
762
     * @link https://github.com/gaarf/XML-string-to-PHP-array
763
     * @param object $node DOMNode object
764
     * @return mixed
765
     */
766
    protected function nodeToArray($node)
767
    {
768
        $output = array();
769
        switch ($node->nodeType) {
770
            case XML_CDATA_SECTION_NODE:
771
            case XML_TEXT_NODE:
772
                $output = trim($node->textContent);
773
                break;
774
            case XML_ELEMENT_NODE:
775
                for ($i=0, $m = $node->childNodes->length; $i < $m; $i++) {
776
                    $child = $node->childNodes->item($i);
777
                    $v = $this->nodeToArray($child);
778
                    if (isset($child->tagName)) {
779
                        $t = $child->tagName;
780
                        if (!isset($output[$t])) {
781
                            $output[$t] = array();
782
                        }
783
                        $output[$t][] = $v;
784
                    } elseif ($v || $v === '0') {
785
                        $output = (string) $v;
786
                    }
787
                }
788
789
                // Edge case of a node containing a text node, which also has
790
                // attributes. this way we'll retain text and attributes for
791
                // this node.
792
                if (is_string($output) && $node->attributes->length) {
793
                    $output = array('@text' => $output);
794
                }
795
796
                if (is_array($output)) {
797
                    if ($node->attributes->length) {
798
                        $a = array();
799
                        foreach ($node->attributes as $attrName => $attrNode) {
800
                            $a[$attrName] = (string) $attrNode->value;
801
                        }
802
                        $output['@attributes'] = $a;
803
                    }
804
                    foreach ($output as $t => $v) {
805
                        if (is_array($v) && count($v)==1 && $t!='@attributes') {
806
                            $output[$t] = $v[0];
807
                        }
808
                    }
809
                }
810
                break;
811
        }
812
813
        return $output;
814
    }
815
}
816