Issues (443)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

common/components/MediaUploadHandler.php (14 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * @link http://www.writesdown.com/
4
 * @copyright Copyright (c) 2015 WritesDown
5
 * @license http://www.writesdown.com/license/
6
 */
7
8
namespace common\components;
9
10
use common\models\Media;
11
use common\models\Post;
12
use Imagine\Image\Box;
13
use Imagine\Image\ManipulatorInterface;
14
use Imagine\Image\Point;
15
use Yii;
16
use yii\data\Pagination;
17
use yii\helpers\ArrayHelper;
18
use yii\helpers\FileHelper;
19
use yii\helpers\Url;
20
use yii\imagine\Image;
21
use yii\web\Response;
22
use yii\web\UploadedFile;
23
24
/**
25
 * Upload handler for Media model.
26
 *
27
 * @author Agiel K. Saputra <[email protected]>
28
 * @since 0.1.0
29
 */
30
class MediaUploadHandler
31
{
32
    const PRINT_RESPONSE = true;
33
    const NOT_PRINT_RESPONSE = false;
34
35
    /**
36
     * @var array Options for upload handler, can be overridden over class constructs.
37
     */
38
    protected $options = [];
39
    /**
40
     * @var array Used to generate response.
41
     */
42
    protected $response = [];
43
    /**
44
     * @var array Grouping files based on its extension.
45
     */
46
    protected $fileTypes = [
47
        'image' => [
48
            'extensions' => '/\.(gif|jpg|jpeg|png)$/i',
49
        ],
50
        'audio' => [
51
            'extensions' => '/\.(m4a|mp3|wav|wma|oga)$/i',
52
            'mime_icon' => 'img/mime/audio.png',
53
        ],
54
        'video' => [
55
            'extensions' => '/\.(3gp|mkv|flv|og?(a|g)|avi|mov|wmv|mp4|m4p|mp?(g|2|eg|e|v))$/i',
56
            'mime_icon' => 'img/mime/video.png',
57
        ],
58
        'pdf' => [
59
            'extensions' => '/\.(pdf|xps)$/i',
60
            'mime_icon' => 'img/mime/pdf.png',
61
        ],
62
        'spreadsheet' => [
63
            'extensions' => '/\.(xls|xlsx|ods|csv|xml)$/i',
64
            'mime_icon' => 'img/mime/spreadsheet.png',
65
        ],
66
        'document' => [
67
            'extensions' => '/\.(doc?(m|x)|odt)$/i',
68
            'mime_icon' => 'img/mime/document.png',
69
        ],
70
        'archive' => [
71
            'extensions' => '/\.(rar|zip|tar|7zip)$/i',
72
            'mime_icon' => 'img/mime/archive.png',
73
        ],
74
        'code' => [
75
            'extensions' => '/\.(php|c?pp|java|vb?s|html|js|css)$/i',
76
            'mime_icon' => 'img/mime/audio.png',
77
        ],
78
        'interactive' => [
79
            'extensions' => '/\.(ppt|pptx|odp)$/i',
80
            'icon' => 'img/mime/interactive.png',
81
        ],
82
        'text' => [
83
            'extensions' => '/\.(txt|md|bat)$/i',
84
            'mime_icon' => 'img/mime/text.png',
85
        ],
86
    ];
87
88
    /**
89
     * @var Media
90
     */
91
    private $_media;
92
    /**
93
     * @var array Used to create Media Meta.
94
     */
95
    private $_meta;
96
97
    /**
98
     * Create object of MediaUploadHandler.
99
     *
100
     * @param array|null $options
101
     * @param bool $initialize
102
     */
103
    public function __construct($options = null, $initialize = true)
104
    {
105
        // Set response format to RAW.
106
        Yii::$app->response->format = Response::FORMAT_RAW;
107
        // Set options of MediaUploadHandler.
108
        $this->setOptions($options);
0 ignored issues
show
It seems like $options defined by parameter $options on line 103 can also be of type null; however, common\components\MediaUploadHandler::setOptions() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
109
110
        if ($initialize) {
111
            $this->initialize();
112
        }
113
    }
114
115
    /**
116
     * Initialize the action of MediaUploadHandler based on request method if set true.
117
     */
118
    protected function initialize()
119
    {
120
        switch (Yii::$app->request->method) {
121
            case 'OPTIONS':
122
            case 'HEAD':
123
                $this->head();
124
                break;
125
            case 'PATCH':
126
            case 'PUT':
127
            case 'POST':
128
                $this->post($this->getOption('print_response'));
0 ignored issues
show
$this->getOption('print_response') is of type string|array|null, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
129
                break;
130
            case 'GET':
131
                $this->get($this->getOption('print_response'));
0 ignored issues
show
It seems like $this->getOption('print_response') targeting common\components\MediaUploadHandler::getOption() can also be of type array or string; however, common\components\MediaUploadHandler::get() does only seem to accept integer|null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
132
                break;
133
            case 'DELETE':
134
                $this->delete($this->getOption('print_response'));
0 ignored issues
show
$this->getOption('print_response') is of type string|array|null, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
135
                break;
136
            default:
137
                $this->setHeader('HTTP/1.1 405 Method Not Allowed');
138
        }
139
    }
140
141
    /**
142
     * Get server var based on id. Return null when it's not exist.
143
     *
144
     * @param $id
145
     * @return mixed
146
     */
147
    protected function getServerVar($id)
0 ignored issues
show
getServerVar uses the super-global variable $_SERVER 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...
148
    {
149
        if (isset($_SERVER[$id])) {
150
            return $_SERVER[$id];
151
        }
152
153
        return null;
154
    }
155
156
    /**
157
     * Get singular param name.
158
     *
159
     * @return string
160
     */
161
    protected function getSingularParamName()
162
    {
163
        return substr($this->options['param_name'], 0, -1);
164
    }
165
166
    /**
167
     * Adds a new header.
168
     * If there is already a header with the same name, it will be replaced.
169
     *
170
     * @param string $name The name of the header.
171
     * @param string $value The value of the header.
172
     */
173
    protected function setHeader($name, $value = '')
174
    {
175
        Yii::$app->response->headers->set($name, $value);
176
    }
177
178
    /**
179
     * Set header content-type.
180
     */
181
    protected function sendContentTypeHeader()
182
    {
183
        $this->setHeader('Vary', 'Accept');
184
185
        if (strpos($this->getServerVar('HTTP_ACCEPT'), 'application/json') !== false) {
186
            $this->setHeader('Content-type', 'application/json');
187
        } else {
188
            $this->setHeader('Content-type', 'text/plain');
189
        }
190
    }
191
192
    /**
193
     * Set header Access-Control-*.
194
     */
195
    protected function sendAccessControlHeaders()
196
    {
197
        $this->setHeader('Access-Control-Allow-Origin', $this->options['access_control_allow_origin']);
198
        $this->setHeader('Access-Control-Allow-Credentials', $this->options['access_control_allow_credentials']
199
            ? 'true'
200
            : 'false'
201
        );
202
        $this->setHeader('Access-Control-Allow-Methods', implode(', ', $this->options['access_control_allow_methods']));
203
        $this->setHeader('Access-Control-Allow-Headers', implode(', ', $this->options['access_control_allow_headers']));
204
    }
205
206
    /**
207
     * Finds the Media model based on its primary key value.
208
     * If the model is not found it will return null.
209
     *
210
     * @param integer $id
211
     * @return Media|array
212
     */
213
    protected function findMedia($id)
214
    {
215
        if (($model = Media::findOne($id)) !== null) {
216
            return $model;
217
        }
218
219
        return null;
220
    }
221
222
    /**
223
     * Finds the Post model based on its primary key value.
224
     * If the model is not found it will return null.
225
     *
226
     * @param integer $id
227
     * @return Post|null
228
     */
229
    protected function findPost($id)
230
    {
231
        if (($model = Post::findOne($id)) !== null) {
232
            return $model;
233
        }
234
235
        return null;
236
    }
237
238
    /**
239
     * Get user path of login user. It can be disabled by override config, set user_dirs to false.
240
     *
241
     * @return string The username of login user.
242
     */
243
    protected function getUserPath()
244
    {
245
        if ($this->options['user_dirs'] && !Yii::$app->user->isGuest) {
246
            return Yii::$app->user->identity->username . '/';
247
        }
248
249
        return '';
250
    }
251
252
    /**
253
     * Year-month path generated by date function can be disable by set year_month_path to false.
254
     *
255
     * @return string date(/Y/m).
256
     */
257
    protected function getYearMonthPath()
258
    {
259
        if ($this->options['year_month_dirs']) {
260
            return date('Y/m/');
261
        }
262
263
        return '';
264
    }
265
266
    /**
267
     * Get upload path based on current config, generate upload_dir/user_path/y/m/filename.ext.
268
     *
269
     * @param null $fileName Filename and extension (filename.ext).
270
     * @return string
271
     */
272
    protected function getUploadPath($fileName = null)
273
    {
274
        return $this->getOption('upload_dir') . $this->getUserPath() . $this->getYearMonthPath() . $fileName;
275
    }
276
277
    /**
278
     * Get file-path of the filename.
279
     *
280
     * @param string|null $fileName
281
     * @return string
282
     */
283
    protected function getFilePath($fileName = null)
284
    {
285
        return $this->getOption('upload_dir') . $fileName;
286
    }
287
288
    /**
289
     * Generate slug for uploaded file.
290
     * Replace all space to - and transform all character to lowercase.
291
     *
292
     * @param string $fileName
293
     * @param array $replace The replace_pairs parameter may be used as a substitute for to and from in which case.
294
     * it's an array in the form array('from' => 'to', ...).
295
     * @param string $delimiter
296
     * @see strtr
297
     * @return string Clean name
298
     */
299
    protected function generateSlug($fileName, $replace = [], $delimiter = '-')
300
    {
301
        setlocale(LC_ALL, 'en_US.UTF8');
302
        $fileName = trim($fileName);
303
304
        if (!empty($replace)) {
305
            $fileName = strtr($fileName, $replace);
306
        }
307
308
        $cleanName = iconv('UTF-8', 'ASCII//TRANSLIT', $fileName);
309
        $cleanName = preg_replace("/[^a-zA-Z0-9\/_|+ -]/", '', $cleanName);
310
        $cleanName = strtolower(trim($cleanName, '-'));
311
        $cleanName = preg_replace("/[\/_|+ -]+/", $delimiter, $cleanName);
312
313
        return $cleanName;
314
    }
315
316
    /**
317
     * Callback function of upCountName.
318
     *
319
     * @param array $matches
320
     * @return string
321
     */
322
    protected function upCountNameCallback($matches)
323
    {
324
        $index = isset($matches[1]) ? intval($matches[1]) + 1 : 1;
325
        $ext = isset($matches[2]) ? $matches[2] : '';
326
327
        return '-' . $index . $ext;
328
    }
329
330
    /**
331
     * The number before fileName extension is replaced by upCountNameCallback.
332
     *
333
     * @param string $fileName
334
     * @return mixed
335
     * @see upCountNameCallback
336
     */
337
    protected function upCountName($fileName)
338
    {
339
        return preg_replace_callback('/(?:(?:\-([\d]+))?(\.[^.]+))?$/', [$this, 'upCountNameCallback'], $fileName, 1);
340
    }
341
342
    /**
343
     * Get filename of uploaded file.
344
     * If the filename is already exist in the upload directory
345
     * then the number between - and before the extension plus 1.
346
     *
347
     * @param UploadedFile $file
348
     * @return string
349
     */
350
    protected function getFileName($file)
351
    {
352
        $index = 0;
353
        $fileName = $this->generateSlug($file->baseName);
354
        $fileName .= '.' . $file->extension;
355
        $fileName = trim(basename(stripslashes($fileName)), ".\x00..\x20");
356
357
        if (!$fileName) {
358
            $fileName = str_replace('.', '-', microtime(true));
359
        }
360
361
        while (is_file($this->getUploadPath($fileName))) {
362
            $fileName = $this->upCountName($fileName);
363
            $index++;
364
        }
365
366
        if ($index !== 0) {
367
            // Replace media title
368
            $this->_media->title = $file->baseName . ' ' . $index;
369
        }
370
371
        return $fileName;
372
    }
373
374
    /**
375
     * @param \imagine\image\ImageInterface $image
376
     * @param string $filePath
377
     * @return bool
378
     */
379
    protected function correctExifRotation($image, $filePath)
380
    {
381
        if (!function_exists('exif_read_data')) {
382
            return false;
383
        }
384
385
        $exif = @exif_read_data($filePath);
386
387
        if ($exif === false) {
388
            return false;
389
        }
390
391
        $orientation = (int)@$exif['Orientation'];
392
393
        if ($orientation < 2 || $orientation > 8) {
394
            return false;
395
        }
396
397
        switch ($orientation) {
398
            case 8:
399
                $image->rotate(-90);
400
                break;
401
            case 3:
402
                $image->rotate(180);
403
                break;
404
            case 6:
405
                $image->rotate(90);
406
                break;
407
        }
408
409
        return true;
410
    }
411
412
    /**
413
     * @param $fileName
414
     * @param $version
415
     * @param $options
416
     * @return bool|\imagine\Image\ManipulatorInterface
417
     */
418
    protected function createScaledImage($fileName, $version, $options)
419
    {
420
        $success = false;
421
        $filePath = $this->getFilePath($fileName);
422
        $image = Image::getImagine()->open($filePath);
423
424
        if ($this->getOption('correct_exif_rotation')) {
425
            $this->correctExifRotation($image, $filePath);
426
        }
427
428
        $maxWidth = $imageWidth = $image->getSize()->getWidth();
429
        $maxHeight = $imageHeight = $image->getSize()->getHeight();
430
431
        if (!empty($options['max_width'])) {
432
            $maxWidth = $options['max_width'];
433
        }
434
435
        if (!empty($options['max_height'])) {
436
            $maxHeight = $options['max_height'];
437
        }
438
439
        $scale = min($maxWidth / $imageWidth, $maxHeight / $imageHeight);
440
441
        if ($scale > 0 && $scale <= 1) {
442
            if (empty($options['crop'])) {
443
                $newWidth = round($imageWidth * $scale);
444
                $newHeight = round($imageHeight * $scale);
445
                $newFileName = substr($fileName, 0, -(strlen($this->_media->file->extension) + 1)) .
446
                    '-' . $newWidth .
447
                    'x' . $newHeight .
448
                    '.' . $this->_media->file->extension;
449
                $newFilePath = $this->getFilePath($newFileName);
450
                $success = $image->thumbnail(new Box($newWidth, $newHeight))
451
                    ->save($newFilePath);
452 View Code Duplication
                if ($success) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
453
                    $this->_meta['versions'][$version] = [
454
                        'url' => $newFileName,
455
                        'width' => $newWidth,
456
                        'height' => $newHeight,
457
                    ];
458
                }
459
            } else {
460
                if (($imageWidth / $imageHeight) >= ($maxWidth / $maxHeight)) {
461
                    $newWidth = round($imageWidth / ($imageHeight / $maxHeight));
462
                    $newHeight = $maxHeight;
463
                } else {
464
                    $newWidth = $maxWidth;
465
                    $newHeight = round($imageHeight / ($imageWidth / $maxWidth));
466
                }
467
                $pointX = abs(round(($newWidth - $maxWidth) / 2));
468
                $pointY = abs(round(($newHeight - $maxHeight) / 2));
469
                $newFileName = substr($fileName, 0, -(strlen($this->_media->file->extension) + 1)) .
470
                    '-' . $maxWidth .
471
                    'x' . $maxHeight .
472
                    '.' . $this->_media->file->extension;
473
                $newFilePath = $this->getFilePath($newFileName);
474
                $success = $image->thumbnail(new Box($newWidth, $newHeight), ManipulatorInterface::THUMBNAIL_OUTBOUND)
475
                    ->crop(new Point($pointX, $pointY), new Box($maxWidth, $maxHeight))
476
                    ->save($newFilePath);
477 View Code Duplication
                if ($success) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
478
                    $this->_meta['versions'][$version] = [
479
                        'url' => $newFileName,
480
                        'width' => $maxWidth,
481
                        'height' => $maxWidth,
482
                    ];
483
                }
484
            }
485
        }
486
487
        return $success;
488
    }
489
490
    /**
491
     * Handle image file.
492
     *
493
     * @param string $fileName
494
     */
495
    protected function handleImageFile($fileName)
496
    {
497
        if ($versions = $this->getOption('versions')) {
498
            foreach ($versions as $version => $options) {
0 ignored issues
show
The expression $versions of type string|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
499
                $this->createScaledImage($fileName, $version, $options);
500
            }
501
        }
502
    }
503
504
    /**
505
     * Set icon url.
506
     *
507
     * @param string $fileName
508
     * @return string
509
     */
510
    protected function setIconUrl($fileName)
511
    {
512
        foreach ($this->fileTypes as $name => $type) {
513
            if (preg_match($type['extensions'], $fileName)) {
514
                if ($name === 'image') {
515
                    return $this->_meta['versions']['thumbnail']['url'];
516
                }
517
518
                return $type['mime_icon'];
519
            }
520
        }
521
522
        return 'img/mime/default.png';
523
    }
524
525
    /**
526
     * Handle uploaded file. If the uploaded file is valid image type, the file will be resize or crop
527
     * based on versions in options.
528
     *
529
     * @param UploadedFile $file
530
     */
531
    protected function handleFileUpload($file)
532
    {
533
        $this->_meta['filename'] = $this->getFileName($file);
534
        $this->_meta['file_size'] = $file->size;
535
        $uploadDir = $this->getUploadPath();
536
        $uploadPath = $this->getUploadPath($this->_meta['filename']);
537
538
        if (!is_dir($uploadDir)) {
539
            FileHelper::createDirectory($uploadDir, $this->getOption('mkdir_mode'));
0 ignored issues
show
$this->getOption('mkdir_mode') is of type string|array|null, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
540
        }
541
542
        if ($file->saveAs($uploadPath)) {
543
            $this->_meta['versions']['full']['url'] = $this->getUserPath()
544
                . $this->getYearMonthPath()
545
                . $this->_meta['filename'];
546
547
            if (preg_match($this->fileTypes['image']['extensions'], $this->_meta['filename'])) {
548
                $image = Image::getImagine()->open($this->getFilePath($this->_meta['versions']['full']['url']));
549
                $this->handleImageFile($this->_meta['versions']['full']['url']);
550
                $this->_meta['versions']['full']['width'] = $image->getSize()->getWidth();
551
                $this->_meta['versions']['full']['height'] = $image->getSize()->getHeight();
552
            }
553
554
            $this->_meta['icon_url'] = $this->setIconUrl($this->_meta['filename']);
555
        }
556
    }
557
558
    /**
559
     * @return \yii\data\Pagination
560
     */
561
    protected function getPages()
562
    {
563
        $query = Media::find()->orderBy(['id' => SORT_DESC]);
564
        $pages = new Pagination([
565
            'totalCount' => $query->count(),
566
            'pageSize' => $this->getOption('files_per_page'),
567
        ]);
568
569
        return $pages;
570
    }
571
572
    /**
573
     * @param $pages Pagination
574
     * @return array
575
     */
576
    protected function getPaging($pages)
577
    {
578
        $currentPage = $pages->getPage();
579
        $perPage = $pages->getPageSize();
580
        $result = [
581
            'next_url' => '',
582
            'current_page' => $currentPage,
583
            'per_page' => $perPage,
584
        ];
585
        $pageCount = $pages->getPageCount();
586
587
        if ($currentPage + 1 < $pageCount) {
588
            if (($page = $currentPage + 1) >= $pageCount - 1) {
589
                $page = $pageCount - 1;
590
            }
591
592
            return [
593
                'next_url' => Url::to(['get-json', 'page' => $page + 1, 'per-page' => $pages->getPageSize()]),
594
                'current_page' => $page,
595
                'per_page' => $perPage,
596
            ];
597
        }
598
599
        return $result;
600
    }
601
602
    /**
603
     * Set options of upload Handler.
604
     *
605
     * @param array $options
606
     */
607
    public function setOptions($options = [])
608
    {
609
        $this->options = [
610
            'script_url' => Yii::$app->request->absoluteUrl,
611
            'upload_dir' => Yii::getAlias('@public/uploads/'),
612
            'upload_url' => Media::getUploadUrl(),
613
            'user_dirs' => true,
614
            'year_month_dirs' => true,
615
            'mkdir_mode' => 0755,
616
            'param_name' => 'files',
617
            'access_control_allow_origin' => '*',
618
            'access_control_allow_credentials' => false,
619
            'correct_exif_rotation' => true,
620
            'pagination_route' => '/media/get-json',
621
            'access_control_allow_methods' => [
622
                'OPTIONS',
623
                'HEAD',
624
                'GET',
625
                'POST',
626
                'PUT',
627
                'PATCH',
628
                'DELETE',
629
            ],
630
            'access_control_allow_headers' => [
631
                'Content-Type',
632
                'Content-Range',
633
                'Content-Disposition',
634
            ],
635
            'versions' => [
636
                'large' => [
637
                    'max_width' => 1024,
638
                    'max_height' => 1024,
639
                ],
640
                'medium' => [
641
                    'max_width' => 300,
642
                    'max_height' => 300,
643
                ],
644
                'thumbnail' => [
645
                    'max_width' => 150,
646
                    'max_height' => 150,
647
                    'crop' => 1,
648
                ],
649
            ],
650
            'files_per_page' => 100,
651
            'print_response' => true,
652
        ];
653
654
        if ($options) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $options of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
655
            $this->options = ArrayHelper::merge($this->options, $options);
656
        }
657
    }
658
659
    /**
660
     * Get all of MediaUploadHandler Options.
661
     */
662
    public function getOptions()
663
    {
664
        return $this->options;
665
    }
666
667
    /**
668
     * Get single option of MediaUploadHandler.
669
     * Return string or array if option exist, or return null if not exist.
670
     *
671
     * @param string $id
672
     * @return string|array|null
673
     */
674
    public function getOption($id)
675
    {
676
        if (isset($this->options[$id])) {
677
            return $this->options[$id];
678
        }
679
680
        return null;
681
    }
682
683
    /**
684
     * Generate response based on Media primary key.
685
     *
686
     * @param Media $media
687
     * @return array
688
     */
689
    public function generateResponse($media)
690
    {
691
        $metadata = $media->getMeta('metadata');
692
        $response = ArrayHelper::merge(ArrayHelper::toArray($media), $metadata);
693
        $response['date_formatted'] = Yii::$app->formatter->asDatetime($media->date);
694
        $response['readable_size'] = Yii::$app->formatter->asShortSize($metadata['file_size']);
695
        $response['delete_url'] = Url::to(['/media/ajax-delete', 'id' => $media->id, 'delete' => '1']);
696
        $response['update_url'] = Url::to(['/media/update', 'id' => $media->id]);
697
        $response['view_url'] = $media->getUrl();
698
699
        if (preg_match('/^image\//', $media->mime_type)) {
700
            $response['type'] = 'image';
701
            $response['icon_url'] = $this->getOption('upload_url') . $metadata['icon_url'];
702
        } else {
703
            $response['icon_url'] = Yii::getAlias('@web') . '/' . $metadata['icon_url'];
704
            if (preg_match('/^video\//', $media->mime_type)) {
705
                $response['type'] = 'video';
706
            } elseif (preg_match('/^audio\//', $media->mime_type)) {
707
                $response['type'] = 'audio';
708
            } else {
709
                $response['type'] = 'file';
710
            }
711
        }
712
713
        return $response;
714
    }
715
716
    /**
717
     * Set response.
718
     *
719
     * @param array $response
720
     */
721
    public function setResponse($response)
722
    {
723
        $this->response = $response;
724
    }
725
726
727
    /**
728
     * Generate response in the form of a string of json.
729
     *
730
     * @param bool $printResponse
731
     * @return array
732
     */
733
    public function getResponse($printResponse = self::PRINT_RESPONSE)
734
    {
735
        if ($printResponse) {
736
            $this->head();
737
            $content = Json::encode($this->response);
738
            $redirect = stripslashes(Yii::$app->request->getQueryParam('redirect'));
739
            if ($redirect) {
740
                $this->setHeader('Location', sprintf($redirect, rawurlencode($content)));
741
742
                return null;
743
            }
744
            echo $content;
745
        }
746
747
        return $this->response;
748
    }
749
750
    /**
751
     * Set response header.
752
     */
753
    public function head()
754
    {
755
        $this->setHeader('Pragma', 'no-cache');
756
        $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
757
        $this->setHeader('Content-Disposition', 'inline; filename="files.json"');
758
        // Prevent Internet Explorer from MIME-sniffing the content-type:
759
        $this->setHeader('X-Content-Type-Options', 'nosniff');
760
        if ($this->options['access_control_allow_origin']) {
761
            $this->sendAccessControlHeaders();
762
        }
763
        $this->sendContentTypeHeader();
764
    }
765
766
    /**
767
     * Get media files.
768
     *
769
     * @param int $id
770
     * @param bool $printResponse
771
     * @return array
772
     */
773
    public function get($id = null, $printResponse = self::PRINT_RESPONSE)
774
    {
775
        $content = [];
776
777
        if ($id && $media = $this->findMedia($id)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
778
            $response = [
779
                $this->getSingularParamName() => $this->generateResponse($media),
0 ignored issues
show
$media is of type array|boolean, but the function expects a object<common\models\Media>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
780
            ];
781
        } else {
782
            $query = Media::find()->orderBy(['id' => SORT_DESC]);
783
            $pages = $this->getPages();
784
785
            $query->andFilterWhere(['like', 'post_id', Yii::$app->request->get('post')])
786
                ->andFilterWhere(['like', 'mime_type', Yii::$app->request->get('type')])
787
                ->andFilterWhere(['like', 'title', Yii::$app->request->get('keyword')])
788
                ->orFilterWhere(['like', 'content', Yii::$app->request->get('keyword')]);
789
790
            if ($models = $query->offset($pages->offset)->limit($pages->limit)->all()) {
791
                foreach ($models as $media) {
792
                    /* @var $media Media */
793
                    $content[] = $this->generateResponse($media);
794
                }
795
            }
796
            $response = [
797
                $this->getOption('param_name') => $content,
798
                'paging' => $this->getPaging($pages),
799
            ];
800
        }
801
802
        $this->setResponse($response);
803
804
        return $this->getResponse($printResponse);
805
    }
806
807
    /**
808
     * Upload file to server.
809
     *
810
     * @param bool $printResponse
811
     * @return array
812
     */
813
    public function post($printResponse = self::PRINT_RESPONSE)
814
    {
815
        if (Yii::$app->request->get('delete') && $id = Yii::$app->request->get('id')) {
816
            return $this->delete($id, $printResponse);
817
        }
818
819
        $response = [];
820
        $this->_media = new Media();
821
        $this->_media->file = UploadedFile::getInstance($this->_media, 'file');
822
823
        if ($this->_media->file !== null && $this->_media->validate(['file'])) {
824
            if ($postId = Yii::$app->request->get('post')) {
825
                $post = $this->findPost($postId);
826
                $this->_media->post_id = $post->id;
827
            }
828
829
            $this->_media->title = $this->_media->file->baseName;
830
            $this->_media->mime_type = $this->_media->file->type;
831
            $this->handleFileUpload($this->_media->file);
832
833
            if ($this->_media->save(false)) {
834
                if ($this->_media->setMeta('metadata', $this->_meta)) {
835
                    $response = $this->generateResponse($this->_media);
836
                }
837
            }
838
        } else {
839
            $response = [
840
                'error' => $this->_media->getFirstError('file'),
841
                'filename' => isset($this->_media->file->name) ? $this->_media->file->name : null,
842
                'file_size' => isset($this->_media->file->size) ? $this->_media->file->size : null,
843
            ];
844
        }
845
846
        $this->setResponse([
847
            $this->getOption('param_name') => [$response],
848
        ]);
849
850
        return $this->getResponse($printResponse);
851
    }
852
853
    /**
854
     * Delete files based on media primary key
855
     *
856
     * @param int $id Primary key of Media
857
     * @param bool $printResponse
858
     * @return array
859
     * @throws \Exception
860
     */
861
    public function delete($id, $printResponse = self::PRINT_RESPONSE)
862
    {
863
        $success = true;
864
        $response = [];
865
        $media = $this->findMedia($id);
866
        $metadata = $media->getMeta('metadata');
0 ignored issues
show
The method getMeta cannot be called on $media (of type array|boolean|null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
867
868
        if ($media->delete()) {
0 ignored issues
show
The method delete cannot be called on $media (of type array|boolean|null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
869
            foreach ($metadata['versions'] as $version) {
870
                $filePath = $this->getFilePath($version['url']);
871
                $success = is_file($filePath) && unlink($filePath);
872
            }
873
            $response[$metadata['filename']] = $success;
874
        }
875
876
        $this->setResponse($response);
877
878
        return $this->getResponse($printResponse);
879
    }
880
}
881