Passed
Push — main ( 93e843...2da435 )
by Thierry
04:54
created

UploadManager::filesystem()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 11
c 0
b 0
f 0
nc 5
nop 1
dl 0
loc 22
rs 9.6111
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 Jaxon\Storage\StorageManager;
21
use League\Flysystem\Filesystem;
22
use League\Flysystem\FilesystemException;
23
use Nyholm\Psr7\UploadedFile;
24
use Psr\Http\Message\ServerRequestInterface;
25
use Psr\Log\LoggerInterface;
26
use Closure;
27
28
use function call_user_func;
29
use function is_array;
30
use function is_string;
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
     * @var array<string, Filesystem>
50
     */
51
    protected $aFilesystems = [];
52
53
    /**
54
     * @var array
55
     */
56
    private $errorMessages = [
57
        0 => 'There is no error, the file uploaded with success',
58
        1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
59
        2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
60
        3 => 'The uploaded file was only partially uploaded',
61
        4 => 'No file was uploaded',
62
        6 => 'Missing a temporary folder',
63
        7 => 'Failed to write file to disk.',
64
        8 => 'A PHP extension stopped the file upload.',
65
    ];
66
67
    /**
68
     * The constructor
69
     *
70
     * @param Validator $xValidator
71
     * @param Translator $xTranslator
72
     * @param LoggerInterface $xLogger
73
     * @param FileNameInterface $xFileName
74
     * @param StorageManager $xStorageManager
75
     * @param ConfigManager $xConfigManager
76
     */
77
    public function __construct(private Validator $xValidator, private Translator $xTranslator,
78
        private LoggerInterface $xLogger, private FileNameInterface $xFileName,
79
        private StorageManager $xStorageManager, private ConfigManager $xConfigManager)
80
    {
81
        // This feature is not yet implemented
82
        $this->setUploadFieldId('');
83
    }
84
85
    /**
86
     * Generate a random name
87
     *
88
     * @return string
89
     */
90
    protected function randomName(): string
91
    {
92
        return $this->xFileName->random(16);
93
    }
94
95
    /**
96
     * Set the id of the upload field in the form
97
     *
98
     * @param string $sUploadFieldId
99
     *
100
     * @return void
101
     */
102
    public function setUploadFieldId(string $sUploadFieldId): void
103
    {
104
        $this->sUploadFieldId = $sUploadFieldId;
105
    }
106
107
    /**
108
     * Filter uploaded file name
109
     *
110
     * @param Closure $cNameSanitizer    The closure which filters filenames
111
     *
112
     * @return void
113
     */
114
    public function setNameSanitizer(Closure $cNameSanitizer): void
115
    {
116
        $this->cNameSanitizer = $cNameSanitizer;
117
    }
118
119
    /**
120
     * Make sure the upload dir exists and is writable
121
     *
122
     * @param Filesystem $xFilesystem
123
     * @param string $sUploadDir
124
     *
125
     * @return string
126
     * @throws RequestException
127
     */
128
    private function _makeUploadDir(Filesystem $xFilesystem, string $sUploadDir): string
129
    {
130
        try
131
        {
132
            $xFilesystem->createDirectory($sUploadDir);
133
            if(!$xFilesystem->directoryExists($sUploadDir))
134
            {
135
                throw new RequestException($this->xTranslator->trans('errors.upload.access'));
136
            }
137
            return $sUploadDir;
138
        }
139
        catch(FilesystemException $e)
140
        {
141
            $this->xLogger->error('Filesystem error.', ['message' => $e->getMessage()]);
142
            throw new RequestException($this->xTranslator->trans('errors.upload.access'));
143
        }
144
    }
145
146
    /**
147
     * @param string $sField
148
     *
149
     * @return Filesystem
150
     * @throws RequestException
151
     */
152
    public function filesystem(string $sField = ''): Filesystem
153
    {
154
        $sField = trim($sField);
155
        if(isset($this->aFilesystems[$sField]))
156
        {
157
            return $this->aFilesystems[$sField];
158
        }
159
160
        // Default upload dir
161
        $sStorage = $this->xConfigManager->getAppOption('upload.default.storage', 'upload');
162
        $sConfigKey = "upload.files.$sField";
163
        if($sField !== '' && $this->xConfigManager->hasOption($sConfigKey))
164
        {
165
            $sStorage = $this->xConfigManager->getAppOption("$sConfigKey.storage", $sStorage);
166
        }
167
        if(!is_string($sStorage))
168
        {
169
            throw new RequestException($this->xTranslator->trans('errors.upload.adapter'));
170
        }
171
172
        $this->aFilesystems[$sField] = $this->xStorageManager->get($sStorage);
173
        return $this->aFilesystems[$sField];
174
    }
175
176
    /**
177
     * Get the path to the upload dir
178
     *
179
     * @param string $sField
180
     *
181
     * @return string
182
     * @throws RequestException
183
     */
184
    private function getUploadDir(string $sField): string
185
    {
186
        $xFileSystem = $this->filesystem($sField);
187
        return $this->_makeUploadDir($xFileSystem, $this->randomName() . '/');
188
    }
189
190
    /**
191
     * Check uploaded files
192
     *
193
     * @param UploadedFile $xHttpFile
194
     * @param string $sUploadDir
195
     * @param string $sField
196
     *
197
     * @return array
198
     * @throws RequestException
199
     */
200
    private function makeUploadedFile(UploadedFile $xHttpFile, string $sUploadDir, string $sField): array
201
    {
202
        // Check the uploaded file validity
203
        $nErrorCode = $xHttpFile->getError();
204
        if($nErrorCode !== UPLOAD_ERR_OK)
205
        {
206
            $this->xLogger->error('File upload error.', [
207
                'code' => $nErrorCode,
208
                'message' => $this->errorMessages[$nErrorCode],
209
            ]);
210
            $sMessage = $this->xTranslator->trans('errors.upload.failed', [
211
                'name' => $sField,
212
            ]);
213
            throw new RequestException($sMessage);
214
        }
215
216
        // Filename without the extension. Needs to be sanitized.
217
        $sName = pathinfo($xHttpFile->getClientFilename(), PATHINFO_FILENAME);
218
        if($this->cNameSanitizer !== null)
219
        {
220
            $sName = (string)call_user_func($this->cNameSanitizer,
221
                $sName, $sField, $this->sUploadFieldId);
222
        }
223
224
        // Set the user file data
225
        $xFile = File::fromHttpFile($this->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

225
        $xFile = File::fromHttpFile($this->filesystem($sField), $xHttpFile, $sUploadDir, /** @scrutinizer ignore-type */ $sName);
Loading history...
226
        // Verify file validity (format, size)
227
        if(!$this->xValidator->validateUploadedFile($sField, $xFile))
228
        {
229
            throw new RequestException($this->xValidator->getErrorMessage());
230
        }
231
232
        // All's right, save the file for copy.
233
        return ['temp' => $xHttpFile, 'user' => $xFile];
234
    }
235
236
    /**
237
     * Read uploaded files info from HTTP request data
238
     *
239
     * @param ServerRequestInterface $xRequest
240
     *
241
     * @return array
242
     * @throws RequestException
243
     */
244
    public function readFromHttpData(ServerRequestInterface $xRequest): array
245
    {
246
        // Get the uploaded files
247
        $aTempFiles = $xRequest->getUploadedFiles();
248
249
        $aUserFiles = [];
250
        $aAllFiles = []; // A flat list of all uploaded files
251
        foreach($aTempFiles as $sField => $aFiles)
252
        {
253
            $aUserFiles[$sField] = [];
254
            // Get the path to the upload dir
255
            $sUploadDir = $this->getUploadDir($sField);
256
            if(!is_array($aFiles))
257
            {
258
                $aFiles = [$aFiles];
259
            }
260
            foreach($aFiles as $xHttpFile)
261
            {
262
                $aFile = $this->makeUploadedFile($xHttpFile, $sUploadDir, $sField);
263
                $aUserFiles[$sField][] = $aFile['user'];
264
                $aAllFiles[] = $aFile;
265
            }
266
        }
267
268
        // Copy the uploaded files from the temp dir to the user dir
269
        try
270
        {
271
            foreach($aAllFiles as $aFiles)
272
            {
273
                $sPath = $aFiles['user']->path();
274
                $xContent = $aFiles['temp']->getStream();
275
                $aFiles['user']->filesystem()->write($sPath, $xContent);
276
            }
277
        }
278
        catch(FilesystemException $e)
279
        {
280
            $this->xLogger->error('Filesystem error.', ['message' => $e->getMessage()]);
281
            throw new RequestException($this->xTranslator->trans('errors.upload.access'));
282
        }
283
284
        return $aUserFiles;
285
    }
286
}
287