Wopi   B
last analyzed

Complexity

Total Complexity 48

Size/Duplication

Total Lines 542
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 8
Bugs 1 Features 0
Metric Value
eloc 278
c 8
b 1
f 0
dl 0
loc 542
ccs 0
cts 309
cp 0
rs 8.5599
wmc 48

16 Methods

Rating   Name   Duplication   Size   Complexity  
A renameFile() 0 43 5
A getLock() 0 27 4
A lock() 0 45 5
A putFile() 0 8 1
A refreshLock() 0 10 1
A __construct() 0 20 1
A getFile() 0 43 3
A deleteFile() 0 26 4
A enumerateAncestors() 0 8 1
B putRelativeFile() 0 115 9
A unlockAndRelock() 0 10 1
A unlock() 0 47 5
A makeDocumentNotFoundResponse() 0 7 1
B checkFileInfo() 0 62 5
A putUserInfo() 0 7 1
A getShareUrl() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Wopi 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.

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

1
<?php
2
3
/**
4
 * For the full copyright and license information, please view
5
 * the LICENSE file that was distributed with this source code.
6
 */
7
8
declare(strict_types=1);
9
10
namespace ChampsLibres\WopiBundle\Service;
11
12
use ChampsLibres\WopiBundle\Contracts\AuthorizationManagerInterface;
13
use ChampsLibres\WopiBundle\Contracts\UserManagerInterface;
14
use ChampsLibres\WopiBundle\Service\Wopi\PutFile;
15
use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
16
use ChampsLibres\WopiLib\Contract\Service\WopiInterface;
17
use DateTimeInterface;
18
19
use Psr\Http\Message\RequestInterface;
20
use Psr\Http\Message\ResponseFactoryInterface;
21
22
use Psr\Http\Message\ResponseInterface;
23
use Psr\Http\Message\StreamFactoryInterface;
24
use Psr\Http\Message\UriFactoryInterface;
25
use Psr\Log\LoggerInterface;
26
use Symfony\Component\HttpFoundation\Response;
27
28
use Symfony\Component\Routing\RouterInterface;
29
30
use const PATHINFO_EXTENSION;
31
use const PATHINFO_FILENAME;
32
33
final class Wopi implements WopiInterface
34
{
35
    private const LOG_PREFIX = '[wopi][Wopi] ';
36
37
    private AuthorizationManagerInterface $authorizationManager;
38
39
    private DocumentManagerInterface $documentManager;
40
41
    private LoggerInterface $logger;
42
43
    private PutFile $putFileExecutor;
44
45
    private ResponseFactoryInterface $responseFactory;
46
47
    private RouterInterface $router;
48
49
    private StreamFactoryInterface $streamFactory;
50
51
    private UriFactoryInterface $uriFactory;
52
53
    private UserManagerInterface $userManager;
54
55
    public function __construct(
56
        AuthorizationManagerInterface $authorizationManager,
57
        DocumentManagerInterface $documentManager,
58
        LoggerInterface $logger,
59
        ResponseFactoryInterface $responseFactory,
60
        RouterInterface $router,
61
        StreamFactoryInterface $streamFactory,
62
        UriFactoryInterface $uriFactory,
63
        UserManagerInterface $userManager,
64
        PutFile $putFile
65
    ) {
66
        $this->authorizationManager = $authorizationManager;
67
        $this->documentManager = $documentManager;
68
        $this->logger = $logger;
69
        $this->responseFactory = $responseFactory;
70
        $this->streamFactory = $streamFactory;
71
        $this->router = $router;
72
        $this->uriFactory = $uriFactory;
73
        $this->userManager = $userManager;
74
        $this->putFileExecutor = $putFile;
75
    }
76
77
    /**
78
     * @param array<string, string|boolean|int|null> $overrideProperties
79
     */
80
    public function checkFileInfo(string $fileId, string $accessToken, RequestInterface $request, array $overrideProperties = []): ResponseInterface
81
    {
82
        $userIdentifier = $this->userManager->getUserId($accessToken, $fileId, $request);
83
84
        if (null === $userIdentifier && false === $this->userManager->isAnonymousUser($accessToken, $fileId, $request)) {
85
            $this->logger->error(self::LOG_PREFIX . 'user not found nor anonymous');
86
87
            return $this->responseFactory
88
                ->createResponse(404)
89
                ->withBody($this->streamFactory->createStream((string) json_encode(['message' => 'user not found nor anonymous'])));
90
        }
91
92
        $document = $this->documentManager->findByDocumentId($fileId);
93
94
        if (null === $document) {
95
            return $this->makeDocumentNotFoundResponse($fileId);
96
        }
97
98
        if (!$this->authorizationManager->userCanRead($accessToken, $document, $request)) {
99
            $this->logger->info(self::LOG_PREFIX . 'user is not allowed to read document', ['fileId' => $fileId, 'userIdentifier' => $userIdentifier]);
100
101
            return $this->responseFactory->createResponse(401)->withBody($this->streamFactory->createStream((string) json_encode([
102
                'message' => 'user is not allowed to see this document',
103
            ])));
104
        }
105
106
        $properties = [
107
            'BaseFileName' => $this->documentManager->getBasename($document),
108
            'OwnerId' => 'Symfony',
109
            'Size' => $this->documentManager->getSize($document),
110
            'UserId' => $userIdentifier,
111
            'ReadOnly' => !$this->authorizationManager->userCanWrite($accessToken, $document, $request),
112
            'RestrictedWebViewOnly' => $this->authorizationManager->isRestrictedWebViewOnly($accessToken, $document, $request),
113
            'UserCanAttend' => $this->authorizationManager->userCanAttend($accessToken, $document, $request),
114
            'UserCanPresent' => $this->authorizationManager->userCanPresent($accessToken, $document, $request),
115
            'UserCanRename' => $this->authorizationManager->userCanRename($accessToken, $document, $request),
116
            'UserCanWrite' => $this->authorizationManager->userCanWrite($accessToken, $document, $request),
117
            'UserCanNotWriteRelative' => $this->authorizationManager->userCannotWriteRelative($accessToken, $document, $request),
118
            'SupportsUserInfo' => false,
119
            'SupportsDeleteFile' => true,
120
            'SupportsLocks' => true,
121
            'SupportsGetLock' => true,
122
            'SupportsExtendedLockLength' => true,
123
            'SupportsUpdate' => true,
124
            'SupportsRename' => true,
125
            'SupportsFolders' => false,
126
            'UserFriendlyName' => $this->userManager->getUserFriendlyName($accessToken, $fileId, $request),
127
            'DisablePrint' => false,
128
            'AllowExternalMarketplace' => false,
129
            'SupportedShareUrlTypes' => [
130
                'ReadOnly',
131
            ],
132
            'SHA256' => $this->documentManager->getSha256($document),
133
            'LastModifiedTime' => $this->documentManager->getLastModifiedDate($document)
134
                ->format(DateTimeInterface::ATOM),
135
        ];
136
137
        return $this
138
            ->responseFactory
139
            ->createResponse()
140
            ->withHeader('Content-Type', 'application/json')
141
            ->withBody($this->streamFactory->createStream((string) json_encode(array_merge($properties, $overrideProperties))));
142
    }
143
144
    public function deleteFile(string $fileId, string $accessToken, RequestInterface $request): ResponseInterface
145
    {
146
        $document = $this->documentManager->findByDocumentId($fileId);
147
148
        if (null === $document) {
149
            return $this->makeDocumentNotFoundResponse($fileId);
150
        }
151
152
        if (
153
            false === $this->authorizationManager->userCanDelete($accessToken, $document, $request)
154
            || false === $this->authorizationManager->userCanWrite($accessToken, $document, $request)
155
        ) {
156
            $this->logger->info(
157
                self::LOG_PREFIX . 'user is not authorized to delete file',
158
                ['fileId' => $fileId, 'userId' => $this->userManager->getUserId($accessToken, $fileId, $request)]
159
            );
160
161
            return $this->responseFactory
162
                ->createResponse(401);
163
        }
164
165
        $this->documentManager->remove($document);
166
167
        return $this
168
            ->responseFactory
169
            ->createResponse(200);
170
    }
171
172
    public function enumerateAncestors(
173
        string $fileId,
174
        string $accessToken,
175
        RequestInterface $request
176
    ): ResponseInterface {
177
        return $this
178
            ->responseFactory
179
            ->createResponse(501);
180
    }
181
182
    public function getFile(
183
        string $fileId,
184
        string $accessToken,
185
        RequestInterface $request
186
    ): ResponseInterface {
187
        $document = $this->documentManager->findByDocumentId($fileId);
188
189
        if (null === $document) {
190
            return $this->makeDocumentNotFoundResponse($fileId);
191
        }
192
193
        if (!$this->authorizationManager->userCanRead($accessToken, $document, $request)) {
194
            $userIdentifier = $this->userManager->getUserId($accessToken, $fileId, $request);
195
            $this->logger->info(self::LOG_PREFIX . 'user is not allowed to read document', ['fileId' => $fileId, 'userIdentifier' => $userIdentifier]);
196
197
            return $this->responseFactory->createResponse(401)->withBody($this->streamFactory->createStream((string) json_encode([
198
                'message' => 'user is not allowed to see this document',
199
            ])));
200
        }
201
202
        $revision = $this->documentManager->getVersion($document);
203
        $content = $this->documentManager->read($document);
204
205
        return $this
206
            ->responseFactory
207
            ->createResponse()
208
            ->withHeader(
209
                WopiInterface::HEADER_ITEM_VERSION,
210
                sprintf('v%s', $revision)
211
            )
212
            ->withHeader(
213
                'Content-Type',
214
                'application/octet-stream',
215
            )
216
            ->withHeader(
217
                'Content-Length',
218
                (string) $this->documentManager->getSize($document)
219
            )
220
            ->withHeader(
221
                'Content-Disposition',
222
                sprintf('attachment; filename=%s', $this->documentManager->getBasename($document))
223
            )
224
            ->withBody($content);
225
    }
226
227
    public function getLock(string $fileId, string $accessToken, RequestInterface $request): ResponseInterface
228
    {
229
        $document = $this->documentManager->findByDocumentId($fileId);
230
231
        if (null === $document) {
232
            return $this->makeDocumentNotFoundResponse($fileId);
233
        }
234
235
        if (!$this->authorizationManager->isTokenValid($accessToken, $document, $request)) {
236
            $this->logger->info(self::LOG_PREFIX . 'invalid access token', ['fileId' => $fileId]);
237
238
            return $this->responseFactory->createResponse(401)->withBody($this->streamFactory->createStream((string) json_encode([
239
                'message' => 'invalid access token',
240
            ])));
241
        }
242
243
        if ($this->documentManager->hasLock($document)) {
244
            return $this
245
                ->responseFactory
246
                ->createResponse()
247
                ->withHeader(WopiInterface::HEADER_LOCK, $this->documentManager->getLock($document));
248
        }
249
250
        return $this
251
            ->responseFactory
252
            ->createResponse(404)
253
            ->withHeader(WopiInterface::HEADER_LOCK, '');
254
    }
255
256
    public function getShareUrl(string $fileId, string $accessToken, RequestInterface $request): ResponseInterface
257
    {
258
        return $this
259
            ->responseFactory
260
            ->createResponse(501);
261
    }
262
263
    public function lock(
264
        string $fileId,
265
        string $accessToken,
266
        string $xWopiLock,
267
        RequestInterface $request
268
    ): ResponseInterface {
269
        $document = $this->documentManager->findByDocumentId($fileId);
270
271
        if (null === $document) {
272
            return $this->makeDocumentNotFoundResponse($fileId);
273
        }
274
275
        if (!$this->authorizationManager->isTokenValid($accessToken, $document, $request)) {
276
            $this->logger->info(self::LOG_PREFIX . 'invalid access token', ['fileId' => $fileId]);
277
278
            return $this->responseFactory->createResponse(401)->withBody($this->streamFactory->createStream((string) json_encode([
279
                'message' => 'invalid access token',
280
            ])));
281
        }
282
283
        $version = $this->documentManager->getVersion($document);
284
285
        if ($this->documentManager->hasLock($document)) {
286
            if ($xWopiLock === $currentLock = $this->documentManager->getLock($document)) {
287
                return $this->refreshLock($fileId, $accessToken, $xWopiLock, $request);
288
            }
289
290
            return $this
291
                ->responseFactory
292
                ->createResponse(409)
293
                ->withHeader(WopiInterface::HEADER_LOCK, $currentLock)
294
                ->withHeader(
295
                    WopiInterface::HEADER_ITEM_VERSION,
296
                    sprintf('v%s', $version)
297
                );
298
        }
299
300
        $this->documentManager->lock($document, $xWopiLock);
301
302
        return $this
303
            ->responseFactory
304
            ->createResponse()
305
            ->withHeader(
306
                WopiInterface::HEADER_ITEM_VERSION,
307
                sprintf('v%s', $version)
308
            );
309
    }
310
311
    public function putFile(
312
        string $fileId,
313
        string $accessToken,
314
        string $xWopiLock,
315
        string $xWopiEditors,
316
        RequestInterface $request
317
    ): ResponseInterface {
318
        return ($this->putFileExecutor)($fileId, $accessToken, $xWopiLock, $xWopiEditors, $request);
319
    }
320
321
    public function putRelativeFile(
322
        string $fileId,
323
        string $accessToken,
324
        ?string $suggestedTarget,
325
        ?string $relativeTarget,
326
        bool $overwriteRelativeTarget,
327
        int $size,
328
        RequestInterface $request
329
    ): ResponseInterface {
330
        if ((null === $suggestedTarget) && (null === $relativeTarget)) {
331
            return $this
332
                ->responseFactory
333
                ->createResponse(400)
334
                ->withBody($this->streamFactory->createStream((string) json_encode([
335
                    'message' => 'target is null',
336
                ])));
337
        }
338
339
        if (null !== $suggestedTarget) {
340
            // If it starts with a dot...
341
            if (0 === strpos($suggestedTarget, '.', 0)) {
342
                $document = $this->documentManager->findByDocumentId($fileId);
343
344
                if (null === $document) {
345
                    return $this->makeDocumentNotFoundResponse($fileId);
346
                }
347
                $filename = pathinfo($this->documentManager->getBasename($document), PATHINFO_EXTENSION | PATHINFO_FILENAME);
348
349
                $suggestedTarget = sprintf('%s%s', $filename, $suggestedTarget);
0 ignored issues
show
Bug introduced by
It seems like $filename can also be of type array; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

349
                $suggestedTarget = sprintf('%s%s', /** @scrutinizer ignore-type */ $filename, $suggestedTarget);
Loading history...
350
            }
351
352
            $target = $suggestedTarget;
353
        } else {
354
            $document = $this->documentManager->findByDocumentFilename($relativeTarget);
0 ignored issues
show
Bug introduced by
It seems like $relativeTarget can also be of type null; however, parameter $documentFilename of ChampsLibres\WopiLib\Con...indByDocumentFilename() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

354
            $document = $this->documentManager->findByDocumentFilename(/** @scrutinizer ignore-type */ $relativeTarget);
Loading history...
355
356
            /**
357
             * If a file with the specified name already exists,
358
             * the host must respond with a 409 Conflict,
359
             * unless the X-WOPI-OverwriteRelativeTarget request header is set to true.
360
             *
361
             * When responding with a 409 Conflict for this reason,
362
             * the host may include an X-WOPI-ValidRelativeTarget specifying a file name that is valid.
363
             *
364
             * If the X-WOPI-OverwriteRelativeTarget request header is set to true
365
             * and a file with the specified name already exists and is locked,
366
             * the host must respond with a 409 Conflict and include an
367
             * X-WOPI-Lock response header containing the value of the current lock on the file.
368
             */
369
            if (null !== $document) {
370
                if (false === $overwriteRelativeTarget) {
371
                    $extension = pathinfo($this->documentManager->getBasename($document), PATHINFO_EXTENSION);
372
373
                    return $this
374
                        ->responseFactory
375
                        ->createResponse(409)
376
                        ->withHeader('Content-Type', 'application/json')
377
                        ->withHeader(
378
                            WopiInterface::HEADER_VALID_RELATIVE_TARGET,
379
                            sprintf('%s.%s', uniqid(), $extension)
380
                        );
381
                }
382
383
                if ($this->documentManager->hasLock($document)) {
384
                    return $this
385
                        ->responseFactory
386
                        ->createResponse(409)
387
                        ->withHeader(WopiInterface::HEADER_LOCK, $this->documentManager->getLock($document));
388
                }
389
            }
390
391
            $target = $relativeTarget;
392
        }
393
394
        /** @var array{filename: string, extension: string} $pathInfo */
395
        $pathInfo = pathinfo($target, PATHINFO_EXTENSION | PATHINFO_FILENAME);
0 ignored issues
show
Bug introduced by
It seems like $target can also be of type null; however, parameter $path of pathinfo() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

395
        $pathInfo = pathinfo(/** @scrutinizer ignore-type */ $target, PATHINFO_EXTENSION | PATHINFO_FILENAME);
Loading history...
396
397
        $new = $this->documentManager->create([
398
            'basename' => $target,
399
            'name' => $pathInfo['filename'],
400
            'extension' => $pathInfo['extension'],
401
            'content' => (string) $request->getBody(),
402
            'size' => $request->getHeaderLine(WopiInterface::HEADER_SIZE),
403
        ]);
404
405
        $this->documentManager->write($new);
406
407
        $uri = $this
408
            ->uriFactory
409
            ->createUri(
410
                $this
411
                    ->router
412
                    ->generate(
413
                        'checkFileInfo',
414
                        [
415
                            'fileId' => $this->documentManager->getDocumentId($new),
416
                        ],
417
                        RouterInterface::ABSOLUTE_URL
418
                    )
419
            )
420
            ->withQuery(http_build_query([
421
                'access_token' => $accessToken,
422
            ]));
423
424
        $properties = [
425
            'Name' => $this->documentManager->getBasename($new),
426
            'Url' => (string) $uri,
427
            'HostEditUrl' => $this->documentManager->getDocumentId($new),
428
            'HostViewUrl' => $this->documentManager->getDocumentId($new),
429
        ];
430
431
        return $this
432
            ->responseFactory
433
            ->createResponse()
434
            ->withHeader('Content-Type', 'application/json')
435
            ->withBody($this->streamFactory->createStream((string) json_encode($properties)));
436
    }
437
438
    public function putUserInfo(string $fileId, string $accessToken, RequestInterface $request): ResponseInterface
439
    {
440
        $this->logger->warning(self::LOG_PREFIX . 'user info called, but not implemented');
441
442
        return $this->responseFactory->createResponse(501)
443
            ->withBody($this->streamFactory->createStream((string) json_encode([
444
                'message' => 'User info not implemented',
445
            ])));
446
    }
447
448
    public function refreshLock(
449
        string $fileId,
450
        string $accessToken,
451
        string $xWopiLock,
452
        RequestInterface $request
453
    ): ResponseInterface {
454
        // note: the validation of access token is done inside unlock and lock methods
455
        $this->unlock($fileId, $accessToken, $xWopiLock, $request);
456
457
        return $this->lock($fileId, $accessToken, $xWopiLock, $request);
458
    }
459
460
    public function renameFile(
461
        string $fileId,
462
        string $accessToken,
463
        string $xWopiLock,
464
        string $xWopiRequestedName,
465
        RequestInterface $request
466
    ): ResponseInterface {
467
        $document = $this->documentManager->findByDocumentId($fileId);
468
469
        if (null === $document) {
470
            return $this->makeDocumentNotFoundResponse($fileId);
471
        }
472
473
        if (!$this->authorizationManager->userCanRename($accessToken, $document, $request)) {
474
            $userIdentifier = $this->userManager->getUserId($accessToken, $fileId, $request);
475
            $this->logger->info(self::LOG_PREFIX . 'user is not allowed to rename', ['fileId' => $fileId, 'userIdentifier' => $userIdentifier]);
476
477
            return $this->responseFactory->createResponse(401)->withBody($this->streamFactory->createStream((string) json_encode([
478
                'message' => 'user is not allowed to rename',
479
            ])));
480
        }
481
482
        if ($this->documentManager->hasLock($document)) {
483
            if ($xWopiLock !== $currentLock = $this->documentManager->getLock($document)) {
484
                return $this
485
                    ->responseFactory
486
                    ->createResponse(409)
487
                    ->withHeader(WopiInterface::HEADER_LOCK, $currentLock);
488
            }
489
        }
490
491
        $this->documentManager->rename($document, $xWopiRequestedName);
492
493
        $data = [
494
            'Name' => $xWopiRequestedName,
495
        ];
496
497
        return $this
498
            ->responseFactory
499
            ->createResponse(200)
500
            ->withHeader('Content-Type', 'application/json')
501
            ->withBody(
502
                $this->streamFactory->createStream((string) json_encode($data))
503
            );
504
    }
505
506
    public function unlock(
507
        string $fileId,
508
        string $accessToken,
509
        string $xWopiLock,
510
        RequestInterface $request
511
    ): ResponseInterface {
512
        $document = $this->documentManager->findByDocumentId($fileId);
513
514
        if (null === $document) {
515
            return $this->makeDocumentNotFoundResponse($fileId);
516
        }
517
518
        if (!$this->authorizationManager->isTokenValid($accessToken, $document, $request)) {
519
            $this->logger->info(self::LOG_PREFIX . 'invalid access token', ['fileId' => $fileId]);
520
521
            return $this->responseFactory->createResponse(401)->withBody($this->streamFactory->createStream((string) json_encode([
522
                'message' => 'invalid access token',
523
            ])));
524
        }
525
526
        $version = $this->documentManager->getVersion($document);
527
528
        if (!$this->documentManager->hasLock($document)) {
529
            return $this
530
                ->responseFactory
531
                ->createResponse(409)
532
                ->withHeader(WopiInterface::HEADER_LOCK, '');
533
        }
534
535
        $currentLock = $this->documentManager->getLock($document);
536
537
        if ($currentLock !== $xWopiLock) {
538
            return $this
539
                ->responseFactory
540
                ->createResponse(409)
541
                ->withHeader(WopiInterface::HEADER_LOCK, $currentLock);
542
        }
543
544
        $this->documentManager->deleteLock($document);
545
546
        return $this
547
            ->responseFactory
548
            ->createResponse()
549
            ->withHeader(WopiInterface::HEADER_LOCK, '')
550
            ->withHeader(
551
                WopiInterface::HEADER_ITEM_VERSION,
552
                sprintf('v%s', $version)
553
            );
554
    }
555
556
    public function unlockAndRelock(
557
        string $fileId,
558
        string $accessToken,
559
        string $xWopiLock,
560
        string $xWopiOldLock,
561
        RequestInterface $request
562
    ): ResponseInterface {
563
        $this->unlock($fileId, $accessToken, $xWopiOldLock, $request);
564
565
        return $this->lock($fileId, $accessToken, $xWopiLock, $request);
566
    }
567
568
    private function makeDocumentNotFoundResponse(string $fileId): ResponseInterface
569
    {
570
        $this->logger->error(self::LOG_PREFIX . 'Document not found', ['fileId' => $fileId]);
571
572
        return $this->responseFactory->createResponse(404)
573
            ->withBody($this->streamFactory->createStream((string) json_encode([
574
                'message' => "Document with id {$fileId} not found",
575
            ])));
576
    }
577
}
578