File::insertTempFile()   A
last analyzed

Complexity

Conditions 4
Paths 6

Size

Total Lines 22
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
eloc 16
c 0
b 0
f 0
dl 0
loc 22
ccs 0
cts 14
cp 0
rs 9.7333
cc 4
nc 6
nop 1
crap 20
1
<?php
2
/**
3
 * Tool file for the field type `File`.
4
 *
5
 * @package App
6
 *
7
 * @copyright YetiForce S.A.
8
 * @license   YetiForce Public License 6.5 (licenses/LicenseEN.txt or yetiforce.com)
9
 * @author    Mariusz Krzaczkowski <[email protected]>
10
 * @author    Radosław Skrzypczak <[email protected]>
11
 */
12
13
namespace App\Fields;
14
15
use App\Log;
16
17
/**
18
 * Tool class for the field type `File`.
19
 */
20
class File
21
{
22
	/** @var string Temporary table name. */
23
	public const TABLE_NAME_TEMP = 'u_#__file_upload_temp';
24
25
	/**
26
	 * Allowed formats.
27
	 *
28
	 * @var array
29
	 */
30
	public static $allowedFormats = ['image' => ['jpeg', 'png', 'jpg', 'pjpeg', 'x-png', 'gif', 'bmp', 'x-ms-bmp', 'webp']];
31
32
	/**
33
	 * Mime types.
34
	 *
35
	 * @var string[]
36
	 */
37
	private static $mimeTypes;
38
39
	/**
40
	 * What file types to validate by php injection.
41
	 *
42
	 * @var string[]
43
	 */
44
	private static $phpInjection = ['image'];
45
46
	/**
47
	 * Directory path used for temporary files.
48
	 *
49
	 * @var string
50
	 */
51
	private static $tmpPath;
52
53
	/**
54
	 * File name.
55
	 *
56
	 * @var string
57
	 */
58
	private $name;
59
60
	/**
61
	 * File path.
62
	 *
63
	 * @var string
64
	 */
65
	private $path;
66
67
	/**
68
	 * File extension.
69
	 *
70
	 * @var string
71
	 */
72
	private $ext;
73
74
	/**
75
	 * File mime type.
76
	 *
77
	 * @var string
78
	 */
79
	private $mimeType;
80
81
	/**
82
	 * File short mime type.
83
	 *
84
	 * @var string
85
	 */
86
	private $mimeShortType;
87
88
	/**
89
	 * Size.
90
	 *
91
	 * @var int
92
	 */
93
	private $size;
94
95
	/**
96
	 * File content.
97
	 *
98
	 * @var string
99
	 */
100
	private $content;
101
102
	/**
103
	 * Error code.
104
	 *
105
	 * @var bool|int
106
	 */
107
	private $error = false;
108
109
	/**
110
	 * Last validate error.
111
	 *
112
	 * @var string
113
	 */
114
	public $validateError = '';
115 16
116
	/**
117 16
	 * Validate all files by code injection.
118 16
	 *
119 16
	 * @var bool
120
	 */
121 16
	private $validateAllCodeInjection = false;
122
123
	/**
124
	 * Load file instance from file info.
125
	 *
126
	 * @param array $fileInfo
127
	 *
128
	 * @return \self
0 ignored issues
show
Bug introduced by
The type self was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
129
	 */
130
	public static function loadFromInfo($fileInfo)
131 1
	{
132
		$instance = new self();
133 1
		foreach ($fileInfo as $key => $value) {
134 1
			$instance->{$key} = $fileInfo[$key];
135 1
		}
136 1
		if (isset($instance->name)) {
137 1
			$instance->name = trim(\App\Purifier::purify($instance->name));
138 1
		}
139
		return $instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $instance returns the type App\Fields\File which is incompatible with the documented return type self.
Loading history...
140
	}
141
142
	/**
143
	 * Load file instance from request.
144
	 *
145
	 * @param array $file
146
	 *
147
	 * @return \self
148 13
	 */
149
	public static function loadFromRequest($file)
150 13
	{
151 13
		$instance = new self();
152 13
		$instance->name = trim(\App\Purifier::purify($file['name']));
153 13
		$instance->path = $file['tmp_name'];
154
		$instance->size = $file['size'];
155
		$instance->error = $file['error'];
156
		return $instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $instance returns the type App\Fields\File which is incompatible with the documented return type self.
Loading history...
157
	}
158
159
	/**
160
	 * Load file instance from file path.
161
	 *
162
	 * @param string $path
163
	 *
164
	 * @return \self
165 1
	 */
166
	public static function loadFromPath(string $path)
167 1
	{
168 1
		$instance = new self();
169
		$instance->name = trim(\App\Purifier::purify(basename($path)));
170
		$instance->path = $path;
171
		return $instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $instance returns the type App\Fields\File which is incompatible with the documented return type self.
Loading history...
172
	}
173
174 1
	/**
175 1
	 * Load file instance from base string.
176
	 *
177 1
	 * @param string $contents
178 1
	 * @param array  $param
179 1
	 *
180
	 * @return \self|null
181
	 */
182
	public static function loadFromBase(string $contents, array $param = []): ?self
183 1
	{
184 1
		$result = explode(',', $contents, 2);
185 1
		$contentType = $isBase64 = false;
186 1
		if (2 === \count($result)) {
187 1
			[$metadata, $data] = $result;
188
			foreach (explode(';', $metadata) as $cur) {
189
				if ('base64' === $cur) {
190 1
					$isBase64 = true;
191 1
				} elseif ('data:' === substr($cur, 0, 5)) {
192
					$contentType = str_replace('data:', '', $cur);
193 1
				}
194
			}
195
		} else {
196
			$data = $result[0];
197
		}
198
		$data = rawurldecode($data);
199
		$rawData = $isBase64 ? base64_decode($data) : $data;
200
		if (\strlen($rawData) < 12) {
201
			Log::error('Incorrect content value: ' . $contents, __CLASS__);
202
			return null;
203
		}
204 1
		return static::loadFromContent($rawData, false, array_merge($param, ['mimeType' => $contentType]));
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $name of App\Fields\File::loadFromContent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

204
		return static::loadFromContent($rawData, /** @scrutinizer ignore-type */ false, array_merge($param, ['mimeType' => $contentType]));
Loading history...
Bug Best Practice introduced by
The expression return static::loadFromC...ype' => $contentType))) could return the type boolean which is incompatible with the type-hinted return App\Fields\File|null. Consider adding an additional type-check to rule them out.
Loading history...
205
	}
206 1
207
	/**
208
	 * Load file instance from content.
209
	 *
210 1
	 * @param string   $contents
211
	 * @param string   $name
212
	 * @param string[] $param
213
	 *
214 1
	 * @return bool|\self
215 1
	 */
216
	public static function loadFromContent(string $contents, $name = false, array $param = [])
217
	{
218
		if (empty($contents)) {
219 1
			Log::warning("Empty content, unable to create file: $name | Size: " . \strlen($contents), __CLASS__);
220
			return false;
221
		}
222
		static::initMimeTypes();
223
		$extension = 'tmp';
224 1
		if (empty($name)) {
225
			if (!empty($param['mimeType']) && !($extension = array_search($param['mimeType'], self::$mimeTypes))) {
226
				[, $extension] = explode('/', $param['mimeType']);
227
			}
228 1
			$name = uniqid() . '.' . $extension;
229
		} elseif ('tmp' === $extension) {
0 ignored issues
show
introduced by
The condition 'tmp' === $extension is always true.
Loading history...
230
			if (($fileExt = pathinfo($name, PATHINFO_EXTENSION)) && isset(self::$mimeTypes[$fileExt])) {
231
				$extension = $fileExt;
232
				if (isset($param['mimeType']) && $param['mimeType'] !== self::$mimeTypes[$fileExt]) {
233
					Log::error("Invalid file content type File: $name  | {$param['mimeType']} <> " . self::$mimeTypes[$fileExt], __CLASS__);
234
					return false;
235
				}
236 17
			} elseif (!empty($param['mimeType']) && !($extension = array_search($param['mimeType'], self::$mimeTypes))) {
237
				[, $extension] = explode('/', $param['mimeType']);
238 17
			}
239 14
		}
240
		$path = tempnam(static::getTmpPath(), 'YFF');
241 17
		if (!file_put_contents($path, $contents)) {
242
			Log::error("Error while saving the file: $path | Size: " . \strlen($contents), __CLASS__);
243
			return false;
244
		}
245
		if (mb_strlen($name) > 180) {
246
			$name = \App\TextUtils::textTruncate($name, 180, false) . '_' . uniqid() . ".$extension";
247
		}
248
		$instance = new self();
249
		$instance->name = trim(\App\Purifier::purify($name));
250
		$instance->path = $path;
251
		$instance->ext = $extension;
0 ignored issues
show
Documentation Bug introduced by
It seems like $extension can also be of type array. However, the property $ext is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

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

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
252
		foreach ($param as $key => $value) {
253
			$instance->{$key} = $value;
254
		}
255
		return $instance;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $instance returns the type App\Fields\File which is incompatible with the documented return type boolean|self.
Loading history...
256
	}
257
258
	/**
259 3
	 * Load file instance from url.
260
	 *
261 3
	 * @param string   $url
262
	 * @param string[] $param
263
	 *
264
	 * @return self|bool
265
	 */
266
	public static function loadFromUrl($url, $param = [])
267
	{
268
		if (empty($url)) {
269 17
			Log::warning('No url: ' . $url, __CLASS__);
270
			return false;
271 17
		}
272 17
		if (!\App\RequestUtil::isNetConnection()) {
273 17
			return false;
274 17
		}
275 17
		try {
276
			\App\Log::beginProfile("GET|File::loadFromUrl|{$url}", __NAMESPACE__);
277
			$response = (new \GuzzleHttp\Client(\App\RequestHttp::getOptions()))->request('GET', $url, ['timeout' => 5, 'connect_timeout' => 1]);
278
			\App\Log::endProfile("GET|File::loadFromUrl|{$url}", __NAMESPACE__);
279
			if (200 !== $response->getStatusCode()) {
280
				Log::warning('Error when downloading content: ' . $url . ' | Status code: ' . $response->getStatusCode(), __CLASS__);
281
				return false;
282
			}
283
			$contents = $response->getBody()->getContents();
284
			$param['mimeType'] = explode(';', $response->getHeaderLine('Content-Type'))[0];
285
			$param['size'] = \strlen($contents);
286 17
		} catch (\Throwable $exc) {
287
			Log::warning('Error when downloading content: ' . $url . ' | ' . $exc->getMessage(), __CLASS__);
288
			return false;
289
		}
290
		if (empty($contents)) {
291
			Log::warning('Url does not contain content: ' . $url, __CLASS__);
292
			return false;
293
		}
294
		return static::loadFromContent($contents, static::sanitizeFileNameFromUrl($url), $param);
295
	}
296 17
297
	/**
298 17
	 * Get size.
299 17
	 *
300
	 * @return int
301 17
	 */
302
	public function getSize()
303
	{
304
		if (empty($this->size)) {
305
			$this->size = filesize($this->path);
306
		}
307
		return $this->size;
308
	}
309
310
	/**
311 17
	 * Function to sanitize the upload file name when the file name is detected to have bad extensions.
312
	 *
313 17
	 * @return string
314 14
	 */
315
	public function getSanitizeName()
316 17
	{
317 17
		return static::sanitizeUploadFileName($this->name);
318 17
	}
319
320
	/**
321
	 * Get file name.
322
	 *
323
	 * @param bool $decode
324
	 *
325
	 * @return string
326
	 */
327
	public function getName(bool $decode = false)
328 14
	{
329
		return $decode ? \App\Purifier::decodeHtml($this->name) : $this->name;
330 14
	}
331
332
	/**
333
	 * Get mime type.
334
	 *
335
	 * @return string
336
	 */
337
	public function getMimeType()
338
	{
339
		if (empty($this->mimeType)) {
340
			static::initMimeTypes();
341
			$extension = $this->getExtension(true);
342
			if (isset(static::$mimeTypes[$extension])) {
0 ignored issues
show
Bug introduced by
Since $mimeTypes is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $mimeTypes to at least protected.
Loading history...
343
				$this->mimeType = static::$mimeTypes[$extension];
344
			} elseif (\function_exists('mime_content_type')) {
345
				$this->mimeType = mime_content_type($this->path);
346
			} elseif (\function_exists('finfo_open')) {
347
				$finfo = finfo_open(FILEINFO_MIME);
348
				$this->mimeType = finfo_file($finfo, $this->path);
349
				finfo_close($finfo);
350
			} else {
351
				$this->mimeType = 'application/octet-stream';
352
			}
353
		}
354
		return $this->mimeType;
355
	}
356
357
	/**
358
	 * Get short mime type.
359
	 *
360
	 * @param int $type 0 or 1
361 17
	 *
362
	 * @return string
363 17
	 */
364
	public function getShortMimeType($type = 1)
365 17
	{
366
		if (empty($this->mimeShortType)) {
367
			$this->mimeShortType = explode('/', $this->getMimeType());
0 ignored issues
show
Documentation Bug introduced by
It seems like explode('/', $this->getMimeType()) of type string[] is incompatible with the declared type string of property $mimeShortType.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
368 17
		}
369 17
		return $this->mimeShortType[$type];
370 17
	}
371 17
372 14
	/**
373
	 * Get file extension.
374 2
	 *
375 2
	 * @param mixed $fromName
376 2
	 *
377 2
	 * @return string
378 2
	 */
379
	public function getExtension($fromName = false)
380
	{
381
		if (isset($this->ext)) {
382
			return $this->ext;
383 2
		}
384 2
		if ($fromName) {
385
			$extension = explode('.', $this->name);
386 17
			return $this->ext = strtolower(array_pop($extension));
387
		}
388
		return $this->ext = strtolower(pathinfo($this->path, PATHINFO_EXTENSION));
389
	}
390
391
	/**
392
	 * Get file path.
393
	 *
394
	 * @return string
395
	 */
396 14
	public function getPath(): string
397
	{
398 14
		return $this->path;
399 14
	}
400
401
	/**
402
	 * Get file encoding.
403
	 *
404
	 * @param array|null $list
405
	 *
406
	 * @return string
407
	 */
408
	public function getEncoding(?array $list = null): string
409
	{
410
		return \strtoupper(mb_detect_encoding($this->getContents(), ($list ?? mb_list_encodings()), true));
411
	}
412
413
	/**
414
	 * Get directory path.
415
	 *
416
	 * @return string
417
	 */
418
	public function getDirectoryPath()
419
	{
420 14
		return pathinfo($this->getPath(), PATHINFO_DIRNAME);
0 ignored issues
show
Bug Best Practice introduced by
The expression return pathinfo($this->g...ields\PATHINFO_DIRNAME) also could return the type array which is incompatible with the documented return type string.
Loading history...
421
	}
422 14
423 14
	/**
424
	 * Validate whether the file is safe.
425
	 *
426
	 * @param string|null $type
427
	 *
428
	 * @throws \Exception
429
	 *
430
	 * @return bool
431
	 */
432
	public function validate(?string $type = null): bool
433
	{
434 14
		$return = true;
435 14
		try {
436 14
			if ($type && $this->getShortMimeType(0) !== $type) {
437 14
				throw new \App\Exceptions\DangerousFile('ERR_FILE_ILLEGAL_FORMAT');
438
			}
439
			$this->checkFile();
440 14
			if (!empty($this->validateAllowedFormat)) {
441
				$this->validateFormat();
442
			}
443
			$this->validateCodeInjection();
444
			if (($type && 'image' === $type) || 'image' === $this->getShortMimeType(0)) {
445
				$this->validateImage();
446
			}
447
		} catch (\Exception $e) {
448 17
			$return = false;
449
			$message = $e->getMessage();
450 17
			if (false === strpos($message, '||')) {
451
				$message = \App\Language::translateSingleMod($message, 'Other.Exceptions');
452
			} else {
453 17
				$params = explode('||', $message);
454
				$message = \call_user_func_array('vsprintf', [\App\Language::translateSingleMod(array_shift($params), 'Other.Exceptions'), $params]);
455
			}
456 17
			$this->validateError = $message;
457
			Log::error("Error during file validation: {$this->getName()} | Size: {$this->getSize()}\n {$e->__toString()}", __CLASS__);
458
		}
459 17
		return $return;
460
	}
461
462
	/**
463
	 * Validate and secure the file.
464
	 *
465
	 * @param string|null $type
466 17
	 *
467
	 * @return bool
468 17
	 */
469
	public function validateAndSecure(?string $type = null): bool
470
	{
471 17
		if ($this->validate($type)) {
472
			return true;
473
		}
474
		$reValidate = false;
475
		if (static::secureFile($this)) {
476
			$this->size = filesize($this->path);
477
			$this->content = file_get_contents($this->path);
478 14
			$reValidate = true;
479
		}
480 14
		if ($reValidate && $this->validate($type)) {
481
			return true;
482
		}
483 14
		return false;
484
	}
485
486 14
	/**
487 14
	 * Validate image content.
488
	 *
489
	 * @throws \App\Exceptions\DangerousFile
490 14
	 *
491
	 * @return bool
492
	 */
493
	public function validateImageContent(): bool
494
	{
495
		$returnVal = false;
496
		if (\extension_loaded('imagick')) {
497 17
			try {
498
				$img = new \imagick($this->path);
499 17
				$returnVal = $img->valid();
500 17
				$img->clear();
501 17
				$img->destroy();
502 17
			} catch (\ImagickException $e) {
503 17
				$this->validateError = $e->getMessage();
504 17
				$returnVal = false;
505
			}
506 2
		} else {
507
			$img = \imagecreatefromstring($this->getContents());
508
			if (false !== $img) {
509 17
				$returnVal = true;
510
				\imagedestroy($img);
511
			}
512
		}
513
		return $returnVal;
514
	}
515
516 2
	/**
517
	 * Basic check file.
518 2
	 *
519
	 * @throws \Exception
520
	 */
521
	private function checkFile()
522 2
	{
523 2
		if (false !== $this->error && UPLOAD_ERR_OK != $this->error) {
524 2
			throw new \App\Exceptions\DangerousFile('ERR_FILE_ERROR_REQUEST||' . self::getErrorMessage($this->error));
525
		}
526
		if (empty($this->name)) {
527
			throw new \App\Exceptions\DangerousFile('ERR_FILE_EMPTY_NAME');
528
		}
529
		if (!$this->validateInjection($this->name)) {
530
			throw new \App\Exceptions\DangerousFile('ERR_FILE_ILLEGAL_NAME');
531
		}
532
		if (0 === $this->getSize()) {
533 2
			throw new \App\Exceptions\DangerousFile('ERR_FILE_WRONG_SIZE');
534
		}
535
	}
536
537
	/**
538
	 * Validate format.
539
	 *
540
	 * @throws \Exception
541
	 */
542
	private function validateFormat()
543
	{
544
		if ($this->validateAllowedFormat !== $this->getShortMimeType(0)) {
0 ignored issues
show
Bug Best Practice introduced by
The property validateAllowedFormat does not exist on App\Fields\File. Did you maybe forget to declare it?
Loading history...
545
			throw new \App\Exceptions\DangerousFile('ERR_FILE_ILLEGAL_MIME_TYPE');
546
		}
547
		if (isset(self::$allowedFormats[$this->validateAllowedFormat]) && !\in_array($this->getShortMimeType(1), self::$allowedFormats[$this->validateAllowedFormat])) {
548
			throw new \App\Exceptions\DangerousFile('ERR_FILE_ILLEGAL_FORMAT');
549
		}
550
	}
551
552
	/**
553
	 * Validate image.
554
	 *
555
	 * @throws \Exception
556
	 */
557
	private function validateImage()
558
	{
559
		if (!getimagesize($this->path)) {
560
			throw new \App\Exceptions\DangerousFile('ERR_FILE_WRONG_IMAGE');
561
		}
562
		if (preg_match('[\x01-\x08\x0c-\x1f]', $this->getContents())) {
563
			throw new \App\Exceptions\DangerousFile('ERR_FILE_WRONG_IMAGE');
564
		}
565
		$this->validateCodeInjectionInMetadata();
566
		if (!$this->validateImageContent()) {
567
			throw new \App\Exceptions\DangerousFile('ERR_FILE_WRONG_IMAGE ||' . $this->validateError);
568
		}
569
	}
570
571
	/**
572
	 * Validate code injection.
573
	 *
574
	 * @throws \Exception
575
	 */
576
	private function validateCodeInjection()
577
	{
578
		$shortMimeType = $this->getShortMimeType(0);
579
		if ($this->validateAllCodeInjection || \in_array($shortMimeType, static::$phpInjection)) {
0 ignored issues
show
Bug introduced by
Since $phpInjection is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $phpInjection to at least protected.
Loading history...
580
			$contents = $this->getContents();
581
			if ((1 === preg_match('/(<\?php?(.*?))/si', $contents)
582
			|| false !== stripos($contents, '<?=')
583
			|| false !== stripos($contents, '<? ')) && $this->searchCodeInjection()
584
			) {
585
				throw new \App\Exceptions\DangerousFile('ERR_FILE_CODE_INJECTION');
586
			}
587
		}
588 2
	}
589
590
	/**
591
	 * Search code injection in content.
592
	 *
593
	 * @return bool
594
	 */
595
	private function searchCodeInjection(): bool
596
	{
597
		if (!\function_exists('token_get_all')) {
598
			return true;
599
		}
600
		try {
601
			$tokens = token_get_all($this->getContents(), TOKEN_PARSE);
602
			foreach ($tokens as $token) {
603
				switch (\is_array($token) ? $token[0] : $token) {
604
						case T_COMMENT:
605
						case T_DOC_COMMENT:
606
						case T_WHITESPACE:
607 14
						case T_CURLY_OPEN:
608
						case T_OPEN_TAG:
609
						case T_CLOSE_TAG:
610 14
						case T_INLINE_HTML:
611 14
						case T_DOLLAR_OPEN_CURLY_BRACES:
612 7
							continue 2;
613
						case T_DOUBLE_COLON:
614 7
						case T_ABSTRACT:
615
						case T_ARRAY:
616 7
						case T_AS:
617 7
						case T_BREAK:
618 7
						case T_CALLABLE:
619
						case T_CASE:
620
						case T_CATCH:
621
						case T_CLASS:
622
						case T_CLONE:
623 14
						case T_CONTINUE:
624
						case T_DEFAULT:
625
						case T_ECHO:
626
						case T_ELSE:
627
						case T_ELSEIF:
628
						case T_EMPTY:
629
						case T_ENDIF:
630
						case T_ENDSWITCH:
631
						case T_ENDWHILE:
632 7
						case T_EXIT:
633
						case T_EXTENDS:
634 7
						case T_FINAL:
635 7
						case T_FINALLY:
636 7
						case T_FOREACH:
637
						case T_FUNCTION:
638
						case T_GLOBAL:
639
						case T_IF:
640
						case T_IMPLEMENTS:
641 7
						case T_INCLUDE:
642
						case T_INCLUDE_ONCE:
643
						case T_INSTANCEOF:
644
						case T_INSTEADOF:
645 7
						case T_INTERFACE:
646
						case T_ISSET:
647
						case T_LOGICAL_AND:
648
						case T_LOGICAL_OR:
649
						case T_LOGICAL_XOR:
650
						case T_NAMESPACE:
651
						case T_NEW:
652
						case T_PRIVATE:
653 17
						case T_PROTECTED:
654
						case T_PUBLIC:
655 17
						case T_REQUIRE:
656 14
						case T_REQUIRE_ONCE:
657
						case T_RETURN:
658 17
						case T_STATIC:
659
						case T_THROW:
660
						case T_TRAIT:
661
						case T_TRY:
662
						case T_UNSET:
663
						case T_USE:
664
						case T_VAR:
665
						case T_WHILE:
666
						case T_YIELD:
667
							return true;
668 1
						default:
669
							$text = \is_array($token) ? $token[1] : $token;
670 1
							if (\function_exists($text) || \defined($text)) {
671
								return true;
672
							}
673 1
					}
674
			}
675 1
		} catch (\Throwable $e) {
676 1
			Log::warning($e->getMessage(), __METHOD__);
677
		}
678
		return false;
679
	}
680
681
	/**
682
	 * Validate code injection in metadata.
683
	 *
684 1
	 * @throws \App\Exceptions\DangerousFile
685
	 */
686 1
	private function validateCodeInjectionInMetadata()
687
	{
688
		if (\extension_loaded('imagick')) {
689 1
			try {
690
				$img = new \imagick($this->path);
691
				$this->validateInjection($img->getImageProperties());
692
			} catch (\Throwable $e) {
693
				throw new \App\Exceptions\DangerousFile('ERR_FILE_CODE_INJECTION', $e->getCode(), $e);
694
			}
695
		} elseif (
696
			\function_exists('exif_read_data')
697
			&& \in_array($this->getMimeType(), ['image/jpeg', 'image/tiff'])
698
			&& \in_array(exif_imagetype($this->path), [IMAGETYPE_JPEG, IMAGETYPE_TIFF_II, IMAGETYPE_TIFF_MM])
699
		) {
700 13
			$imageSize = getimagesize($this->path, $imageInfo);
701
			try {
702 13
				if (
703 13
					$imageSize
0 ignored issues
show
Bug Best Practice introduced by
The expression $imageSize 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...
704 13
					&& (empty($imageInfo['APP1']) || 0 === strpos($imageInfo['APP1'], 'Exif'))
705
					&& ($exifData = exif_read_data($this->path)) && !$this->validateInjection($exifData)
706
				) {
707 13
					throw new \App\Exceptions\DangerousFile('ERR_FILE_CODE_INJECTION');
708
				}
709
			} catch (\Throwable $e) {
710
				throw new \App\Exceptions\DangerousFile('ERR_FILE_CODE_INJECTION', $e->getCode(), $e);
711
			}
712
		}
713
	}
714
715
	/**
716
	 * Validate injection.
717
	 *
718
	 * @param string|array $data
719
	 *
720 2
	 * @return bool
721
	 */
722 2
	private function validateInjection($data): bool
723 2
	{
724
		$return = true;
725 2
		if (\is_array($data)) {
726 2
			foreach ($data as $value) {
727
				if (!$this->validateInjection($value)) {
728 2
					return false;
729 2
				}
730 2
			}
731 2
		} else {
732
			if (1 === preg_match('/(<\?php?(.*?))/i', $data) || false !== stripos($data, '<?=') || false !== stripos($data, '<? ')) {
733
				$return = false;
734
			} else {
735
				\App\Purifier::purifyHtmlEventAttributes($data);
736 2
			}
737 2
		}
738
		return $return;
739
	}
740 2
741
	/**
742
	 * Get file ontent.
743
	 *
744
	 * @return string
745
	 */
746
	public function getContents()
747
	{
748
		if (empty($this->content)) {
749
			$this->content = file_get_contents($this->path);
750 1
		}
751
		return $this->content;
752 1
	}
753 1
754
	/**
755
	 * Move file.
756
	 *
757
	 * @param string $target
758
	 *
759
	 * @return bool
760
	 */
761 3
	public function moveFile($target)
762
	{
763 3
		if (is_uploaded_file($this->path)) {
764 2
			$uploadStatus = move_uploaded_file($this->path, $target);
765
		} else {
766 1
			$uploadStatus = rename($this->path, $target);
767 1
		}
768
		$this->path = $target;
769
		return $uploadStatus;
770
	}
771
772 1
	/**
773 1
	 * Delete file.
774 1
	 *
775 1
	 * @return bool
776
	 */
777
	public function delete()
778
	{
779
		if (file_exists($this->path)) {
780 1
			return unlink($this->path);
781
		}
782
		return false;
783
	}
784
785
	/**
786 17
	 * Generate file hash.
787
	 *
788 17
	 * @param bool   $checkInAttachments
789 1
	 * @param string $uploadFilePath
790
	 *
791 17
	 * @return string File hash sha256
792
	 */
793
	public function generateHash(bool $checkInAttachments = false, string $uploadFilePath = '')
794
	{
795
		if ($checkInAttachments) {
796
			$hash = hash('sha1', $this->getContents()) . \App\Encryption::generatePassword(10);
797
			if ($uploadFilePath && file_exists($uploadFilePath . $hash)) {
798
				$hash = $this->generateHash($checkInAttachments);
799
			}
800
			return $hash;
801
		}
802
		return hash('sha256', $this->getContents() . \App\Encryption::generatePassword(10));
803
	}
804
805
	/**
806
	 * Function to sanitize the upload file name when the file name is detected to have bad extensions.
807
	 *
808
	 * @param string      $fileName          File name to be sanitized
809
	 * @param bool|string $badFileExtensions
810
	 *
811
	 * @return string
812
	 */
813
	public static function sanitizeUploadFileName($fileName, $badFileExtensions = false)
814
	{
815
		if (!$badFileExtensions) {
816
			$badFileExtensions = \App\Config::main('upload_badext');
817
		}
818
		$fileName = preg_replace('/\s+/', '_', \App\Utils::sanitizeSpecialChars($fileName));
819
		$fileName = rtrim($fileName, '\\/<>?*:"<>|');
820
821
		$fileNameParts = explode('.', $fileName);
822
		$badExtensionFound = false;
823
		foreach ($fileNameParts as $key => &$partOfFileName) {
824
			if (\in_array(strtolower($partOfFileName), $badFileExtensions)) {
825
				$badExtensionFound = true;
826
				$fileNameParts[$key] = $partOfFileName;
827
			}
828
		}
829
		$newFileName = implode('.', $fileNameParts);
830
		if ($badExtensionFound) {
831
			$newFileName .= '.txt';
832
		}
833
		return $newFileName;
834
	}
835
836
	/**
837
	 * Function to get base name of file.
838
	 *
839
	 * @param string $url
840
	 *
841
	 * @return string
842
	 */
843
	public static function sanitizeFileNameFromUrl($url)
844
	{
845
		$partsUrl = parse_url($url);
846
		return static::sanitizeUploadFileName(basename($partsUrl['path']));
847
	}
848
849
	/**
850
	 * Get temporary directory path.
851
	 *
852
	 * @return string
853
	 */
854
	public static function getTmpPath()
855
	{
856
		if (isset(self::$tmpPath)) {
857
			return self::$tmpPath;
858
		}
859
		$hash = hash('crc32', ROOT_DIRECTORY);
860
		if (!empty(ini_get('upload_tmp_dir')) && is_writable(ini_get('upload_tmp_dir'))) {
861
			self::$tmpPath = ini_get('upload_tmp_dir') . \DIRECTORY_SEPARATOR . 'YetiForceTemp' . $hash . \DIRECTORY_SEPARATOR;
862
			if (!is_dir(self::$tmpPath)) {
863
				mkdir(self::$tmpPath, 0755);
864 1
			}
865
		} elseif (is_writable(sys_get_temp_dir())) {
866 1
			self::$tmpPath = sys_get_temp_dir() . \DIRECTORY_SEPARATOR . 'YetiForceTemp' . $hash . \DIRECTORY_SEPARATOR;
867 1
			if (!is_dir(self::$tmpPath)) {
868
				mkdir(self::$tmpPath, 0755);
869
			}
870 1
		} elseif (is_writable(ROOT_DIRECTORY . \DIRECTORY_SEPARATOR . 'cache' . \DIRECTORY_SEPARATOR . 'upload')) {
871 1
			self::$tmpPath = ROOT_DIRECTORY . \DIRECTORY_SEPARATOR . 'cache' . \DIRECTORY_SEPARATOR . 'upload' . \DIRECTORY_SEPARATOR;
872
		}
873
		return self::$tmpPath;
874
	}
875
876
	/**
877
	 * Init mime types.
878
	 */
879
	public static function initMimeTypes()
880
	{
881
		if (empty(self::$mimeTypes)) {
882
			self::$mimeTypes = require ROOT_DIRECTORY . '/config/mimetypes.php';
883
		}
884
	}
885
886 1
	/**
887
	 * Get mime content type ex. image/png.
888 1
	 *
889 1
	 * @param string $fileName
890 1
	 *
891 1
	 * @return string
892
	 */
893
	public static function getMimeContentType($fileName)
894
	{
895
		static::initMimeTypes();
896
		$extension = explode('.', $fileName);
897
		$extension = strtolower(array_pop($extension));
898
		if (isset(self::$mimeTypes[$extension])) {
899
			$mimeType = self::$mimeTypes[$extension];
900 1
		} elseif (\function_exists('mime_content_type')) {
901
			$mimeType = mime_content_type($fileName);
902 1
		} elseif (\function_exists('finfo_open')) {
903 1
			$finfo = finfo_open(FILEINFO_MIME);
904 1
			$mimeType = finfo_file($finfo, $fileName);
905 1
			finfo_close($finfo);
906 1
		} else {
907 1
			$mimeType = 'application/octet-stream';
908 1
		}
909 1
		return $mimeType;
910 1
	}
911 1
912 1
	/**
913 1
	 * Create document from string.
914
	 *
915 1
	 * @param string $contents
916 1
	 * @param array  $param
917 1
	 *
918 1
	 * @return bool|self
919
	 */
920
	public static function saveFromString(string $contents, array $param = [])
921
	{
922
		$fileInstance = static::loadFromBase($contents, $param);
923
		if ($fileInstance->validateAndSecure()) {
924
			return $fileInstance;
925
		}
926
		$fileInstance->delete();
927
		return false;
928
	}
929
930 1
	/**
931
	 * Create document from url.
932 1
	 *
933 1
	 * @param string $url    Url
934 1
	 * @param array  $params
935
	 *
936 1
	 * @return array|bool
937 1
	 */
938
	public static function saveFromUrl($url, $params = [])
939 1
	{
940 1
		$fileInstance = static::loadFromUrl($url, $params['param'] ?? []);
941 1
		if (!$fileInstance) {
942 1
			return false;
943 1
		}
944 1
		if ($fileInstance->validateAndSecure() && ($id = static::saveFromContent($fileInstance, $params))) {
0 ignored issues
show
Bug introduced by
$fileInstance of type true is incompatible with the type App\Fields\File expected by parameter $file of App\Fields\File::saveFromContent(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

944
		if ($fileInstance->validateAndSecure() && ($id = static::saveFromContent(/** @scrutinizer ignore-type */ $fileInstance, $params))) {
Loading history...
945
			return $id;
946 1
		}
947 1
		$fileInstance->delete();
948 1
		return false;
949
	}
950 1
951 1
	/**
952
	 * Create document from content.
953
	 *
954
	 * @param \self $file
955
	 * @param array $params
956
	 *
957
	 * @throws \Exception
958
	 *
959
	 * @return array|bool
960
	 */
961 1
	public static function saveFromContent(self $file, $params = [])
962 1
	{
963 1
		$notesTitle = $fileName = \App\Purifier::decodeHtml(\App\Purifier::purify($file->getName()));
964
		$fileNameLength = \App\TextUtils::getTextLength($fileName);
965 1
		$record = \Vtiger_Record_Model::getCleanInstance('Documents');
966
		if ($fileNameLength > ($maxLength = $record->getField('filename')->getMaxValue())) {
967
			$extLength = 0;
968
			if ($ext = $file->getExtension()) {
969
				$ext .= ".{$ext}";
970
				$extLength = \App\TextUtils::getTextLength($ext);
971
				$fileName = substr($fileName, 0, $fileNameLength - $extLength);
972
			}
973
			$fileName = \App\TextUtils::textTruncate($fileName, $maxLength - $extLength, false) . $ext;
974
		}
975
		if ($fileNameLength > ($maxLength = $record->getField('notes_title')->getMaxValue())) {
976
			$notesTitle = \App\TextUtils::textTruncate((($params['titlePrefix'] ?? '') . $fileName), $maxLength, false);
977
		}
978
		$record->setData($params);
979
		$record->set('notes_title', $notesTitle);
980
		$record->set('filename', $fileName);
981
		$record->set('filestatus', 1);
982
		$record->set('filelocationtype', 'I');
983
		$record->file = [
984
			'name' => $fileName,
985
			'size' => $file->getSize(),
986
			'type' => $file->getMimeType(),
987
			'tmp_name' => $file->getPath(),
988
			'error' => 0,
989
		];
990
		$record->save();
991
		$file->delete();
992
		if (isset($record->ext['attachmentsId'])) {
993
			return array_merge(['crmid' => $record->getId()], $record->ext);
994
		}
995
		return false;
996
	}
997
998
	/**
999
	 * Init storage diractory.
1000
	 *
1001
	 * @param string $path
1002
	 *
1003
	 * @return string
1004
	 */
1005
	public static function initStorage(string $path): string
1006
	{
1007
		if ('/' !== substr($path, '-1')) {
1008
			$path .= '/';
1009
		}
1010
		$result = 0 === strpos($path, 'storage/') || 0 === strpos($path, 'public_html/storage/');
1011
		if ($result && !is_dir($path)) {
1012
			$result = mkdir($path, 0755, true);
1013
		}
1014
1015
		return $result ? $path : '';
1016
	}
1017
1018
	/**
1019
	 * Init storage file directory.
1020
	 *
1021
	 * @param string $suffix
1022
	 *
1023
	 * @return string
1024
	 */
1025
	public static function initStorageFileDirectory($suffix = false)
1026
	{
1027
		if (!$filepath = \App\Config::module($suffix, 'storagePath')) {
1028
			$filepath = 'storage' . \DIRECTORY_SEPARATOR;
1029
		}
1030
		if ($suffix) {
1031
			$filepath .= $suffix . \DIRECTORY_SEPARATOR;
1032
		}
1033
		if (!is_dir($filepath)) { //create new folder
1034
			mkdir($filepath, 0755, true);
1035
		}
1036
		$year = date('Y');
1037
		$month = date('F');
1038
		$day = date('j');
1039
		$filepath .= $year;
1040
		if (!is_dir($filepath)) { //create new folder
1041
			mkdir($filepath, 0755, true);
1042
		}
1043
		$filepath .= \DIRECTORY_SEPARATOR . $month;
1044
		if (!is_dir($filepath)) { //create new folder
1045
			mkdir($filepath, 0755, true);
1046
		}
1047
		if ($day > 0 && $day <= 7) {
1048
			$week = 'week1';
1049
		} elseif ($day > 7 && $day <= 14) {
1050
			$week = 'week2';
1051
		} elseif ($day > 14 && $day <= 21) {
1052
			$week = 'week3';
1053
		} elseif ($day > 21 && $day <= 28) {
1054
			$week = 'week4';
1055
		} else {
1056
			$week = 'week5';
1057
		}
1058
		$filepath .= \DIRECTORY_SEPARATOR . $week;
1059
		if (!is_dir($filepath)) { //create new folder
1060
			mkdir($filepath, 0755, true);
1061
		}
1062
		return str_replace('\\', '/', $filepath . \DIRECTORY_SEPARATOR);
1063
	}
1064
1065
	/**
1066
	 * Get error message by code.
1067
	 *
1068
	 * @param int $code
1069
	 *
1070
	 * @return string
1071
	 */
1072
	public static function getErrorMessage(int $code): string
1073
	{
1074
		switch ($code) {
1075
			case UPLOAD_ERR_INI_SIZE:
1076
				$message = 'The uploaded file exceeds the upload_max_filesize directive in php.ini';
1077
				break;
1078 1
			case UPLOAD_ERR_FORM_SIZE:
1079
				$message = 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form';
1080
				break;
1081 1
			case UPLOAD_ERR_PARTIAL:
1082 1
				$message = 'The uploaded file was only partially uploaded';
1083 1
				break;
1084
			case UPLOAD_ERR_NO_FILE:
1085
				$message = 'No file was uploaded';
1086
				break;
1087
			case UPLOAD_ERR_NO_TMP_DIR:
1088
				$message = 'Missing a temporary folder';
1089
				break;
1090
			case UPLOAD_ERR_CANT_WRITE:
1091
				$message = 'Failed to write file to disk';
1092
				break;
1093
			case UPLOAD_ERR_EXTENSION:
1094
				$message = 'File upload stopped by extension';
1095
				break;
1096
			default:
1097
				$message = 'Unknown upload error | Code: ' . $code;
1098
				break;
1099
		}
1100
		return $message;
1101 15
	}
1102
1103 15
	/**
1104 2
	 * Get image base data.
1105 2
	 *
1106
	 * @param string $path
1107
	 *
1108 2
	 * @return string
1109
	 */
1110 15
	public static function getImageBaseData($path)
1111
	{
1112
		if ($path) {
1113
			$mime = static::getMimeContentType($path);
1114
			$mimeParts = explode('/', $mime);
1115
			if ($mime && file_exists($path) && isset(static::$allowedFormats[$mimeParts[0]]) && \in_array($mimeParts[1], static::$allowedFormats[$mimeParts[0]])) {
1116
				return "data:$mime;base64," . base64_encode(file_get_contents($path));
1117
			}
1118
		}
1119
		return '';
1120
	}
1121
1122
	/**
1123
	 * Check if give path is writeable.
1124
	 *
1125
	 * @param string $path
1126
	 * @param bool   $absolutePaths
1127
	 *
1128
	 * @return bool
1129
	 */
1130
	public static function isWriteable(string $path, bool $absolutePaths = false): bool
1131
	{
1132
		if (!$absolutePaths) {
1133
			$path = ROOT_DIRECTORY . \DIRECTORY_SEPARATOR . $path;
1134
		}
1135
		if (is_dir($path)) {
1136
			return static::isDirWriteable($path);
1137
		}
1138
		return is_writable($path);
1139
	}
1140
1141
	/**
1142
	 * Check if given directory is writeable.
1143
	 * NOTE: The check is made by trying to create a random file in the directory.
1144
	 *
1145
	 * @param string $dirPath
1146
	 *
1147
	 * @return bool
1148
	 */
1149
	public static function isDirWriteable($dirPath)
1150
	{
1151
		if (is_dir($dirPath)) {
1152
			do {
1153
				$tmpFile = 'tmpfile' . time() . '-' . random_int(1, 1000) . '.tmp';
1154
				// Continue the loop unless we find a name that does not exists already.
1155
				$useFilename = "$dirPath/$tmpFile";
1156
				if (!file_exists($useFilename)) {
1157
					break;
1158
				}
1159
			} while (true);
1160
			$fh = fopen($useFilename, 'a');
1161
			if ($fh) {
0 ignored issues
show
introduced by
$fh is of type resource, thus it always evaluated to false.
Loading history...
1162
				fclose($fh);
1163
				unlink($useFilename);
1164
1165
				return true;
1166
			}
1167
		}
1168
		return false;
1169
	}
1170
1171
	/**
1172
	 * Check if give URL exists.
1173
	 *
1174
	 * @param string $url
1175
	 *
1176
	 * @return bool
1177
	 */
1178
	public static function isExistsUrl($url)
1179
	{
1180
		\App\Log::beginProfile("GET|File::isExistsUrl|{$url}", __NAMESPACE__);
1181
		try {
1182
			$response = (new \GuzzleHttp\Client(\App\RequestHttp::getOptions()))->request('HEAD', $url, ['timeout' => 1, 'connect_timeout' => 1, 'verify' => false, 'http_errors' => false, 'allow_redirects' => false]);
1183
			$status = \in_array($response->getStatusCode(), [200, 302]);
1184
		} catch (\Throwable $th) {
1185
			$status = false;
1186
		}
1187
		\App\Log::endProfile("GET|File::isExistsUrl|{$url}", __NAMESPACE__);
1188
		\App\Log::info("Checked URL: $url | Status: " . $status, __CLASS__);
1189
		return $status;
1190
	}
1191
1192
	/**
1193
	 * Get crm pathname or relative path.
1194
	 *
1195
	 * @param string $path       Absolute pathname
1196
	 * @param string $pathToTrim Path to trim
1197
	 *
1198
	 * @return string Local pathname
1199
	 */
1200
	public static function getLocalPath(string $path, string $pathToTrim = ROOT_DIRECTORY): string
1201
	{
1202
		if (0 === strpos($path, $pathToTrim)) {
1203
			$index = \strlen($pathToTrim) + 1;
1204
			if (strrpos($pathToTrim, '/') === \strlen($pathToTrim) - 1) {
1205
				--$index;
1206
			}
1207
			$path = substr($path, $index);
1208
		}
1209
		return $path;
1210
	}
1211
1212
	/**
1213
	 * Transform mulitiple uploaded file information into useful format.
1214
	 *
1215
	 * @param array $files $_FILES
1216
	 * @param bool  $top
1217
	 *
1218
	 * @return array
1219
	 */
1220
	public static function transform(array $files, $top = true)
1221
	{
1222
		$rows = [];
1223
		foreach ($files as $name => $file) {
1224
			$subName = $top ? $file['name'] : $name;
1225
			if (\is_array($subName)) {
1226
				foreach (array_keys($subName) as $key) {
1227
					$rows[$name][$key] = [
1228
						'name' => $file['name'][$key],
1229
						'type' => $file['type'][$key],
1230
						'tmp_name' => $file['tmp_name'][$key],
1231
						'error' => $file['error'][$key],
1232
						'size' => $file['size'][$key],
1233
					];
1234
					$rows[$name] = static::transform($rows[$name], false);
1235
				}
1236
			} else {
1237
				$rows[$name] = $file;
1238
			}
1239
		}
1240
		return $rows;
1241
	}
1242
1243
	/**
1244
	 * Delete data from the temporary table.
1245
	 *
1246
	 * @param string|string[] $keys
1247
	 *
1248
	 * @return int
1249
	 */
1250
	public static function cleanTemp($keys)
1251
	{
1252
		return \App\Db::getInstance()->createCommand()->delete(static::TABLE_NAME_TEMP, ['key' => $keys])->execute();
1253
	}
1254
1255
	/**
1256
	 * Add an entry to the temporary table of files.
1257
	 *
1258
	 * @param array $params
1259
	 *
1260
	 * @return int
1261
	 */
1262
	public function insertTempFile(array $params): int
1263
	{
1264
		$db = \App\Db::getInstance();
1265
		$result = 0;
1266
		$data = [
1267
			'name' => $this->getName(true),
1268
			'type' => $this->getMimeType(),
1269
			'path' => null,
1270
			'createdtime' => date('Y-m-d H:i:s'),
1271
			'fieldname' => null,
1272
			'key' => null,
1273
			'crmid' => 0,
1274
		];
1275
		foreach ($data as $key => &$value) {
1276
			if (isset($params[$key])) {
1277
				$value = $params[$key];
1278
			}
1279
		}
1280
		if ($db->createCommand()->insert(static::TABLE_NAME_TEMP, $data)->execute()) {
1281
			$result = $db->getLastInsertID(static::TABLE_NAME_TEMP . '_id_seq');
1282
		}
1283
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type string which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
1284
	}
1285
1286
	/**
1287
	 * Add an entry to the media table of files.
1288
	 *
1289
	 * @param array $params
1290
	 *
1291
	 * @return int
1292
	 */
1293
	public function insertMediaFile(array $params): int
1294
	{
1295
		$db = \App\Db::getInstance();
1296
		$result = 0;
1297
		$data = [
1298
			'name' => $this->getName(true),
1299
			'type' => $this->getMimeType(),
1300
			'path' => null,
1301
			'ext' => $this->getExtension(),
1302
			'createdtime' => date('Y-m-d H:i:s'),
1303
			'fieldname' => '',
1304
			'key' => null,
1305
			'status' => 0,
1306
			'user' => \App\User::getCurrentUserRealId()
1307
		];
1308
		foreach ($data as $key => &$value) {
1309
			if (isset($params[$key])) {
1310
				$value = $params[$key];
1311
			}
1312
		}
1313
		if ($db->createCommand()->insert(\App\Layout\Media::TABLE_NAME_MEDIA, $data)->execute()) {
1314
			$result = $db->getLastInsertID(\App\Layout\Media::TABLE_NAME_MEDIA . '_id_seq');
1315
		}
1316
1317
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result could return the type string which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
1318
	}
1319
1320
	/**
1321
	 * Secure image file.
1322
	 *
1323
	 * @param \App\Fields\File $file
1324
	 *
1325
	 * @return bool
1326
	 */
1327
	public static function secureFile(self $file): bool
1328
	{
1329
		if ('image' !== $file->getShortMimeType(0)) {
1330
			return false;
1331
		}
1332
		$result = false;
1333
		if (\extension_loaded('imagick')) {
1334
			try {
1335
				$img = new \imagick($file->getPath());
1336
				$img->stripImage();
1337
				switch ($file->getExtension()) {
1338
					case 'jpg':
1339
					case 'jpeg':
1340
						$img->setImageCompression(\Imagick::COMPRESSION_JPEG);
1341
						$img->setImageCompressionQuality(99);
1342
						break;
1343
					default:
1344
						break;
1345
				}
1346
				$img->writeImage($file->getPath());
1347
				$img->clear();
1348
				$img->destroy();
1349
				$result = true;
1350
			} catch (\ImagickException $e) {
1351
				$result = false;
1352
			}
1353
		} else {
1354
			if (\in_array($file->getExtension(), ['jpeg', 'png', 'gif', 'bmp', 'wbmp', 'gd2', 'webp'])) {
1355
				$img = \imagecreatefromstring($file->getContents());
1356
				if (false !== $img) {
1357
					switch ($file->getExtension()) {
1358
						case 'jpg':
1359
						case 'jpeg':
1360
							$result = \imagejpeg($img, $file->getPath());
1361
							break;
1362
						case 'png':
1363
							$result = \imagepng($img, $file->getPath());
1364
							break;
1365
						case 'gif':
1366
							$result = \imagegif($img, $file->getPath());
1367
							break;
1368
						case 'bmp':
1369
							$result = \imagebmp($img, $file->getPath());
1370
							break;
1371
						default:
1372
							break;
1373
					}
1374 8
					\imagedestroy($img);
1375
				}
1376 8
			}
1377 8
		}
1378
		return $result;
1379
	}
1380 8
1381 8
	/**
1382 8
	 * Parse.
1383
	 *
1384
	 * @param array $value
1385 8
	 *
1386
	 * @return array
1387
	 */
1388 8
	public static function parse(array $value)
1389
	{
1390
		return array_reduce($value, function ($result, $item) {
1391 8
			if (isset($item['key'])) {
1392
				$result[$item['key']] = $item;
1393
			}
1394
			return $result;
1395
		}, []);
1396
	}
1397
1398
	/**
1399
	 * Get upload file details from db.
1400
	 *
1401
	 * @param string $key
1402
	 *
1403
	 * @return array
1404
	 */
1405
	public static function getUploadFile(string $key)
1406
	{
1407
		$row = (new \App\Db\Query())->from(static::TABLE_NAME_TEMP)->where(['key' => $key])->one();
1408
		return $row ?: [];
1409
	}
1410
1411
	/**
1412
	 * Check is it an allowed directory.
1413
	 *
1414
	 * @param string $fullPath
1415
	 *
1416
	 * @return bool
1417
	 */
1418
	public static function isAllowedDirectory(string $fullPath)
1419
	{
1420
		return !(!is_readable($fullPath) || !is_dir($fullPath) || is_file($fullPath));
1421
	}
1422
1423
	/**
1424
	 * Check is it an allowed file directory.
1425
	 *
1426
	 * @param string $fullPath
1427
	 *
1428
	 * @return bool
1429
	 */
1430
	public static function isAllowedFileDirectory(string $fullPath)
1431
	{
1432
		return !(!is_readable($fullPath) || is_dir($fullPath) || !is_file($fullPath));
1433
	}
1434
1435
	/**
1436
	 * Creates a temporary file.
1437
	 *
1438
	 * @param string $prefix The prefix of the generated temporary filename Note: Windows uses only the first three characters of prefix
1439
	 * @param string $ext    File extension, default: .tmp
1440
	 *
1441
	 * @return string The new temporary filename (with path), or throw an exception on failure
1442
	 */
1443
	public static function createTempFile(string $prefix = '', string $ext = 'tmp'): string
1444
	{
1445
		return (new \Symfony\Component\Filesystem\Filesystem())->tempnam(self::getTmpPath(), $prefix, '.' . $ext);
1446
	}
1447
1448
	/**
1449
	 * Delete files from record.
1450
	 *
1451
	 * @param \Vtiger_Record_Model $recordModel
1452
	 */
1453
	public static function deleteForRecord(\Vtiger_Record_Model $recordModel)
1454
	{
1455
		foreach ($recordModel->getModule()->getFieldsByType(['multiAttachment', 'multiImage', 'image']) as $fieldModel) {
1456
			if (!$recordModel->isEmpty($fieldModel->getName()) && !\App\Json::isEmpty($recordModel->get($fieldModel->getName()))) {
1457
				foreach (\App\Json::decode($recordModel->get($fieldModel->getName())) as $file) {
1458
					$path = ROOT_DIRECTORY . \DIRECTORY_SEPARATOR . $file['path'];
1459
					if (file_exists($path)) {
1460
						unlink($path);
1461
					} else {
1462
						\App\Log::warning('Deleted file does not exist: ' . print_r($file, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($file, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1462
						\App\Log::warning('Deleted file does not exist: ' . /** @scrutinizer ignore-type */ print_r($file, true));
Loading history...
1463
					}
1464
				}
1465
			}
1466
		}
1467
	}
1468
}
1469