UploadManager::readFromHttpData()   A
last analyzed

Complexity

Conditions 6
Paths 15

Size

Total Lines 35
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 17
nc 15
nop 1
dl 0
loc 35
rs 9.0777
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\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
26
use Closure;
27
28
use function Jaxon\jaxon;
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
     * The constructor
57
     *
58
     * @param FileStorage $xFileStorage
59
     * @param FileNameInterface $xFileName
60
     * @param ConfigManager $xConfigManager
61
     * @param Validator $xValidator
62
     * @param Translator $xTranslator
63
     */
64
    public function __construct(private FileStorage $xFileStorage,
65
        private FileNameInterface $xFileName, private ConfigManager $xConfigManager,
66
        private Validator $xValidator, private Translator $xTranslator)
67
    {
68
        // This feature is not yet implemented
69
        $this->setUploadFieldId('');
70
    }
71
72
    /**
73
     * Generate a random name
74
     *
75
     * @return string
76
     */
77
    protected function randomName(): string
78
    {
79
        return $this->xFileName->random(16);
80
    }
81
82
    /**
83
     * Set the id of the upload field in the form
84
     *
85
     * @param string $sUploadFieldId
86
     *
87
     * @return void
88
     */
89
    public function setUploadFieldId(string $sUploadFieldId)
90
    {
91
        $this->sUploadFieldId = $sUploadFieldId;
92
    }
93
94
    /**
95
     * Filter uploaded file name
96
     *
97
     * @param Closure $cNameSanitizer    The closure which filters filenames
98
     *
99
     * @return void
100
     */
101
    public function setNameSanitizer(Closure $cNameSanitizer)
102
    {
103
        $this->cNameSanitizer = $cNameSanitizer;
104
    }
105
106
    /**
107
     * Make sure the upload dir exists and is writable
108
     *
109
     * @param Filesystem $xFilesystem
110
     * @param string $sUploadDir
111
     *
112
     * @return string
113
     * @throws RequestException
114
     */
115
    private function _makeUploadDir(Filesystem $xFilesystem, string $sUploadDir): string
116
    {
117
        try
118
        {
119
            $xFilesystem->createDirectory($sUploadDir);
120
            if($xFilesystem->visibility($sUploadDir) !== Visibility::PUBLIC)
121
            {
122
                throw new RequestException($this->xTranslator->trans('errors.upload.access'));
123
            }
124
            return $sUploadDir;
125
        }
126
        catch(FilesystemException $e)
127
        {
128
            jaxon()->logger()->error('Filesystem error', ['message' => $e->getMessage()]);
129
            throw new RequestException($this->xTranslator->trans('errors.upload.access'));
130
        }
131
    }
132
133
    /**
134
     * Get the path to the upload dir
135
     *
136
     * @param string $sField
137
     *
138
     * @return string
139
     * @throws RequestException
140
     */
141
    private function getUploadDir(string $sField): string
142
    {
143
        return $this->_makeUploadDir($this->xFileStorage->filesystem($sField), $this->randomName() . '/');
144
    }
145
146
    /**
147
     * Check uploaded files
148
     *
149
     * @param UploadedFile $xHttpFile
150
     * @param string $sUploadDir
151
     * @param string $sField
152
     *
153
     * @return File
154
     * @throws RequestException
155
     */
156
    private function makeUploadedFile(UploadedFile $xHttpFile, string $sUploadDir, string $sField): File
157
    {
158
        // Check the uploaded file validity
159
        if($xHttpFile->getError())
160
        {
161
            throw new RequestException($this->xTranslator->trans('errors.upload.failed', ['name' => $sField]));
162
        }
163
164
        // Filename without the extension. Needs to be sanitized.
165
        $sName = pathinfo($xHttpFile->getClientFilename(), PATHINFO_FILENAME);
166
        if($this->cNameSanitizer !== null)
167
        {
168
            $sName = (string)call_user_func($this->cNameSanitizer, $sName, $sField, $this->sUploadFieldId);
169
        }
170
171
        // Set the user file data
172
        $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

172
        $xFile = File::fromHttpFile($this->xFileStorage->filesystem($sField), $xHttpFile, $sUploadDir, /** @scrutinizer ignore-type */ $sName);
Loading history...
173
        // Verify file validity (format, size)
174
        if(!$this->xValidator->validateUploadedFile($sField, $xFile))
175
        {
176
            throw new RequestException($this->xValidator->getErrorMessage());
177
        }
178
179
        // All's right, save the file for copy.
180
        $this->aAllFiles[] = ['temp' => $xHttpFile, 'user' => $xFile];
181
        return $xFile;
182
    }
183
184
    /**
185
     * Read uploaded files info from HTTP request data
186
     *
187
     * @param ServerRequestInterface $xRequest
188
     *
189
     * @return array
190
     * @throws RequestException
191
     */
192
    public function readFromHttpData(ServerRequestInterface $xRequest): array
193
    {
194
        // Get the uploaded files
195
        $aTempFiles = $xRequest->getUploadedFiles();
196
197
        $aUserFiles = [];
198
        $this->aAllFiles = []; // A flat list of all uploaded files
199
        foreach($aTempFiles as $sField => $aFiles)
200
        {
201
            $aUserFiles[$sField] = [];
202
            // Get the path to the upload dir
203
            $sUploadDir = $this->getUploadDir($sField);
204
            if(!is_array($aFiles))
205
            {
206
                $aFiles = [$aFiles];
207
            }
208
            foreach($aFiles as $xHttpFile)
209
            {
210
                $aUserFiles[$sField][] = $this->makeUploadedFile($xHttpFile, $sUploadDir, $sField);
211
            }
212
        }
213
        // Copy the uploaded files from the temp dir to the user dir
214
        try
215
        {
216
            foreach($this->aAllFiles as $aFiles)
217
            {
218
                $aFiles['user']->filesystem()->write($aFiles['user']->path(), $aFiles['temp']->getStream());
219
            }
220
        }
221
        catch(FilesystemException $e)
222
        {
223
            jaxon()->logger()->error('Filesystem error', ['message' => $e->getMessage()]);
224
            throw new RequestException($this->xTranslator->trans('errors.upload.access'));
225
        }
226
        return $aUserFiles;
227
    }
228
}
229