Completed
Push — master ( ba1c59...9de6f9 )
by Gareth
03:03
created

ExchangeWebServices   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 451
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 67.46%

Importance

Changes 12
Bugs 1 Features 2
Metric Value
wmc 52
c 12
b 1
f 2
lcom 1
cbo 10
dl 0
loc 451
ccs 114
cts 169
cp 0.6746
rs 7.9487

20 Methods

Rating   Name   Duplication   Size   Complexity  
B handleNonSuccessfulResponses() 0 18 6
A getClient() 0 4 1
A setClient() 0 6 1
B cleanServerUrl() 0 21 5
A getPrimarySmtpMailbox() 0 4 1
A getPrimarySmtpEmailAddress() 0 8 2
A setPrimarySmtpEmailAddress() 0 8 1
A setTimezone() 0 4 1
A getServer() 0 4 1
A __construct() 0 12 2
A fromUsernameAndPassword() 0 8 1
A fromCallbackToken() 0 8 1
A getVersion() 0 4 1
B createClient() 0 32 3
A __call() 0 8 1
A processResponse() 0 19 3
B drillDownResponseLevels() 0 24 6
B getItemsFromResponse() 0 22 5
C buildMiddlewareStack() 0 42 7
A executeMiddlewareStack() 0 23 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
16
/**
17
 * Base class of the Exchange Web Services application.
18
 *
19
 *
20
 *
21
 * @package php-ews\Client
22
 *
23
 * @method Type AddDelegate($request)
24
 * @method Type ApplyConversationAction($request)
25
 * @method Type ConvertId($request)
26
 * @method Type CopyFolder($request)
27
 * @method Type CopyItem($request)
28
 * @method Type CreateAttachment($request)
29
 * @method Type CreateFolder($request)
30
 * @method Type CreateItem($request)
31
 * @method Type CreateManagedFolder($request)
32
 * @method Type CreateUserConfiguration($request)
33
 * @method Type DeleteAttachment($request)
34
 * @method Type DeleteFolder($request)
35
 * @method Type DeleteItem($request)
36
 * @method Type DeleteUserConfiguration($request)
37
 * @method Type DisconnectPhoneCall($request)
38
 * @method Type EmptyFolder($request)
39
 * @method Type ExpandDL($request)
40
 * @method Type ExportItems($request)
41
 * @method Type FindConversation($request)
42
 * @method Type FindFolder($request)
43
 * @method Type FindItem($request)
44
 * @method Type FindMessageTrackingReport($request)
45
 * @method Type GetAttachment($request)
46
 * @method Type GetDelegate($request)
47
 * @method Type GetEvents($request)
48
 * @method Type GetFolder($request)
49
 * @method Type GetInboxRules($request)
50
 * @method Type GetItem($request)
51
 * @method Type GetMailTips($request)
52
 * @method Type GetMessageTrackingReport($request)
53
 * @method Type GetPasswordExpirationDate($request)
54
 * @method Type GetPhoneCallInformation($request)
55
 * @method Type GetRoomLists($request)
56
 * @method Type GetRooms($request)
57
 * @method Type GetServerTimeZones($request)
58
 * @method Type GetServiceConfiguration($request)
59
 * @method Type GetSharingFolder($request)
60
 * @method Type GetSharingMetadata($request)
61
 * @method Type GetStreamingEvents($request)
62
 * @method Type GetUserAvailability($request)
63
 * @method Type GetUserConfiguration($request)
64
 * @method Type GetUserOofSettings($request)
65
 * @method Type MoveFolder($request)
66
 * @method Type MoveItem($request)
67
 * @method Type PlayOnPhone($request)
68
 * @method Type RefreshSharingFolder($request)
69
 * @method Type RemoveDelegate($request)
70
 * @method Type ResolveNames($request)
71
 * @method Type SendItem($request)
72
 * @method Type SetUserOofSettings($request)
73
 * @method Type Subscribe($request)
74
 * @method Type SyncFolderHierarchy($request)
75
 * @method Type SyncFolderItems($request)
76
 * @method Type Unsubscribe($request)
77
 * @method Type UpdateDelegate($request)
78
 * @method Type UpdateFolder($request)
79
 * @method Type UpdateInboxRules($request)
80
 * @method Type UpdateItem($request)
81
 * @method Type UpdateUserConfiguration($request)
82
 * @method Type UploadItems($request)
83
 */
84
class ExchangeWebServices
85
{
86
    const VERSION_2007 = 'Exchange2007';
87
88
    const VERSION_2007_SP1 = 'Exchange2007_SP1';
89
90
    const VERSION_2010 = 'Exchange2010';
91
92
    const VERSION_2010_SP1 = 'Exchange2010_SP1';
93
94
    const VERSION_2010_SP2 = 'Exchange2010_SP2';
95
96
    const VERSION_2013 = 'Exchange2013';
97
98
    const VERSION_2013_SP1 = 'Exchange2013_SP1';
99
100
    /**
101
     * Password to use when connecting to the Exchange server.
102
     *
103
     * @var string
104
     */
105
    protected $password = null;
106
107
    /**
108
     * Location of the Exchange server.
109
     *
110
     * @var string
111
     */
112
    protected $server = null;
113
114
    /**
115
     * SOAP client used to make the request
116
     *
117
     * @var NTLMSoapClient
118
     */
119
    protected $soap = null;
120
121
    /**
122
     * Username to use when connecting to the Exchange server.
123
     *
124
     * @var string
125
     */
126
    protected $username = null;
127
128
    /**
129
     * @var EmailAddressType
130
     */
131
    protected $primarySmtpMailbox = null;
132
133
    protected static $middlewareStack = false;
134
135
    /**
136
     * A setting to check whether or not responses should be drilled down before being
137
     * returned. Setting this to false
138
     * will return the raw responses without any filtering
139
     *
140
     * @var bool
141
     */
142
    protected $drillDownResponses = true;
143
144
    /**
145
     * Miscrosoft Exchange version that we are going to connect to
146
     *
147
     * @var string
148
     */
149
    protected $version = null;
150
151
    protected $options = null;
152
153
    /**
154
     * The timezone for the client
155
     *
156
     * @var bool
157
     */
158
    protected $timezone = false;
159
160
    /**
161
     * @return EmailAddressType
162
     */
163 25
    public function getPrimarySmtpMailbox()
164
    {
165 25
        return $this->primarySmtpMailbox;
166
    }
167
168 1
    public function getPrimarySmtpEmailAddress()
169
    {
170 1
        if ($this->primarySmtpMailbox == null) {
171 1
            return null;
172
        }
173
174 1
        return $this->primarySmtpMailbox->getEmailAddress();
175
    }
176
177 2
    public function setPrimarySmtpEmailAddress($emailAddress)
178
    {
179 2
        $mailbox = new EmailAddressType();
180 2
        $mailbox->setEmailAddress($emailAddress);
181 2
        $this->primarySmtpMailbox = $mailbox;
182
183 2
        return $this;
184
    }
185
186
    /**
187
     * @param boolean $timezone
188
     */
189
    public function setTimezone($timezone)
190
    {
191
        $this->timezone = $timezone;
192
    }
193
194
    /**
195
     * @return string
196
     */
197
    public function getVersion()
198
    {
199
        return $this->version;
200
    }
201
202
    /**
203
     * @return string
204
     */
205
    public function getServer()
206
    {
207
        return $this->server;
208
    }
209
210
    /**
211
     * Constructor for the ExchangeWebServices class
212
     *
213
     * @param string $server
214
     * @param string $username
215
     * @param string $password
216
     * @param array $options
217
     */
218 33
    protected function __construct($server = null, $username = null, $password = null, $options = array())
219
    {
220 33
        if ($server !== null) {
221
            $this->createClient(
222
                $server,
223
                ExchangeWebServicesAuth::fromUsernameAndPassword($username, $password),
224
                $options
225
            );
226
        }
227
228 33
        $this->buildMiddlewareStack();
229 33
    }
230
231 32
    public static function fromUsernameAndPassword($server, $username, $password, $options)
232
    {
233 32
        $self = new self();
234 32
        $self->createClient($server, ExchangeWebServicesAuth::fromUsernameAndPassword($username, $password), $options);
235 32
        $self->options = $options;
236
237 32
        return $self;
238
    }
239
240 1
    public static function fromCallbackToken($server, $token, $options)
241
    {
242 1
        $self = new self();
243 1
        $self->createClient($server, ExchangeWebServicesAuth::fromCallbackToken($token), $options);
244 1
        $self->options = $options;
245
246 1
        return $self;
247
    }
248
249 33
    protected function createClient($server, $auth, $options)
250
    {
251 33
        $location = 'https://' . $this->cleanServerUrl($server) . '/EWS/Exchange.asmx';
252
253 33
        $options = array_replace_recursive([
254 33
            'version' => self::VERSION_2007,
255 33
            'trace' => 1,
256 33
            'exceptions' => true,
257 33
            'classmap' => ClassMap::getClassMap(),
258
            'drillDownResponses' => true
259 33
        ], $options);
260
261 33
        $this->server = $server;
262 33
        $this->version = $options['version'];
263
264 33
        $this->soap = new NTLMSoapClient(
265 33
            $location,
266 33
            $auth,
267 33
            dirname(__FILE__) . '/../../Resources/wsdl/services.wsdl',
268
            $options
269 33
        );
270
271 33
        if (isset($options['primarySmtpEmailAddress'])) {
272 1
            $this->setPrimarySmtpEmailAddress($options['primarySmtpEmailAddress']);
273 1
        }
274
275 33
        if (isset($options['impersonation'])) {
276 1
            $this->setPrimarySmtpEmailAddress($options['impersonation']);
277 1
        }
278
279 33
        $this->drillDownResponses = $options['drillDownResponses'];
280 33
    }
281
282
    /**
283
     * @codeCoverageIgnore
284
     *
285
     * @param $name
286
     * @param $arguments
287
     * @return Type
288
     * @throws \garethp\ews\API\Exception
289
     */
290
    public function __call($name, $arguments)
291
    {
292
        $request = MiddlewareRequest::newRequest($name, $arguments, $this->options);
293
        $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...
294
        $response = $response->getResponse();
295
296
        return $this->processResponse($response);
297
    }
298
299
    /**
300
     * Returns the SOAP Client that may be used to make calls against the server
301
     *
302
     * @return NTLMSoapClient
303
     */
304 31
    public function getClient()
305
    {
306 31
        return $this->soap;
307
    }
308
309
    /**
310
     * Sets the client
311
     *
312
     * @param NTLMSoapClient $client
313
     * @return $this
314
     */
315 2
    public function setClient($client)
316
    {
317 2
        $this->soap = $client;
318
319 2
        return $this;
320
    }
321
322
    /**
323
     * Cleans the server URL for usage
324
     *
325
     * @param $server
326
     * @return string
327
     */
328 40
    public function cleanServerUrl($server)
329
    {
330 40
        $url = parse_url($server);
331 40
        if (!isset($url['host']) && isset($url['path'])) {
332 35
            $url['host'] = $url['path'];
333 35
            unset($url['path']);
334 35
        }
335
336 40
        $server = $url['host'];
337 40
        if (isset($url['port'])) {
338 2
            $server .= ':' . $url['port'];
339 2
        }
340
341 40
        if (isset($url['path'])) {
342 4
            $server .= $url['path'];
343 4
        }
344
345 40
        $server = rtrim($server, "/");
346
347 40
        return $server;
348
    }
349
350
    /**
351
     * Process a response to verify that it succeeded and take the appropriate
352
     * action
353
     *
354
     * @param \garethp\ews\API\Message\BaseResponseMessageType $response
355
     * @return Type[]
356
     * @throws \garethp\ews\API\Exception
357
     */
358 29
    protected function processResponse($response)
359
    {
360
        // If the soap call failed then we need to thow an exception.
361 29
        $code = $this->getClient()->getResponseCode();
362 29
        $this->handleNonSuccessfulResponses($response, $code);
363
364
        if (!$this->drillDownResponses) {
365
            return $response;
366
        }
367
368
        if (!$response->exists('responseMessages')) {
369
            return $response;
370
        }
371
372
        $response = $response->getResponseMessages();
373
        $response = $this->drillDownResponseLevels($response);
374
375
        return $response;
376
    }
377
378
    /**
379
     * @param $response
380
     * @return array
381
     * @throws \garethp\ews\API\Exception
382
     */
383
    public function drillDownResponseLevels($response)
384
    {
385
        $items = $this->getItemsFromResponse($response);
386
387
        if (count($items) == 1) {
388
            reset($items);
389
            $key = key($items);
390
            $methodName = "get$key";
391
            $response = $response->$methodName();
392
393
            return $this->drillDownResponseLevels($response);
394
        }
395
396
        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...
397
            $response = array();
398
            foreach ($items as $responseItem) {
399
                $response[] = $this->drillDownResponseLevels($responseItem);
400
            }
401
402
            return $response;
403
        }
404
405
        return $response;
406
    }
407
408
    /**
409
     * @param $response
410
     * @return array
411
     * @throws ExchangeException
412
     */
413
    protected function getItemsFromResponse($response)
414
    {
415
        $items = array();
416
        if ($response instanceof Type) {
417
            $items = $response->getNonNullItems();
418
        }
419
420
        if (is_array($response)) {
421
            $items = $response;
422
        }
423
424
        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...
425
            if ($response->getResponseClass() !== "Success") {
426
                throw new ExchangeException($response->getMessageText());
427
            }
428
429
            unset($items['responseClass']);
430
            unset($items['responseCode']);
431
        }
432
433
        return $items;
434
    }
435
436
    /**
437
     * @param Message\BaseResponseMessageType $response
438
     * @param $code
439
     * @throws ExchangeException
440
     * @throws NoResponseReturnedException
441
     * @throws ServiceUnavailableException
442
     * @throws UnauthorizedException
443
     */
444 29
    protected function handleNonSuccessfulResponses($response, $code)
445
    {
446 29
        if ($code == 401) {
447
            throw new UnauthorizedException();
448
        }
449
450 29
        if ($code == 503) {
451
            throw new ServiceUnavailableException();
452
        }
453
454 29
        if ($code >= 300) {
455 2
            throw new ExchangeException('SOAP client returned status of ' . $code, $code);
456
        }
457
458 27
        if (empty($response) || empty($response->getNonNullResponseMessages())) {
459 27
            throw new NoResponseReturnedException();
460
        }
461
    }
462
463 33
    protected function buildMiddlewareStack()
464
    {
465 33
        if (self::$middlewareStack === false) {
466 1
            $ews = $this;
467
468 1
            self::$middlewareStack = [
0 ignored issues
show
Documentation Bug introduced by
It seems like array(function (\garethp...urn $next($request); }) of type array<integer,object<Clo..."2":"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...
469
                //Make the actual SOAP call
470
                function (MiddlewareRequest $request, callable $next = null) use ($ews) {
0 ignored issues
show
Unused Code introduced by
The parameter $next is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
471 27
                    $client = $ews->getClient();
472 27
                    $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...
473 27
                    $response = MiddlewareResponse::newResponse($response);
474
475 27
                    return $response;
476 1
                },
477
478
                //Transform an objcet of type Type to an XML Object
479
                function (MiddlewareRequest $request, callable $next) {
480 27
                    if ($request->getRequest() instanceof Type) {
481 27
                        $request->setRequest($request->getRequest()->toXmlObject());
482 27
                    }
483
484 27
                    return $next($request);
485 1
                },
486
487
                //The SyncScope option isn't available for Exchange 2007 SP1 and below
488
                function (MiddlewareRequest $request, callable $next) {
489 27
                    $options = $request->getOptions();
490 27
                    $version2007SP1 = ($options['version'] == ExchangeWebServices::VERSION_2007
491 27
                        || $options['version'] == ExchangeWebServices::VERSION_2007_SP1);
492
493 27
                    $requestObj = $request->getName();
494
495 27
                    if ($request->getName() == "SyncFolderItems" && $version2007SP1 && isset($requestObj->SyncScope)) {
496
                        unset($requestObj->SyncScope);
497
                        $request->setRequest($requestObj);
498
                    }
499
500 27
                    return $next($request);
501
                }
502 1
            ];
503 1
        }
504 33
    }
505
506
    /**
507
     * @param array $middlewareStack
508
     * @param MiddlewareRequest $request
509
     * @return MiddlewareResponse
510
     */
511 27
    protected function executeMiddlewareStack(array $middlewareStack, MiddlewareRequest $request)
512
    {
513 27
        $newStack = [];
514 27
        foreach ($middlewareStack as $key => $current) {
515
            /** @var $current callable */
516 27
            if ($key == 0) {
517
                $last = function (MiddlewareRequest $request) {
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
518 27
                };
519 27
            } else {
520 27
                $last = $newStack[$key - 1];
521
            }
522
523 27
            $newStack[] = function (MiddlewareRequest $request) use ($current, $last) {
524 27
                return $current($request, $last);
525
            };
526 27
        }
527
528 27
        $newStack = array_reverse($newStack);
529
530 27
        $top = $newStack[0];
531
532 27
        return $top($request);
533
    }
534
}
535