Passed
Push — main ( 4ddaea...7c5fe7 )
by Thierry
04:58
created

UploadManager::readFromHttpData()   B

Complexity

Conditions 6
Paths 25

Size

Total Lines 41
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 21
nc 25
nop 1
dl 0
loc 41
rs 8.9617
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * UploadManager.php
5
 *
6
 * This class processes uploaded files.
7
 *
8
 * @package jaxon-upload
9
 * @author Thierry Feuzeu <[email protected]>
10
 * @copyright 2022 Thierry Feuzeu <[email protected]>
11
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
12
 * @link https://github.com/jaxon-php/jaxon-core
13
 */
14
15
namespace Jaxon\Upload\Manager;
16
17
use Jaxon\App\I18n\Translator;
18
use Jaxon\Exception\RequestException;
19
use League\Flysystem\Filesystem;
20
use League\Flysystem\FilesystemException;
21
use Nyholm\Psr7\UploadedFile;
22
use Psr\Http\Message\ServerRequestInterface;
23
use Psr\Log\LoggerInterface;
24
use Closure;
25
26
use function call_user_func;
27
use function is_array;
28
29
class UploadManager
30
{
31
    /**
32
     * The id of the upload field in the form
33
     *
34
     * @var string
35
     */
36
    protected $sUploadFieldId = '';
37
38
    /**
39
     * A user defined function to transform uploaded file names
40
     *
41
     * @var Closure
42
     */
43
    protected $cNameSanitizer = null;
44
45
    /**
46
     * @var array
47
     */
48
    private $errorMessages = [
49
        0 => 'There is no error, the file uploaded with success',
50
        1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
51
        2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
52
        3 => 'The uploaded file was only partially uploaded',
53
        4 => 'No file was uploaded',
54
        6 => 'Missing a temporary folder',
55
        7 => 'Failed to write file to disk.',
56
        8 => 'A PHP extension stopped the file upload.',
57
    ];
58
59
    /**
60
     * The constructor
61
     *
62
     * @param LoggerInterface $xLogger
63
     * @param Validator $xValidator
64
     * @param Translator $xTranslator
65
     * @param FileStorage $xFileStorage
66
     * @param FileNameInterface $xFileName
67
     */
68
    public function __construct(private LoggerInterface $xLogger,
69
        private Validator $xValidator, private Translator $xTranslator,
70
        private FileStorage $xFileStorage, private FileNameInterface $xFileName)
71
    {
72
        // This feature is not yet implemented
73
        $this->setUploadFieldId('');
74
    }
75
76
    /**
77
     * Generate a random name
78
     *
79
     * @return string
80
     */
81
    protected function randomName(): string
82
    {
83
        return $this->xFileName->random(16);
84
    }
85
86
    /**
87
     * Set the id of the upload field in the form
88
     *
89
     * @param string $sUploadFieldId
90
     *
91
     * @return void
92
     */
93
    public function setUploadFieldId(string $sUploadFieldId): void
94
    {
95
        $this->sUploadFieldId = $sUploadFieldId;
96
    }
97
98
    /**
99
     * Filter uploaded file name
100
     *
101
     * @param Closure $cNameSanitizer    The closure which filters filenames
102
     *
103
     * @return void
104
     */
105
    public function setNameSanitizer(Closure $cNameSanitizer): void
106
    {
107
        $this->cNameSanitizer = $cNameSanitizer;
108
    }
109
110
    /**
111
     * Make sure the upload dir exists and is writable
112
     *
113
     * @param Filesystem $xFilesystem
114
     * @param string $sUploadDir
115
     *
116
     * @return string
117
     * @throws RequestException
118
     */
119
    private function _makeUploadDir(Filesystem $xFilesystem, string $sUploadDir): string
120
    {
121
        try
122
        {
123
            $xFilesystem->createDirectory($sUploadDir);
124
            if(!$xFilesystem->directoryExists($sUploadDir))
125
            {
126
                throw new RequestException($this->xTranslator->trans('errors.upload.access'));
127
            }
128
            return $sUploadDir;
129
        }
130
        catch(FilesystemException $e)
131
        {
132
            $this->xLogger->error('Filesystem error.', ['message' => $e->getMessage()]);
133
            throw new RequestException($this->xTranslator->trans('errors.upload.access'));
134
        }
135
    }
136
137
    /**
138
     * Get the path to the upload dir
139
     *
140
     * @param string $sField
141
     *
142
     * @return string
143
     * @throws RequestException
144
     */
145
    private function getUploadDir(string $sField): string
146
    {
147
        $xFileSystem = $this->xFileStorage->filesystem($sField);
148
        return $this->_makeUploadDir($xFileSystem, $this->randomName() . '/');
149
    }
150
151
    /**
152
     * Check uploaded files
153
     *
154
     * @param UploadedFile $xHttpFile
155
     * @param string $sUploadDir
156
     * @param string $sField
157
     *
158
     * @return array
159
     * @throws RequestException
160
     */
161
    private function makeUploadedFile(UploadedFile $xHttpFile, string $sUploadDir, string $sField): array
162
    {
163
        // Check the uploaded file validity
164
        $nErrorCode = $xHttpFile->getError();
165
        if($nErrorCode !== UPLOAD_ERR_OK)
166
        {
167
            $this->xLogger->error('File upload error.', [
168
                'code' => $nErrorCode,
169
                'message' => $this->errorMessages[$nErrorCode],
170
            ]);
171
            $sMessage = $this->xTranslator->trans('errors.upload.failed', [
172
                'name' => $sField,
173
            ]);
174
            throw new RequestException($sMessage);
175
        }
176
177
        // Filename without the extension. Needs to be sanitized.
178
        $sName = pathinfo($xHttpFile->getClientFilename(), PATHINFO_FILENAME);
179
        if($this->cNameSanitizer !== null)
180
        {
181
            $sName = (string)call_user_func($this->cNameSanitizer,
182
                $sName, $sField, $this->sUploadFieldId);
183
        }
184
185
        // Set the user file data
186
        $xFile = File::fromHttpFile($this->xFileStorage->filesystem($sField), $xHttpFile, $sUploadDir, $sName);
0 ignored issues
show
Bug introduced by
It seems like $sName can also be of type array; however, parameter $sName of Jaxon\Upload\Manager\File::fromHttpFile() 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

186
        $xFile = File::fromHttpFile($this->xFileStorage->filesystem($sField), $xHttpFile, $sUploadDir, /** @scrutinizer ignore-type */ $sName);
Loading history...
187
        // Verify file validity (format, size)
188
        if(!$this->xValidator->validateUploadedFile($sField, $xFile))
189
        {
190
            throw new RequestException($this->xValidator->getErrorMessage());
191
        }
192
193
        // All's right, save the file for copy.
194
        return ['temp' => $xHttpFile, 'user' => $xFile];
195
    }
196
197
    /**
198
     * Read uploaded files info from HTTP request data
199
     *
200
     * @param ServerRequestInterface $xRequest
201
     *
202
     * @return array
203
     * @throws RequestException
204
     */
205
    public function readFromHttpData(ServerRequestInterface $xRequest): array
206
    {
207
        // Get the uploaded files
208
        $aTempFiles = $xRequest->getUploadedFiles();
209
210
        $aUserFiles = [];
211
        $aAllFiles = []; // A flat list of all uploaded files
212
        foreach($aTempFiles as $sField => $aFiles)
213
        {
214
            $aUserFiles[$sField] = [];
215
            // Get the path to the upload dir
216
            $sUploadDir = $this->getUploadDir($sField);
217
            if(!is_array($aFiles))
218
            {
219
                $aFiles = [$aFiles];
220
            }
221
            foreach($aFiles as $xHttpFile)
222
            {
223
                $aFile = $this->makeUploadedFile($xHttpFile, $sUploadDir, $sField);
224
                $aUserFiles[$sField][] = $aFile['user'];
225
                $aAllFiles[] = $aFile;
226
            }
227
        }
228
229
        // Copy the uploaded files from the temp dir to the user dir
230
        try
231
        {
232
            foreach($aAllFiles as $aFiles)
233
            {
234
                $sPath = $aFiles['user']->path();
235
                $xContent = $aFiles['temp']->getStream();
236
                $aFiles['user']->filesystem()->write($sPath, $xContent);
237
            }
238
        }
239
        catch(FilesystemException $e)
240
        {
241
            $this->xLogger->error('Filesystem error.', ['message' => $e->getMessage()]);
242
            throw new RequestException($this->xTranslator->trans('errors.upload.access'));
243
        }
244
245
        return $aUserFiles;
246
    }
247
}
248