Completed
Push — feature/other-validation ( 3b2e71...c4ebdf )
by Narcotic
184:05 queued 119:47
created

FileManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
ccs 0
cts 4
cp 0
cc 1
eloc 3
nc 1
nop 2
crap 2
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' => $content
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
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...
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 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...
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
        $now = new \DateTime();
229
        /** Original Metadata
230
         * @var FileMetadata $meta */
231
        $meta = $file->getMetadata();
232
        if (!$meta || !$meta->getCreatedate()) {
233
            $meta = $this->fileDocumentFactory->createFileMataData();
234
            $meta->setId($file->getId());
235
            $meta->setCreatedate($now);
236
        }
237
238
        /** Posted Metadata
239
         * @var FileMetadata $postedMeta */
240
        if (!empty($fileData) && !empty($postedMeta = $fileData->getMetadata())) {
241
            $postedMeta->setId($meta->getId());
242
            $postedMeta->setCreatedate($meta->getCreatedate());
243
            // If no file sent and no hash change sent, keep original.
244
            if (empty($fileInfo['data']['filename'])) {
245
                $postedMeta->setHash($meta->getHash());
246
                $postedMeta->setMime($meta->getMime());
247
                $postedMeta->setSize($meta->getSize());
248
                $postedMeta->setFilename($meta->getFilename());
249
            }
250
            $meta = $postedMeta;
251
        }
252
        // If no hash defined use the content if there was so.
253
        if (empty($meta->getHash()) && !empty($fileInfo['data']['hash'])) {
254
            $meta->setHash($fileInfo['data']['hash']);
255
        }
256
257
        if (empty($meta->getFilename()) && !empty($fileInfo['data']['filename'])) {
258
            $meta->setFilename($fileInfo['data']['filename']);
259
        }
260
        if (empty($meta->getMime()) && !empty($fileInfo['data']['mimetype'])) {
261
            $meta->setMime($fileInfo['data']['mimetype']);
262
        }
263
264
        $meta->setSize($fileSize);
265
        $meta->setModificationdate($now);
266
        $file->setMetadata($meta);
267
    }
268
269
    /**
270
     * Extracts different information sent in the request content.
271
     *
272
     * @param Request $request Current http request
273
     *
274
     * @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...
275
     */
276
    public function extractDataFromRequestContent(Request $request)
277
    {
278
        // split content
279
        $contentType = $request->headers->get('Content-Type');
280
        list(, $boundary) = explode('; boundary=', $contentType);
281
282
        // fix boundary dash count
283
        $boundary = '--' . $boundary;
284
285
        $content = $request->getContent();
286
        $contentBlocks = explode($boundary, $content, -1);
287
        $metadataInfo = '';
288
        $fileInfo = '';
289
290
        // determine content blocks usage
291
        foreach ($contentBlocks as $contentBlock) {
292
            if (empty($contentBlock)) {
293
                continue;
294
            }
295
            if (40 === strpos($contentBlock, 'upload')) {
296
                $fileInfo = $contentBlock;
297
                continue;
298
            }
299
            if (40 === strpos($contentBlock, 'metadata')) {
300
                $metadataInfo = $contentBlock;
301
                continue;
302
            }
303
        }
304
305
        $attributes = array_merge(
306
            $request->attributes->all(),
307
            $this->extractMetaDataFromContent($metadataInfo)
308
        );
309
        $files = $this->extractFileFromContent($fileInfo);
310
311
        return ['files' => $files, 'attributes' => $attributes];
312
    }
313
314
    /**
315
     * Extracts meta information from request content.
316
     *
317
     * @param string $metadataInfoString Information about metadata information
318
     *
319
     * @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...
320
     */
321
    private function extractMetaDataFromContent($metadataInfoString)
322
    {
323
        if (empty($metadataInfoString)) {
324
            return ['metadata' => '{}'];
325
        }
326
327
        $metadataInfo = explode("\r\n", ltrim($metadataInfoString));
328
        return ['metadata' => $metadataInfo[2]];
329
    }
330
331
    /**
332
     * Extracts file data from request content
333
     *
334
     * @param string $fileInfoString Information about uploaded files.
335
     *
336
     * @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...
337
     */
338
    private function extractFileFromContent($fileInfoString)
339
    {
340
        if (empty($fileInfoString)) {
341
            return null;
342
        }
343
344
        $fileInfo = explode("\r\n\r\n", ltrim($fileInfoString), 2);
345
346
        // write content to file ("upload_tmp_dir" || sys_get_temp_dir() )
347
        preg_match('@name=\"([^"]*)\";\sfilename=\"([^"]*)\"\s*Content-Type:\s([^"]*)@', $fileInfo[0], $matches);
348
        $originalName = $matches[2];
349
        $dir = ini_get('upload_tmp_dir');
350
        $dir = (empty($dir)) ? sys_get_temp_dir() : $dir;
351
        $file = $dir . '/' . $originalName;
352
        $fileContent = substr($fileInfo[1], 0, -2);
353
354
        // create file
355
        touch($file);
356
        $size = file_put_contents($file, $fileContent, LOCK_EX);
357
358
        $files = [
359
            $matches[1] => new UploadedFile(
360
                $file,
361
                $originalName,
362
                $matches[3],
363
                $size
364
            )
365
        ];
366
367
        return $files;
368
    }
369
}
370