Completed
Push — master ( 132976...bb2167 )
by Gareth
02:42
created

ExchangeAutodiscover   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 370
Duplicated Lines 3.78 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 93.02%

Importance

Changes 0
Metric Value
dl 14
loc 370
ccs 120
cts 129
cp 0.9302
rs 8.2769
c 0
b 0
f 0
wmc 41
lcom 1
cbo 7

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
B parseServerVersion() 0 36 4
A newAPI() 0 22 3
B getServerVersionFromResponse() 0 13 5
A getServerFromResponse() 0 10 4
A getAPI() 0 6 1
B discover() 0 22 5
A tryTopLevelDomain() 7 7 1
A tryAutoDiscoverSubDomain() 7 7 1
B trySubdomainUnauthenticatedGet() 0 26 4
A trySRVRecord() 0 14 2
B doNTLMPost() 0 32 2
B parseAutodiscoverResponse() 0 20 5
A getTopLevelDomainFromEmail() 0 9 2
A responseToArray() 0 6 1

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 ExchangeAutodiscover 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 ExchangeAutodiscover, and based on these observations, apply Extract Interface, too.

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