Completed
Push — master ( 35728e...5c8980 )
by Gareth
03:35
created

ExchangeAutodiscover::tryAutoDiscoverSubDomain()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 7
Ratio 100 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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