Completed
Push — master ( 0967fb...923f89 )
by Raffael
09:50 queued 05:40
created

Files::postContents()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
dl 0
loc 45
ccs 0
cts 38
cp 0
rs 8.8888
c 0
b 0
f 0
cc 5
nc 5
nop 2
crap 30
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\Api\v2\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);
101
102
        $this->logger->info('incoming GET wopi operation', [
103
            'category' => get_class($this),
104
        ]);
105
106
        $this->validateProof($access_token);
107
108
        return (new Response())->setCode(200)->setBody($session->getAttributes(), true);
109
    }
110
111
    /**
112
     * Lock file.
113
     */
114
    public function post(ObjectId $id, string $access_token): Response
115
    {
116
        $file = $this->server->getFilesystem()->findNodeById($id, File::class);
117
        $session = $this->session_manager->getByToken($file, $access_token);
118
119
        $op = $_SERVER['HTTP_X_WOPI_OVERRIDE'] ?? null;
120
        $identifier = $_SERVER['HTTP_X_WOPI_LOCK'] ?? null;
121
        $previous = $_SERVER['HTTP_X_WOPI_OLDLOCK'] ?? null;
122
        $_SERVER['HTTP_LOCK_TOKEN'] = $identifier;
123
124
        $this->logger->info('incoming POST wopi operation [{operation}] width id [{identifier}]', [
125
            'category' => get_class($this),
126
            'operation' => $op,
127
            'identifier' => $identifier,
128
            'previous' => $previous,
129
        ]);
130
131
        $this->validateProof($access_token);
132
        $response = (new Response())
133
            ->setCode(200)
134
            ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion());
135
136
        try {
137
            switch ($op) {
138
                case self::WOPI_GET_LOCK:
139
                    $lock = $file->getLock();
140
                    $response->setHeader('X-WOPI-Lock', $lock['id']);
141
                    $response->setBody($this->decorator->decorate($file, ['lock'])['lock']);
142
143
                break;
144
                case self::WOPI_LOCK:
145
                    if ($previous !== null) {
146
                        $file->unlock($previous);
147
                    }
148
149
                    $file->lock($identifier);
150
                    $response->setBody($this->decorator->decorate($file, ['lock'])['lock']);
151
152
                break;
153
                case self::WOPI_REFRESH_LOCK:
154
                    $file->lock($identifier, 1800);
155
156
                break;
157
                case self::WOPI_UNLOCK:
158
                    if (!$file->isLocked()) {
159
                        $response->setCode(409)
160
                            ->setHeader('X-WOPI-Lock', '');
161
162
                        return $response;
163
                    }
164
165
                    $file->unlock($identifier);
166
167
                break;
168
                case self::WOPI_RENAME_FILE:
169
                    return $this->renameFile($file, $response);
170
171
                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...
172
                case self::WOPI_DELETE:
173
                    $file->delete();
174
175
                break;
176
                case self::WOPI_PUT_RELATIVE:
177
                    return $this->putRelative($file, $response, $session);
178
179
                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...
180
                case null:
181
                    throw new MissingWopiOperationException('no wopi operation provided');
182
183
                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...
184
                default:
185
                    throw new UnknownWopiOperationException('unknown wopi operation '.$op);
186
            }
187
        } catch (Exception\NotLocked $e) {
188
            return (new Response())
189
                ->setCode(200)
190
                ->setHeader('X-WOPI-Lock', '')
191
                ->setBody($e);
192
        } catch (Exception\Locked | Exception\LockIdMissmatch | Exception\Forbidden $e) {
193
            $lock = $file->getLock();
194
195
            return (new Response())
196
                ->setCode(409)
197
                ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
198
                ->setHeader('X-WOPI-Lock', $lock['id'])
199
                ->setHeader('X-WOPI-LockFailureReason', $e->getMessage())
200
                ->setBody($e);
201
        }
202
203
        return $response;
204
    }
205
206
    /**
207
     * Save document contents.
208
     */
209
    public function postContents(ObjectId $id, string $access_token): Response
210
    {
211
        $op = $_SERVER['HTTP_X_WOPI_OVERRIDE'] ?? null;
212
        $identifier = $_SERVER['HTTP_X_WOPI_LOCK'] ?? null;
213
        $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...
214
        $_SERVER['HTTP_LOCK_TOKEN'] = $identifier;
215
        $agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
216
217
        $this->logger->info('incoming POST wopi operation [{operation}] width id [{identifier}]', [
218
            'category' => get_class($this),
219
            'operation' => $op,
220
            'identifier' => $identifier,
221
        ]);
222
223
        $this->validateProof($access_token);
224
        $file = $this->server->getFilesystem()->findNodeById($id, File::class);
225
        $session = $this->session_manager->getByToken($file, $access_token);
0 ignored issues
show
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...
226
        $response = new Response();
227
228
        //loolwsd does not support locking (unlike the wopi specs require) #359
229
        if (!$file->isLocked() && $file->getSize() > 0 && strpos($agent, 'LOOLWSD') === false) {
230
            return $response
231
                ->setCode(409)
232
                ->setBody(new Exception\NotLocked('file needs to be locked first'));
233
        }
234
235
        try {
236
            $content = fopen('php://input', 'rb');
237
            $result = $file->put($content, false);
238
239
            return $response
240
                ->setCode(200)
241
                ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
242
                ->setBody($result);
243
        } catch (Exception\Locked | Exception\LockIdMissmatch $e) {
244
            $lock = $file->getLock();
245
246
            return $response
247
                ->setCode(409)
248
                ->setHeader('X-WOPI-Lock', $lock['id'])
249
                ->setHeader('X-WOPI-LockFailureReason', $e->getMessage())
250
                ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
251
                ->setBody($e);
252
        }
253
    }
254
255
    /**
256
     * Get document contents.
257
     */
258
    public function getContents(ObjectId $id, string $access_token): Response
259
    {
260
        $file = $this->server->getFilesystem()->findNodeById($id, File::class);
261
        $session = $this->session_manager->getByToken($file, $access_token);
0 ignored issues
show
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...
262
        $this->validateProof($access_token);
263
        $stream = $file->get();
264
265
        $response = (new Response())
266
            ->setCode(200)
267
            ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
268
            ->setBody(function () use ($stream) {
269
                if ($stream === null) {
270
                    echo '';
271
272
                    return;
273
                }
274
275
                while (!feof($stream)) {
276
                    echo fread($stream, 8192);
277
                }
278
            });
279
280
        return $response;
281
    }
282
283
    /**
284
     * Validate proof.
285
     */
286
    protected function validateProof(string $access_token): bool
287
    {
288
        if (isset($_SERVER['HTTP_X_WOPI_PROOF'])) {
289
            $data = [
290
                'proof' => $_SERVER['HTTP_X_WOPI_PROOF'],
291
                'proof-old' => $_SERVER['HTTP_X_WOPI_PROOFOLD'] ?? '',
292
                'access-token' => $access_token,
293
                'host-url' => 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'],
294
                'timestamp' => $_SERVER['HTTP_X_WOPI_TIMESTAMP'] ?? '',
295
            ];
296
297
            return $this->host_manager->verifyWopiProof($data);
298
        }
299
300
        return false;
301
    }
302
303
    /**
304
     * Put relative file.
305
     */
306
    protected function putRelative(File $file, Response $response, SessionInterface $session): Response
307
    {
308
        $suggested = $_SERVER['HTTP_X_WOPI_SUGGESTEDTARGET'] ?? null;
309
        $relative = $_SERVER['HTTP_X_WOPI_RELATIVETARGET'] ?? null;
310
        $conversion = $_SERVER['HTTP_X_WOPI_FILECONVERSION'] ?? null;
311
        $overwrite = $_SERVER['HTTP_X_WOPI_OVERWRITERELATIVETARGET'] ?? false;
312
        $overwrite = ($overwrite === 'False' || $overwrite === false) ? false : true;
313
        $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...
314
        $new = null;
315
        $name = null;
316
317
        $parent = $file->getParent();
318
        $content = fopen('php://input', 'rb');
319
320
        $this->logger->debug('wopi PutRelative request', [
321
            'category' => get_class($this),
322
            'X-Wopi-SuggestedTarget' => $suggested,
323
            'X-Wopi-RelativeTarget' => $relative,
324
            'X-Wopi-OverwriteRelativeTarget' => $overwrite,
325
            'X-Wopi-FileConversion' => $conversion,
326
        ]);
327
328
        if ($suggested !== null && $relative !== null) {
329
            return $response
330
                ->setCode(400)
331
                ->setBody([
332
                    'Name' => $file->getName(),
0 ignored issues
show
Bug introduced by
Consider using $file->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
333
                    'Url' => $session->getWopiUrl(),
334
                ]);
335
        }
336
337
        try {
338
            if ($suggested !== null) {
339
                if ($suggested[0] === '.') {
340
                    $suggested = substr($file->getName(), 0, strpos($file->getName(), '.')).$suggested;
0 ignored issues
show
Bug introduced by
Consider using $file->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
341
                }
342
343
                try {
344
                    $name = mb_convert_encoding($suggested, 'UTF-8', 'UTF-7');
345
                    $new = $parent->addFile($name);
346
                    $new->put($content, false);
347
                } catch (Exception\Conflict $e) {
348
                    $name = $file->getDuplicateName($name);
349
                    $new = $parent->addFile($name);
350
                    $new->put($content, false);
351
                }
352
            } elseif ($relative !== null) {
353
                try {
354
                    $name = mb_convert_encoding($relative, 'UTF-8', 'UTF-7');
355
                    $new = $parent->addFile($name);
356
                    $new->put($content, false);
357
                } catch (Exception\Conflict $e) {
358
                    if ($e->getCode() === Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS && $overwrite === true) {
359
                        $new = $parent->getChild($name);
360
                        $new->put($content, false);
361
                    } else {
362
                        return $response
363
                            ->setCode(409)
364
                            ->setBody([
365
                                'Name' => $name,
366
                                'Url' => $session->getWopiUrl(),
367
                            ]);
368
                    }
369
                }
370
            } else {
371
                return $response
372
                    ->setCode(400)
373
                    ->setBody([
374
                        'Name' => $name,
375
                        'Url' => $session->getWopiUrl(),
376
                    ]);
377
            }
378
        } catch (Exception\InvalidArgument $e) {
379
            return $response
380
                ->setCode(400)
381
                ->setBody([
382
                    'Name' => $name,
383
                    'Url' => $session->getWopiUrl(),
384
                ]);
385
        } catch (Exception\Locked $e) {
386
            return $response
387
                ->setCode(409)
388
                ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
389
                ->setHeader('X-WOPI-LockFailureReason', $e->getMessage())
390
                ->setBody([
391
                    'Name' => $name,
392
                    'Url' => $session->getWopiUrl(),
393
                ]);
394
        }
395
396
        $session = $this->session_manager->create($new, $this->server->getUserById($new->getOwner()));
0 ignored issues
show
Bug introduced by
The method getOwner does only exist in Balloon\Filesystem\Node\File, but not in Balloon\Filesystem\Node\NodeInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

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