Completed
Push — master ( 1f7c12...27a001 )
by Jan
04:25
created

AttachmentSubmitHandler::downloadURL()   B

Complexity

Conditions 8
Paths 144

Size

Total Lines 73
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 37
nc 144
nop 2
dl 0
loc 73
rs 7.7902
c 1
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 *
4
 * part-db version 0.1
5
 * Copyright (C) 2005 Christoph Lechner
6
 * http://www.cl-projects.de/
7
 *
8
 * part-db version 0.2+
9
 * Copyright (C) 2009 K. Jacobs and others (see authors.php)
10
 * http://code.google.com/p/part-db/
11
 *
12
 * Part-DB Version 0.4+
13
 * Copyright (C) 2016 - 2019 Jan Böhmer
14
 * https://github.com/jbtronics
15
 *
16
 * This program is free software; you can redistribute it and/or
17
 * modify it under the terms of the GNU General Public License
18
 * as published by the Free Software Foundation; either version 2
19
 * of the License, or (at your option) any later version.
20
 *
21
 * This program is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
24
 * GNU General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU General Public License
27
 * along with this program; if not, write to the Free Software
28
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
29
 *
30
 */
31
32
namespace App\Services\Attachments;
33
34
35
use App\Entity\Attachments\Attachment;
36
use App\Entity\Attachments\AttachmentContainingDBElement;
37
use App\Entity\Attachments\AttachmentTypeAttachment;
38
use App\Entity\Attachments\CategoryAttachment;
39
use App\Entity\Attachments\CurrencyAttachment;
40
use App\Entity\Attachments\DeviceAttachment;
41
use App\Entity\Attachments\FootprintAttachment;
42
use App\Entity\Attachments\GroupAttachment;
43
use App\Entity\Attachments\ManufacturerAttachment;
44
use App\Entity\Attachments\MeasurementUnitAttachment;
45
use App\Entity\Attachments\PartAttachment;
46
use App\Entity\Attachments\StorelocationAttachment;
47
use App\Entity\Attachments\SupplierAttachment;
48
use App\Entity\Attachments\UserAttachment;
49
use App\Exceptions\AttachmentDownloadException;
50
use App\Services\AttachmentHelper;
51
use Doctrine\Common\Annotations\IndexedReader;
52
use Nyholm\Psr7\Request;
53
use Symfony\Component\Filesystem\Filesystem;
54
use Symfony\Component\HttpClient\HttpClient;
55
use Symfony\Component\HttpFoundation\File\UploadedFile;
56
use Symfony\Component\Mime\MimeTypeGuesserInterface;
57
use Symfony\Component\Mime\MimeTypes;
58
use Symfony\Component\Mime\MimeTypesInterface;
59
use Symfony\Component\OptionsResolver\OptionsResolver;
60
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
61
use Symfony\Contracts\HttpClient\HttpClientInterface;
62
63
/**
64
 * This service handles the form submitting of an attachment and handles things like file uploading and downloading.
65
 * @package App\Services\Attachments
66
 */
67
class AttachmentSubmitHandler
68
{
69
    protected $pathResolver;
70
    protected $folder_mapping;
71
    protected $allow_attachments_downloads;
72
    protected $httpClient;
73
    protected $mimeTypes;
74
75
    public function __construct(AttachmentPathResolver $pathResolver, bool $allow_attachments_downloads,
76
                                HttpClientInterface $httpClient, MimeTypesInterface $mimeTypes)
77
    {
78
        $this->pathResolver = $pathResolver;
79
        $this->allow_attachments_downloads = $allow_attachments_downloads;
80
        $this->httpClient = $httpClient;
81
        $this->mimeTypes = $mimeTypes;
82
83
        //The mapping used to determine which folder will be used for an attachment type
84
        $this->folder_mapping = [PartAttachment::class => 'part', AttachmentTypeAttachment::class => 'attachment_type',
85
            CategoryAttachment::class => 'category', CurrencyAttachment::class => 'currency',
86
            DeviceAttachment::class => 'device', FootprintAttachment::class => 'footprint',
87
            GroupAttachment::class => 'group', ManufacturerAttachment::class => 'manufacturer',
88
            MeasurementUnitAttachment::class => 'measurement_unit', StorelocationAttachment::class => 'storelocation',
89
            SupplierAttachment::class => 'supplier', UserAttachment::class => 'user'];
90
    }
91
92
    protected function configureOptions(OptionsResolver $resolver) : void
93
    {
94
        $resolver->setDefaults([
95
            //If no preview image was set yet, the new uploaded file will become the preview image
96
            'become_preview_if_empty' => true,
97
            //When an URL is given download the URL
98
            'download_url' => false,
99
            'secure_attachment' => false,
100
        ]);
101
    }
102
103
    /**
104
     * Generates a filename for the given attachment and extension.
105
     * The filename contains a random id, so every time this function is called you get an unique name.
106
     * @param Attachment $attachment The attachment that should be used for generating an attachment
107
     * @param string $extension The extension that the new file should have (must only contain chars allowed in pathes)
108
     * @return string The new filename.
109
     */
110
    public function generateAttachmentFilename(Attachment $attachment, string $extension) : string
111
    {
112
        //Normalize extension
113
        $extension = transliterator_transliterate(
114
            'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()',
115
            $extension
116
        );
117
118
        //Use the (sanatized) attachment name as an filename part
119
        $safeName = transliterator_transliterate(
120
            'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()',
121
            $attachment->getName()
122
        );
123
124
        return $safeName . '-' . uniqid('', false) . '.' . $extension;
125
    }
126
127
    /**
128
     * Generates an (absolute) path to a folder where the given attachment should be stored.
129
     * @param Attachment $attachment The attachment that should be used for
130
     * @param bool $secure_upload True if the file path should be located in a safe location
131
     * @return string The absolute path for the attachment folder.
132
     */
133
    public function generateAttachmentPath(Attachment $attachment, bool $secure_upload = false) : string
134
    {
135
        if ($secure_upload) {
136
            $base_path = $this->pathResolver->getSecurePath();
137
        } else {
138
            $base_path = $this->pathResolver->getMediaPath();
139
        }
140
141
        //Ensure the given attachment class is known to mapping
142
        if (!isset($this->folder_mapping[get_class($attachment)])) {
143
            throw new \InvalidArgumentException(
144
                'The given attachment class is not known! The passed class was: ' . get_class($attachment)
145
            );
146
        }
147
        //Ensure the attachment has an assigned element
148
        if ($attachment->getElement() === null) {
149
            throw new \InvalidArgumentException(
150
                'The given attachment is not assigned to an element! An element is needed to generate a path!'
151
            );
152
        }
153
154
        //Build path
155
        return
156
            $base_path . DIRECTORY_SEPARATOR //Base path
157
            . $this->folder_mapping[get_class($attachment)] . DIRECTORY_SEPARATOR . $attachment->getElement()->getID();
158
    }
159
160
    /**
161
     * Handle the submit of an attachment form.
162
     * This function will move the uploaded file or download the URL file to server, if needed.
163
     * @param Attachment $attachment The attachment that should be used for handling.
164
     * @param UploadedFile|null $file If given, that file will be moved to the right location
165
     * @param array $options The options to use with the upload. Here you can specify that an URL should be downloaded,
166
     * or an file should be moved to a secure location.
167
     * @return Attachment The attachment with the new filename (same instance as passed $attachment)
168
     */
169
    public function handleFormSubmit(Attachment $attachment, ?UploadedFile $file, array $options = []) : Attachment
170
    {
171
        $resolver = new OptionsResolver();
172
        $this->configureOptions($resolver);
173
        $options = $resolver->resolve($options);
174
175
        //When a file is given then upload it, otherwise check if we need to download the URL
176
        if ($file) {
177
            $this->upload($attachment, $file, $options);
178
        } elseif ($options['download_url'] && $attachment->isExternal()) {
179
            $this->downloadURL($attachment, $options);
180
        }
181
182
        //Check if we should assign this attachment to master picture
183
        //this is only possible if the attachment is new (not yet persisted to DB)
184
        if ($options['become_preview_if_empty'] && $attachment->getID() === null && $attachment->isPicture()) {
0 ignored issues
show
introduced by
The condition $attachment->getID() === null is always false.
Loading history...
185
            $element = $attachment->getElement();
186
            if ($element instanceof AttachmentContainingDBElement && $element->getMasterPictureAttachment() === null) {
187
                $element->setMasterPictureAttachment($attachment);
188
            }
189
        }
190
191
        return $attachment;
192
    }
193
194
    /**
195
     * Download the URL set in the attachment and save it on the server
196
     * @param Attachment $attachment
197
     * @param array $options The options from the handleFormSubmit function
198
     * @return Attachment The attachment with the new filepath
199
     * @throws AttachmentDownloadException
200
     */
201
    protected function downloadURL(Attachment $attachment, array $options) : Attachment
202
    {
203
        //Check if we are allowed to download files
204
        if (!$this->allow_attachments_downloads) {
205
            throw new \RuntimeException('Download of attachments is not allowed!');
206
        }
207
208
        $url = $attachment->getURL();
209
210
        $fs = new Filesystem();
211
        $attachment_folder = $this->generateAttachmentPath($attachment, $options['secure_attachment']);
212
        $tmp_path = $attachment_folder . DIRECTORY_SEPARATOR . $this->generateAttachmentFilename($attachment, 'tmp');
213
214
        try {
215
            $response = $this->httpClient->request('GET', $url, [
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type null; however, parameter $url of Symfony\Contracts\HttpCl...entInterface::request() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

215
            $response = $this->httpClient->request('GET', /** @scrutinizer ignore-type */ $url, [
Loading history...
216
                'buffer' => false,
217
            ]);
218
219
            if (200 !== $response->getStatusCode()) {
220
                throw new AttachmentDownloadException('Statuscode:' . $response->getStatusCode());
221
            }
222
223
            //Open a temporary file in the attachment folder
224
            $fs->mkdir($attachment_folder);
225
            $fileHandler = fopen($tmp_path, 'wb');
226
            //Write the downloaded data to file
227
            foreach ($this->httpClient->stream($response) as $chunk) {
228
                fwrite($fileHandler, $chunk->getContent());
0 ignored issues
show
Bug introduced by
It seems like $fileHandler can also be of type false; however, parameter $handle of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

228
                fwrite(/** @scrutinizer ignore-type */ $fileHandler, $chunk->getContent());
Loading history...
229
            }
230
            fclose($fileHandler);
0 ignored issues
show
Bug introduced by
It seems like $fileHandler can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

230
            fclose(/** @scrutinizer ignore-type */ $fileHandler);
Loading history...
231
232
            //File download should be finished here, so determine the new filename and extension
233
            $headers = $response->getHeaders();
234
            //Try to determine an filename
235
            $filename = "";
236
237
            //If an content disposition header was set try to extract the filename out of it
238
            if (isset($headers['content-disposition'])) {
239
                $tmp = [];
240
                preg_match('/[^;\\n=]*=([\'\"])*(.*)(?(1)\1|)/', $headers['content-disposition'][0], $tmp);
241
                $filename = $tmp[2];
242
            }
243
244
            //If we dont know filename yet, try to determine it out of url
245
            if ($filename === "") {
246
                $filename = basename(parse_url($url, PHP_URL_PATH));
247
            }
248
249
            //Set original file
250
            $attachment->setFilename($filename);
251
252
            //Check if we have a extension given
253
            $pathinfo = pathinfo($filename);
254
            if (!empty($pathinfo['extension'])) {
255
                $new_ext = $pathinfo['extension'];
256
            } else { //Otherwise we have to guess the extension for the new file, based on its content
257
                $new_ext = $this->mimeTypes->getExtensions($this->mimeTypes->guessMimeType($tmp_path))[0] ?? 'tmp';
0 ignored issues
show
Bug introduced by
It seems like $this->mimeTypes->guessMimeType($tmp_path) can also be of type null; however, parameter $mimeType of Symfony\Component\Mime\M...erface::getExtensions() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

257
                $new_ext = $this->mimeTypes->getExtensions(/** @scrutinizer ignore-type */ $this->mimeTypes->guessMimeType($tmp_path))[0] ?? 'tmp';
Loading history...
258
            }
259
260
            //Rename the file to its new name and save path to attachment entity
261
            $new_path = $attachment_folder . DIRECTORY_SEPARATOR . $this->generateAttachmentFilename($attachment, $new_ext);
262
            $fs->rename($tmp_path, $new_path);
263
264
            //Make our file path relative to %BASE%
265
            $new_path = $this->pathResolver->realPathToPlaceholder($new_path);
266
            //Save the path to the attachment
267
            $attachment->setPath($new_path);
0 ignored issues
show
Bug introduced by
It seems like $new_path can also be of type null; however, parameter $path of App\Entity\Attachments\Attachment::setPath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

267
            $attachment->setPath(/** @scrutinizer ignore-type */ $new_path);
Loading history...
268
269
        } catch (TransportExceptionInterface $exception) {
270
            throw new AttachmentDownloadException('Transport error!');
271
        }
272
273
        return $attachment;
274
    }
275
276
    /**
277
     * Moves the given uploaded file to a permanent place and saves it into the attachment
278
     * @param Attachment $attachment The attachment in which the file should be saved
279
     * @param UploadedFile $file The file which was uploaded
280
     * @param array $options The options from the handleFormSubmit function
281
     * @return Attachment The attachment with the new filepath
282
     */
283
    protected function upload(Attachment $attachment, UploadedFile $file, array $options) : Attachment
284
    {
285
        //Move our temporay attachment to its final location
286
        $file_path = $file->move(
287
            $this->generateAttachmentPath($attachment, $options['secure_attachment']),
288
            $this->generateAttachmentFilename($attachment, $file->getClientOriginalExtension())
289
        )->getRealPath();
290
291
        //Make our file path relative to %BASE%
292
        $file_path = $this->pathResolver->realPathToPlaceholder($file_path);
293
        //Save the path to the attachment
294
        $attachment->setPath($file_path);
0 ignored issues
show
Bug introduced by
It seems like $file_path can also be of type null; however, parameter $path of App\Entity\Attachments\Attachment::setPath() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

294
        $attachment->setPath(/** @scrutinizer ignore-type */ $file_path);
Loading history...
295
        //And save original filename
296
        $attachment->setFilename($file->getClientOriginalName());
297
298
        return $attachment;
299
    }
300
}