Completed
Push — master ( 5011ba...5ed41a )
by Gareth
03:56
created

ExchangeAutodiscover::doNTLMPost()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 4.3145

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 32
ccs 2
cts 12
cp 0.1666
rs 8.8571
cc 2
eloc 20
nc 2
nop 4
crap 4.3145
1
<?php
2
namespace garethp\ews\API;
3
4
use garethp\ews\API;
5
use garethp\ews\API\Exception\AutodiscoverFailed;
6
use garethp\ews\HttpPlayback\HttpPlayback;
7
8
class ExchangeAutodiscover
9
{
10
    protected $autodiscoverPath = '/autodiscover/autodiscover.xml';
11
12
    /**
13
     * @var HttpPlayback
14
     */
15
    protected $httpPlayback;
16
17
    protected function __construct()
18
    {
19
    }
20
21
    /**
22
     * Parse the hex ServerVersion value and return a valid
23
     * ExchangeWebServices::VERSION_* constant.
24
     *
25
     * @param $versionHex
26
     * @return string|boolean A known version constant, or FALSE if it could not
27
     * be determined.
28
     *
29
     * @link http://msdn.microsoft.com/en-us/library/bb204122(v=exchg.140).aspx
30
     * @link http://blogs.msdn.com/b/pcreehan/archive/2009/09/21/parsing-serverversion-when-an-int-is-really-5-ints.aspx
31
     */
32 4
    protected function parseServerVersion($versionHex)
33
    {
34
        //Convert from hex to binary
35 4
        $versionBinary = base_convert($versionHex, 16, 2);
36 4
        $versionBinary = str_pad($versionBinary, 32, "0", STR_PAD_LEFT);
37
38
        //Get the relevant parts of the binary and convert them to base 10
39 4
        $majorVersion = base_convert(substr($versionBinary, 4, 6), 2, 10);
40 4
        $minorVersion = base_convert(substr($versionBinary, 10, 6), 2, 10);
41
42
        $versions = [
43
            8 => [
44
                'name' => 'VERSION_2007',
45
                'spCount' => 3
46 4
            ],
47
            14 => [
48
                'name' => 'VERSION_2010',
49
                'spCount' => 3
50
            ],
51
            15 => [
52
                'name' => 'VERSION_2013',
53
                'spCount' => 1
54
            ]
55
        ];
56
57 4
        if (!isset($versions[$majorVersion])) {
58 1
            return false;
59
        }
60
61 3
        $constant = $versions[$majorVersion]['name'];
62 3
        if ($minorVersion > 0 && $minorVersion <= $versions[$majorVersion]['spCount']) {
63 1
            $constant .= "_SP$minorVersion";
64
        }
65
66 3
        return constant(ExchangeWebServices::class . "::$constant");
67
    }
68
69 2
    protected function newAPI($email, $password, $username = null, $options = [])
70
    {
71 2
        $options = array_replace_recursive([
72
            'httpPlayback' => [
73
                'mode' => null
74
            ]
75 2
        ], $options);
76
77 2
        $this->httpPlayback = HttpPlayback::getInstance($options['httpPlayback']);
78
79 2
        if (!$username) {
80
            $username = $email;
81
        }
82
83 2
        $settings = $this->discover($email, $password, $username);
84
        if (!$settings) {
85
            throw new AutodiscoverFailed();
86
        }
87
88
        $server = $this->getServerFromResponse($settings);
89
        $version = $this->getServerVersionFromResponse($settings);
90
91
        if (!$server) {
92
            throw new AutodiscoverFailed();
93
        }
94
95
        $options = [];
96
        if ($version) {
97
            $options['version'] = $version;
98
        }
99
100
        return API::withUsernameAndPassword($server, $email, $password, $options);
101
    }
102
103
    protected function getServerVersionFromResponse($response)
104
    {
105
        // Pick out the host from the EXPR (Exchange RPC over HTTP).
106
        foreach ($response['Account']['Protocol'] as $protocol) {
107
            if (($protocol['Type'] == 'EXCH' || $protocol['Type'] == 'EXPR')
108
                && isset($protocol['ServerVersion'])
109
            ) {
110
                return $this->parseServerVersion($protocol['ServerVersion']);
111
            }
112
        }
113
114
        return false;
115
    }
116
117
    protected function getServerFromResponse($response)
118
    {
119
        foreach ($response['Account']['Protocol'] as $protocol) {
120
            if ($protocol['Type'] == 'EXPR' && isset($protocol['Server'])) {
121
                return $protocol['Server'];
122
            }
123
        }
124
125
        return false;
126
    }
127
128
    /**
129
     * Static method may fail if there are issues surrounding SSL certificates.
130
     * In such cases, set up the object as needed, and then call newEWS().
131
     *
132
     * @param string $email
133
     * @param string $password
134
     * @param string $username If left blank, the email provided will be used.
135
     * @throws AutodiscoverFailed
136
     * @return API
137
     */
138 2
    public static function getAPI($email, $password, $username = null, $options = [])
139
    {
140 2
        $auto = new static();
141
142 2
        return $auto->newAPI($email, $password, $username, $options);
143
    }
144
145
    /**
146
     * Execute the full discovery chain of events in the correct sequence
147
     * until a valid response is received, or all methods have failed.
148
     *
149
     * @param string $email
150
     * @param string $password
151
     * @param string $username
152
     *
153
     * @return string The discovered settings
154
     */
155 2
    protected function discover($email, $password, $username)
156
    {
157 2
        $result = $this->tryTopLevelDomain($email, $password, $username);
158
159
        if ($result === false) {
160
            $result = $this->tryAutoDiscoverSubDomain($email, $password, $username);
161
        }
162
163
        if ($result === false) {
164
            $result = $this->trySubdomainUnauthenticatedGet($email, $password, $username);
165
        }
166
167
        if ($result === false) {
168
            $result = $this->trySRVRecord($email, $password, $username);
169
        }
170
171
        return $result;
172
    }
173
174
    /**
175
     * Perform an NTLM authenticated HTTPS POST to the top-level
176
     * domain of the email address.
177
     *
178
     * @param string $email
179
     * @param string $password
180
     * @param string $username
181
     *
182
     * @return string The discovered settings
183
     */
184 2 View Code Duplication
    protected function tryTopLevelDomain($email, $password, $username)
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...
185
    {
186 2
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
187 2
        $url = 'https://www.' . $topLevelDomain . $this->autodiscoverPath;
188
189 2
        return $this->doNTLMPost($url, $email, $password, $username);
190
    }
191
192
    /**
193
     * Perform an NTLM authenticated HTTPS POST to the 'autodiscover'
194
     * subdomain of the email address' TLD.
195
     *
196
     * @param string $email
197
     * @param string $password
198
     * @param string $username
199
     *
200
     * @return string The discovered settings
201
     */
202 View Code Duplication
    protected function tryAutoDiscoverSubDomain($email, $password, $username)
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...
203
    {
204
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
205
        $url = 'https://autodiscover.' . $topLevelDomain . $this->autodiscoverPath;
206
207
        return $this->doNTLMPost($url, $email, $password, $username);
208
    }
209
210
    /**
211
     * Perform an unauthenticated HTTP GET in an attempt to get redirected
212
     * via 302 to the correct location to perform the HTTPS POST.
213
     *
214
     * @param string $email
215
     * @param string $password
216
     * @param string $username
217
     *
218
     * @return string The discovered settings
219
     */
220
    protected function trySubdomainUnauthenticatedGet($email, $password, $username)
221
    {
222
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
223
224
        $url = 'http://autodiscover.' . $topLevelDomain . $this->autodiscoverPath;
225
226
        $client = $this->httpPlayback->getHttpClient();
227
        $postOptions = [
228
            'timeout' => 2,
229
            'allow_redirects' => false,
230
            'headers' => [
231
                'Content-Type' => 'text/xml; charset=utf-8'
232
            ],
233
            'curl' => []
234
        ];
235
236
        try {
237
            $response = $client->get($url, $postOptions);
238
239
            if ($response->getStatusCode() == 301 || $response->getStatusCode() == 302) {
240
                return $this->doNTLMPost($response->getHeaderLine('Location'), $email, $password, $username);
241
            }
242
        } catch (\Exception $e) {
243
        }
244
245
        return false;
246
    }
247
248
    /**
249
     * Attempt to retrieve the autodiscover host from an SRV DNS record.
250
     *
251
     * @link http://support.microsoft.com/kb/940881
252
     *
253
     * @param string $email
254
     * @param string $password
255
     * @param string $username
256
     *
257
     * @return string The discovered settings
258
     */
259
    protected function trySRVRecord($email, $password, $username)
260
    {
261
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
262
        $srvHost = '_autodiscover._tcp.' . $topLevelDomain;
263
        $lookup = dns_get_record($srvHost, DNS_SRV);
264
        if (sizeof($lookup) > 0) {
265
            $host = $lookup[0]['target'];
266
            $url = 'https://' . $host . $this->autodiscoverPath;
267
268
            return $this->doNTLMPost($url, $email, $password, $username);
269
        }
270
271
        return false;
272
    }
273
274
    /**
275
     * Perform the NTLM authenticated post against one of the chosen
276
     * endpoints.
277
     *
278
     * @param string $url URL to try posting to
279
     * @param string $email
280
     * @param string $password
281
     * @param string $username
282
     *
283
     * @return string The discovered settings
284
     */
285 2
    protected function doNTLMPost($url, $email, $password, $username)
286
    {
287 2
        $client = $this->httpPlayback->getHttpClient();
288
        $autodiscoverXml = <<<XML
289
<?xml version="1.0" encoding="UTF-8"?>
290
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
291
 <Request>
292
  <EMailAddress>$email</EMailAddress>
293
  <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
294
 </Request>
295
</Autodiscover>
296
XML;
297
        $postOptions = [
298
            'body' => $autodiscoverXml,
299
            'timeout' => 2,
300
            'allow_redirects' => true,
301
            'headers' => [
302
                'Content-Type' => 'text/xml; charset=utf-8'
303
            ],
304
            'curl' => []
305
        ];
306
        $auth = ExchangeWebServicesAuth::fromUsernameAndPassword($username, $password);
307
        $postOptions = array_replace_recursive($postOptions, $auth);
308
309
        try {
310
            $response = $client->post($url, $postOptions);
311
        } catch (\Exception $e) {
312
            return false;
313
        }
314
315
        return $this->parseAutodiscoverResponse($response->getBody()->__toString());
316
    }
317
318
    /**
319
     * Parse the Autoresponse Payload, particularly to determine if an
320
     * additional request is necessary.
321
     *
322
     * @param $response
323
     * @return array|bool
324
     * @throws AutodiscoverFailed
325
     */
326
    protected function parseAutodiscoverResponse($response)
327
    {
328
        // Content-type isn't trustworthy, unfortunately. Shame on Microsoft.
329
        if (substr($response, 0, 5) !== '<?xml') {
330
            throw new AutodiscoverFailed();
331
        }
332
333
        $response = $this->responseToArray($response);
334
335
        if (isset($response['Error'])) {
336
            return false;
337
        }
338
339
        $action = $response['Account']['Action'];
340
        if ($action == 'redirectUrl' || $action == 'redirectAddr') {
341
            return false;
342
        }
343
344
        return $response;
345
    }
346
347
    /**
348
     * Get a top level domain based on an email address
349
     *
350
     * @param $email
351
     * @return bool|string
352
     */
353 2
    protected function getTopLevelDomainFromEmail($email)
354
    {
355 2
        $pos = strpos($email, '@');
356 2
        if ($pos !== false) {
357 1
            return trim(substr($email, $pos + 1));
358
        }
359
360 1
        return false;
361
    }
362
363
    /**
364
     * Utility function to parse XML payloads from the response into easier
365
     * to manage associative arrays.
366
     *
367
     * @param string $xml XML to parse
368
     * @return array
369
     */
370
    protected function responseToArray($xml)
371
    {
372
        $xml = simplexml_load_string($xml, "SimpleXMLElement", LIBXML_NOCDATA);
373
374
        return json_decode(json_encode($xml), true)['Response'];
375
    }
376
}
377