Completed
Pull Request — master (#141)
by Raffael
11:16
created

Files::putChunk()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 40
ccs 0
cts 35
cp 0
rs 9.28
c 0
b 0
f 0
cc 4
nc 5
nop 13
crap 20

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\App\Api\v2;
13
14
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
15
use Balloon\Filesystem\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\App\Api\v2\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
16
use Balloon\Filesystem\Node\Collection;
17
use Balloon\Server\AttributeDecorator as RoleAttributeDecorator;
18
use Micro\Http\Response;
19
use MongoDB\BSON\ObjectId;
20
21
class Files extends Nodes
22
{
23
    /**
24
     * @api {get} /api/v2/files/:id/history Get history
25
     * @apiVersion 2.0.0
26
     * @apiName getHistory
27
     * @apiGroup Node\File
28
     * @apiPermission none
29
     * @apiDescription Get a full change history of a file
30
     * @apiUse _getNode
31
     *
32
     * @apiExample (cURL) example:
33
     * curl -XGET "https://SERVER/api/v2/files/history?id=544627ed3c58891f058b4686&pretty"
34
     * curl -XGET "https://SERVER/api/v2/files/544627ed3c58891f058b4686/history?pretty"
35
     * curl -XGET "https://SERVER/api/v2/files/history?p=/absolute/path/to/my/file&pretty"
36
     *
37
     * @apiSuccess (200 OK) {object[]} - History
38
     * @apiSuccess (200 OK) {number} -.version Version
39
     * @apiSuccess (200 OK) {string} -.changed ISO806 timestamp
40
     * @apiSuccess (200 OK) {object} -.user User object
41
     * @apiSuccess (200 OK) {number} -.type Change type, there are five different change types including:</br>
42
     *  0 - Initially added</br>
43
     *  1 - Content modified</br>
44
     *  2 - Version rollback</br>
45
     *  3 - Deleted</br>
46
     *  4 - Undeleted
47
     * @apiSuccess (200 OK) {number} -.size Content size in bytes
48
     * @apiSuccessExample {json} Success-Response:
49
     * HTTP/1.1 200 OK
50
     * [
51
     *  {
52
     *      "version": 1,
53
     *      "changed": ""
54
     *      "user": {
55
     *          "id": "544627ed3c58891f058b4611",
56
     *          "name": "peter.meier"
57
     *      },
58
     *      "type": 0,
59
     *      "size": 178,
60
     *  }
61
     * ]
62
     *
63
     * @param string $id
64
     * @param string $p
65
     */
66
    public function getHistory(RoleAttributeDecorator $role_decorator, ?string $id = null, ?string $p = null): Response
67
    {
68
        $result = $this->_getNode($id, $p)->getHistory();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Balloon\Filesystem\Node\NodeInterface as the method getHistory() does only exist in the following implementations of said interface: Balloon\Filesystem\Node\File.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
69
        $body = [];
70
        foreach ($result as $version) {
71
            if ($version['user'] === null) {
72
                $user = null;
73
            } else {
74
                $user = $this->server->getUserById($version['user']);
75
                $user = $role_decorator->decorate($user, ['id', 'name', '_links']);
76
            }
77
78
            $body[] = [
79
                'version' => $version['version'],
80
                'changed' => $version['changed']->toDateTime()->format('c'),
81
                'type' => $version['type'],
82
                'size' => $version['size'],
83
                'user' => $user,
84
            ];
85
        }
86
87
        return (new Response())->setCode(200)->setBody(['data' => $body]);
88
    }
89
90
    /**
91
     * @api {post} /api/v2/files/:id/restore Rollback version
92
     * @apiVersion 2.0.0
93
     * @apiName postRestore
94
     * @apiGroup Node\File
95
     * @apiPermission none
96
     * @apiDescription Rollback to a recent version from history. Use the version number from history.
97
     * @apiUse _getNode
98
     *
99
     * @apiExample (cURL) example:
100
     * curl -XPOST "https://SERVER/api/v2/files/restore?id=544627ed3c58891f058b4686&pretty&vesion=11"
101
     * curl -XPOST "https://SERVER/api/v2/files/544627ed3c58891f058b4686/restore?pretty&version=1"
102
     * curl -XPOST "https://SERVER/api/v2/files/restore?p=/absolute/path/to/my/file&pretty&version=3"
103
     *
104
     * @apiParam (GET Parameter) {number} version The version from history to rollback to
105
     *
106
     * @apiSuccessExample {json} Success-Response:
107
     * HTTP/1.1 200 OK
108
     * {
109
     *      "id": "544627ed3c58891f058b4686"
110
     * }
111
     *
112
     * @param string $id
113
     * @param string $p
114
     * @param string $version
115
     */
116
    public function postRestore(int $version, ?string $id = null, ?string $p = null): Response
117
    {
118
        $node = $this->_getNode($id, $p);
119
        $node->restore($version);
120
        $result = $this->node_decorator->decorate($node);
121
122
        return (new Response())->setCode(200)->setBody($result);
123
    }
124
125
    /**
126
     * @api {put} /api/v2/files/chunk Upload file chunk
127
     * @apiVersion 2.0.0
128
     * @apiName putChunk
129
     * @apiGroup Node\File
130
     * @apiPermission none
131
     * @apiUse _getNode
132
     * @apuUse _conflictNode
133
     * @apiUse _writeAction
134
     * @apiDescription Upload a file chunk. Use this method if you have possible big files!
135
     * You have to manually splitt the binary data into
136
     * multiple chunks and upload them successively using this method. Once uploading the last chunk,
137
     * the server will automatically create or update the file node.
138
     * You may set the parent collection, name and or custom attributes only with the last request to save traffic.
139
     *
140
     * @apiExample (cURL) example:
141
     * # Upload a new file myfile.jpg into the collection 544627ed3c58891f058b4686.
142
     * 1. First splitt the file into multiple 8M (For example, you could also use a smaller or bigger size) chunks
143
     * 2. Create a unique name for the session (Could also be the filename), best thing is to create a UUIDv4
144
     * 3. Upload each chunk successively (follow the binary order of your file!) using the chunk PUT method
145
     *   (The server identifies each chunk with the index parameter, beginning with #1).
146
     * 4. If chunk number 3 will be reached, the server automatically place all chunks to the new file node
147
     *
148
     * curl -XPUT "https://SERVER/api/v2/files/chunk?collection=544627ed3c58891f058b4686&name=myfile.jpg&index=1&chunks=3&session=myuniquesession&size=12342442&pretty" --data-binary @chunk1.bin
149
     * curl -XPUT "https://SERVER/api/v2/files/chunk?collection=544627ed3c58891f058b4686&name=myfile.jpg&index=2&chunks=3&session=myuniquesession&size=12342442&pretty" --data-binary @chunk2.bin
150
     * curl -XPUT "https://SERVER/api/v2/files/chunk?collection=544627ed3c58891f058b4686&name=myfile.jpg&index=3&chunks=3&session=myuniquesession&size=12342442&pretty" --data-binary @chunk3.bin
151
     *
152
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
153
     * @apiParam (GET Parameter) {string} [p] Either id, p (path) of a file node or a parent collection id must be given
154
     * @apiParam (GET Parameter) {string} [collection] Either id, p (path) of a file node or a parent collection id must be given
155
     * (If none of them are given, the file will be placed to the root)
156
     * @apiParam (GET Parameter) {string} [name] Needs to be set if the chunk belongs to a new file
157
     * @apiParam (GET Parameter) {number} index Chunk ID
158
     * @apiParam (GET Parameter) {number} chunks Total number of chunks
159
     * @apiParam (GET Parameter) {string} session Session ID you have received during uploading the first chunk
160
     * @apiParam (GET Parameter) {number} size The total file size in bytes
161
     * @apiParam (GET Parameter) {string} [created] Set specific created ISO806 timestamp
162
     * @apiParam (GET Parameter) {string} [changed] Set specific changed ISO806 timestamp
163
     * @apiParam (GET Parameter) {bool} [readonly] Mark node readonly
164
     * @apiParam (GET Parameter) {object} [meta] Meta attributes
165
     *
166
     *
167
     * @apiSuccess (200 OK) {number} status Status Code
168
     * @apiSuccess (200 OK) {number} data Increased version number if the last chunk was uploaded and existing node was updated.
169
     * It will return the old version if the submited file content was equal to the existing one.
170
     *
171
     * @apiSuccess (201 Created) {number} status Status Code
172
     * @apiSuccess (201 Created) {string} data Node ID if the last chunk was uploaded and a new node was added
173
     *
174
     * @apiSuccess (206 Partial Content) {number} status Status Code
175
     * @apiSuccess (206 Partial Content) {string} data Chunk ID if it was not the last chunk
176
     * @apiSuccessExample {json} Success-Response (Not the last chunk yet):
177
     * HTTP/1.1 206 Partial Content
178
     * {
179
     *      "session": "78297329329389e332234341",
180
     *      "size": 12323244224,
181
     *      "chunks_left": 4
182
     * }
183
     *
184
     * @apiSuccessExample {json} Success-Response (New file created, Last chunk):
185
     * HTTP/1.1 201 Created
186
     * {
187
     *      "id": "78297329329389e332234342",
188
     *      "version": 1
189
     * }
190
     *
191
     * @apiSuccessExample {json} Success-Response (File updated, Last chunk):
192
     * HTTP/1.1 200 OK
193
     * {
194
     *      "id": "78297329329389e332234342",
195
     *      "version": 2
196
     * }
197
     *
198
     * @apiErrorExample {json} Error-Response (quota full):
199
     * HTTP/1.1 507 Insufficient Storage
200
     * {
201
     *      "status": 507
202
     *      "data": {
203
     *          "error": "Balloon\Exception\InsufficientStorage",
204
     *          "message": "user quota is full",
205
     *          "code": 66
206
     *      }
207
     * }
208
     *
209
     * @apiErrorExample {json} Error-Response (Size limit exceeded):
210
     * HTTP/1.1 400 Bad Request
211
     * {
212
     *      "status": 400,
213
     *      "data": {
214
     *          "error": "Balloon\\Exception\\Conflict",
215
     *          "message": "file size exceeded limit",
216
     *          "code": 17
217
     *      }
218
     * }
219
     *
220
     * @apiErrorExample {json} Error-Response (Chunks lost):
221
     * HTTP/1.1 400 Bad Request
222
     * {
223
     *      "status": 400,
224
     *      "data": {
225
     *          "error": "Balloon\\Exception\\Conflict",
226
     *          "message": "chunks lost, reupload all chunks",
227
     *          "code": 275
228
     *      }
229
     * }
230
     *
231
     * @apiErrorExample {json} Error-Response (Chunks invalid size):
232
     * HTTP/1.1 400 Bad Request
233
     * {
234
     *      "status": 400,
235
     *      "data": {
236
     *          "error": "Balloon\\Exception\\Conflict",
237
     *          "message": "merged chunks temp file size is not as expected",
238
     *          "code": 276
239
     *      }
240
     * }
241
     *
242
     * @param string $id
243
     * @param string $p
244
     * @param string $collection
245
     * @param string $name
246
     * @param string $session
247
     * @param string $changed
248
     * @param string $created
249
     * @param bool   $readonly
250
     * @param array  $meta
251
     *
252
     * @return Response
253
     */
254
    public function putChunk(
255
        ?ObjectId $session = null,
256
        ?string $id = null,
257
        ?string $p = null,
258
        ?string $collection = null,
259
        ?string $name = null,
260
        int $index = 1,
261
        int $chunks = 0,
262
        int $size = 0,
0 ignored issues
show
Unused Code introduced by
The parameter $size is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
263
        int $conflict = 0,
264
        ?string $changed = null,
265
        ?string $created = null,
266
        ?bool $readonly = null,
267
        ?array $meta = null
268
    ) {
269
        ini_set('auto_detect_line_endings', '1');
270
        $input = fopen('php://input', 'rb');
271
        if ($index > $chunks) {
272
            throw new Exception\InvalidArgument('chunk index can not be greater than the total number of chunks');
273
        }
274
275
        if ($session === null) {
276
            $session = $this->storage->storeTemporaryFile($input, $this->server->getIdentity());
277
        } else {
278
            $this->storage->storeTemporaryFile($input, $this->server->getIdentity(), $session);
279
        }
280
281
        if ($index === $chunks) {
282
            $attributes = compact('changed', 'created', 'readonly', 'meta');
283
            $attributes = array_filter($attributes, function ($attribute) {return !is_null($attribute); });
284
            $attributes = $this->_verifyAttributes($attributes);
285
286
            return $this->_put($session, $id, $p, $collection, $name, $attributes, $conflict);
0 ignored issues
show
Bug introduced by
It seems like $session defined by parameter $session on line 255 can also be of type string; however, Balloon\App\Api\v2\Files::_put() does only seem to accept object<MongoDB\BSON\ObjectId>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
287
        }
288
289
        return (new Response())->setCode(206)->setBody([
290
                'session' => (string) $session,
291
                'chunks_left' => $chunks - $index,
292
            ]);
293
    }
294
295
    /**
296
     * @api {put} /api/v2/files Upload file
297
     * @apiVersion 2.0.0
298
     * @apiName put
299
     * @apiGroup Node\File
300
     * @apiPermission none
301
     * @apiUse _getNode
302
     * @apiUse _conflictNode
303
     * @apiUse _writeAction
304
     *
305
     * @apiDescription Upload an entire file in one-shot. Attention, there is file size limit,
306
     * if you have possible big files use the method PUT chunk!
307
     *
308
     * @apiExample (cURL) example:
309
     * #Update content of file 544627ed3c58891f058b4686
310
     * curl -XPUT "https://SERVER/api/v2/files?id=544627ed3c58891f058b4686" --data-binary myfile.txt
311
     * curl -XPUT "https://SERVER/api/v2/files/544627ed3c58891f058b4686" --data-binary myfile.txt
312
     *
313
     * #Upload new file under collection 544627ed3c58891f058b3333
314
     * curl -XPUT "https://SERVER/api/v2/files?collection=544627ed3c58891f058b3333&name=myfile.txt" --data-binary myfile.txt
315
     *
316
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
317
     *
318
     * @apiParam (GET Parameter) {string} [id] Either id, p (path) of a file node or a parent collection id must be given
319
     * @apiParam (GET Parameter) {string} [p] Either id, p (path) of a file node or a parent collection id must be given
320
     * @apiParam (GET Parameter) {string} [collection] Either id, p (path) of a file node or a parent collection id must be given
321
     * (If none of them are given, the file will be placed to the root)
322
     * @apiParam (GET Parameter) {string} [name] Needs to be set if the chunk belongs to a new file
323
     * or to identify an existing child file if a collection id was set
324
     * @apiParam (GET Parameter) {string} [created] Set specific created ISO806 timestamp
325
     * @apiParam (GET Parameter) {string} [changed] Set specific changed ISO806 timestamp
326
     * @apiParam (GET Parameter) {bool} [readonly] Mark node readonly
327
     * @apiParam (GET Parameter) {object} [meta] Meta attributes
328
     *
329
     * @apiSuccess (200 OK) {number} status Status Code
330
     * @apiSuccess (200 OK) {number} data Increased version number if an existing file was updated. It will return
331
     * the old version if the submited file content was equal to the existing one.
332
     *
333
     * @apiSuccess (201 Created) {number} status Status Code
334
     * @apiSuccess (201 Created) {string} data Node ID
335
     * @apiSuccessExample {json} Success-Response (New file created):
336
     * HTTP/1.1 201 Created
337
     * {
338
     *      "status": 201,
339
     *      "data": "78297329329389e332234342"
340
     * }
341
     *
342
     * @apiSuccessExample {json} Success-Response (File updated):
343
     * HTTP/1.1 200 OK
344
     * {
345
     *      "status": 200,
346
     *      "data": 2
347
     * }
348
     *
349
     * @apiErrorExample {json} Error-Response (quota full):
350
     * HTTP/1.1 507 Insufficient Storage
351
     * {
352
     *      "status": 507
353
     *      "data": {
354
     *          "error": "Balloon\Exception\InsufficientStorage",
355
     *          "message": "user quota is full",
356
     *          "code": 65
357
     *      }
358
     * }
359
     *
360
     * @apiErrorExample {json} Error-Response (Size limit exceeded):
361
     * HTTP/1.1 400 Bad Request
362
     * {
363
     *      "status": 400,
364
     *      "data": {
365
     *          "error": "Balloon\\Exception\\Conflict",
366
     *          "message": "file size exceeded limit",
367
     *          "code": 17
368
     *      }
369
     * }
370
     *
371
     * @param string $id
372
     * @param string $p
373
     * @param string $collection
374
     * @param string $name
375
     * @param string $changed
376
     * @param string $created
377
     * @param bool   $readonly
378
     * @param array  $meta
379
     */
380
    public function put(
381
        ?string $id = null,
382
        ?string $p = null,
383
        ?string $collection = null,
384
        ?string $name = null,
385
        int $conflict = 0,
386
        ?string $changed = null,
387
        ?string $created = null,
388
        ?bool $readonly = null,
389
        ?array $meta = null
390
    ): Response {
391
        ini_set('auto_detect_line_endings', '1');
392
        $input = fopen('php://input', 'rb');
393
        $session = $this->storage->storeTemporaryFile($input, $this->server->getIdentity());
394
395
        $attributes = compact('changed', 'created', 'readonly', 'meta');
396
        $attributes = array_filter($attributes, function ($attribute) {return !is_null($attribute); });
397
        $attributes = $this->_verifyAttributes($attributes);
398
399
        return $this->_put($session, $id, $p, $collection, $name, $attributes, $conflict);
400
    }
401
402
    /**
403
     * Add or update file.
404
     *
405
     * @param string $id
406
     * @param string $p
407
     * @param string $collection
408
     * @param string $name
409
     */
410
    protected function _put(
411
        ObjectId $session,
412
        ?string $id = null,
413
        ?string $p = null,
414
        ?string $collection = null,
415
        ?string $name = null,
416
        array $attributes = [],
417
        int $conflict = 0
0 ignored issues
show
Unused Code introduced by
The parameter $conflict is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
418
    ): Response {
419
        if (null === $id && null === $p && null === $name) {
420
            throw new Exception\InvalidArgument('neither id, p nor name was set');
421
        }
422
423
        if (null !== $p && null !== $name) {
424
            throw new Exception\InvalidArgument('p and name can not be used at the same time');
425
        }
426
427
        try {
428
            if (null !== $p) {
429
                $node = $this->_getNode(null, $p);
430
                $node->setContent($session, $attributes);
431
                $result = $this->node_decorator->decorate($node);
432
433
                return (new Response())->setCode(200)->setBody($result);
434
            }
435
            if (null !== $id && null === $collection) {
436
                $node = $this->_getNode($id);
437
                $node->setContent($session, $attributes);
438
                $result = $this->node_decorator->decorate($node);
439
440
                return (new Response())->setCode(200)->setBody($result);
441
            }
442
            if (null === $p && null === $id && null !== $name) {
443
                $collection = $this->_getNode($collection, null, Collection::class, false, true);
444
445
                if ($collection->childExists($name)) {
446
                    $child = $collection->getChild($name);
447
                    $child->setContent($session, $attributes);
448
                    $result = $this->node_decorator->decorate($child);
449
450
                    return (new Response())->setCode(200)->setBody($result);
451
                }
452
                if (!is_string($name) || empty($name)) {
453
                    throw new Exception\InvalidArgument('name must be a valid string');
454
                }
455
456
                $result = $collection->addFile($name, $session, $attributes);
457
                $result = $this->node_decorator->decorate($result);
458
459
                return (new Response())->setCode(201)->setBody($result);
460
            }
461
        } catch (ForbiddenException $e) {
462
            throw new Exception\Conflict(
463
                'a node called '.$name.' does already exists in this collection',
464
                Exception\Conflict::NODE_WITH_SAME_NAME_ALREADY_EXISTS,
465
                $e
466
            );
467
        } catch (Exception\NotFound $e) {
468
            if (null !== $p && null === $id) {
469
                if (!is_string($p) || empty($p)) {
470
                    throw new Exception\InvalidArgument('path (p) must be a valid string');
471
                }
472
473
                $parent_path = dirname($p);
474
                $name = basename($p);
475
476
                try {
477
                    $parent = $this->fs->findNodeByPath($parent_path, Collection::class);
478
479
                    if (!is_string($name) || empty($name)) {
480
                        throw new Exception\InvalidArgument('name must be a valid string');
481
                    }
482
483
                    $result = $parent->addFile($name, $session, $attributes);
484
                    $result = $this->node_decorator->decorate($result);
485
486
                    return (new Response())->setCode(201)->setBody($result);
487
                } catch (Exception\NotFound $e) {
488
                    throw new Exception('parent collection '.$parent_path.' was not found');
489
                }
490
            } else {
491
                throw $e;
492
            }
493
        }
494
    }
495
}
496