Completed
Push — master ( fb1c43...54cc50 )
by Gareth
04:48
created

getServerVersionFromResponse()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.3906

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 13
ccs 6
cts 8
cp 0.75
rs 8.8571
cc 5
eloc 6
nc 3
nop 1
crap 5.3906
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 2
    protected function newAPI($email, $password, $username = null, $options = [])
70
    {
71 2
        $options = array_replace_recursive([
72
            'httpPlayback' => [
73
                'mode' => null
74 2
            ]
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 2
        if (!$settings) {
85 1
            throw new AutodiscoverFailed();
86
        }
87
88 1
        $server = $this->getServerFromResponse($settings);
89 1
        $version = $this->getServerVersionFromResponse($settings);
90
91 1
        if (!$server) {
92 1
            throw new AutodiscoverFailed();
93
        }
94
95 1
        $options = [];
96 1
        if ($version) {
97 1
            $options['version'] = $version;
98 1
        }
99
100 1
        return API::withUsernameAndPassword($server, $email, $password, $options);
101
    }
102
103 1
    protected function getServerVersionFromResponse($response)
104
    {
105
        // Pick out the host from the EXPR (Exchange RPC over HTTP).
106 1
        foreach ($response['Account']['Protocol'] as $protocol) {
107 1
            if (($protocol['Type'] == 'EXCH' || $protocol['Type'] == 'EXPR')
108 1
                && isset($protocol['ServerVersion'])
109 1
            ) {
110 1
                return $this->parseServerVersion($protocol['ServerVersion']);
111
            }
112
        }
113
114
        return false;
115
    }
116
117 1
    protected function getServerFromResponse($response)
118
    {
119 1
        foreach ($response['Account']['Protocol'] as $protocol) {
120 1
            if ($protocol['Type'] == 'EXPR' && isset($protocol['Server'])) {
121 1
                return $protocol['Server'];
122
            }
123 1
        }
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 2
        if ($result === false) {
160 2
            $result = $this->tryAutoDiscoverSubDomain($email, $password, $username);
161 2
        }
162
163 2
        if ($result === false) {
164 2
            $result = $this->trySubdomainUnauthenticatedGet($email, $password, $username);
165 2
        }
166
167 2
        if ($result === false) {
168 1
            $result = $this->trySRVRecord($email, $password, $username);
169 1
        }
170
171 2
        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 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...
203
    {
204 2
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
205 2
        $url = 'https://autodiscover.' . $topLevelDomain . $this->autodiscoverPath;
206
207 2
        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 2
    protected function trySubdomainUnauthenticatedGet($email, $password, $username)
221
    {
222 2
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
223
224 2
        $url = 'http://autodiscover.' . $topLevelDomain . $this->autodiscoverPath;
225
226 2
        $client = $this->httpPlayback->getHttpClient();
227
        $postOptions = [
228 2
            'timeout' => 2,
229 2
            'allow_redirects' => false,
230
            'headers' => [
231
                'Content-Type' => 'text/xml; charset=utf-8'
232 2
            ],
233 2
            'curl' => []
234 2
        ];
235
236
        try {
237 2
            $response = $client->get($url, $postOptions);
238
239 1
            if ($response->getStatusCode() == 301 || $response->getStatusCode() == 302) {
240 1
                return $this->doNTLMPost($response->getHeaderLine('Location'), $email, $password, $username);
241
            }
242 1
        } catch (\Exception $e) {
243
        }
244
245 1
        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 1
    protected function trySRVRecord($email, $password, $username)
260
    {
261 1
        $topLevelDomain = $this->getTopLevelDomainFromEmail($email);
262 1
        $srvHost = '_autodiscover._tcp.' . $topLevelDomain;
263 1
        $lookup = dns_get_record($srvHost, DNS_SRV);
264 1
        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 1
        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 2
</Autodiscover>
296 2
XML;
297
        $postOptions = [
298 2
            'body' => $autodiscoverXml,
299 2
            'timeout' => 2,
300 2
            'allow_redirects' => true,
301
            'headers' => [
302
                'Content-Type' => 'text/xml; charset=utf-8'
303 2
            ],
304 2
            'curl' => []
305 2
        ];
306 2
        $auth = ExchangeWebServicesAuth::fromUsernameAndPassword($username, $password);
307 2
        $postOptions = array_replace_recursive($postOptions, $auth);
308
309
        try {
310 2
            $response = $client->post($url, $postOptions);
311 2
        } catch (\Exception $e) {
312 2
            return false;
313
        }
314
315 1
        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 1
    protected function parseAutodiscoverResponse($response)
327
    {
328
        // Content-type isn't trustworthy, unfortunately. Shame on Microsoft.
329 1
        if (substr($response, 0, 5) !== '<?xml') {
330
            throw new AutodiscoverFailed();
331
        }
332
333 1
        $response = $this->responseToArray($response);
334
335 1
        if (isset($response['Error'])) {
336
            return false;
337
        }
338
339 1
        $action = $response['Account']['Action'];
340 1
        if ($action == 'redirectUrl' || $action == 'redirectAddr') {
341
            return false;
342
        }
343
344 1
        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 1
    protected function responseToArray($xml)
371
    {
372 1
        $xml = simplexml_load_string($xml, "SimpleXMLElement", LIBXML_NOCDATA);
373
374 1
        return json_decode(json_encode($xml), true)['Response'];
375
    }
376
}
377