Completed
Push — master ( 37faaa...541bbf )
by Raffael
10:18 queued 06:30
created

Files::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.7998
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\App\Wopi\Wopi;
13
14
use Balloon\App\Wopi\Exception\MissingWopiOperation as MissingWopiOperationException;
15
use Balloon\App\Wopi\Exception\UnknownWopiOperation as UnknownWopiOperationException;
16
use Balloon\App\Wopi\HostManager;
17
use Balloon\App\Wopi\Session\SessionInterface;
18
use Balloon\App\Wopi\SessionManager;
19
use Balloon\Filesystem\Exception;
20
use Balloon\Filesystem\Node\AttributeDecorator;
21
use Balloon\Filesystem\Node\File;
22
use Balloon\Server;
23
use Micro\Http\Response;
24
use MongoDB\BSON\ObjectId;
25
use Psr\Log\LoggerInterface;
26
27
/**
28
 * Implements the full WOPI protocol.
29
 *
30
 * @see https://wopi.readthedocs.io/projects/wopirest/en/latest
31
 */
32
class Files
33
{
34
    /**
35
     * WOPI operations.
36
     */
37
    const WOPI_GET_LOCK = 'GET_LOCK';
38
    const WOPI_LOCK = 'LOCK';
39
    const WOPI_REFRESH_LOCK = 'REFRESH_LOCK';
40
    const WOPI_UNLOCK = 'UNLOCK';
41
    const WOPI_PUT = 'PUT';
42
    const WOPI_PUT_RELATIVE = 'PUT_RELATIVE';
43
    const WOPI_RENAME_FILE = 'RENAME_FILE';
44
    const WOPI_DELETE = 'DELETE';
45
    const WOPI_PUT_USERINFO = 'PUT_USERINFO';
46
47
    /**
48
     * Server.
49
     *
50
     * @var Server
51
     */
52
    protected $server;
53
54
    /**
55
     * Session manager.
56
     *
57
     * @var SessionManager
58
     */
59
    protected $session_manager;
60
61
    /**
62
     * Host manager.
63
     *
64
     * @var HostManager
65
     */
66
    protected $host_manager;
67
68
    /**
69
     * Attribute decorator.
70
     *
71
     * @var AttributeDecorator
72
     */
73
    protected $decorator;
74
75
    /**
76
     * Logger.
77
     *
78
     * @var LoggerInterface
79
     */
80
    protected $logger;
81
82
    /**
83
     * Constructor.
84
     */
85
    public function __construct(SessionManager $session_manager, HostManager $host_manager, Server $server, AttributeDecorator $decorator, LoggerInterface $logger)
86
    {
87
        $this->session_manager = $session_manager;
88
        $this->host_manager = $host_manager;
89
        $this->server = $server;
90
        $this->decorator = $decorator;
91
        $this->logger = $logger;
92
    }
93
94
    /**
95
     * Get document sesssion information.
96
     */
97
    public function get(ObjectId $id, string $access_token): Response
98
    {
99
        $file = $this->server->getFilesystem()->findNodeById($id, File::class);
100
        $session = $this->session_manager->getByToken($file, $access_token);
0 ignored issues
show
Compatibility introduced by
$file of type object<Balloon\Filesystem\Node\NodeInterface> is not a sub-type of object<Balloon\Filesystem\Node\File>. It seems like you assume a concrete implementation of the interface Balloon\Filesystem\Node\NodeInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
101
102
        $this->logger->info('incoming GET wopi operation', [
103
            'category' => get_class($this),
104
            'session' => $session->getAttributes(),
105
        ]);
106
107
        $this->validateProof($access_token);
108
109
        return (new Response())->setCode(200)->setBody($session->getAttributes(), true);
0 ignored issues
show
Unused Code introduced by
The call to Response::setBody() has too many arguments starting with true.

This check compares calls to functions or methods with their respective definitions. If the call has more 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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
110
    }
111
112
    /**
113
     * Lock file.
114
     */
115
    public function post(ObjectId $id, string $access_token): Response
116
    {
117
        $file = $this->server->getFilesystem()->findNodeById($id, File::class);
118
        $session = $this->session_manager->getByToken($file, $access_token);
0 ignored issues
show
Compatibility introduced by
$file of type object<Balloon\Filesystem\Node\NodeInterface> is not a sub-type of object<Balloon\Filesystem\Node\File>. It seems like you assume a concrete implementation of the interface Balloon\Filesystem\Node\NodeInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
119
120
        $op = $_SERVER['HTTP_X_WOPI_OVERRIDE'] ?? null;
121
        $identifier = $_SERVER['HTTP_X_WOPI_LOCK'] ?? null;
122
        $previous = $_SERVER['HTTP_X_WOPI_OLDLOCK'] ?? null;
123
        $_SERVER['HTTP_LOCK_TOKEN'] = $identifier;
124
125
        $this->logger->info('incoming POST wopi operation [{operation}] with id [{identifier}]', [
126
            'category' => get_class($this),
127
            'operation' => $op,
128
            'identifier' => $identifier,
129
            'previous' => $previous,
130
        ]);
131
132
        $this->validateProof($access_token);
133
        $response = (new Response())
134
            ->setCode(200)
135
            ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion());
136
137
        try {
138
            switch ($op) {
139
                case self::WOPI_GET_LOCK:
140
                    $lock = $file->getLock();
141
                    $response->setHeader('X-WOPI-Lock', $lock['id']);
142
                    $response->setBody($this->decorator->decorate($file, ['lock'])['lock']);
143
144
                break;
145
                case self::WOPI_LOCK:
146
                    if ($previous !== null) {
147
                        $file->unlock($previous);
148
                    }
149
150
                    $file->lock($identifier);
151
                    $response->setBody($this->decorator->decorate($file, ['lock'])['lock']);
152
153
                break;
154
                case self::WOPI_REFRESH_LOCK:
155
                    $file->lock($identifier, 1800);
156
157
                break;
158
                case self::WOPI_UNLOCK:
159
                    if (!$file->isLocked()) {
160
                        $response->setCode(409)
161
                            ->setHeader('X-WOPI-Lock', '');
162
163
                        return $response;
164
                    }
165
166
                    $file->unlock($identifier);
167
168
                break;
169
                case self::WOPI_RENAME_FILE:
170
                    return $this->renameFile($file, $response);
0 ignored issues
show
Compatibility introduced by
$file of type object<Balloon\Filesystem\Node\NodeInterface> is not a sub-type of object<Balloon\Filesystem\Node\File>. It seems like you assume a concrete implementation of the interface Balloon\Filesystem\Node\NodeInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
171
172
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
173
                case self::WOPI_DELETE:
174
                    $file->delete();
175
176
                break;
177
                case self::WOPI_PUT_RELATIVE:
178
                    return $this->putRelative($file, $response, $session);
0 ignored issues
show
Compatibility introduced by
$file of type object<Balloon\Filesystem\Node\NodeInterface> is not a sub-type of object<Balloon\Filesystem\Node\File>. It seems like you assume a concrete implementation of the interface Balloon\Filesystem\Node\NodeInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
179
180
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
181
                case null:
182
                    throw new MissingWopiOperationException('no wopi operation provided');
183
184
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
185
                default:
186
                    throw new UnknownWopiOperationException('unknown wopi operation '.$op);
187
            }
188
        } catch (Exception\NotLocked $e) {
189
            return (new Response())
190
                ->setCode(200)
191
                ->setHeader('X-WOPI-Lock', '')
192
                ->setBody($e);
193
        } catch (Exception\Locked | Exception\LockIdMissmatch | Exception\Forbidden $e) {
194
            $lock = $file->getLock();
195
196
            return (new Response())
197
                ->setCode(409)
198
                ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
199
                ->setHeader('X-WOPI-Lock', $lock['id'])
200
                ->setHeader('X-WOPI-LockFailureReason', $e->getMessage())
201
                ->setBody($e);
202
        }
203
204
        return $response;
205
    }
206
207
    /**
208
     * Save document contents.
209
     */
210
    public function postContents(ObjectId $id, string $access_token): Response
211
    {
212
        $op = $_SERVER['HTTP_X_WOPI_OVERRIDE'] ?? null;
213
        $identifier = $_SERVER['HTTP_X_WOPI_LOCK'] ?? null;
214
        $previous = $_SERVER['HTTP_X_WOPI_OLDLOCK'] ?? null;
0 ignored issues
show
Unused Code introduced by
$previous 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...
215
        $_SERVER['HTTP_LOCK_TOKEN'] = $identifier;
216
        $agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
217
218
        $this->logger->info('incoming POST wopi operation [{operation}] with id [{identifier}]', [
219
            'category' => get_class($this),
220
            'operation' => $op,
221
            'identifier' => $identifier,
222
        ]);
223
224
        $this->validateProof($access_token);
225
        $file = $this->server->getFilesystem()->findNodeById($id, File::class);
226
        $session = $this->session_manager->getByToken($file, $access_token);
0 ignored issues
show
Compatibility introduced by
$file of type object<Balloon\Filesystem\Node\NodeInterface> is not a sub-type of object<Balloon\Filesystem\Node\File>. It seems like you assume a concrete implementation of the interface Balloon\Filesystem\Node\NodeInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
Unused Code introduced by
$session 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...
227
        $response = new Response();
228
229
        //loolwsd does not support locking (unlike the wopi specs require) #359
230
        if (!$file->isLocked() && $file->getSize() > 0 && strpos($agent, 'LOOLWSD') === false) {
231
            return $response
232
                ->setCode(409)
233
                ->setBody(new Exception\NotLocked('file needs to be locked first'));
234
        }
235
236
        try {
237
            $content = fopen('php://input', 'rb');
238
            $version = $file->getVersion();
239
            $result = $file->put($content);
240
241
            return $response
242
                ->setCode(200)
243
                ->setHeader('X-WOPI-ItemVersion', (string) ($version == $result ? $result : $result))
244
                ->setBody($result);
245
        } catch (Exception\Locked | Exception\LockIdMissmatch $e) {
246
            $lock = $file->getLock();
247
248
            return $response
249
                ->setCode(409)
250
                ->setHeader('X-WOPI-Lock', $lock['id'])
251
                ->setHeader('X-WOPI-LockFailureReason', $e->getMessage())
252
                ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
253
                ->setBody($e);
254
        }
255
    }
256
257
    /**
258
     * Get document contents.
259
     */
260
    public function getContents(ObjectId $id, string $access_token): Response
261
    {
262
        $file = $this->server->getFilesystem()->findNodeById($id, File::class);
263
        $session = $this->session_manager->getByToken($file, $access_token);
0 ignored issues
show
Compatibility introduced by
$file of type object<Balloon\Filesystem\Node\NodeInterface> is not a sub-type of object<Balloon\Filesystem\Node\File>. It seems like you assume a concrete implementation of the interface Balloon\Filesystem\Node\NodeInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
Unused Code introduced by
$session 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...
264
        $this->validateProof($access_token);
265
        $stream = $file->get();
266
267
        $response = (new Response())
268
            ->setCode(200)
269
            ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
270
            ->setBody(function () use ($stream) {
271
                if ($stream === null) {
272
                    echo '';
273
274
                    return;
275
                }
276
277
                while (!feof($stream)) {
278
                    echo fread($stream, 8192);
279
                }
280
            });
281
282
        return $response;
283
    }
284
285
    /**
286
     * Validate proof.
287
     */
288
    protected function validateProof(string $access_token): bool
289
    {
290
        if (isset($_SERVER['HTTP_X_WOPI_PROOF'])) {
291
            $data = [
292
                'proof' => $_SERVER['HTTP_X_WOPI_PROOF'],
293
                'proof-old' => $_SERVER['HTTP_X_WOPI_PROOFOLD'] ?? '',
294
                'access-token' => $access_token,
295
                'host-url' => 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'],
296
                'timestamp' => $_SERVER['HTTP_X_WOPI_TIMESTAMP'] ?? '',
297
            ];
298
299
            return $this->host_manager->verifyWopiProof($data);
300
        }
301
302
        return false;
303
    }
304
305
    /**
306
     * Put relative file.
307
     */
308
    protected function putRelative(File $file, Response $response, SessionInterface $session): Response
309
    {
310
        $suggested = $_SERVER['HTTP_X_WOPI_SUGGESTEDTARGET'] ?? null;
311
        $relative = $_SERVER['HTTP_X_WOPI_RELATIVETARGET'] ?? null;
312
        $conversion = $_SERVER['HTTP_X_WOPI_FILECONVERSION'] ?? null;
313
        $overwrite = $_SERVER['HTTP_X_WOPI_OVERWRITERELATIVETARGET'] ?? false;
314
        $overwrite = ($overwrite === 'False' || $overwrite === false) ? false : true;
315
        $size = $_SERVER['HTTP_X_WOPI_SIZE'] ?? null;
0 ignored issues
show
Unused Code introduced by
$size 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...
316
        $new = null;
317
        $name = null;
318
        $url = ($_SERVER['REQUEST_SCHEME'] ?? 'http').'://'.($_SERVER['HTTP_X_FORWARDED_HOST'] ?? $_SERVER['HTTP_HOST'] ?? 'localhost');
319
320
        $parent = $file->getParent();
321
        $content = fopen('php://input', 'rb');
322
323
        $this->logger->debug('wopi PutRelative request', [
324
            'category' => get_class($this),
325
            'X-Wopi-SuggestedTarget' => $suggested,
326
            'X-Wopi-RelativeTarget' => $relative,
327
            'X-Wopi-OverwriteRelativeTarget' => $overwrite,
328
            'X-Wopi-FileConversion' => $conversion,
329
        ]);
330
331
        if ($suggested !== null && $relative !== null) {
332
            return $response
333
                ->setCode(400)
334
                ->setBody([
335
                    'Name' => $file->getName(),
336
                    'Url' => $session->getWopiUrl($url),
337
                ]);
338
        }
339
340
        try {
341
            if ($suggested !== null) {
342
                if ($suggested[0] === '.') {
343
                    $suggested = substr($file->getName(), 0, strpos($file->getName(), '.')).$suggested;
344
                }
345
346
                try {
347
                    $name = mb_convert_encoding($suggested, 'UTF-8', 'UTF-7');
348
                    $new = $parent->addFile($name);
349
                    $new->put($content, false);
0 ignored issues
show
Unused Code introduced by
The call to File::put() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more 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.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
350
                } catch (Exception\Conflict $e) {
351
                    $name = $file->getDuplicateName($name);
352
                    $new = $parent->addFile($name);
353
                    $new->put($content);
354
                }
355
            } elseif ($relative !== null) {
356
                try {
357
                    $name = mb_convert_encoding($relative, 'UTF-8', 'UTF-7');
358
                    $new = $parent->addFile($name);
359
                    $new->put($content);
360
                } catch (Exception\Conflict $e) {
361
                    if ($e->getCode() === Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS && $overwrite === true) {
362
                        $new = $parent->getChild($name);
363
                        $new->put($content);
364
                    } else {
365
                        return $response
366
                            ->setCode(409)
367
                            ->setBody([
368
                                'Name' => $name,
369
                                'Url' => $session->getWopiUrl($url),
370
                            ]);
371
                    }
372
                }
373
            } else {
374
                return $response
375
                    ->setCode(400)
376
                    ->setBody([
377
                        'Name' => $name,
378
                        'Url' => $session->getWopiUrl($url),
379
                    ]);
380
            }
381
        } catch (Exception\InvalidArgument $e) {
382
            return $response
383
                ->setCode(400)
384
                ->setBody([
385
                    'Name' => $name,
386
                    'Url' => $session->getWopiUrl($url),
387
                ]);
388
        } catch (Exception\Locked $e) {
389
            return $response
390
                ->setCode(409)
391
                ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
392
                ->setHeader('X-WOPI-LockFailureReason', $e->getMessage())
393
                ->setBody([
394
                    'Name' => $name,
395
                    'Url' => $session->getWopiUrl($url),
396
                ]);
397
        }
398
399
        $session = $this->session_manager->create($new, $this->server->getUserById($new->getOwner()));
0 ignored issues
show
Compatibility introduced by
$new of type object<Sabre\DAV\INode> is not a sub-type of object<Balloon\Filesystem\Node\File>. It seems like you assume a concrete implementation of the interface Sabre\DAV\INode to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
400
        $response->setBody([
401
            'Name' => $new->getName(),
402
            'Url' => $session->getWopiUrl($url),
403
        ]);
404
405
        return $response;
406
    }
407
408
    /**
409
     * Rename file.
410
     */
411
    protected function renameFile(File $file, Response $response): Response
412
    {
413
        $name = $_SERVER['HTTP_X_WOPI_REQUESTEDNAME'] ?? '';
414
        $name = mb_convert_encoding($name, 'UTF-8', 'UTF-7');
415
        $full = $name;
416
417
        try {
418
            $ext = $file->getExtension();
419
            $full = $name.'.'.$ext;
420
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
421
        }
422
423
        try {
424
            $file->setName($full);
425
426
            if ($file->isDeleted()) {
427
                $response->setCode(404);
428
            }
429
        } catch (Exception\Conflict $e) {
430
            return (new Response())
431
                ->setCode(400)
432
                ->setHeader('X-WOPI-InvalidFileNameError', (string) $e->getMessage())
433
                ->setBody($e);
434
        }
435
436
        $response->setBody([
437
            'Name' => $name,
438
        ]);
439
440
        return $response;
441
    }
442
}
443