Issues (18)

src/Manager/UploadManager.php (1 issue)

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

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