Completed
Push — feature/other-validation ( 898e90...d247e5 )
by Narcotic
18:07 queued 12:32
created

FileManager   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 376
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 0%

Importance

Changes 7
Bugs 2 Features 0
Metric Value
wmc 48
lcom 1
cbo 12
dl 0
loc 376
rs 8.4864
c 7
b 2
f 0
ccs 0
cts 204
cp 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
B createOrUpdateRecord() 0 16 7
A __construct() 0 5 1
A has() 0 4 1
A delete() 0 4 1
A read() 0 4 1
B saveFiles() 0 28 2
A saveFile() 0 10 2
B extractUploadedFiles() 0 35 4
C initOrUpdateMetaData() 0 42 12
B extractDataFromRequestContent() 0 40 6
A extractMetaDataFromContent() 0 16 4
C extractFileFromContent() 0 50 7

How to fix   Complexity   

Complex Class

Complex classes like FileManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FileManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Handles file specific actions
4
 */
5
6
namespace Graviton\FileBundle;
7
8
use Gaufrette\File;
9
use Gaufrette\Filesystem;
10
use Graviton\RestBundle\Model\DocumentModel;
11
use GravitonDyn\FileBundle\Document\File as FileDocument;
12
use Symfony\Component\HttpFoundation\File\Exception\UploadException;
13
use Symfony\Component\HttpFoundation\File\UploadedFile;
14
use Symfony\Component\HttpFoundation\Request;
15
use Symfony\Component\HttpFoundation\Response;
16
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
17
use GravitonDyn\FileBundle\Document\FileMetadata;
18
use Symfony\Component\HttpKernel\Exception\HttpException;
19
20
/**
21
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
22
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
23
 * @link     http://swisscom.ch
24
 */
25
class FileManager
26
{
27
    /**
28
     * @var Filesystem
29
     */
30
    private $fileSystem;
31
32
    /**
33
     * @var FileDocumentFactory
34
     */
35
    private $fileDocumentFactory;
36
37
    /**
38
     * FileManager constructor.
39
     *
40
     * @param Filesystem          $fileSystem          file system abstraction layer for s3 and more
41
     * @param FileDocumentFactory $fileDocumentFactory Instance to be used to create action entries.
42
     */
43
    public function __construct(Filesystem $fileSystem, FileDocumentFactory $fileDocumentFactory)
44
    {
45
        $this->fileSystem = $fileSystem;
46
        $this->fileDocumentFactory = $fileDocumentFactory;
47
    }
48
49
    /**
50
     * Indicates whether the file matching the specified key exists
51
     *
52
     * @param string $key Identifier to be found
53
     *
54
     * @return boolean TRUE if the file exists, FALSE otherwise
55
     */
56
    public function has($key)
57
    {
58
        return $this->fileSystem->has($key);
59
    }
60
61
    /**
62
     * Deletes the file matching the specified key
63
     *
64
     * @param string $key Identifier to be deleted
65
     *
66
     * @throws \RuntimeException when cannot read file
67
     *
68
     * @return boolean
69
     */
70
    public function delete($key)
0 ignored issues
show
Coding Style introduced by
function delete() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
71
    {
72
        return $this->fileSystem->delete($key);
73
    }
74
75
    /**
76
     * Reads the content from the file
77
     *
78
     * @param  string $key Key of the file
79
     *
80
     * @throws \Gaufrette\Exception\FileNotFound when file does not exist
81
     * @throws \RuntimeException                 when cannot read file
82
     *
83
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|boolean?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
84
     */
85
    public function read($key)
86
    {
87
        return $this->fileSystem->read($key);
88
    }
89
90
    /**
91
     * Stores uploaded files to CDN
92
     *
93
     * @param Request           $request  Current Http request
94
     * @param DocumentModel     $model    Model to be used to manage entity
95
     * @param FileDocument|null $fileData meta information about the file to be stored.
96
     *
97
     * @return array
98
     */
99
    public function saveFiles(Request $request, DocumentModel $model, FileDocument $fileData = null)
100
    {
101
        $inStore = [];
102
        $files = $this->extractUploadedFiles($request);
103
104
        foreach ($files as $key => $fileInfo) {
105
            /** @var FileDocument $record */
106
            $record = $this->createOrUpdateRecord($model, $fileData, $request->get('id'));
107
            $inStore[] = $record->getId();
108
109
            /** @var \Gaufrette\File $file */
110
            $file = $this->saveFile($record->getId(), $fileInfo['content']);
111
112
            $this->initOrUpdateMetaData(
113
                $record,
114
                $file->getSize(),
115
                $fileInfo,
116
                $fileData
117
            );
118
119
            $model->updateRecord($record->getId(), $record);
120
121
            // TODO NOTICE: ONLY UPLOAD OF ONE FILE IS CURRENTLY SUPPORTED
122
            break;
123
        }
124
125
        return $inStore;
126
    }
127
128
    /**
129
     * Save or update a file
130
     *
131
     * @param string $id   ID of file
132
     * @param String $data content to save
133
     *
134
     * @return File
135
     *
136
     * @throws BadRequestHttpException
137
     */
138
    public function saveFile($id, $data)
139
    {
140
        if (is_resource($data)) {
141
            throw new BadRequestHttpException('/file does not support storing resources');
142
        }
143
        $file = new File($id, $this->fileSystem);
144
        $file->setContent($data);
145
146
        return $file;
147
    }
148
149
    /**
150
     * Moves uploaded files to tmp directory
151
     *
152
     * @param Request $request Current http request
153
     *
154
     * @return array
155
     */
156
    private function extractUploadedFiles(Request $request)
157
    {
158
        $uploadedFiles = [];
159
160
        /** @var  $uploadedFile \Symfony\Component\HttpFoundation\File\UploadedFile */
161
        foreach ($request->files->all() as $field => $uploadedFile) {
162
            if (0 === $uploadedFile->getError()) {
163
                $content = file_get_contents($uploadedFile->getPathname());
164
                $uploadedFiles[$field] = [
165
                    'data' => [
166
                        'mimetype' => $uploadedFile->getMimeType(),
167
                        'filename' => $uploadedFile->getClientOriginalName(),
168
                        'hash'     => hash('sha256', $content)
169
                    ],
170
                    'content' => $content
171
                ];
172
            } else {
173
                throw new UploadException($uploadedFile->getErrorMessage());
174
            }
175
        }
176
177
        if (empty($uploadedFiles)) {
178
            $content = $request->getContent();
179
            $uploadedFiles['upload'] = [
180
                'data' => [
181
                    'mimetype' => $request->headers->get('Content-Type'),
182
                    'filename' => '',
183
                    'hash'     => hash('sha256', $content)
184
                ],
185
                'content' => $content
186
            ];
187
        }
188
189
        return $uploadedFiles;
190
    }
191
192
    /**
193
     * Creates a new or updates an existing instance of the file document
194
     *
195
     * @param DocumentModel     $model    Document model
196
     * @param FileDocument|null $fileData File information
197
     * @param string            $id       Alternative Id to be checked
198
     *
199
     * @return FileDocument
0 ignored issues
show
Documentation introduced by
Should the return type not be FileDocument|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
200
     */
201
    private function createOrUpdateRecord(DocumentModel $model, FileDocument $fileData = null, $id = '')
202
    {
203
        $id = empty($id) && !empty($fileData) ? $fileData->getId() : $id;
204
205
        if (($recordExists = empty($record = $model->find($id))) && empty($record = $fileData)) {
206
            $entityClass = $model->getEntityClass();
207
208
            $record = new $entityClass();
209
        }
210
211
        if (!empty($id)) {
212
            $record->setId($id);
213
        }
214
215
        return $recordExists ? $model->updateRecord($record->getId(), $record) : $model->insertRecord($record);
0 ignored issues
show
Bug introduced by
It seems like $record can also be of type null; however, Graviton\RestBundle\Mode...ntModel::updateRecord() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $record can also be of type null; however, Graviton\RestBundle\Mode...ntModel::insertRecord() does only seem to accept object, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
216
    }
217
218
    /**
219
     * Updates or initialzes the metadata information of the current entity.
220
     *
221
     * @param FileDocument $file     Document to be used
222
     * @param integer      $fileSize Size of the uploaded file
223
     * @param array        $fileInfo Additional info about the file
224
     * @param FileDocument $fileData File data to be updated
0 ignored issues
show
Documentation introduced by
Should the type for parameter $fileData not be null|FileDocument?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
225
     *
226
     * @return void
227
     */
228
    private function initOrUpdateMetaData(FileDocument $file, $fileSize, array $fileInfo, FileDocument $fileData = null)
229
    {
230
        $now = new \DateTime();
231
        /** Original Metadata
232
         * @var FileMetadata $meta */
233
        $meta = $file->getMetadata();
234
        if (!$meta || !$meta->getCreatedate()) {
235
            $meta = $this->fileDocumentFactory->createFileMataData();
236
            $meta->setId($file->getId());
237
            $meta->setCreatedate($now);
238
        }
239
240
        /** Posted Metadata
241
         * @var FileMetadata $postedMeta */
242
        if (!empty($fileData) && !empty($postedMeta = $fileData->getMetadata())) {
243
            $postedMeta->setId($meta->getId());
244
            $postedMeta->setCreatedate($meta->getCreatedate());
245
            // If no file sent and no hash change sent, keep original.
246
            if (empty($fileInfo['data']['filename'])) {
247
                $postedMeta->setHash($meta->getHash());
248
                $postedMeta->setMime($meta->getMime());
249
                $postedMeta->setSize($meta->getSize());
250
                $postedMeta->setFilename($meta->getFilename());
251
            }
252
            $meta = $postedMeta;
253
        }
254
        // If no hash defined use the content if there was so.
255
        if (empty($meta->getHash()) && !empty($fileInfo['data']['hash'])) {
256
            $meta->setHash($fileInfo['data']['hash']);
257
        }
258
259
        if (empty($meta->getFilename()) && !empty($fileInfo['data']['filename'])) {
260
            $meta->setFilename($fileInfo['data']['filename']);
261
        }
262
        if (empty($meta->getMime()) && !empty($fileInfo['data']['mimetype'])) {
263
            $meta->setMime($fileInfo['data']['mimetype']);
264
        }
265
266
        $meta->setSize($fileSize);
267
        $meta->setModificationdate($now);
268
        $file->setMetadata($meta);
269
    }
270
271
    /**
272
     * Extracts different information sent in the request content.
273
     *
274
     * @param Request $request Current http request
275
     *
276
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,null|array<...ng,UploadedFile>|array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
277
     */
278
    public function extractDataFromRequestContent(Request $request)
279
    {
280
        // split content
281
        $contentType = $request->headers->get('Content-Type');
282
        list(, $boundary) = explode('; boundary=', $contentType);
283
284
        // fix boundary dash count
285
        $boundary = '--' . $boundary;
286
287
        $content = $request->getContent();
288
        $contentBlocks = explode($boundary, $content, -1);
289
        $metadataInfo = '';
290
        $fileInfo = '';
291
292
        // determine content blocks usage
293
        foreach ($contentBlocks as $contentBlock) {
294
            if (empty($contentBlock)) {
295
                continue;
296
            }
297
            preg_match('/name=\"(.*?)\"[^"]/i', $contentBlock, $matches);
298
            $name = isset($matches[1]) ? $matches[1] : '';
299
300
            if ($name === 'upload') {
301
                $fileInfo = $contentBlock;
302
                continue;
303
            }
304
            if ($name === 'metadata') {
305
                $metadataInfo = $contentBlock;
306
                continue;
307
            }
308
        }
309
310
        $attributes = array_merge(
311
            $request->attributes->all(),
312
            $this->extractMetaDataFromContent($metadataInfo)
313
        );
314
        $files = $this->extractFileFromContent($fileInfo);
315
316
        return ['files' => $files, 'attributes' => $attributes];
317
    }
318
319
    /**
320
     * Extracts meta information from request content.
321
     *
322
     * @param string $metadataInfoString Information about metadata information
323
     *
324
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
325
     */
326
    private function extractMetaDataFromContent($metadataInfoString)
327
    {
328
        if (empty($metadataInfoString)) {
329
            return ['metadata' => '{}'];
330
        }
331
332
        // When using curl or Guzzle the position of data can change.
333
        // Here we grab the first valid json start.
334
        $metadataInfo = explode("\r\n", ltrim($metadataInfoString));
335
        foreach ($metadataInfo as $data) {
336
            if (substr($data, 0, 1) === '{') {
337
                return ['metadata' => $data];
338
            }
339
        }
340
        return ['metadata' => '{}'];
341
    }
342
343
    /**
344
     * Extracts file data from request content
345
     *
346
     * @param string $fileInfoString Information about uploaded files.
347
     *
348
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be null|array<string,UploadedFile>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
349
     */
350
    private function extractFileFromContent($fileInfoString)
351
    {
352
        if (empty($fileInfoString)) {
353
            return null;
354
        }
355
356
        $fileInfo = explode("\r\n\r\n", ltrim($fileInfoString), 2);
357
358
        preg_match('/name=\"(.*?)\"[^"]/i', $fileInfo[0], $matches);
359
        $name = isset($matches[1]) ? $matches[1] : '';
360
361
        preg_match('/filename=\"(.*?)\"[^"]/i', $fileInfo[0], $matches);
362
        $fileName = isset($matches[1]) ? $matches[1] : '';
363
364
        $dir = ini_get('upload_tmp_dir');
365
        $dir = (empty($dir)) ? sys_get_temp_dir() : $dir;
366
        $file = $dir . DIRECTORY_SEPARATOR . $fileName;
367
368
        $fileContent = substr($fileInfo[1], 0, -2);
369
370
        // create file
371
        touch($file);
372
        $size = file_put_contents($file, $fileContent, LOCK_EX);
373
374
        // FileType Content-Type
375
        preg_match('/Content-Type=\"(.*?)\"[^"]/i', $fileInfo[0], $matches);
376
        if (isset($matches[1])) {
377
            $contentType = $matches[1];
378
        } else {
379
            $fInfo = finfo_open(FILEINFO_MIME_TYPE);
380
            $contentType = finfo_file($fInfo, $file);
381
            if (!$contentType) {
382
                throw new HttpException(
383
                    Response::HTTP_NOT_ACCEPTABLE,
384
                    'Could not determine Content type of file: '.$fileName
385
                );
386
            }
387
        }
388
389
        $files = [
390
            $name => new UploadedFile(
391
                $file,
392
                $fileName,
393
                $contentType,
394
                $size
395
            )
396
        ];
397
398
        return $files;
399
    }
400
}
401