GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( a83ae8...8a5262 )
by François
02:30
created

RemoteStorageService   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 551
Duplicated Lines 3.09 %

Coupling/Cohesion

Components 1
Dependencies 21

Importance

Changes 34
Bugs 3 Features 5
Metric Value
wmc 65
c 34
b 3
f 5
lcom 1
cbo 21
dl 17
loc 551
rs 1.9075

11 Methods

Rating   Name   Duplication   Size   Complexity  
B getObject() 0 22 5
C getFolder() 9 46 7
C getDocument() 8 62 10
C putDocument() 0 43 8
C deleteDocument() 0 47 8
A optionsRequest() 0 4 1
A hasReadScope() 0 17 3
A hasWriteScope() 0 15 3
B stripQuotes() 0 26 6
C run() 0 47 7
C __construct() 0 199 7

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

1
<?php
2
3
/**
4
 *  This program is free software: you can redistribute it and/or modify
5
 *  it under the terms of the GNU Lesser General Public License as published by
6
 *  the Free Software Foundation, either version 3 of the License, or
7
 *  (at your option) any later version.
8
 *
9
 *  This program is distributed in the hope that it will be useful,
10
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 *  GNU Lesser General Public License for more details.
13
 *
14
 *  You should have received a copy of the GNU Lesser General Public License
15
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17
namespace fkooman\RemoteStorage;
18
19
use fkooman\Http\Exception\BadRequestException;
20
use fkooman\Http\Exception\ForbiddenException;
21
use fkooman\Http\Exception\NotFoundException;
22
use fkooman\Http\Exception\PreconditionFailedException;
23
use fkooman\Http\Exception\UnauthorizedException;
24
use fkooman\Http\Request;
25
use fkooman\Http\Response;
26
use fkooman\IO\IO;
27
use fkooman\OAuth\AccessTokenStorageInterface;
28
use fkooman\OAuth\ApprovalStorageInterface;
29
use fkooman\OAuth\AuthorizationCodeStorageInterface;
30
use fkooman\OAuth\ClientStorageInterface;
31
use fkooman\OAuth\OAuthService;
32
use fkooman\OAuth\ResourceServerStorageInterface;
33
use fkooman\RemoteStorage\Exception\PathException;
34
use fkooman\Rest\Plugin\Authentication\Bearer\Scope;
35
use fkooman\Rest\Plugin\Authentication\Bearer\TokenInfo;
36
use fkooman\Tpl\TemplateManagerInterface;
37
use InvalidArgumentException;
38
use fkooman\Rest\Plugin\Authentication\UserInfoInterface;
39
use fkooman\OAuth\Approval;
40
use fkooman\Http\RedirectResponse;
41
use fkooman\Json\Json;
42
43
class RemoteStorageService extends OAuthService
44
{
45
    /** @var RemoteStorage */
46
    private $remoteStorage;
47
48
    /** @var ApprovalManagementStorage */
49
    private $approvalManagementStorage;
50
51
    public function __construct(RemoteStorage $remoteStorage, ApprovalManagementStorage $approvalManagementStorage, TemplateManagerInterface $templateManager, ClientStorageInterface $clientStorage, ResourceServerStorageInterface $resourceServerStorage, ApprovalStorageInterface $approvalStorage, AuthorizationCodeStorageInterface $authorizationCodeStorage, AccessTokenStorageInterface $accessTokenStorage, array $options = array(), IO $io = null)
52
    {
53
        $this->remoteStorage = $remoteStorage;
54
        $this->approvalManagementStorage = $approvalManagementStorage;
55
56
        parent::__construct(
57
            $templateManager,
58
            $clientStorage,
59
            $resourceServerStorage,
60
            $approvalStorage,
61
            $authorizationCodeStorage,
62
            $accessTokenStorage,
63
            $options,
64
            $io
65
        );
66
67
        $this->get(
68
            '/_account',
69
            function (Request $request, UserInfoInterface $userInfo) {
70
                $approvalList = $this->approvalManagementStorage->getApprovalList($userInfo->getUserId());
71
72
                return $this->templateManager->render(
73
                    'getAccountPage',
74
                    array(
75
                        'approval_list' => $approvalList,
76
                        'host' => $request->getHeader('Host'),
77
                        'user_id' => $userInfo->getUserId(),
78
                        'disk_usage' => $this->remoteStorage->getFolderSize(new Path('/'.$userInfo->getUserId().'/')),
79
                        'request_url' => $request->getUrl()->toString(),
80
                        'show_account_icon' => true,
81
                    )
82
                );
83
            },
84
            array(
85
                'fkooman\Rest\Plugin\Authentication\AuthenticationPlugin' => array(
86
                    'activate' => array('user'),
87
                ),
88
            )
89
        );
90
91
        $this->delete(
92
            '/_approvals',
93
            function (Request $request, UserInfoInterface $userInfo) {
94
                $deleteApprovalRequest = RequestValidation::validateDeleteApprovalRequest($request);
95
96
                $approval = new Approval(
97
                    $userInfo->getUserId(),
98
                    $deleteApprovalRequest['client_id'],
99
                    $deleteApprovalRequest['response_type'],
100
                    $deleteApprovalRequest['scope']
101
                );
102
                $this->approvalManagementStorage->deleteApproval($approval);
103
104
                return new RedirectResponse($request->getUrl()->getRootUrl().'_account', 302);
105
            },
106
            array(
107
                'fkooman\Rest\Plugin\Authentication\AuthenticationPlugin' => array(
108
                    'activate' => array('user'),
109
                ),
110
            )
111
        );
112
113
        $this->get(
114
            '/.well-known/webfinger',
115
            function (Request $request) {
116
                $resource = $request->getUrl()->getQueryParameter('resource');
117
                if (null === $resource) {
118
                    throw new BadRequestException('resource parameter missing');
119
                }
120
                if (0 !== strpos($resource, 'acct:')) {
121
                    throw new BadRequestException('unsupported resource type');
122
                }
123
                $userAddress = substr($resource, 5);
124
                $atPos = strpos($userAddress, '@');
125
                if (false === $atPos) {
126
                    throw new BadRequestException('invalid user address');
127
                }
128
                $user = substr($userAddress, 0, $atPos);
129
                $host = substr($userAddress, $atPos + 1);
0 ignored issues
show
Unused Code introduced by
$host 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...
130
131
                //if($host !== $request->getUrl()->getHost()) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
69% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
132
                //    throw new BadRequestException(sprintf('host of webfinger resource does not match host of request %s', $host));
0 ignored issues
show
Unused Code Comprehensibility introduced by
63% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
133
                //}
134
135
                $webFingerData = array(
136
                    'links' => array(
137
                        array(
138
                            'href' => sprintf('%s%s', $request->getUrl()->getRootUrl(), $user),
139
                            'properties' => array(
140
                                'http://remotestorage.io/spec/version' => 'draft-dejong-remotestorage-05',
141
                                'http://remotestorage.io/spec/web-authoring' => null,
142
                                'http://tools.ietf.org/html/rfc6749#section-4.2' => sprintf('%s_oauth/authorize?login_hint=%s', $request->getUrl()->getRootUrl(), $user),
143
                                'http://tools.ietf.org/html/rfc6750#section-2.3' => null,
144
                                'http://tools.ietf.org/html/rfc7233' => 'development' !== $this->options['server_mode'] ? 'GET' : null,
145
                            ),
146
                            'rel' => 'http://tools.ietf.org/id/draft-dejong-remotestorage',
147
                        ),
148
                        // legacy -03 WebFinger response
149
                        array(
150
                            'href' => sprintf('%s%s', $request->getUrl()->getRootUrl(), $user),
151
                            'properties' => array(
152
                                'http://remotestorage.io/spec/version' => 'draft-dejong-remotestorage-03',
153
                                'http://tools.ietf.org/html/rfc2616#section-14.16' => 'development' !== $this->options['server_mode'] ? 'GET' : false,
154
                                'http://tools.ietf.org/html/rfc6749#section-4.2' => sprintf('%s_oauth/authorize?login_hint=%s', $request->getUrl()->getRootUrl(), $user),
155
                                'http://tools.ietf.org/html/rfc6750#section-2.3' => false,
156
                            ),
157
                            'rel' => 'remotestorage',
158
                        ),
159
                    ),
160
                );
161
162
                $response = new Response(200, 'application/jrd+json');
163
                $response->setBody(Json::encode($webFingerData));
164
165
                return $response;
166
            },
167
            array(
168
                'fkooman\Rest\Plugin\Authentication\AuthenticationPlugin' => array(
169
                    'enabled' => false,
170
                ),
171
            )
172
        );
173
174
        $this->get(
175
            '/',
176
            function (Request $request, UserInfoInterface $userInfo = null) {
177
                return $this->templateManager->render(
178
                    'indexPage',
179
                    array(
180
                        'user_id' => null !== $userInfo ? $userInfo->getUserId() : null,
181
                        'show_account_icon' => true,
182
                    )
183
                );
184
            },
185
            array(
186
                'fkooman\Rest\Plugin\Authentication\AuthenticationPlugin' => array(
187
                    'activate' => array('user'),
188
                    'require' => false,
189
                ),
190
            )
191
        );
192
193
        $this->addRoute(
194
            ['GET', 'HEAD'],
195
            '*',
196
            function (Request $request, TokenInfo $tokenInfo = null) {
197
                return $this->getObject($request, $tokenInfo);
198
            },
199
            array(
200
                'fkooman\Rest\Plugin\Authentication\AuthenticationPlugin' => array(
201
                    'activate' => array('api'),
202
                    'require' => false,
203
                ),
204
            )
205
        );
206
207
        // put a document
208
        $this->put(
209
            '*',
210
            function (Request $request, TokenInfo $tokenInfo) {
211
                return $this->putDocument($request, $tokenInfo);
212
            },
213
            array(
214
                'fkooman\Rest\Plugin\Authentication\AuthenticationPlugin' => array(
215
                    'activate' => array('api'),
216
                ),
217
                'fkooman\Rest\Plugin\ReferrerCheck\ReferrerCheckPlugin' => array(
218
                    'enabled' => false,
219
                ),
220
            )
221
        );
222
223
        // delete a document
224
        $this->delete(
225
            '*',
226
            function (Request $request, TokenInfo $tokenInfo) {
227
                return $this->deleteDocument($request, $tokenInfo);
228
            },
229
            array(
230
                'fkooman\Rest\Plugin\Authentication\AuthenticationPlugin' => array(
231
                    'activate' => array('api'),
232
                ),
233
                'fkooman\Rest\Plugin\ReferrerCheck\ReferrerCheckPlugin' => array(
234
                    'enabled' => false,
235
                ),
236
            )
237
        );
238
239
        // options request
240
        $this->options(
241
            '*',
242
            function (Request $request) {
243
                return $this->optionsRequest($request);
244
            },
245
            array(
246
                'fkooman\Rest\Plugin\Authentication\AuthenticationPlugin' => array('enabled' => false),
247
            )
248
        );
249
    }
250
251
    public function getObject(Request $request, $tokenInfo)
252
    {
253
        $path = new Path($request->getUrl()->getPathInfo());
254
255
        // allow requests to public files (GET|HEAD) without authentication
256
        if ($path->getIsPublic() && $path->getIsDocument()) {
257
            return $this->getDocument($path, $request, $tokenInfo);
258
        }
259
260
        // past this point we MUST be authenticated
261
        if (null === $tokenInfo) {
262
            $e = new UnauthorizedException('unauthorized', 'must authenticate to view folder listing');
263
            $e->addScheme('Bearer', array('realm' => 'remoteStorage API'));
264
            throw $e;
265
        }
266
267
        if ($path->getIsFolder()) {
268
            return $this->getFolder($path, $request, $tokenInfo);
269
        }
270
271
        return $this->getDocument($path, $request, $tokenInfo);
272
    }
273
274
    public function getFolder(Path $path, Request $request, TokenInfo $tokenInfo)
275
    {
276
        if ($path->getUserId() !== $tokenInfo->getUserId()) {
277
            throw new ForbiddenException('path does not match authorized subject');
278
        }
279
        if (!$this->hasReadScope($tokenInfo->getScope(), $path->getModuleName())) {
280
            throw new ForbiddenException('path does not match authorized scope');
281
        }
282
283
        $folderVersion = $this->remoteStorage->getVersion($path);
284
        if (null === $folderVersion) {
285
            // folder does not exist, so we just invent this
286
            // ETag that will be the same for all empty folders
287
            $folderVersion = 'e:404';
288
        }
289
290
        $requestedVersion = $this->stripQuotes(
291
            $request->getHeader('If-None-Match')
292
        );
293
294 View Code Duplication
        if (null !== $requestedVersion) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
295
            if (in_array($folderVersion, $requestedVersion)) {
296
                //return new RemoteStorageResponse($request, 304, $folderVersion);
0 ignored issues
show
Unused Code Comprehensibility introduced by
67% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
297
                $response = new Response(304, 'application/ld+json');
298
                $response->setHeader('ETag', '"'.$folderVersion.'"');
299
300
                return $response;
301
            }
302
        }
303
304
        $rsr = new Response(200, 'application/ld+json');
305
        $rsr->setHeader('ETag', '"'.$folderVersion.'"');
306
307
        if ('GET' === $request->getMethod()) {
308
            $rsr->setBody(
309
                $this->remoteStorage->getFolder(
310
                    $path,
311
                    $this->stripQuotes(
312
                        $request->getHeader('If-None-Match')
313
                    )
314
                )
315
            );
316
        }
317
318
        return $rsr;
319
    }
320
321
    public function getDocument(Path $path, Request $request, TokenInfo $tokenInfo = null)
322
    {
323
        if (null !== $tokenInfo) {
324
            if ($path->getUserId() !== $tokenInfo->getUserId()) {
325
                throw new ForbiddenException('path does not match authorized subject');
326
            }
327
            if (!$this->hasReadScope($tokenInfo->getScope(), $path->getModuleName())) {
328
                throw new ForbiddenException('path does not match authorized scope');
329
            }
330
        }
331
        $documentVersion = $this->remoteStorage->getVersion($path);
332
        if (null === $documentVersion) {
333
            throw new NotFoundException(
334
                sprintf('document "%s" not found', $path->getPath())
335
            );
336
        }
337
338
        $requestedVersion = $this->stripQuotes(
339
            $request->getHeader('If-None-Match')
340
        );
341
        $documentContentType = $this->remoteStorage->getContentType($path);
342
343 View Code Duplication
        if (null !== $requestedVersion) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
344
            if (in_array($documentVersion, $requestedVersion)) {
345
                $response = new Response(304, $documentContentType);
346
                $response->setHeader('ETag', '"'.$documentVersion.'"');
347
348
                return $response;
349
            }
350
        }
351
352
        $rsr = new Response(200, $documentContentType);
353
        $rsr->setHeader('ETag', '"'.$documentVersion.'"');
354
355
        if ('development' !== $this->options['server_mode']) {
356
            $rsr->setHeader('Accept-Ranges', 'bytes');
357
        }
358
359
        if ('GET' === $request->getMethod()) {
360
            if ('development' === $this->options['server_mode']) {
361
                // use body
362
                $rsr->setBody(
363
                    file_get_contents(
364
                        $this->remoteStorage->getDocument(
365
                            $path,
366
                            $requestedVersion
367
                        )
368
                    )
369
                );
370
            } else {
371
                // use X-SendFile
372
                $rsr->setFile(
373
                    $this->remoteStorage->getDocument(
374
                        $path,
375
                        $requestedVersion
376
                    )
377
                );
378
            }
379
        }
380
381
        return $rsr;
382
    }
383
384
    public function putDocument(Request $request, TokenInfo $tokenInfo)
385
    {
386
        $path = new Path($request->getUrl()->getPathInfo());
387
388
        if ($path->getUserId() !== $tokenInfo->getUserId()) {
389
            throw new ForbiddenException('path does not match authorized subject');
390
        }
391
        if (!$this->hasWriteScope($tokenInfo->getScope(), $path->getModuleName())) {
392
            throw new ForbiddenException('path does not match authorized scope');
393
        }
394
395
        $ifMatch = $this->stripQuotes(
396
            $request->getHeader('If-Match')
397
        );
398
        $ifNoneMatch = $this->stripQuotes(
399
            $request->getHeader('If-None-Match')
400
        );
401
402
        $documentVersion = $this->remoteStorage->getVersion($path);
403
        if (null !== $ifMatch && !in_array($documentVersion, $ifMatch)) {
404
            throw new PreconditionFailedException('version mismatch');
405
        }
406
407
        if (null !== $ifNoneMatch && in_array('*', $ifNoneMatch) && null !== $documentVersion) {
408
            throw new PreconditionFailedException('document already exists');
409
        }
410
411
        $x = $this->remoteStorage->putDocument(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $x is correct as $this->remoteStorage->pu...$ifMatch, $ifNoneMatch) (which targets fkooman\RemoteStorage\RemoteStorage::putDocument()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
412
            $path,
413
            $request->getHeader('Content-Type'),
414
            $request->getBody(),
415
            $ifMatch,
416
            $ifNoneMatch
417
        );
418
        // we have to get the version again after the PUT
419
        $documentVersion = $this->remoteStorage->getVersion($path);
420
421
        $rsr = new Response();
422
        $rsr->setHeader('ETag', '"'.$documentVersion.'"');
423
        $rsr->setBody($x);
424
425
        return $rsr;
426
    }
427
428
    public function deleteDocument(Request $request, TokenInfo $tokenInfo)
429
    {
430
        $path = new Path($request->getUrl()->getPathInfo());
431
432
        if ($path->getUserId() !== $tokenInfo->getUserId()) {
433
            throw new ForbiddenException('path does not match authorized subject');
434
        }
435
        if (!$this->hasWriteScope($tokenInfo->getScope(), $path->getModuleName())) {
436
            throw new ForbiddenException('path does not match authorized scope');
437
        }
438
439
        // need to get the version before the delete
440
        $documentVersion = $this->remoteStorage->getVersion($path);
441
442
        $ifMatch = $this->stripQuotes(
443
            $request->getHeader('If-Match')
444
        );
445
446
        // if document does not exist, and we have If-Match header set we should
447
        // return a 412 instead of a 404
448
        if (null !== $ifMatch && !in_array($documentVersion, $ifMatch)) {
449
            throw new PreconditionFailedException('version mismatch');
450
        }
451
452
        if (null === $documentVersion) {
453
            throw new NotFoundException(
454
                sprintf('document "%s" not found', $path->getPath())
455
            );
456
        }
457
458
        $ifMatch = $this->stripQuotes(
459
            $request->getHeader('If-Match')
460
        );
461
        if (null !== $ifMatch && !in_array($documentVersion, $ifMatch)) {
462
            throw new PreconditionFailedException('version mismatch');
463
        }
464
465
        $x = $this->remoteStorage->deleteDocument(
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $x is correct as $this->remoteStorage->de...cument($path, $ifMatch) (which targets fkooman\RemoteStorage\Re...orage::deleteDocument()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
466
            $path,
467
            $ifMatch
468
        );
469
        $rsr = new Response();
470
        $rsr->setHeader('ETag', '"'.$documentVersion.'"');
471
        $rsr->setBody($x);
472
473
        return $rsr;
474
    }
475
476
    public function optionsRequest(Request $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...
477
    {
478
        return new Response();
479
    }
480
481
    private function hasReadScope(Scope $i, $moduleName)
482
    {
483
        $validReadScopes = array(
484
            '*:r',
485
            '*:rw',
486
            sprintf('%s:%s', $moduleName, 'r'),
487
            sprintf('%s:%s', $moduleName, 'rw'),
488
        );
489
490
        foreach ($validReadScopes as $scope) {
491
            if ($i->hasScope($scope)) {
492
                return true;
493
            }
494
        }
495
496
        return false;
497
    }
498
499
    private function hasWriteScope(Scope $i, $moduleName)
500
    {
501
        $validWriteScopes = array(
502
            '*:rw',
503
            sprintf('%s:%s', $moduleName, 'rw'),
504
        );
505
506
        foreach ($validWriteScopes as $scope) {
507
            if ($i->hasScope($scope)) {
508
                return true;
509
            }
510
        }
511
512
        return false;
513
    }
514
515
    /**
516
     * ETag/If-Match/If-None-Match are always quoted, this method removes
517
     * the quotes.
518
     */
519
    public function stripQuotes($versionHeader)
520
    {
521
        if (null === $versionHeader) {
522
            return;
523
        }
524
525
        $versions = array();
526
527
        if ('*' === $versionHeader) {
528
            return array('*');
529
        }
530
531
        foreach (explode(',', $versionHeader) as $v) {
532
            $v = trim($v);
533
            $startQuote = strpos($v, '"');
534
            $endQuote = strrpos($v, '"');
535
            $length = strlen($v);
536
537
            if (0 !== $startQuote || $length - 1 !== $endQuote) {
538
                throw new BadRequestException('version header must start and end with a double quote');
539
            }
540
            $versions[] = substr($v, 1, $length - 2);
541
        }
542
543
        return $versions;
544
    }
545
546
    public function run(Request $request = null)
547
    {
548
        if (null === $request) {
549
            throw new InvalidArgumentException('must provide Request object');
550
        }
551
552
        $response = null;
553
        try {
554
            $response = parent::run($request);
555
        } catch (PathException $e) {
556
            $e = new BadRequestException($e->getMessage());
557
            $response = $e->getJsonResponse();
558
        }
559
560
        // XXX Expires should only be for successful GET??
561
        if ('GET' === $request->getMethod()) {
562
            $response->setHeader('Expires', 0);
563
            $response->setHeader('Cache-Control', 'no-cache');
564
        }
565
566
        // CORS
567
        if (null !== $request->getHeader('Origin')) {
568
            $response->setHeader('Access-Control-Allow-Origin', $request->getHeader('Origin'));
569
        } elseif (in_array($request->getMethod(), array('GET', 'HEAD', 'OPTIONS'))) {
570
            $response->setHeader('Access-Control-Allow-Origin', '"*"');
571
        }
572
573
        $response->setHeader(
574
            'Access-Control-Expose-Headers',
575
            'ETag, Content-Length'
576
        );
577
578
        // this is only needed for OPTIONS requests
579
        if ('OPTIONS' === $request->getMethod()) {
580
            $response->setHeader(
581
                'Access-Control-Allow-Methods',
582
                'GET, PUT, DELETE, HEAD, OPTIONS'
583
            );
584
            // FIXME: are Origin and X-Requested-With really needed?
585
            $response->setHeader(
586
                'Access-Control-Allow-Headers',
587
                'Authorization, Content-Length, Content-Type, Origin, X-Requested-With, If-Match, If-None-Match'
588
            );
589
        }
590
591
        return $response;
592
    }
593
}
594