Passed
Pull Request — master (#38)
by
unknown
26:44 queued 11:37
created

Wopi::renameFile()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 43
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

352
                $suggestedTarget = sprintf('%s%s', /** @scrutinizer ignore-type */ $filename, $suggestedTarget);
Loading history...
353
            }
354
355
            $target = $suggestedTarget;
356
        } else {
357
            $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

357
            $document = $this->documentManager->findByDocumentFilename(/** @scrutinizer ignore-type */ $relativeTarget);
Loading history...
358
359
            /**
360
             * If a file with the specified name already exists,
361
             * the host must respond with a 409 Conflict,
362
             * unless the X-WOPI-OverwriteRelativeTarget request header is set to true.
363
             *
364
             * When responding with a 409 Conflict for this reason,
365
             * the host may include an X-WOPI-ValidRelativeTarget specifying a file name that is valid.
366
             *
367
             * If the X-WOPI-OverwriteRelativeTarget request header is set to true
368
             * and a file with the specified name already exists and is locked,
369
             * the host must respond with a 409 Conflict and include an
370
             * X-WOPI-Lock response header containing the value of the current lock on the file.
371
             */
372
            if (null !== $document) {
373
                if (false === $overwriteRelativeTarget) {
374
                    $extension = pathinfo($this->documentManager->getBasename($document), PATHINFO_EXTENSION);
375
376
                    return $this
377
                        ->responseFactory
378
                        ->createResponse(409)
379
                        ->withHeader('Content-Type', 'application/json')
380
                        ->withHeader(
381
                            WopiInterface::HEADER_VALID_RELATIVE_TARGET,
382
                            sprintf('%s.%s', uniqid(), $extension)
383
                        );
384
                }
385
386
                if ($this->enableLock && $this->documentManager->hasLock($document)) {
387
                    return $this
388
                        ->responseFactory
389
                        ->createResponse(409)
390
                        ->withHeader(WopiInterface::HEADER_LOCK, $this->documentManager->getLock($document));
391
                }
392
            }
393
394
            $target = $relativeTarget;
395
        }
396
397
        /** @var array{filename: string, extension: string} $pathInfo */
398
        $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

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