Completed
Push — master ( b97427...e235cc )
by Raffael
30:35 queued 26:08
created

Files::post()   C

Complexity

Conditions 13
Paths 43

Size

Total Lines 91

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 0
Metric Value
dl 0
loc 91
ccs 0
cts 74
cp 0
rs 5.4896
c 0
b 0
f 0
cc 13
nc 43
nop 2
crap 182

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
216
        $this->logger->info('incoming POST wopi operation [{operation}] width id [{identifier}]', [
217
            'category' => get_class($this),
218
            'operation' => $op,
219
            'identifier' => $identifier,
220
        ]);
221
222
        $this->validateProof($access_token);
223
        $file = $this->server->getFilesystem()->findNodeById($id, File::class);
224
        $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...
225
        $response = new Response();
226
227
        if (!$file->isLocked() && $file->getSize() > 0) {
228
            return $response
229
                ->setCode(409)
230
                ->setBody(new Exception\NotLocked('file needs to be locked first'));
231
        }
232
233
        try {
234
            $content = fopen('php://input', 'rb');
235
            $result = $file->put($content, false);
236
237
            return $response
238
                ->setCode(200)
239
                ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
240
                ->setBody($result);
241
        } catch (Exception\Locked | Exception\LockIdMissmatch $e) {
242
            $lock = $file->getLock();
243
244
            return $response
245
                ->setCode(409)
246
                ->setHeader('X-WOPI-Lock', $lock['id'])
247
                ->setHeader('X-WOPI-LockFailureReason', $e->getMessage())
248
                ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
249
                ->setBody($e);
250
        }
251
    }
252
253
    /**
254
     * Get document contents.
255
     */
256
    public function getContents(ObjectId $id, string $access_token): Response
257
    {
258
        $file = $this->server->getFilesystem()->findNodeById($id, File::class);
259
        $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...
260
        $this->validateProof($access_token);
261
        $stream = $file->get();
262
263
        $response = (new Response())
264
            ->setCode(200)
265
            ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
266
            ->setBody(function () use ($stream) {
267
                if ($stream === null) {
268
                    echo '';
269
270
                    return;
271
                }
272
273
                while (!feof($stream)) {
274
                    echo fread($stream, 8192);
275
                }
276
            });
277
278
        return $response;
279
    }
280
281
    /**
282
     * Validate proof.
283
     */
284
    protected function validateProof(string $access_token): bool
285
    {
286
        if (isset($_SERVER['HTTP_X_WOPI_PROOF'])) {
287
            $data = [
288
                'proof' => $_SERVER['HTTP_X_WOPI_PROOF'],
289
                'proof-old' => $_SERVER['HTTP_X_WOPI_PROOFOLD'] ?? '',
290
                'access-token' => $access_token,
291
                'host-url' => 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'],
292
                'timestamp' => $_SERVER['HTTP_X_WOPI_TIMESTAMP'] ?? '',
293
            ];
294
295
            return $this->host_manager->verifyWopiProof($data);
296
        }
297
298
        return false;
299
    }
300
301
    /**
302
     * Put relative file.
303
     */
304
    protected function putRelative(File $file, Response $response, SessionInterface $session): Response
305
    {
306
        $suggested = $_SERVER['HTTP_X_WOPI_SUGGESTEDTARGET'] ?? null;
307
        $relative = $_SERVER['HTTP_X_WOPI_RELATIVETARGET'] ?? null;
308
        $conversion = $_SERVER['HTTP_X_WOPI_FILECONVERSION'] ?? null;
309
        $overwrite = $_SERVER['HTTP_X_WOPI_OVERWRITERELATIVETARGET'] ?? false;
310
        $overwrite = ($overwrite === 'False' || $overwrite === false) ? false : true;
311
        $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...
312
        $new = null;
313
        $name = null;
314
315
        $parent = $file->getParent();
316
        $content = fopen('php://input', 'rb');
317
318
        $this->logger->debug('wopi PutRelative request', [
319
            'category' => get_class($this),
320
            'X-Wopi-SuggestedTarget' => $suggested,
321
            'X-Wopi-RelativeTarget' => $relative,
322
            'X-Wopi-OverwriteRelativeTarget' => $overwrite,
323
            'X-Wopi-FileConversion' => $conversion,
324
        ]);
325
326
        if ($suggested !== null && $relative !== null) {
327
            return $response
328
                ->setCode(400)
329
                ->setBody([
330
                    '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...
331
                    'Url' => $session->getWopiUrl(),
332
                ]);
333
        }
334
335
        try {
336
            if ($suggested !== null) {
337
                if ($suggested[0] === '.') {
338
                    $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...
339
                }
340
341
                try {
342
                    $name = mb_convert_encoding($suggested, 'UTF-8', 'UTF-7');
343
                    $new = $parent->addFile($name);
344
                    $new->put($content, false);
345
                } catch (Exception\Conflict $e) {
346
                    $name = $file->getDuplicateName($name);
347
                    $new = $parent->addFile($name);
348
                    $new->put($content, false);
349
                }
350
            } elseif ($relative !== null) {
351
                try {
352
                    $name = mb_convert_encoding($relative, 'UTF-8', 'UTF-7');
353
                    $new = $parent->addFile($name);
354
                    $new->put($content, false);
355
                } catch (Exception\Conflict $e) {
356
                    if ($e->getCode() === Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS && $overwrite === true) {
357
                        $new = $parent->getChild($name);
358
                        $new->put($content, false);
359
                    } else {
360
                        return $response
361
                            ->setCode(409)
362
                            ->setBody([
363
                                'Name' => $name,
364
                                'Url' => $session->getWopiUrl(),
365
                            ]);
366
                    }
367
                }
368
            } else {
369
                return $response
370
                    ->setCode(400)
371
                    ->setBody([
372
                        'Name' => $name,
373
                        'Url' => $session->getWopiUrl(),
374
                    ]);
375
            }
376
        } catch (Exception\InvalidArgument $e) {
377
            return $response
378
                ->setCode(400)
379
                ->setBody([
380
                    'Name' => $name,
381
                    'Url' => $session->getWopiUrl(),
382
                ]);
383
        } catch (Exception\Locked $e) {
384
            return $response
385
                ->setCode(409)
386
                ->setHeader('X-WOPI-ItemVersion', (string) $file->getVersion())
387
                ->setHeader('X-WOPI-LockFailureReason', $e->getMessage())
388
                ->setBody([
389
                    'Name' => $name,
390
                    'Url' => $session->getWopiUrl(),
391
                ]);
392
        }
393
394
        $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...
395
        $response->setBody([
396
            'Name' => $new->getName(),
397
            'Url' => $session->getWopiUrl(),
398
        ]);
399
400
        return $response;
401
    }
402
403
    /**
404
     * Rename file.
405
     */
406
    protected function renameFile(File $file, Response $response): Response
407
    {
408
        $name = $_SERVER['HTTP_X_WOPI_REQUESTEDNAME'] ?? '';
409
        $name = mb_convert_encoding($name, 'UTF-8', 'UTF-7');
410
        $full = $name;
411
412
        try {
413
            $ext = $file->getExtension();
414
            $full = $name.'.'.$ext;
415
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
416
        }
417
418
        try {
419
            $file->setName($full);
420
        } catch (Exception\Conflict $e) {
421
            return (new Response())
422
                ->setCode(400)
423
                ->setHeader('X-WOPI-InvalidFileNameError', (string) $e->getMessage())
424
                ->setBody($e);
425
        }
426
427
        $response->setBody([
428
            'Name' => $name,
429
        ]);
430
431
        return $response;
432
    }
433
}
434