Completed
Push — master ( 650999...e7806b )
by Gareth
03:32
created

ExchangeWebServices::executeMiddlewareStack()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3

Importance

Changes 5
Bugs 1 Features 0
Metric Value
c 5
b 1
f 0
dl 0
loc 26
ccs 14
cts 14
cp 1
rs 8.8571
cc 3
eloc 13
nc 3
nop 2
crap 3
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
     * Sets the client
314
     *
315
     * @param NTLMSoapClient $client
316
     * @return $this
317
     */
318 2
    public function setClient($client)
319
    {
320 2
        $this->soap = $client;
321
322 2
        return $this;
323
    }
324
325
    /**
326
     * Cleans the server URL for usage
327
     *
328
     * @param $server
329
     * @return string
330
     */
331 41
    public function cleanServerUrl($server)
332
    {
333 41
        $url = parse_url($server);
334 41
        if (!isset($url['host']) && isset($url['path'])) {
335 36
            $url['host'] = $url['path'];
336 36
            unset($url['path']);
337 36
        }
338
339 41
        $server = $url['host'];
340 41
        if (isset($url['port'])) {
341 2
            $server .= ':' . $url['port'];
342 2
        }
343
344 41
        if (isset($url['path'])) {
345 4
            $server .= $url['path'];
346 4
        }
347
348 41
        $server = rtrim($server, "/");
349
350 41
        return $server;
351
    }
352
353
    /**
354
     * Process a response to verify that it succeeded and take the appropriate
355
     * action
356
     *
357
     * @param \garethp\ews\API\Message\BaseResponseMessageType $response
358
     * @return Type[]
359
     * @throws \garethp\ews\API\Exception
360
     */
361 30
    protected function processResponse($response)
362
    {
363
        // If the soap call failed then we need to thow an exception.
364 30
        $code = $this->getClient()->getResponseCode();
365 30
        $this->handleNonSuccessfulResponses($response, $code);
366
367 28
        if (!$this->drillDownResponses) {
368
            return $response;
369
        }
370
371 28
        if (!$response->exists('responseMessages')) {
372
            return $response;
373
        }
374
375 28
        $response = $response->getResponseMessages();
376 28
        $response = $this->drillDownResponseLevels($response);
377
378 28
        return $response;
379
    }
380
381
    /**
382
     * @param $response
383
     * @return array
384
     * @throws \garethp\ews\API\Exception
385
     */
386 28
    public function drillDownResponseLevels($response)
387
    {
388 28
        $items = $this->getItemsFromResponse($response);
389
390 28
        if (count($items) == 1) {
391 28
            reset($items);
392 28
            $key = key($items);
393 28
            $methodName = "get$key";
394 28
            $response = $response->$methodName();
395
396 28
            return $this->drillDownResponseLevels($response);
397
        }
398
399 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...
400 2
            $response = array();
401 2
            foreach ($items as $responseItem) {
402 2
                $response[] = $this->drillDownResponseLevels($responseItem);
403 2
            }
404
405 2
            return $response;
406
        }
407
408 28
        return $response;
409
    }
410
411
    /**
412
     * @param $response
413
     * @return array
414
     * @throws ExchangeException
415
     */
416 28
    protected function getItemsFromResponse($response)
417
    {
418 28
        $items = array();
419 28
        if ($response instanceof Type) {
420 28
            $items = $response->getNonNullItems();
421 28
        }
422
423 28
        if (is_array($response)) {
424 2
            $items = $response;
425 2
        }
426
427 28
        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...
428 28
            if ($response->getResponseClass() !== "Success") {
429 1
                throw new ExchangeException($response->getMessageText());
430
            }
431
432 28
            unset($items['responseClass']);
433 28
            unset($items['responseCode']);
434 28
        }
435
436 28
        return $items;
437
    }
438
439
    /**
440
     * @param Message\BaseResponseMessageType $response
441
     * @param $code
442
     * @throws ExchangeException
443
     * @throws NoResponseReturnedException
444
     * @throws ServiceUnavailableException
445
     * @throws UnauthorizedException
446
     */
447 30
    protected function handleNonSuccessfulResponses($response, $code)
448
    {
449 30
        if ($code == 401) {
450
            throw new UnauthorizedException();
451
        }
452
453 30
        if ($code == 503) {
454
            throw new ServiceUnavailableException();
455
        }
456
457 30
        if ($code >= 300) {
458 2
            throw new ExchangeException('SOAP client returned status of ' . $code, $code);
459
        }
460
461 28
        if (empty($response) || empty($response->getNonNullResponseMessages())) {
462
            throw new NoResponseReturnedException();
463
        }
464 28
    }
465
466 34
    protected function buildMiddlewareStack()
467
    {
468 34
        if (self::$middlewareStack === false) {
469 1
            $ews = $this;
0 ignored issues
show
Unused Code introduced by
$ews is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
470
471 1
            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...
472
                //Make the actual SOAP call
473
                function (MiddlewareRequest $request, callable $next = null) {
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...
474 28
                    $client = $this->getClient();
475 28
                    $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...
476 28
                    $response = MiddlewareResponse::newResponse($response);
477
478 28
                    return $response;
479 1
                },
480
481
                //Transform an objcet of type Type to an XML Object
482
                function (MiddlewareRequest $request, callable $next) {
483 28
                    if ($request->getRequest() instanceof Type) {
484 28
                        $request->setRequest($request->getRequest()->toXmlObject());
485 28
                    }
486
487 28
                    return $next($request);
488 1
                },
489
490
                //The SyncScope option isn't available for Exchange 2007 SP1 and below
491
                function (MiddlewareRequest $request, callable $next) {
492 28
                    $options = $request->getOptions();
493 28
                    $version2007SP1 = ($options['version'] == ExchangeWebServices::VERSION_2007
494 28
                        || $options['version'] == ExchangeWebServices::VERSION_2007_SP1);
495
496 28
                    $requestObj = $request->getName();
497
498 28
                    if ($request->getName() == "SyncFolderItems" && $version2007SP1 && isset($requestObj->SyncScope)) {
499
                        unset($requestObj->SyncScope);
500
                        $request->setRequest($requestObj);
501
                    }
502
503 28
                    return $next($request);
504 1
                },
505
506
                //Add response processing
507
                function (MiddlewareRequest $request, callable $next) {
508 28
                    $response = $next($request);
509
510 28
                    $response->setResponse($this->processResponse($response->getResponse()));
511
512 28
                    return $response;
513 1
                },
514
515
                //Adds last request to FindFolder and FindItem responses
516
                function (MiddlewareRequest $request, callable $next) {
517 28
                    $response = $next($request);
518
519 28
                    $responseObject = $response->getResponse();
520
                    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...
521 28
                            || $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...
522 28
                        && !$responseObject->isIncludesLastItemInRange()) {
523 1
                        $responseObject->setLastRequest($request->getRequest());
524 1
                        $response->setResponse($responseObject);
525 1
                    }
526
527 28
                    return $response;
528
                }
529 1
            ];
530 1
        }
531 34
    }
532
533
    /**
534
     * @param array $middlewareStack
535
     * @param MiddlewareRequest $request
536
     * @return MiddlewareResponse
537
     */
538 28
    protected function executeMiddlewareStack(array $middlewareStack, MiddlewareRequest $request)
539
    {
540 28
        $newStack = [];
541 28
        foreach ($middlewareStack as $key => $current) {
542
            /** @var $current callable */
543 28
            if ($key == 0) {
544
                $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...
545 28
                };
546 28
            } else {
547 28
                $last = $newStack[$key - 1];
548
            }
549
550 28
            $current = Closure::bind($current, $this);
551 28
            $newStack[] = function (MiddlewareRequest $request) use ($current, $last) {
552 28
                return $current($request, $last);
553
            };
554 28
        }
555
556
        /** @var $newStack callable[] */
557 28
        $newStack = array_reverse($newStack);
558
559 28
        $top = $newStack[0];
560
561
        /** @var $top callable */
562 28
        return $top($request);
563
    }
564
}
565