Completed
Push — feature/EVO-5985 ( 3c8987 )
by
unknown
47:11 queued 40:29
created

FileManager::initOrUpdateMetaData()   C

Complexity

Conditions 8
Paths 16

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
ccs 0
cts 20
cp 0
rs 6.6037
cc 8
eloc 14
nc 16
nop 4
crap 72
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\HttpKernel\Exception\BadRequestHttpException;
16
use GravitonDyn\FileBundle\Document\FileMetadata;
17
18
/**
19
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
20
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
21
 * @link     http://swisscom.ch
22
 */
23
class FileManager
24
{
25
    /**
26
     * @var Filesystem
27
     */
28
    private $fileSystem;
29
30
    /**
31
     * @var FileDocumentFactory
32
     */
33
    private $fileDocumentFactory;
34
35
    /**
36
     * FileManager constructor.
37
     *
38
     * @param Filesystem          $fileSystem          file system abstraction layer for s3 and more
39
     * @param FileDocumentFactory $fileDocumentFactory Instance to be used to create action entries.
40
     */
41
    public function __construct(Filesystem $fileSystem, FileDocumentFactory $fileDocumentFactory)
42
    {
43
        $this->fileSystem = $fileSystem;
44
        $this->fileDocumentFactory = $fileDocumentFactory;
45
    }
46
47
    /**
48
     * Indicates whether the file matching the specified key exists
49
     *
50
     * @param string $key Identifier to be found
51
     *
52
     * @return boolean TRUE if the file exists, FALSE otherwise
53
     */
54
    public function has($key)
55
    {
56
        return $this->fileSystem->has($key);
57
    }
58
59
    /**
60
     * Deletes the file matching the specified key
61
     *
62
     * @param string $key Identifier to be deleted
63
     *
64
     * @throws \RuntimeException when cannot read file
65
     *
66
     * @return boolean
67
     */
68
    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...
69
    {
70
        return $this->fileSystem->delete($key);
71
    }
72
73
    /**
74
     * Reads the content from the file
75
     *
76
     * @param  string $key Key of the file
77
     *
78
     * @throws \Gaufrette\Exception\FileNotFound when file does not exist
79
     * @throws \RuntimeException                 when cannot read file
80
     *
81
     * @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...
82
     */
83
    public function read($key)
84
    {
85
        return $this->fileSystem->read($key);
86
    }
87
88
    /**
89
     * Stores uploaded files to CDN
90
     *
91
     * @param Request           $request  Current Http request
92
     * @param DocumentModel     $model    Model to be used to manage entity
93
     * @param FileDocument|null $fileData meta information about the file to be stored.
94
     *
95
     * @return array
96
     */
97
    public function saveFiles(Request $request, DocumentModel $model, FileDocument $fileData = null)
98
    {
99
        $inStore = [];
100
        $files = $this->extractUploadedFiles($request);
101
102
        foreach ($files as $key => $fileInfo) {
103
            /** @var FileDocument $record */
104
            $record = $this->createOrUpdateRecord($model, $fileData, $request->get('id'));
105
            $inStore[] = $record->getId();
106
107
            /** @var \Gaufrette\File $file */
108
            $file = $this->saveFile($record->getId(), $fileInfo['content']);
109
110
            $this->initOrUpdateMetaData(
111
                $record,
112
                $file->getSize(),
113
                $fileInfo,
114
                $fileData
115
            );
116
117
            $model->updateRecord($record->getId(), $record);
118
119
            // TODO NOTICE: ONLY UPLOAD OF ONE FILE IS CURRENTLY SUPPORTED
120
            break;
121
        }
122
123
        return $inStore;
124
    }
125
126
    /**
127
     * Save or update a file
128
     *
129
     * @param string $id   ID of file
130
     * @param String $data content to save
131
     *
132
     * @return File
133
     *
134
     * @throws BadRequestHttpException
135
     */
136
    public function saveFile($id, $data)
137
    {
138
        if (is_resource($data)) {
139
            throw new BadRequestHttpException('/file does not support storing resources');
140
        }
141
        $file = new File($id, $this->fileSystem);
142
        $file->setContent($data);
143
144
        return $file;
145
    }
146
147
    /**
148
     * Moves uploaded files to tmp directory
149
     *
150
     * @param Request $request Current http request
151
     *
152
     * @return array
153
     */
154
    private function extractUploadedFiles(Request $request)
155
    {
156
        $uploadedFiles = [];
157
158
        /** @var  $uploadedFile \Symfony\Component\HttpFoundation\File\UploadedFile */
159
        foreach ($request->files->all() as $field => $uploadedFile) {
160
            if (0 === $uploadedFile->getError()) {
161
                $content = file_get_contents($uploadedFile->getPathname());
162
                $uploadedFiles[$field] = [
163
                    'data' => [
164
                        'mimetype' => $uploadedFile->getMimeType(),
165
                        'filename' => $uploadedFile->getClientOriginalName(),
166
                        'hash'     => hash('sha256', $content)
167
                    ],
168
                    'content' => $content
169
                ];
170
            } else {
171
                throw new UploadException($uploadedFile->getErrorMessage());
172
            }
173
        }
174
175
        if (empty($uploadedFiles)) {
176
            $content = $request->getContent();
177
            $uploadedFiles['upload'] = [
178
                'data' => [
179
                    'mimetype' => $request->headers->get('Content-Type'),
180
                    'filename' => '',
181
                    'hash'     => hash('sha256', $content)
182
                ],
183
                'content' => $request->getContent()
184
            ];
185
        }
186
187
        return $uploadedFiles;
188
    }
189
190
    /**
191
     * Creates a new or updates an existing instance of the file document
192
     *
193
     * @param DocumentModel     $model    Document model
194
     * @param FileDocument|null $fileData File information
195
     * @param string            $id       Alternative Id to be checked
196
     *
197
     * @return FileDocument
198
     */
199
    private function createOrUpdateRecord(DocumentModel $model, FileDocument $fileData = null, $id = '')
200
    {
201
        $id = empty($id) && !empty($fileData) ? $fileData->getId() : $id;
202
203
        if (($recordExists = empty($record = $model->find($id))) && empty($record = $fileData)) {
204
            $entityClass = $model->getEntityClass();
205
206
            $record = new $entityClass();
207
        }
208
209
        if (!empty($id)) {
210
            $record->setId($id);
211
        }
212
213
        return $recordExists ? $model->updateRecord($record->getId(), $record) : $model->insertRecord($record);
0 ignored issues
show
Bug introduced by
It seems like $record defined by $fileData on line 203 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...
Documentation introduced by
$record is of type null|object, but the function expects a object<Graviton\I18nBundle\Document\Translatable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
214
    }
215
216
    /**
217
     * Updates or initialzes the metadata information of the current entity.
218
     *
219
     * @param FileDocument $file     Document to be used
220
     * @param integer      $fileSize Size of the uploaded file
221
     * @param array        $fileInfo Additional info about the file
222
     * @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...
223
     *
224
     * @return void
225
     */
226
    private function initOrUpdateMetaData(FileDocument $file, $fileSize, array $fileInfo, FileDocument $fileData = null)
227
    {
228
        /** @var FileMetadata $meta */
229
        if (empty($meta = $file->getMetadata()) && (empty($fileData) || empty($meta = $fileData->getMetadata()))) {
230
            $meta = $this->fileDocumentFactory->createFileMataData();
231
            $meta->setId($file->getId());
232
            $meta->setCreatedate(new \DateTime());
233
        }
234
235
        $meta->setModificationdate(new \DateTime());
236
        if (empty($meta->getFilename()) && !empty($fileInfo['data']['filename'])) {
237
            $meta->setFilename($fileInfo['data']['filename']);
238
        }
239
        if (!empty($fileInfo['data']['mimetype'])) {
240
            $meta->setMime($fileInfo['data']['mimetype']);
241
        }
242
        if (!empty($fileInfo['data']['hash'])) {
243
            $meta->setHash($fileInfo['data']['hash']);
244
        }
245
        $meta->setSize($fileSize);
246
        $file->setMetadata($meta);
247
    }
248
249
    /**
250
     * Extracts different information sent in the request content.
251
     *
252
     * @param Request $request Current http request
253
     *
254
     * @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...
255
     */
256
    public function extractDataFromRequestContent(Request $request)
257
    {
258
        // split content
259
        $contentType = $request->headers->get('Content-Type');
260
        list(, $boundary) = explode('; boundary=', $contentType);
261
262
        // fix boundary dash count
263
        $boundary = '--' . $boundary;
264
265
        $content = $request->getContent();
266
        $contentBlocks = explode($boundary, $content, -1);
267
        $metadataInfo = '';
268
        $fileInfo = '';
269
270
        // determine content blocks usage
271
        foreach ($contentBlocks as $contentBlock) {
272
            if (empty($contentBlock)) {
273
                continue;
274
            }
275
            if (40 === strpos($contentBlock, 'upload')) {
276
                $fileInfo = $contentBlock;
277
                continue;
278
            }
279
            if (40 === strpos($contentBlock, 'metadata')) {
280
                $metadataInfo = $contentBlock;
281
                continue;
282
            }
283
        }
284
285
        $attributes = array_merge(
286
            $request->attributes->all(),
287
            $this->extractMetaDataFromContent($metadataInfo)
288
        );
289
        $files = $this->extractFileFromContent($fileInfo);
290
291
        return ['files' => $files, 'attributes' => $attributes];
292
    }
293
294
    /**
295
     * Extracts meta information from request content.
296
     *
297
     * @param string $metadataInfoString Information about metadata information
298
     *
299
     * @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...
300
     */
301
    private function extractMetaDataFromContent($metadataInfoString)
302
    {
303
        if (empty($metadataInfoString)) {
304
            return ['metadata' => '{}'];
305
        }
306
307
        $metadataInfo = explode("\r\n", ltrim($metadataInfoString));
308
        return ['metadata' => $metadataInfo[2]];
309
    }
310
311
    /**
312
     * Extracts file data from request content
313
     *
314
     * @param string $fileInfoString Information about uploaded files.
315
     *
316
     * @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...
317
     */
318
    private function extractFileFromContent($fileInfoString)
319
    {
320
        if (empty($fileInfoString)) {
321
            return null;
322
        }
323
324
        $fileInfo = explode("\r\n\r\n", ltrim($fileInfoString), 2);
325
326
        // write content to file ("upload_tmp_dir" || sys_get_temp_dir() )
327
        preg_match('@name=\"([^"]*)\";\sfilename=\"([^"]*)\"\s*Content-Type:\s([^"]*)@', $fileInfo[0], $matches);
328
        $originalName = $matches[2];
329
        $dir = ini_get('upload_tmp_dir');
330
        $dir = (empty($dir)) ? sys_get_temp_dir() : $dir;
331
        $file = $dir . '/' . $originalName;
332
        $fileContent = substr($fileInfo[1], 0, -2);
333
334
        // create file
335
        touch($file);
336
        $size = file_put_contents($file, $fileContent, LOCK_EX);
337
338
        $files = [
339
            $matches[1] => new UploadedFile(
340
                $file,
341
                $originalName,
342
                $matches[3],
343
                $size
344
            )
345
        ];
346
347
        return $files;
348
    }
349
}
350