Completed
Push — master ( e11ac2...ace11e )
by Thierry
03:34
created

FileUpload::processRequest()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
nc 4
nop 0
dl 0
loc 16
rs 9.2
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * FileUpload.php - This class implements file upload with Ajax.
5
 *
6
 * @package jaxon-core
7
 * @author Thierry Feuzeu <[email protected]>
8
 * @copyright 2017 Thierry Feuzeu <[email protected]>
9
 * @license https://opensource.org/licenses/BSD-3-Clause BSD 3-Clause License
10
 * @link https://github.com/jaxon-php/jaxon-core
11
 */
12
13
namespace Jaxon\Request\Plugin;
14
15
use Jaxon\Jaxon;
16
use Jaxon\Plugin\Request as RequestPlugin;
17
use Jaxon\Request\Support\UploadedFile;
18
19
class FileUpload extends RequestPlugin
20
{
21
    use \Jaxon\Utils\Traits\Validator;
22
    use \Jaxon\Utils\Traits\Translator;
23
24
    /**
25
     * The uploaded files copied in the user dir
26
     *
27
     * @var array
28
     */
29
    protected $aUserFiles;
30
31
    /**
32
     * The name of file containing upload data
33
     *
34
     * @var string
35
     */
36
    protected $sTempFile = '';
37
38
    /**
39
     * A user defined function to transform uploaded file names
40
     *
41
     * @var Closure
42
     */
43
    protected $fFileFilter = null;
44
45
    /**
46
     * Read uploaded files info from the $_FILES global var
47
     */
48
    public function __construct()
0 ignored issues
show
Coding Style introduced by
__construct uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
__construct uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
49
    {
50
        $this->aUserFiles = [];
51
52
        if(array_key_exists('jxnupl', $_POST))
53
        {
54
            $this->sTempFile = $_POST['jxnupl'];
55
        }
56
        else if(array_key_exists('jxnupl', $_GET))
57
        {
58
            $this->sTempFile = $_GET['jxnupl'];
59
        }
60
    }
61
62
    /**
63
     * Filter uploaded file name
64
     *
65
     * @param Closure       $fFileFilter            The closure which filters filenames
66
     *
67
     * @return void
68
     */
69
    public function setFileFilter($fFileFilter)
70
    {
71
        $this->fFileFilter = $fFileFilter;
72
    }
73
74
    /**
75
     * Filter uploaded file name
76
     *
77
     * @param string        $sFilename              The filename
78
     * @param string        $sVarName               The associated variable name
79
     *
80
     * @return string
81
     */
82
    protected function filterFilename($sFilename, $sVarName)
83
    {
84
        if(($this->fFileFilter))
85
        {
86
            $fFileFilter = $this->fFileFilter;
87
            $sFilename = (string)$fFileFilter($sFilename, $sVarName);
88
        }
89
        return $sFilename;
90
    }
91
92
    /**
93
     * Read uploaded files info from HTTP request data
94
     *
95
     * @return void
96
     */
97
    protected function readFromHttpData()
0 ignored issues
show
Coding Style introduced by
readFromHttpData uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
98
    {
99
        // Check validity of the uploaded files
100
        $aTempFiles = [];
101
        foreach($_FILES as $sVarName => $aFile)
102
        {
103
            if(is_array($aFile['name']))
104
            {
105
                for($i = 0; $i < count($aFile['name']); $i++)
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
106
                {
107
                    if(!$aFile['name'][$i])
108
                    {
109
                        continue;
110
                    }
111
                    if(!array_key_exists($sVarName, $aTempFiles))
112
                    {
113
                        $aTempFiles[$sVarName] = [];
114
                    }
115
                    // Filename without the extension
116
                    $sFilename = $this->filterFilename(pathinfo($aFile['name'][$i], PATHINFO_FILENAME), $sVarName);
117
                    // Copy the file data into the local array
118
                    $aTempFiles[$sVarName][] = [
119
                        'name' => $aFile['name'][$i],
120
                        'type' => $aFile['type'][$i],
121
                        'tmp_name' => $aFile['tmp_name'][$i],
122
                        'error' => $aFile['error'][$i],
123
                        'size' => $aFile['size'][$i],
124
                        'filename' => $sFilename,
125
                        'extension' => pathinfo($aFile['name'][$i], PATHINFO_EXTENSION),
126
                    ];
127
                }
128
            }
129
            else
130
            {
131
                if(!$aFile['name'])
132
                {
133
                    continue;
134
                }
135
                if(!array_key_exists($sVarName, $aTempFiles))
136
                {
137
                    $aTempFiles[$sVarName] = [];
138
                }
139
                // Filename without the extension
140
                $sFilename = $this->filterFilename(pathinfo($aFile['name'], PATHINFO_FILENAME), $sVarName);
141
                // Copy the file data into the local array
142
                $aTempFiles[$sVarName][] = [
143
                    'name' => $aFile['name'],
144
                    'type' => $aFile['type'],
145
                    'tmp_name' => $aFile['tmp_name'],
146
                    'error' => $aFile['error'],
147
                    'size' => $aFile['size'],
148
                    'filename' => $sFilename,
149
                    'extension' => pathinfo($aFile['name'], PATHINFO_EXTENSION),
150
                ];
151
            }
152
        }
153
154
        // Default upload dir
155
        $sDefaultUploadDir = $this->getOption('upload.default.dir');
156
157
        // Check uploaded files validity
158
        foreach($aTempFiles as $sVarName => $aFiles)
159
        {
160
            foreach($aFiles as $aFile)
161
            {
162
                // Verify upload result
163
                if($aFile['error'] != 0)
164
                {
165
                    throw new \Jaxon\Exception\Error($this->trans('errors.upload.failed', $aFile));
166
                }
167
                // Verify file validity (format, size)
168
                if(!$this->validateUploadedFile($sVarName, $aFile))
169
                {
170
                    throw new \Jaxon\Exception\Error($this->getValidatorMessage());
171
                }
172
                // Verify that the upload dir exists and is writable
173
                $sUploadDir = $this->getOption('upload.files.' . $sVarName . '.dir', $sDefaultUploadDir);
174
                $sUploadDir = rtrim(trim($sUploadDir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
175
                if(!is_writable($sUploadDir))
176
                {
177
                    throw new \Jaxon\Exception\Error($this->trans('errors.upload.access'));
178
                }
179
            }
180
        }
181
182
        // Copy the uploaded files from the temp dir to the user dir
183
        foreach($aTempFiles as $sVarName => $aTempFiles)
184
        {
185
            $this->aUserFiles[$sVarName] = [];
186
            foreach($aTempFiles as $aFile)
187
            {
188
                // Set the user file data
189
                $sUploadDir = $this->getOption('upload.files.' . $sVarName . '.dir', $sDefaultUploadDir);
190
                $sUploadDir = rtrim(trim($sUploadDir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
191
                $xUploadedFile = UploadedFile::fromHttpData($sUploadDir, $aFile);
192
                // All's right, move the file to the user dir.
193
                move_uploaded_file($aFile["tmp_name"], $xUploadedFile->path());
194
                $this->aUserFiles[$sVarName][] = $xUploadedFile;
195
            }
196
        }
197
    }
198
199
    /**
200
     * Save uploaded files info to a temp file
201
     *
202
     * @return void
203
     */
204
    protected function saveToTempFile()
205
    {
206
        // Default upload dir
207
        $sUploadDir = $this->getOption('upload.default.dir');
208
        $sUploadDir = rtrim(trim($sUploadDir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
209
        if(!is_writable($sUploadDir))
210
        {
211
            throw new \Jaxon\Exception\Error($this->trans('errors.upload.access'));
212
        }
213
        // Convert uploaded file to an array
214
        $aFiles = [];
215
        foreach($this->aUserFiles as $sVarName => $aUserFiles)
216
        {
217
            $aFiles[$sVarName] = [];
218
            foreach($aUserFiles as $aUserFile)
219
            {
220
                 $aFiles[$sVarName][] = $aUserFile->toTempData();
221
            }
222
        }
223
        // Save upload data in a temp file
224
        $this->sTempFile = uniqid();
225
        $sUploadDir .= 'tmp' . DIRECTORY_SEPARATOR;
226
        @mkdir($sUploadDir);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
227
        file_put_contents($sUploadDir . $this->sTempFile . '.json', json_encode($aFiles));
228
    }
229
230
    /**
231
     * Read uploaded files info from a temp file
232
     *
233
     * @return void
234
     */
235
    protected function readFromTempFile()
236
    {
237
        // Default upload dir
238
        $sUploadDir = $this->getOption('upload.default.dir');
239
        $sUploadDir = rtrim(trim($sUploadDir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
240
        $sUploadDir .= 'tmp' . DIRECTORY_SEPARATOR;
241
        if(!is_readable($sUploadDir . $this->sTempFile . '.json'))
242
        {
243
            throw new \Jaxon\Exception\Error($this->trans('errors.upload.access'));
244
        }
245
        $aFiles = file_get_contents($sUploadDir . $this->sTempFile . '.json');
246
        $aFiles = json_decode($aFiles, true);
247
        foreach($aFiles as $sVarName => $aUserFiles)
248
        {
249
            $this->aUserFiles[$sVarName] = [];
250
            foreach($aUserFiles as $aUserFile)
251
            {
252
                $this->aUserFiles[$sVarName][] = UploadedFile::fromTempData($aUserFile);
253
            }
254
        }
255
        unlink($sUploadDir . $this->sTempFile . '.json');
256
    }
257
258
    /**
259
     * Return the name of this plugin
260
     *
261
     * @return string
262
     */
263
    public function getName()
264
    {
265
        return Jaxon::FILE_UPLOAD;
266
    }
267
268
    /**
269
     * Get the uploaded files
270
     *
271
     * @return array
272
     */
273
    public function getUploadedFiles()
274
    {
275
        return $this->aUserFiles;
276
    }
277
278
    /**
279
     * Register a browser event
280
     *
281
     * @param array         $aArgs                An array containing the event specification
282
     *
283
     * @return \Jaxon\Request\Request
284
     */
285
    public function register($aArgs)
286
    {
287
        return false;
288
    }
289
290
    /**
291
     * Generate a hash for the registered browser events
292
     *
293
     * @return string
294
     */
295
    public function generateHash()
296
    {
297
        return '';
298
    }
299
300
    /**
301
     * Generate client side javascript code for the registered browser events
302
     *
303
     * @return string
304
     */
305
    public function getScript()
306
    {
307
        return '';
308
    }
309
310
    /**
311
     * Check if this plugin can process the incoming Jaxon request
312
     *
313
     * @return boolean
314
     */
315
    public function canProcessRequest()
0 ignored issues
show
Coding Style introduced by
canProcessRequest uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
316
    {
317
        return (count($_FILES) > 0 || ($this->sTempFile));
318
    }
319
320
    /**
321
     * Process the uploaded files into the HTTP request
322
     *
323
     * @return boolean
324
     */
325
    public function processRequest()
0 ignored issues
show
Coding Style introduced by
processRequest uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
326
    {
327
        if(!$this->canProcessRequest())
328
        {
329
            return false;
330
        }
331
        if(count($_FILES) > 0)
332
        {
333
            $this->readFromHttpData();
334
        }
335
        else if(($this->sTempFile))
336
        {
337
            $this->readFromTempFile();
338
        }
339
        return true;
340
    }
341
342
    /**
343
     * Check uploaded files validity and move them to the user dir
344
     *
345
     * @return boolean
346
     */
347
    public function saveUploadedFiles()
348
    {
349
        // Process uploaded files
350
        if(!$this->processRequest())
351
        {
352
            return '';
353
        }
354
        // Save upload data in a temp file
355
        $this->saveToTempFile();
356
        return $this->sTempFile;
357
    }
358
}
359