Completed
Push — feature/guzzle-filename-multip... ( f0865e...dd6213 )
by
unknown
10:01
created

FileManager::extractFileFromContent()   C

Complexity

Conditions 7
Paths 25

Size

Total Lines 47
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 47
ccs 0
cts 36
cp 0
rs 6.7272
cc 7
eloc 29
nc 25
nop 1
crap 56
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
use Symfony\Component\HttpKernel\Exception\HttpException;
18
19
/**
20
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
21
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
22
 * @link     http://swisscom.ch
23
 */
24
class FileManager
25
{
26
    /**
27
     * @var Filesystem
28
     */
29
    private $fileSystem;
30
31
    /**
32
     * @var FileDocumentFactory
33
     */
34
    private $fileDocumentFactory;
35
36
    /**
37
     * FileManager constructor.
38
     *
39
     * @param Filesystem          $fileSystem          file system abstraction layer for s3 and more
40
     * @param FileDocumentFactory $fileDocumentFactory Instance to be used to create action entries.
41
     */
42
    public function __construct(Filesystem $fileSystem, FileDocumentFactory $fileDocumentFactory)
43
    {
44
        $this->fileSystem = $fileSystem;
45
        $this->fileDocumentFactory = $fileDocumentFactory;
46
    }
47
48
    /**
49
     * Indicates whether the file matching the specified key exists
50
     *
51
     * @param string $key Identifier to be found
52
     *
53
     * @return boolean TRUE if the file exists, FALSE otherwise
54
     */
55
    public function has($key)
56
    {
57
        return $this->fileSystem->has($key);
58
    }
59
60
    /**
61
     * Deletes the file matching the specified key
62
     *
63
     * @param string $key Identifier to be deleted
64
     *
65
     * @throws \RuntimeException when cannot read file
66
     *
67
     * @return boolean
68
     */
69
    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...
70
    {
71
        return $this->fileSystem->delete($key);
72
    }
73
74
    /**
75
     * Reads the content from the file
76
     *
77
     * @param  string $key Key of the file
78
     *
79
     * @throws \Gaufrette\Exception\FileNotFound when file does not exist
80
     * @throws \RuntimeException                 when cannot read file
81
     *
82
     * @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...
83
     */
84
    public function read($key)
85
    {
86
        return $this->fileSystem->read($key);
87
    }
88
89
    /**
90
     * Stores uploaded files to CDN
91
     *
92
     * @param Request           $request  Current Http request
93
     * @param DocumentModel     $model    Model to be used to manage entity
94
     * @param FileDocument|null $fileData meta information about the file to be stored.
95
     *
96
     * @return array
97
     */
98
    public function saveFiles(Request $request, DocumentModel $model, FileDocument $fileData = null)
99
    {
100
        $inStore = [];
101
        $files = $this->extractUploadedFiles($request);
102
103
        foreach ($files as $key => $fileInfo) {
104
            /** @var FileDocument $record */
105
            $record = $this->createOrUpdateRecord($model, $fileData, $request->get('id'));
106
            $inStore[] = $record->getId();
107
108
            /** @var \Gaufrette\File $file */
109
            $file = $this->saveFile($record->getId(), $fileInfo['content']);
110
111
            $this->initOrUpdateMetaData(
112
                $record,
113
                $file->getSize(),
114
                $fileInfo,
115
                $fileData
116
            );
117
118
            $model->updateRecord($record->getId(), $record);
119
120
            // TODO NOTICE: ONLY UPLOAD OF ONE FILE IS CURRENTLY SUPPORTED
121
            break;
122
        }
123
124
        return $inStore;
125
    }
126
127
    /**
128
     * Save or update a file
129
     *
130
     * @param string $id   ID of file
131
     * @param String $data content to save
132
     *
133
     * @return File
134
     *
135
     * @throws BadRequestHttpException
136
     */
137
    public function saveFile($id, $data)
138
    {
139
        if (is_resource($data)) {
140
            throw new BadRequestHttpException('/file does not support storing resources');
141
        }
142
        $file = new File($id, $this->fileSystem);
143
        $file->setContent($data);
144
145
        return $file;
146
    }
147
148
    /**
149
     * Moves uploaded files to tmp directory
150
     *
151
     * @param Request $request Current http request
152
     *
153
     * @return array
154
     */
155
    private function extractUploadedFiles(Request $request)
156
    {
157
        $uploadedFiles = [];
158
159
        /** @var  $uploadedFile \Symfony\Component\HttpFoundation\File\UploadedFile */
160
        foreach ($request->files->all() as $field => $uploadedFile) {
161
            if (0 === $uploadedFile->getError()) {
162
                $content = file_get_contents($uploadedFile->getPathname());
163
                $uploadedFiles[$field] = [
164
                    'data' => [
165
                        'mimetype' => $uploadedFile->getMimeType(),
166
                        'filename' => $uploadedFile->getClientOriginalName(),
167
                        'hash'     => hash('sha256', $content)
168
                    ],
169
                    'content' => $content
170
                ];
171
            } else {
172
                throw new UploadException($uploadedFile->getErrorMessage());
173
            }
174
        }
175
176
        if (empty($uploadedFiles)) {
177
            $content = $request->getContent();
178
            $uploadedFiles['upload'] = [
179
                'data' => [
180
                    'mimetype' => $request->headers->get('Content-Type'),
181
                    'filename' => '',
182
                    'hash'     => hash('sha256', $content)
183
                ],
184
                'content' => $content
185
            ];
186
        }
187
188
        return $uploadedFiles;
189
    }
190
191
    /**
192
     * Creates a new or updates an existing instance of the file document
193
     *
194
     * @param DocumentModel     $model    Document model
195
     * @param FileDocument|null $fileData File information
196
     * @param string            $id       Alternative Id to be checked
197
     *
198
     * @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...
199
     */
200
    private function createOrUpdateRecord(DocumentModel $model, FileDocument $fileData = null, $id = '')
201
    {
202
        $id = empty($id) && !empty($fileData) ? $fileData->getId() : $id;
203
204
        if (($recordExists = empty($record = $model->find($id))) && empty($record = $fileData)) {
205
            $entityClass = $model->getEntityClass();
206
207
            $record = new $entityClass();
208
        }
209
210
        if (!empty($id)) {
211
            $record->setId($id);
212
        }
213
214
        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...
Documentation introduced by
$record is of type object|null, 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...
215
    }
216
217
    /**
218
     * Updates or initialzes the metadata information of the current entity.
219
     *
220
     * @param FileDocument $file     Document to be used
221
     * @param integer      $fileSize Size of the uploaded file
222
     * @param array        $fileInfo Additional info about the file
223
     * @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...
224
     *
225
     * @return void
226
     */
227
    private function initOrUpdateMetaData(FileDocument $file, $fileSize, array $fileInfo, FileDocument $fileData = null)
228
    {
229
        $now = new \DateTime();
230
        /** Original Metadata
231
         * @var FileMetadata $meta */
232
        $meta = $file->getMetadata();
233
        if (!$meta || !$meta->getCreatedate()) {
234
            $meta = $this->fileDocumentFactory->createFileMataData();
235
            $meta->setId($file->getId());
236
            $meta->setCreatedate($now);
237
        }
238
239
        /** Posted Metadata
240
         * @var FileMetadata $postedMeta */
241
        if (!empty($fileData) && !empty($postedMeta = $fileData->getMetadata())) {
242
            $postedMeta->setId($meta->getId());
243
            $postedMeta->setCreatedate($meta->getCreatedate());
244
            // If no file sent and no hash change sent, keep original.
245
            if (empty($fileInfo['data']['filename'])) {
246
                $postedMeta->setHash($meta->getHash());
247
                $postedMeta->setMime($meta->getMime());
248
                $postedMeta->setSize($meta->getSize());
249
                $postedMeta->setFilename($meta->getFilename());
250
            }
251
            $meta = $postedMeta;
252
        }
253
        // If no hash defined use the content if there was so.
254
        if (empty($meta->getHash()) && !empty($fileInfo['data']['hash'])) {
255
            $meta->setHash($fileInfo['data']['hash']);
256
        }
257
258
        if (empty($meta->getFilename()) && !empty($fileInfo['data']['filename'])) {
259
            $meta->setFilename($fileInfo['data']['filename']);
260
        }
261
        if (empty($meta->getMime()) && !empty($fileInfo['data']['mimetype'])) {
262
            $meta->setMime($fileInfo['data']['mimetype']);
263
        }
264
265
        $meta->setSize($fileSize);
266
        $meta->setModificationdate($now);
267
        $file->setMetadata($meta);
268
    }
269
270
    /**
271
     * Extracts different information sent in the request content.
272
     *
273
     * @param Request $request Current http request
274
     *
275
     * @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...
276
     */
277
    public function extractDataFromRequestContent(Request $request)
278
    {
279
        // split content
280
        $contentType = $request->headers->get('Content-Type');
281
        list(, $boundary) = explode('; boundary=', $contentType);
282
283
        // fix boundary dash count
284
        $boundary = '--' . $boundary;
285
286
        $content = $request->getContent();
287
        $contentBlocks = explode($boundary, $content, -1);
288
        $metadataInfo = '';
289
        $fileInfo = '';
290
291
        // determine content blocks usage
292
        foreach ($contentBlocks as $contentBlock) {
293
            if (empty($contentBlock)) {
294
                continue;
295
            }
296
            preg_match('/name=\"(.*?)\"[^"]/i', $contentBlock, $matches);
297
            $name = isset($matches[1]) ? $matches[1] : '';
298
299
            if ($name === 'upload') {
300
                $fileInfo = $contentBlock;
301
                continue;
302
            }
303
            if ($name === 'metadata') {
304
                $metadataInfo = $contentBlock;
305
                continue;
306
            }
307
        }
308
309
        $attributes = array_merge(
310
            $request->attributes->all(),
311
            $this->extractMetaDataFromContent($metadataInfo)
312
        );
313
        $files = $this->extractFileFromContent($fileInfo);
314
315
        return ['files' => $files, 'attributes' => $attributes];
316
    }
317
318
    /**
319
     * Extracts meta information from request content.
320
     *
321
     * @param string $metadataInfoString Information about metadata information
322
     *
323
     * @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...
324
     */
325
    private function extractMetaDataFromContent($metadataInfoString)
326
    {
327
        if (empty($metadataInfoString)) {
328
            return ['metadata' => '{}'];
329
        }
330
331
        // When using curl or Guzzle the position of data can change.
332
        // Here we grab the first valid json start.
333
        $metadataInfo = explode("\r\n", ltrim($metadataInfoString));
334
        foreach ($metadataInfo as $data) {
335
            if (substr($data, 0, 1) === '{') {
336
                return ['metadata' => $data];
337
            }
338
        }
339
        return ['metadata' => '{}'];
340
    }
341
342
    /**
343
     * Extracts file data from request content
344
     *
345
     * @param string $fileInfoString Information about uploaded files.
346
     *
347
     * @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...
348
     */
349
    private function extractFileFromContent($fileInfoString)
350
    {
351
        if (empty($fileInfoString)) {
352
            return null;
353
        }
354
355
        $fileInfo = explode("\r\n\r\n", ltrim($fileInfoString), 2);
356
357
        preg_match('/name=\"(.*?)\"[^"]/i', $fileInfo[0], $matches);
358
        $name = isset($matches[1]) ? $matches[1] : '';
359
360
        preg_match('/filename=\"(.*?)\"[^"]/i', $fileInfo[0], $matches);
361
        $fileName = isset($matches[1]) ? $matches[1] : '';
362
363
        $dir = ini_get('upload_tmp_dir');
364
        $dir = (empty($dir)) ? sys_get_temp_dir() : $dir;
365
        $file = $dir . '/' . $fileName;
366
367
        $fileContent = substr($fileInfo[1], 0, -2);
368
369
        // create file
370
        touch($file);
371
        $size = file_put_contents($file, $fileContent, LOCK_EX);
372
373
        // FileType Content-Type
374
        preg_match('/Content-Type=\"(.*?)\"[^"]/i', $fileInfo[0], $matches);
375
        if (isset($matches[1])) {
376
            $contentType = $matches[1];
377
        } else {
378
            $fInfo = finfo_open(FILEINFO_MIME_TYPE);
379
            $contentType = finfo_file($fInfo, $file);
380
            if (!$contentType) {
381
                throw new HttpException(400, "Could not determine Content type of file: ".$fileName);
382
            }
383
        }
384
385
        $files = [
386
            $name => new UploadedFile(
387
                $file,
388
                $fileName,
389
                $contentType,
390
                $size
391
            )
392
        ];
393
394
        return $files;
395
    }
396
}
397