Completed
Push — master ( 3aa769...65f5e6 )
by Gareth
05:03 queued 01:51
created

ExchangeWebServices   B

Complexity

Total Complexity 54

Size/Duplication

Total Lines 462
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 88.57%

Importance

Changes 16
Bugs 2 Features 2
Metric Value
wmc 54
c 16
b 2
f 2
lcom 1
cbo 10
dl 0
loc 462
ccs 155
cts 175
cp 0.8857
rs 7.0642

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getPrimarySmtpMailbox() 0 4 1
A getPrimarySmtpEmailAddress() 0 8 2
A setPrimarySmtpEmailAddress() 0 8 1
A setTimezone() 0 4 1
A getVersion() 0 4 1
A getServer() 0 4 1
A __construct() 0 12 2
A fromUsernameAndPassword() 0 8 1
A fromCallbackToken() 0 8 1
B createClient() 0 32 3
A __call() 0 8 1
A getClient() 0 4 1
B cleanServerUrl() 0 21 5
A processResponse() 0 19 3
B drillDownResponseLevels() 0 24 6
B getItemsFromResponse() 0 22 5
B handleNonSuccessfulResponses() 0 18 6
C buildMiddlewareStack() 0 64 10
B executeMiddlewareStack() 0 25 3

How to fix   Complexity   

Complex Class

Complex classes like ExchangeWebServices 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 ExchangeWebServices, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Contains ExchangeWebServices.
4
 */
5
6
7
namespace garethp\ews\API;
8
9
use garethp\ews\API\Exception\ExchangeException;
10
use garethp\ews\API\Exception\NoResponseReturnedException;
11
use garethp\ews\API\Exception\ServiceUnavailableException;
12
use garethp\ews\API\Exception\UnauthorizedException;
13
use garethp\ews\API\Message;
14
use garethp\ews\API\Type\EmailAddressType;
15
use garethp\ews\API\Type\FindFolderParentType;
16
use garethp\ews\API\Type\FindItemParentType;
17
use \Closure;
18
19
/**
20
 * Base class of the Exchange Web Services application.
21
 *
22
 *
23
 *
24
 * @package php-ews\Client
25
 *
26
 * @method Type AddDelegate($request)
27
 * @method Type ApplyConversationAction($request)
28
 * @method Type ConvertId($request)
29
 * @method Type CopyFolder($request)
30
 * @method Type CopyItem($request)
31
 * @method Type CreateAttachment($request)
32
 * @method Type CreateFolder($request)
33
 * @method Type CreateItem($request)
34
 * @method Type CreateManagedFolder($request)
35
 * @method Type CreateUserConfiguration($request)
36
 * @method Type DeleteAttachment($request)
37
 * @method Type DeleteFolder($request)
38
 * @method Type DeleteItem($request)
39
 * @method Type DeleteUserConfiguration($request)
40
 * @method Type DisconnectPhoneCall($request)
41
 * @method Type EmptyFolder($request)
42
 * @method Type ExpandDL($request)
43
 * @method Type ExportItems($request)
44
 * @method Type FindConversation($request)
45
 * @method Type FindFolder($request)
46
 * @method Type FindItem($request)
47
 * @method Type FindMessageTrackingReport($request)
48
 * @method Type GetAttachment($request)
49
 * @method Type GetDelegate($request)
50
 * @method Type GetEvents($request)
51
 * @method Type GetFolder($request)
52
 * @method Type GetInboxRules($request)
53
 * @method Type GetItem($request)
54
 * @method Type GetMailTips($request)
55
 * @method Type GetMessageTrackingReport($request)
56
 * @method Type GetPasswordExpirationDate($request)
57
 * @method Type GetPhoneCallInformation($request)
58
 * @method Type GetRoomLists($request)
59
 * @method Type GetRooms($request)
60
 * @method Type GetServerTimeZones($request)
61
 * @method Type GetServiceConfiguration($request)
62
 * @method Type GetSharingFolder($request)
63
 * @method Type GetSharingMetadata($request)
64
 * @method Type GetStreamingEvents($request)
65
 * @method Type GetUserAvailability($request)
66
 * @method Type GetUserConfiguration($request)
67
 * @method Type GetUserOofSettings($request)
68
 * @method Type MoveFolder($request)
69
 * @method Type MoveItem($request)
70
 * @method Type PlayOnPhone($request)
71
 * @method Type RefreshSharingFolder($request)
72
 * @method Type RemoveDelegate($request)
73
 * @method Type ResolveNames($request)
74
 * @method Type SendItem($request)
75
 * @method Type SetUserOofSettings($request)
76
 * @method Type Subscribe($request)
77
 * @method Type SyncFolderHierarchy($request)
78
 * @method Type SyncFolderItems($request)
79
 * @method Type Unsubscribe($request)
80
 * @method Type UpdateDelegate($request)
81
 * @method Type UpdateFolder($request)
82
 * @method Type UpdateInboxRules($request)
83
 * @method Type UpdateItem($request)
84
 * @method Type UpdateUserConfiguration($request)
85
 * @method Type UploadItems($request)
86
 */
87
class ExchangeWebServices
88
{
89
    const VERSION_2007 = 'Exchange2007';
90
91
    const VERSION_2007_SP1 = 'Exchange2007_SP1';
92
93
    const VERSION_2010 = 'Exchange2010';
94
95
    const VERSION_2010_SP1 = 'Exchange2010_SP1';
96
97
    const VERSION_2010_SP2 = 'Exchange2010_SP2';
98
99
    const VERSION_2013 = 'Exchange2013';
100
101
    const VERSION_2013_SP1 = 'Exchange2013_SP1';
102
103
    /**
104
     * Password to use when connecting to the Exchange server.
105
     *
106
     * @var string
107
     */
108
    protected $password = null;
109
110
    /**
111
     * Location of the Exchange server.
112
     *
113
     * @var string
114
     */
115
    protected $server = null;
116
117
    /**
118
     * SOAP client used to make the request
119
     *
120
     * @var NTLMSoapClient
121
     */
122
    protected $soap = null;
123
124
    /**
125
     * Username to use when connecting to the Exchange server.
126
     *
127
     * @var string
128
     */
129
    protected $username = null;
130
131
    /**
132
     * @var EmailAddressType
133
     */
134
    protected $primarySmtpMailbox = null;
135
136
    protected static $middlewareStack = false;
137
138
    /**
139
     * A setting to check whether or not responses should be drilled down before being
140
     * returned. Setting this to false
141
     * will return the raw responses without any filtering
142
     *
143
     * @var bool
144
     */
145
    protected $drillDownResponses = true;
146
147
    /**
148
     * Miscrosoft Exchange version that we are going to connect to
149
     *
150
     * @var string
151
     */
152
    protected $version = null;
153
154
    protected $options = null;
155
156
    /**
157
     * The timezone for the client
158
     *
159
     * @var bool
160
     */
161
    protected $timezone = false;
162
163
    /**
164
     * @return EmailAddressType
165
     */
166 26
    public function getPrimarySmtpMailbox()
167
    {
168 26
        return $this->primarySmtpMailbox;
169
    }
170
171 1
    public function getPrimarySmtpEmailAddress()
172
    {
173 1
        if ($this->primarySmtpMailbox == null) {
174 1
            return null;
175
        }
176
177 1
        return $this->primarySmtpMailbox->getEmailAddress();
178
    }
179
180 2
    public function setPrimarySmtpEmailAddress($emailAddress)
181
    {
182 2
        $mailbox = new EmailAddressType();
183 2
        $mailbox->setEmailAddress($emailAddress);
184 2
        $this->primarySmtpMailbox = $mailbox;
185
186 2
        return $this;
187
    }
188
189
    /**
190
     * @param boolean $timezone
191
     */
192
    public function setTimezone($timezone)
193
    {
194
        $this->timezone = $timezone;
195
    }
196
197
    /**
198
     * @return string
199
     */
200
    public function getVersion()
201
    {
202
        return $this->version;
203
    }
204
205
    /**
206
     * @return string
207
     */
208
    public function getServer()
209
    {
210
        return $this->server;
211
    }
212
213
    /**
214
     * Constructor for the ExchangeWebServices class
215
     *
216
     * @param string $server
217
     * @param string $username
218
     * @param string $password
219
     * @param array $options
220
     */
221 34
    protected function __construct($server = null, $username = null, $password = null, $options = array())
222
    {
223 34
        if ($server !== null) {
224
            $this->createClient(
225
                $server,
226
                ExchangeWebServicesAuth::fromUsernameAndPassword($username, $password),
227
                $options
228
            );
229
        }
230
231 34
        $this->buildMiddlewareStack();
232 34
    }
233
234 33
    public static function fromUsernameAndPassword($server, $username, $password, $options)
235
    {
236 33
        $self = new self();
237 33
        $self->createClient($server, ExchangeWebServicesAuth::fromUsernameAndPassword($username, $password), $options);
238 33
        $self->options = $options;
239
240 33
        return $self;
241
    }
242
243 1
    public static function fromCallbackToken($server, $token, $options)
244
    {
245 1
        $self = new self();
246 1
        $self->createClient($server, ExchangeWebServicesAuth::fromCallbackToken($token), $options);
247 1
        $self->options = $options;
248
249 1
        return $self;
250
    }
251
252 34
    protected function createClient($server, $auth, $options)
253
    {
254 34
        $location = 'https://' . $this->cleanServerUrl($server) . '/EWS/Exchange.asmx';
255
256 34
        $options = array_replace_recursive([
257 34
            'version' => self::VERSION_2007,
258 34
            'trace' => 1,
259 34
            'exceptions' => true,
260 34
            'classmap' => ClassMap::getClassMap(),
261
            'drillDownResponses' => true
262 34
        ], $options);
263
264 34
        $this->server = $server;
265 34
        $this->version = $options['version'];
266
267 34
        $this->soap = new NTLMSoapClient(
268 34
            $location,
269 34
            $auth,
270 34
            dirname(__FILE__) . '/../../Resources/wsdl/services.wsdl',
271
            $options
272 34
        );
273
274 34
        if (isset($options['primarySmtpEmailAddress'])) {
275 1
            $this->setPrimarySmtpEmailAddress($options['primarySmtpEmailAddress']);
276 1
        }
277
278 34
        if (isset($options['impersonation'])) {
279 1
            $this->setPrimarySmtpEmailAddress($options['impersonation']);
280 1
        }
281
282 34
        $this->drillDownResponses = $options['drillDownResponses'];
283 34
    }
284
285
    /**
286
     * @codeCoverageIgnore
287
     *
288
     * @param $name
289
     * @param $arguments
290
     * @return Type
291
     * @throws \garethp\ews\API\Exception
292
     */
293
    public function __call($name, $arguments)
294
    {
295
        $request = MiddlewareRequest::newRequest($name, $arguments, $this->options);
296
        $response = $this->executeMiddlewareStack(self::$middlewareStack, $request);
0 ignored issues
show
Documentation introduced by
self::$middlewareStack is of type boolean, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
297
        $response = $response->getResponse();
298
        return $response;
299
        return $this->processResponse($response);
0 ignored issues
show
Unused Code introduced by
return $this->processResponse($response); does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
300
    }
301
302
    /**
303
     * Returns the SOAP Client that may be used to make calls against the server
304
     *
305
     * @return NTLMSoapClient
306
     */
307 32
    public function getClient()
308
    {
309 32
        return $this->soap;
310
    }
311
312
    /**
313
     * Cleans the server URL for usage
314
     *
315
     * @param $server
316
     * @return string
317
     */
318 2
    public function cleanServerUrl($server)
319
    {
320 2
        $url = parse_url($server);
321
        if (!isset($url['host']) && isset($url['path'])) {
322 2
            $url['host'] = $url['path'];
323
            unset($url['path']);
324
        }
325
326
        $server = $url['host'];
327
        if (isset($url['port'])) {
328
            $server .= ':' . $url['port'];
329
        }
330
331 41
        if (isset($url['path'])) {
332
            $server .= $url['path'];
333 41
        }
334 41
335 36
        $server = rtrim($server, "/");
336 36
337 36
        return $server;
338
    }
339 41
340 41
    /**
341 2
     * Process a response to verify that it succeeded and take the appropriate
342 2
     * action
343
     *
344 41
     * @param \garethp\ews\API\Message\BaseResponseMessageType $response
345 4
     * @return Type[]
346 4
     * @throws \garethp\ews\API\Exception
347
     */
348 41
    protected function processResponse($response)
349
    {
350 41
        // If the soap call failed then we need to thow an exception.
351
        $code = $this->getClient()->getResponseCode();
352
        $this->handleNonSuccessfulResponses($response, $code);
353
354
        if (!$this->drillDownResponses) {
355
            return $response;
356
        }
357
358
        if (!$response->exists('responseMessages')) {
359
            return $response;
360
        }
361 30
362
        $response = $response->getResponseMessages();
363
        $response = $this->drillDownResponseLevels($response);
364 30
365 30
        return $response;
366
    }
367 28
368
    /**
369
     * @param $response
370
     * @return array
371 28
     * @throws \garethp\ews\API\Exception
372
     */
373
    public function drillDownResponseLevels($response)
374
    {
375 28
        $items = $this->getItemsFromResponse($response);
376 28
377
        if (count($items) == 1) {
378 28
            reset($items);
379
            $key = key($items);
380
            $methodName = "get$key";
381
            $response = $response->$methodName();
382
383
            return $this->drillDownResponseLevels($response);
384
        }
385
386 28
        if (is_array($items) && isset($items[1]) && $items[1] instanceof Message\ResponseMessageType) {
0 ignored issues
show
Bug introduced by
The class garethp\ews\API\Message\ResponseMessageType does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
387
            $response = array();
388 28
            foreach ($items as $responseItem) {
389
                $response[] = $this->drillDownResponseLevels($responseItem);
390 28
            }
391 28
392 28
            return $response;
393 28
        }
394 28
395
        return $response;
396 28
    }
397
398
    /**
399 28
     * @param $response
400 2
     * @return array
401 2
     * @throws ExchangeException
402 2
     */
403 2
    protected function getItemsFromResponse($response)
404
    {
405 2
        $items = array();
406
        if ($response instanceof Type) {
407
            $items = $response->getNonNullItems();
408 28
        }
409
410
        if (is_array($response)) {
411
            $items = $response;
412
        }
413
414
        if ($response instanceof Message\ResponseMessageType) {
0 ignored issues
show
Bug introduced by
The class garethp\ews\API\Message\ResponseMessageType does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
415
            if ($response->getResponseClass() !== "Success") {
416 28
                throw new ExchangeException($response->getMessageText());
417
            }
418 28
419 28
            unset($items['responseClass']);
420 28
            unset($items['responseCode']);
421 28
        }
422
423 28
        return $items;
424 2
    }
425 2
426
    /**
427 28
     * @param Message\BaseResponseMessageType $response
428 28
     * @param $code
429 1
     * @throws ExchangeException
430
     * @throws NoResponseReturnedException
431
     * @throws ServiceUnavailableException
432 28
     * @throws UnauthorizedException
433 28
     */
434 28
    protected function handleNonSuccessfulResponses($response, $code)
435
    {
436 28
        if ($code == 401) {
437
            throw new UnauthorizedException();
438
        }
439
440
        if ($code == 503) {
441
            throw new ServiceUnavailableException();
442
        }
443
444
        if ($code >= 300) {
445
            throw new ExchangeException('SOAP client returned status of ' . $code, $code);
446
        }
447 30
448
        if (empty($response) || empty($response->getNonNullResponseMessages())) {
449 30
            throw new NoResponseReturnedException();
450
        }
451
    }
452
453 30
    protected function buildMiddlewareStack()
454
    {
455
        if (self::$middlewareStack === false) {
456
            self::$middlewareStack = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array(function (\garethp... return $response; }) of type array<integer,object<Clo..."4":"object<Closure>"}> is incompatible with the declared type boolean of property $middlewareStack.

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...
457 30
                //Make the actual SOAP call
458 2
                function (MiddlewareRequest $request) {
459
                    $client = $this->getClient();
460
                    $response = $client->__call($request->getName(), $request->getArguments());
0 ignored issues
show
Documentation introduced by
$request->getArguments() is of type array<integer,?,{"0":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
461 28
                    $response = MiddlewareResponse::newResponse($response);
462
463
                    return $response;
464 28
                },
465
466 34
                //Transform an objcet of type Type to an XML Object
467
                function (MiddlewareRequest $request, callable $next) {
468 34
                    if ($request->getRequest() instanceof Type) {
469 1
                        $request->setRequest($request->getRequest()->toXmlObject());
470
                    }
471 1
472
                    return $next($request);
473
                },
474 28
475 28
                //The SyncScope option isn't available for Exchange 2007 SP1 and below
476 28
                function (MiddlewareRequest $request, callable $next) {
477
                    $options = $request->getOptions();
478 28
                    $version2007SP1 = ($options['version'] == ExchangeWebServices::VERSION_2007
479 1
                        || $options['version'] == ExchangeWebServices::VERSION_2007_SP1);
480
481
                    $requestObj = $request->getName();
482
483 28
                    if ($request->getName() == "SyncFolderItems" && $version2007SP1 && isset($requestObj->SyncScope)) {
484 28
                        unset($requestObj->SyncScope);
485 28
                        $request->setRequest($requestObj);
486
                    }
487 28
488 1
                    return $next($request);
489
                },
490
491
                //Add response processing
492 28
                function (MiddlewareRequest $request, callable $next) {
493 28
                    $response = $next($request);
494 28
495
                    $response->setResponse($this->processResponse($response->getResponse()));
496 28
497
                    return $response;
498 28
                },
499
500
                //Adds last request to FindFolder and FindItem responses
501
                function (MiddlewareRequest $request, callable $next) {
502
                    $response = $next($request);
503 28
504 1
                    $responseObject = $response->getResponse();
505
                    if (($responseObject instanceof FindItemParentType
0 ignored issues
show
Bug introduced by
The class garethp\ews\API\Type\FindItemParentType does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
506
                            || $responseObject instanceof FindFolderParentType)
0 ignored issues
show
Bug introduced by
The class garethp\ews\API\Type\FindFolderParentType does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
507
                        && !$responseObject->isIncludesLastItemInRange()) {
508 28
                        $responseObject->setLastRequest($request->getRequest());
509
                        $response->setResponse($responseObject);
510 28
                    }
511
512 28
                    return $response;
513 1
                }
514
            ];
515
        }
516
    }
517 28
518
    /**
519 28
     * @param array $middlewareStack
520
     * @param MiddlewareRequest $request
521 28
     * @return MiddlewareResponse
522 28
     */
523 1
    protected function executeMiddlewareStack(array $middlewareStack, MiddlewareRequest $request)
524 1
    {
525 1
        $newStack = [];
526
        foreach ($middlewareStack as $key => $current) {
527 28
            /** @var $current callable */
528
            $last = function () { };
529 1
530 1
            if ($key != 0) {
531 34
                $last = $newStack[$key - 1];
532
            }
533
534
            $current = Closure::bind($current, $this);
535
            $newStack[] = function (MiddlewareRequest $request) use ($current, $last) {
536
                return $current($request, $last);
537
            };
538 28
        }
539
540 28
        /** @var $newStack callable[] */
541 28
        $newStack = array_reverse($newStack);
542
543
        $top = $newStack[0];
544
545 28
        /** @var $top callable */
546 28
        return $top($request);
547 28
    }
548
}
549