Passed
Pull Request — master (#34)
by
unknown
25:42 queued 10:38
created

Wopi::makeDocumentNotFoundResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 3
c 1
b 1
f 0
nc 1
nop 1
dl 0
loc 5
rs 10
ccs 0
cts 3
cp 0
crap 2
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\Service\Wopi\PutFile;
13
use ChampsLibres\WopiLib\Contract\Service\DocumentManagerInterface;
14
use ChampsLibres\WopiLib\Contract\Service\WopiInterface;
15
use DateTimeInterface;
16
use Psr\Cache\CacheItemPoolInterface;
17
use Psr\Http\Message\RequestInterface;
18
19
use Psr\Http\Message\ResponseFactoryInterface;
20
use Psr\Http\Message\ResponseInterface;
21
22
use Psr\Http\Message\StreamFactoryInterface;
23
use Psr\Http\Message\UriFactoryInterface;
24
use Symfony\Component\HttpFoundation\Response;
25
use Symfony\Component\Routing\RouterInterface;
26
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
27
28
use const PATHINFO_EXTENSION;
29
use const PATHINFO_FILENAME;
30
31
final class Wopi implements WopiInterface
32
{
33
    private CacheItemPoolInterface $cache;
34
35
    private DocumentManagerInterface $documentManager;
36
37
    private PutFile $putFileExecutor;
38
39
    private ResponseFactoryInterface $responseFactory;
40
41
    private RouterInterface $router;
42
43
    private StreamFactoryInterface $streamFactory;
44
45
    private TokenStorageInterface $tokenStorage;
46
47
    private UriFactoryInterface $uriFactory;
48
49
    public function __construct(
50
        CacheItemPoolInterface $cache,
51
        DocumentManagerInterface $documentManager,
52
        ResponseFactoryInterface $responseFactory,
53
        RouterInterface $router,
54
        StreamFactoryInterface $streamFactory,
55
        TokenStorageInterface $tokenStorage,
56
        UriFactoryInterface $uriFactory,
57
        PutFile $putFile
58
    ) {
59
        $this->cache = $cache;
60
        $this->documentManager = $documentManager;
61
        $this->responseFactory = $responseFactory;
62
        $this->streamFactory = $streamFactory;
63
        $this->router = $router;
64
        $this->tokenStorage = $tokenStorage;
65
        $this->uriFactory = $uriFactory;
66
        $this->putFileExecutor = $putFile;
67
    }
68
69
    public function checkFileInfo(string $fileId, string $accessToken, RequestInterface $request): ResponseInterface
70
    {
71
        $document = $this->documentManager->findByDocumentId($fileId);
72
73
        if (null === $document) {
74
            return $this->makeDocumentNotFoundResponse($fileId);
75
        }
76
77
        $userIdentifier = $this->tokenStorage->getToken()->getUser()->getUserIdentifier();
78
        $userCacheKey = sprintf('wopi_putUserInfo_%s', $this->tokenStorage->getToken()->getUser()->getUserIdentifier());
79
80
        return $this
81
            ->responseFactory
82
            ->createResponse()
83
            ->withHeader('Content-Type', 'application/json')
84
            ->withBody($this->streamFactory->createStream((string) json_encode(
85
                [
86
                    'BaseFileName' => $this->documentManager->getBasename($document),
87
                    'OwnerId' => 'Symfony',
88
                    'Size' => $this->documentManager->getSize($document),
89
                    'UserId' => $userIdentifier,
90
                    'ReadOnly' => false,
91
                    'UserCanAttend' => true,
92
                    'UserCanPresent' => true,
93
                    'UserCanRename' => true,
94
                    'UserCanWrite' => true,
95
                    'UserCanNotWriteRelative' => false,
96
                    'SupportsUserInfo' => true,
97
                    'SupportsDeleteFile' => true,
98
                    'SupportsLocks' => true,
99
                    'SupportsGetLock' => true,
100
                    'SupportsExtendedLockLength' => true,
101
                    'UserFriendlyName' => $userIdentifier,
102
                    'SupportsUpdate' => true,
103
                    'SupportsRename' => true,
104
                    'DisablePrint' => false,
105
                    'AllowExternalMarketplace' => true,
106
                    'SupportedShareUrlTypes' => [
107
                        'ReadOnly',
108
                    ],
109
                    'SHA256' => $this->documentManager->getSha256($document),
110
                    'UserInfo' => (string) $this->cache->getItem($userCacheKey)->get(),
111
                    'LastModifiedTime' => $this->documentManager->getLastModifiedDate($document)
112
                        ->format(DateTimeInterface::ATOM),
113
                ]
114
            )));
115
    }
116
117
    public function deleteFile(string $fileId, string $accessToken, RequestInterface $request): ResponseInterface
118
    {
119
        $document = $this->documentManager->findByDocumentId($fileId);
120
121
        if (null === $document) {
122
            return $this->makeDocumentNotFoundResponse($fileId);
123
        }
124
125
        $this->documentManager->remove($document);
126
127
        return $this
128
            ->responseFactory
129
            ->createResponse(200);
130
    }
131
132
    public function enumerateAncestors(
133
        string $fileId,
134
        string $accessToken,
135
        RequestInterface $request
136
    ): ResponseInterface {
137
        return $this
138
            ->responseFactory
139
            ->createResponse(501);
140
    }
141
142
    public function getFile(
143
        string $fileId,
144
        string $accessToken,
145
        RequestInterface $request
146
    ): ResponseInterface {
147
        $document = $this->documentManager->findByDocumentId($fileId);
148
149
        if (null === $document) {
150
            return $this->makeDocumentNotFoundResponse($fileId);
151
        }
152
153
        $revision = $this->documentManager->getVersion($document);
154
        $content = $this->documentManager->read($document);
155
156
        return $this
157
            ->responseFactory
158
            ->createResponse()
159
            ->withHeader(
160
                WopiInterface::HEADER_ITEM_VERSION,
161
                sprintf('v%s', $revision)
162
            )
163
            ->withHeader(
164
                'Content-Type',
165
                'application/octet-stream',
166
            )
167
            ->withHeader(
168
                'Content-Length',
169
                (string) $this->documentManager->getSize($document)
170
            )
171
            ->withHeader(
172
                'Content-Disposition',
173
                sprintf('attachment; filename=%s', $this->documentManager->getBasename($document))
174
            )
175
            ->withBody($content);
176
    }
177
178
    public function getLock(string $fileId, string $accessToken, RequestInterface $request): ResponseInterface
179
    {
180
        $document = $this->documentManager->findByDocumentId($fileId);
181
182
        if (null === $document) {
183
            return $this->makeDocumentNotFoundResponse($fileId);
184
        }
185
186
        if ($this->documentManager->hasLock($document)) {
187
            return $this
188
                ->responseFactory
189
                ->createResponse()
190
                ->withHeader(WopiInterface::HEADER_LOCK, $this->documentManager->getLock($document));
191
        }
192
193
        return $this
194
            ->responseFactory
195
            ->createResponse(404)
196
            ->withHeader(WopiInterface::HEADER_LOCK, '');
197
    }
198
199
    public function getShareUrl(string $fileId, string $accessToken, RequestInterface $request): ResponseInterface
200
    {
201
        return $this
202
            ->responseFactory
203
            ->createResponse(501);
204
    }
205
206
    public function lock(
207
        string $fileId,
208
        string $accessToken,
209
        string $xWopiLock,
210
        RequestInterface $request
211
    ): ResponseInterface {
212
        $document = $this->documentManager->findByDocumentId($fileId);
213
214
        if (null === $document) {
215
            return $this->makeDocumentNotFoundResponse($fileId);
216
        }
217
218
        $version = $this->documentManager->getVersion($document);
219
220
        if ($this->documentManager->hasLock($document)) {
221
            if ($xWopiLock === $currentLock = $this->documentManager->getLock($document)) {
222
                return $this->refreshLock($fileId, $accessToken, $xWopiLock, $request);
223
            }
224
225
            return $this
226
                ->responseFactory
227
                ->createResponse(409)
228
                ->withHeader(WopiInterface::HEADER_LOCK, $currentLock)
229
                ->withHeader(
230
                    WopiInterface::HEADER_ITEM_VERSION,
231
                    sprintf('v%s', $version)
232
                );
233
        }
234
235
        $this->documentManager->lock($document, $xWopiLock);
236
237
        return $this
238
            ->responseFactory
239
            ->createResponse()
240
            ->withHeader(
241
                WopiInterface::HEADER_ITEM_VERSION,
242
                sprintf('v%s', $version)
243
            );
244
    }
245
246
    public function putFile(
247
        string $fileId,
248
        string $accessToken,
249
        string $xWopiLock,
250
        string $xWopiEditors,
251
        RequestInterface $request
252
    ): ResponseInterface {
253
        return ($this->putFileExecutor)($fileId, $accessToken, $xWopiLock, $xWopiEditors, $request);
254
    }
255
256
    public function putRelativeFile(
257
        string $fileId,
258
        string $accessToken,
259
        ?string $suggestedTarget,
260
        ?string $relativeTarget,
261
        bool $overwriteRelativeTarget,
262
        int $size,
263
        RequestInterface $request
264
    ): ResponseInterface {
265
        if ((null === $suggestedTarget) && (null === $relativeTarget)) {
266
            return $this
267
                ->responseFactory
268
                ->createResponse(400)
269
                ->withBody($this->streamFactory->createStream((string) json_encode([
270
                    'message' => 'target is null',
271
                ])));
272
        }
273
274
        if (null !== $suggestedTarget) {
275
            // If it starts with a dot...
276
            if (0 === strpos($suggestedTarget, '.', 0)) {
277
                $document = $this->documentManager->findByDocumentId($fileId);
278
279
                if (null === $document) {
280
                    return $this->makeDocumentNotFoundResponse();
0 ignored issues
show
Bug introduced by
The call to ChampsLibres\WopiBundle\...umentNotFoundResponse() has too few arguments starting with fileId. ( Ignorable by Annotation )

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

280
                    return $this->/** @scrutinizer ignore-call */ makeDocumentNotFoundResponse();

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
281
                }
282
                $filename = pathinfo($this->documentManager->getBasename($document), PATHINFO_EXTENSION | PATHINFO_FILENAME);
283
284
                $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

284
                $suggestedTarget = sprintf('%s%s', /** @scrutinizer ignore-type */ $filename, $suggestedTarget);
Loading history...
285
            }
286
287
            $target = $suggestedTarget;
288
        }
289
290
        if (null !== $relativeTarget) {
291
            $document = $this->documentManager->findByDocumentFilename($relativeTarget);
292
293
            /**
294
             * If a file with the specified name already exists,
295
             * the host must respond with a 409 Conflict,
296
             * unless the X-WOPI-OverwriteRelativeTarget request header is set to true.
297
             *
298
             * When responding with a 409 Conflict for this reason,
299
             * the host may include an X-WOPI-ValidRelativeTarget specifying a file name that is valid.
300
             *
301
             * If the X-WOPI-OverwriteRelativeTarget request header is set to true
302
             * and a file with the specified name already exists and is locked,
303
             * the host must respond with a 409 Conflict and include an
304
             * X-WOPI-Lock response header containing the value of the current lock on the file.
305
             */
306
            if (null !== $document) {
307
                if (false === $overwriteRelativeTarget) {
308
                    $extension = pathinfo($this->documentManager->getBasename($document), PATHINFO_EXTENSION);
309
310
                    return $this
311
                        ->responseFactory
312
                        ->createResponse(409)
313
                        ->withHeader('Content-Type', 'application/json')
314
                        ->withHeader(
315
                            WopiInterface::HEADER_VALID_RELATIVE_TARGET,
316
                            sprintf('%s.%s', uniqid(), $extension)
317
                        );
318
                }
319
320
                if ($this->documentManager->hasLock($document)) {
321
                    return $this
322
                        ->responseFactory
323
                        ->createResponse(409)
324
                        ->withHeader(WopiInterface::HEADER_LOCK, $this->documentManager->getLock($document));
325
                }
326
            }
327
328
            $target = $relativeTarget;
329
        }
330
331
        $pathInfo = pathinfo($target, PATHINFO_EXTENSION | PATHINFO_FILENAME);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $target does not seem to be defined for all execution paths leading up to this point.
Loading history...
332
333
        $new = $this->documentManager->create([
334
            'basename' => $target,
335
            'name' => $pathInfo['filename'],
336
            'extension' => $pathInfo['extension'],
337
            'content' => (string) $request->getBody(),
338
            'size' => $request->getHeaderLine(WopiInterface::HEADER_SIZE),
339
        ]);
340
341
        $this->documentManager->write($new);
342
343
        $uri = $this
344
            ->uriFactory
345
            ->createUri(
346
                $this
347
                    ->router
348
                    ->generate(
349
                        'checkFileInfo',
350
                        [
351
                            'fileId' => $this->documentManager->getDocumentId($new),
352
                        ],
353
                        RouterInterface::ABSOLUTE_URL
354
                    )
355
            )
356
            ->withQuery(http_build_query([
357
                'access_token' => $accessToken,
358
            ]));
359
360
        $properties = [
361
            'Name' => $this->documentManager->getBasename($new),
362
            'Url' => (string) $uri,
363
            'HostEditUrl' => $this->documentManager->getDocumentId($new),
364
            'HostViewUrl' => $this->documentManager->getDocumentId($new),
365
        ];
366
367
        return $this
368
            ->responseFactory
369
            ->createResponse()
370
            ->withHeader('Content-Type', 'application/json')
371
            ->withBody($this->streamFactory->createStream((string) json_encode($properties)));
372
    }
373
374
    public function putUserInfo(string $fileId, string $accessToken, RequestInterface $request): ResponseInterface
375
    {
376
        $userCacheKey = sprintf('wopi_putUserInfo_%s', $this->tokenStorage->getToken()->getUser()->getUserIdentifier());
377
378
        $cacheItem = $this->cache->getItem($userCacheKey);
379
        $cacheItem->set((string) $request->getBody());
380
        $this->cache->save($cacheItem);
381
382
        return $this
383
            ->responseFactory
384
            ->createResponse();
385
    }
386
387
    public function refreshLock(
388
        string $fileId,
389
        string $accessToken,
390
        string $xWopiLock,
391
        RequestInterface $request
392
    ): ResponseInterface {
393
        $this->unlock($fileId, $accessToken, $xWopiLock, $request);
394
395
        return $this->lock($fileId, $accessToken, $xWopiLock, $request);
396
    }
397
398
    public function renameFile(
399
        string $fileId,
400
        string $accessToken,
401
        string $xWopiLock,
402
        string $xWopiRequestedName,
403
        RequestInterface $request
404
    ): ResponseInterface {
405
        $document = $this->documentManager->findByDocumentId($fileId);
406
407
        if (null === $document) {
408
            return $this->makeDocumentNotFoundResponse($fileId);
409
        }
410
411
        if ($this->documentManager->hasLock($document)) {
412
            if ($xWopiLock !== $currentLock = $this->documentManager->getLock($document)) {
413
                return $this
414
                    ->responseFactory
415
                    ->createResponse(409)
416
                    ->withHeader(WopiInterface::HEADER_LOCK, $currentLock);
417
            }
418
        }
419
420
        $this->documentManager->write($document, ['filename' => $xWopiRequestedName]);
421
422
        $data = [
423
            'Name' => $xWopiRequestedName,
424
        ];
425
426
        return $this
427
            ->responseFactory
428
            ->createResponse(200)
429
            ->withHeader('Content-Type', 'application/json')
430
            ->withBody(
431
                $this->streamFactory->createStream((string) json_encode($data))
432
            );
433
    }
434
435
    public function unlock(
436
        string $fileId,
437
        string $accessToken,
438
        string $xWopiLock,
439
        RequestInterface $request
440
    ): ResponseInterface {
441
        $document = $this->documentManager->findByDocumentId($fileId);
442
443
        if (null === $document) {
444
            return $this->makeDocumentNotFoundResponse($fileId);
445
        }
446
447
        $version = $this->documentManager->getVersion($document);
448
449
        if (!$this->documentManager->hasLock($document)) {
450
            return $this
451
                ->responseFactory
452
                ->createResponse(409)
453
                ->withHeader(WopiInterface::HEADER_LOCK, '');
454
        }
455
456
        $currentLock = $this->documentManager->getLock($document);
457
458
        if ($currentLock !== $xWopiLock) {
459
            return $this
460
                ->responseFactory
461
                ->createResponse(409)
462
                ->withHeader(WopiInterface::HEADER_LOCK, $currentLock);
463
        }
464
465
        $this->documentManager->deleteLock($document);
466
467
        return $this
468
            ->responseFactory
469
            ->createResponse()
470
            ->withHeader(WopiInterface::HEADER_LOCK, '')
471
            ->withHeader(
472
                WopiInterface::HEADER_ITEM_VERSION,
473
                sprintf('v%s', $version)
474
            );
475
    }
476
477
    public function unlockAndRelock(
478
        string $fileId,
479
        string $accessToken,
480
        string $xWopiLock,
481
        string $xWopiOldLock,
482
        RequestInterface $request
483
    ): ResponseInterface {
484
        $this->unlock($fileId, $accessToken, $xWopiOldLock, $request);
485
486
        return $this->lock($fileId, $accessToken, $xWopiLock, $request);
487
    }
488
489
    private function makeDocumentNotFoundResponse(string $fileId): ResponseInterface
490
    {
491
        return $this->responseFactory->createResponse(404)
492
            ->withBody($this->streamFactory->createStream((string) json_encode([
493
                'message' => "Document with id {$fileId} not found",
494
            ])));
495
    }
496
}
497