Passed
Push — master ( 3a20e7...179271 )
by Aimeos
04:17
created

Standard::image()   A

Complexity

Conditions 6
Paths 12

Size

Total Lines 32
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 17
c 0
b 0
f 0
dl 0
loc 32
rs 9.0777
cc 6
nc 12
nop 2
1
<?php
2
3
/**
4
 * @license LGPLv3, https://opensource.org/licenses/LGPL-3.0
5
 * @copyright Metaways Infosystems GmbH, 2011
6
 * @copyright Aimeos (aimeos.org), 2015-2023
7
 * @package MShop
8
 * @subpackage Media
9
 */
10
11
12
namespace Aimeos\MShop\Media\Manager;
13
14
use enshrined\svgSanitize\Sanitizer;
15
use \Psr\Http\Message\UploadedFileInterface;
0 ignored issues
show
Bug introduced by
The type \Psr\Http\Message\UploadedFileInterface 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...
16
use \Intervention\Image\Interfaces\ImageInterface;
0 ignored issues
show
Bug introduced by
The type \Intervention\Image\Interfaces\ImageInterface 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...
17
18
19
/**
20
 * Default media manager implementation.
21
 *
22
 * @package MShop
23
 * @subpackage Media
24
 */
25
class Standard
26
	extends \Aimeos\MShop\Common\Manager\Base
27
	implements \Aimeos\MShop\Media\Manager\Iface, \Aimeos\MShop\Common\Manager\Factory\Iface
28
{
29
	use \Aimeos\MShop\Upload;
30
31
32
	/** mshop/media/manager/name
33
	 * Class name of the used media manager implementation
34
	 *
35
	 * Each default manager can be replace by an alternative imlementation.
36
	 * To use this implementation, you have to set the last part of the class
37
	 * name as configuration value so the manager factory knows which class it
38
	 * has to instantiate.
39
	 *
40
	 * For example, if the name of the default class is
41
	 *
42
	 *  \Aimeos\MShop\Media\Manager\Standard
43
	 *
44
	 * and you want to replace it with your own version named
45
	 *
46
	 *  \Aimeos\MShop\Media\Manager\Mymanager
47
	 *
48
	 * then you have to set the this configuration option:
49
	 *
50
	 *  mshop/media/manager/name = Mymanager
51
	 *
52
	 * The value is the last part of your own class name and it's case sensitive,
53
	 * so take care that the configuration value is exactly named like the last
54
	 * part of the class name.
55
	 *
56
	 * The allowed characters of the class name are A-Z, a-z and 0-9. No other
57
	 * characters are possible! You should always start the last part of the class
58
	 * name with an upper case character and continue only with lower case characters
59
	 * or numbers. Avoid chamel case names like "MyManager"!
60
	 *
61
	 * @param string Last part of the class name
62
	 * @since 2014.03
63
	 * @category Developer
64
	 */
65
66
	/** mshop/media/manager/decorators/excludes
67
	 * Excludes decorators added by the "common" option from the media manager
68
	 *
69
	 * Decorators extend the functionality of a class by adding new aspects
70
	 * (e.g. log what is currently done), executing the methods of the underlying
71
	 * class only in certain conditions (e.g. only for logged in users) or
72
	 * modify what is returned to the caller.
73
	 *
74
	 * This option allows you to remove a decorator added via
75
	 * "mshop/common/manager/decorators/default" before they are wrapped
76
	 * around the media manager.
77
	 *
78
	 *  mshop/media/manager/decorators/excludes = array( 'decorator1' )
79
	 *
80
	 * This would remove the decorator named "decorator1" from the list of
81
	 * common decorators ("\Aimeos\MShop\Common\Manager\Decorator\*") added via
82
	 * "mshop/common/manager/decorators/default" for the media manager.
83
	 *
84
	 * @param array List of decorator names
85
	 * @since 2014.03
86
	 * @category Developer
87
	 * @see mshop/common/manager/decorators/default
88
	 * @see mshop/media/manager/decorators/global
89
	 * @see mshop/media/manager/decorators/local
90
	 */
91
92
	/** mshop/media/manager/decorators/global
93
	 * Adds a list of globally available decorators only to the media manager
94
	 *
95
	 * Decorators extend the functionality of a class by adding new aspects
96
	 * (e.g. log what is currently done), executing the methods of the underlying
97
	 * class only in certain conditions (e.g. only for logged in users) or
98
	 * modify what is returned to the caller.
99
	 *
100
	 * This option allows you to wrap global decorators
101
	 * ("\Aimeos\MShop\Common\Manager\Decorator\*") around the media manager.
102
	 *
103
	 *  mshop/media/manager/decorators/global = array( 'decorator1' )
104
	 *
105
	 * This would add the decorator named "decorator1" defined by
106
	 * "\Aimeos\MShop\Common\Manager\Decorator\Decorator1" only to the media
107
	 * manager.
108
	 *
109
	 * @param array List of decorator names
110
	 * @since 2014.03
111
	 * @category Developer
112
	 * @see mshop/common/manager/decorators/default
113
	 * @see mshop/media/manager/decorators/excludes
114
	 * @see mshop/media/manager/decorators/local
115
	 */
116
117
	/** mshop/media/manager/decorators/local
118
	 * Adds a list of local decorators only to the media manager
119
	 *
120
	 * Decorators extend the functionality of a class by adding new aspects
121
	 * (e.g. log what is currently done), executing the methods of the underlying
122
	 * class only in certain conditions (e.g. only for logged in users) or
123
	 * modify what is returned to the caller.
124
	 *
125
	 * This option allows you to wrap local decorators
126
	 * ("\Aimeos\MShop\Media\Manager\Decorator\*") around the media manager.
127
	 *
128
	 *  mshop/media/manager/decorators/local = array( 'decorator2' )
129
	 *
130
	 * This would add the decorator named "decorator2" defined by
131
	 * "\Aimeos\MShop\Media\Manager\Decorator\Decorator2" only to the media
132
	 * manager.
133
	 *
134
	 * @param array List of decorator names
135
	 * @since 2014.03
136
	 * @category Developer
137
	 * @see mshop/common/manager/decorators/default
138
	 * @see mshop/media/manager/decorators/excludes
139
	 * @see mshop/media/manager/decorators/global
140
	 */
141
142
143
	use \Aimeos\MShop\Common\Manager\ListsRef\Traits;
144
	use \Aimeos\MShop\Common\Manager\PropertyRef\Traits;
145
146
147
	private array $searchConfig = array(
148
		'media.id' => array(
149
			'label' => 'ID',
150
			'code' => 'media.id',
151
			'internalcode' => 'mmed."id"',
152
			'type' => 'int',
153
		),
154
		'media.siteid' => array(
155
			'label' => 'Site ID',
156
			'code' => 'media.siteid',
157
			'internalcode' => 'mmed."siteid"',
158
			'public' => false,
159
		),
160
		'media.type' => array(
161
			'label' => 'Type',
162
			'code' => 'media.type',
163
			'internalcode' => 'mmed."type"',
164
		),
165
		'media.label' => array(
166
			'label' => 'Label',
167
			'code' => 'media.label',
168
			'internalcode' => 'mmed."label"',
169
		),
170
		'media.domain' => array(
171
			'label' => 'Domain',
172
			'code' => 'media.domain',
173
			'internalcode' => 'mmed."domain"',
174
		),
175
		'media.languageid' => array(
176
			'label' => 'Language code',
177
			'code' => 'media.languageid',
178
			'internalcode' => 'mmed."langid"',
179
		),
180
		'media.mimetype' => array(
181
			'label' => 'Mime type',
182
			'code' => 'media.mimetype',
183
			'internalcode' => 'mmed."mimetype"',
184
		),
185
		'media.url' => array(
186
			'label' => 'URL',
187
			'code' => 'media.url',
188
			'internalcode' => 'mmed."link"',
189
		),
190
		'media.preview' => array(
191
			'label' => 'Preview URLs as JSON encoded string',
192
			'code' => 'media.preview',
193
			'internalcode' => 'mmed."preview"',
194
		),
195
		'media.previews' => array(
196
			'label' => 'Preview URLs as JSON encoded string',
197
			'code' => 'media.previews',
198
			'internalcode' => 'mmed."previews"',
199
			'type' => 'json',
200
		),
201
		'media.filesystem' => array(
202
			'label' => 'File sytem name',
203
			'code' => 'media.filesystem',
204
			'internalcode' => 'mmed."fsname"',
205
		),
206
		'media.status' => array(
207
			'label' => 'Status',
208
			'code' => 'media.status',
209
			'internalcode' => 'mmed."status"',
210
			'type' => 'int',
211
		),
212
		'media.ctime' => array(
213
			'code' => 'media.ctime',
214
			'internalcode' => 'mmed."ctime"',
215
			'label' => 'Create date/time',
216
			'type' => 'datetime',
217
			'public' => false,
218
		),
219
		'media.mtime' => array(
220
			'code' => 'media.mtime',
221
			'internalcode' => 'mmed."mtime"',
222
			'label' => 'Modify date/time',
223
			'type' => 'datetime',
224
			'public' => false,
225
		),
226
		'media.editor' => array(
227
			'code' => 'media.editor',
228
			'internalcode' => 'mmed."editor"',
229
			'label' => 'Editor',
230
			'public' => false,
231
		),
232
		'media:has' => array(
233
			'code' => 'media:has()',
234
			'internalcode' => ':site AND :key AND mmedli."id"',
235
			'internaldeps' => ['LEFT JOIN "mshop_media_list" AS mmedli ON ( mmedli."parentid" = mmed."id" )'],
236
			'label' => 'Media has list item, parameter(<domain>[,<list type>[,<reference ID>)]]',
237
			'type' => 'null',
238
			'public' => false,
239
		),
240
		'media:prop' => array(
241
			'code' => 'media:prop()',
242
			'internalcode' => ':site AND :key AND mmedpr."id"',
243
			'internaldeps' => ['LEFT JOIN "mshop_media_property" AS mmedpr ON ( mmedpr."parentid" = mmed."id" )'],
244
			'label' => 'Media has property item, parameter(<property type>[,<language code>[,<property value>]])',
245
			'type' => 'null',
246
			'public' => false,
247
		),
248
	);
249
250
	private ?string $languageId;
251
252
253
	/**
254
	 * Initializes the object.
255
	 *
256
	 * @param \Aimeos\MShop\ContextIface $context Context object
257
	 */
258
	public function __construct( \Aimeos\MShop\ContextIface $context )
259
	{
260
		parent::__construct( $context );
261
262
		/** mshop/media/manager/resource
263
		 * Name of the database connection resource to use
264
		 *
265
		 * You can configure a different database connection for each data domain
266
		 * and if no such connection name exists, the "db" connection will be used.
267
		 * It's also possible to use the same database connection for different
268
		 * data domains by configuring the same connection name using this setting.
269
		 *
270
		 * @param string Database connection name
271
		 * @since 2023.04
272
		 */
273
		$this->setResourceName( $context->config()->get( 'mshop/media/manager/resource', 'db-media' ) );
274
		$this->languageId = $context->locale()->getLanguageId();
275
276
		$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
277
		$level = $context->config()->get( 'mshop/media/manager/sitemode', $level );
278
279
280
		$this->searchConfig['media:has']['function'] = function( &$source, array $params ) use ( $level ) {
281
282
			$keys = [];
283
284
			foreach( (array) ( $params[1] ?? '' ) as $type ) {
285
				foreach( (array) ( $params[2] ?? '' ) as $id ) {
286
					$keys[] = $params[0] . '|' . ( $type ? $type . '|' : '' ) . $id;
287
				}
288
			}
289
290
			$sitestr = $this->siteString( 'mmedli."siteid"', $level );
291
			$keystr = $this->toExpression( 'mmedli."key"', $keys, ( $params[2] ?? null ) ? '==' : '=~' );
292
			$source = str_replace( [':site', ':key'], [$sitestr, $keystr], $source );
293
294
			return $params;
295
		};
296
297
298
		$this->searchConfig['media:prop']['function'] = function( &$source, array $params ) use ( $level ) {
299
300
			$keys = [];
301
			$langs = array_key_exists( 1, $params ) ? ( $params[1] ?? 'null' ) : '';
302
303
			foreach( (array) $langs as $lang ) {
304
				foreach( (array) ( $params[2] ?? '' ) as $val ) {
305
					$keys[] = substr( $params[0] . '|' . ( $lang === null ? 'null|' : ( $lang ? $lang . '|' : '' ) ) . $val, 0, 255 );
306
				}
307
			}
308
309
			$sitestr = $this->siteString( 'mmedpr."siteid"', $level );
310
			$keystr = $this->toExpression( 'mmedpr."key"', $keys, ( $params[2] ?? null ) ? '==' : '=~' );
311
			$source = str_replace( [':site', ':key'], [$sitestr, $keystr], $source );
312
313
			return $params;
314
		};
315
	}
316
317
318
	/**
319
	 * Removes old entries from the storage.
320
	 *
321
	 * @param iterable $siteids List of IDs for sites whose entries should be deleted
322
	 * @return \Aimeos\MShop\Media\Manager\Iface Manager object for chaining method calls
323
	 */
324
	public function clear( iterable $siteids ) : \Aimeos\MShop\Common\Manager\Iface
325
	{
326
		$path = 'mshop/media/manager/submanagers';
327
		$default = ['lists', 'property', 'type'];
328
329
		foreach( $this->context()->config()->get( $path, $default ) as $domain ) {
330
			$this->object()->getSubManager( $domain )->clear( $siteids );
331
		}
332
333
		return $this->clearBase( $siteids, 'mshop/media/manager/delete' );
334
	}
335
336
337
	/**
338
	 * Copies the media item and the referenced files
339
	 *
340
	 * @param \Aimeos\MShop\Media\Item\Iface $item Media item whose files should be copied
341
	 * @return \Aimeos\MShop\Media\Item\Iface Copied media item with new files
342
	 */
343
	public function copy( \Aimeos\MShop\Media\Item\Iface $item ) : \Aimeos\MShop\Media\Item\Iface
344
	{
345
		$item = ( clone $item )->setId( null );
346
347
		$path = $item->getUrl();
348
		$mime = $item->getMimeType();
349
		$domain = $item->getDomain();
350
		$previews = $item->getPreviews();
351
		$fsname = $item->getFileSystem();
352
		$fs = $this->context()->fs( $fsname );
353
354
		if( $fs->has( $path ) )
355
		{
356
			$newPath = $this->path( substr( basename( $path ), 9 ), $mime, $domain );
357
			$fs->copy( $path, $newPath );
358
			$item->setUrl( $newPath );
359
		}
360
361
		if( empty( $previews ) ) {
362
			return $this->scale( $item, true );
363
		}
364
365
		foreach( $previews as $size => $preview )
366
		{
367
			if( $fsname !== 'fs-mimeicon' && $fs->has( $preview ) )
368
			{
369
				$newPath = $this->path( substr( basename( $preview ), 9 ), $mime, $domain );
370
				$fs->copy( $preview, $newPath );
371
				$previews[$size] = $newPath;
372
			}
373
		}
374
375
		return $item->setPreviews( $previews );
376
	}
377
378
379
	/**
380
	 * Creates a new empty item instance
381
	 *
382
	 * @param array $values Values the item should be initialized with
383
	 * @return \Aimeos\MShop\Media\Item\Iface New media item object
384
	 */
385
	public function create( array $values = [] ) : \Aimeos\MShop\Common\Item\Iface
386
	{
387
		$values['media.siteid'] = $values['media.siteid'] ?? $this->context()->locale()->getSiteId();
388
		return $this->createItemBase( $values );
389
	}
390
391
392
	/**
393
	 * Removes multiple items.
394
	 *
395
	 * @param \Aimeos\MShop\Common\Item\Iface[]|string[] $items List of item objects or IDs of the items
396
	 * @return \Aimeos\MShop\Media\Manager\Iface Manager object for chaining method calls
397
	 */
398
	public function delete( $items ) : \Aimeos\MShop\Common\Manager\Iface
399
	{
400
		/** mshop/media/manager/delete/mysql
401
		 * Deletes the items matched by the given IDs from the database
402
		 *
403
		 * @see mshop/media/manager/delete/ansi
404
		 */
405
406
		/** mshop/media/manager/delete/ansi
407
		 * Deletes the items matched by the given IDs from the database
408
		 *
409
		 * Removes the records specified by the given IDs from the media database.
410
		 * The records must be from the site that is configured via the
411
		 * context item.
412
		 *
413
		 * The ":cond" placeholder is replaced by the name of the ID column and
414
		 * the given ID or list of IDs while the site ID is bound to the question
415
		 * mark.
416
		 *
417
		 * The SQL statement should conform to the ANSI standard to be
418
		 * compatible with most relational database systems. This also
419
		 * includes using double quotes for table and column names.
420
		 *
421
		 * @param string SQL statement for deleting items
422
		 * @since 2014.03
423
		 * @category Developer
424
		 * @see mshop/media/manager/insert/ansi
425
		 * @see mshop/media/manager/update/ansi
426
		 * @see mshop/media/manager/newid/ansi
427
		 * @see mshop/media/manager/search/ansi
428
		 * @see mshop/media/manager/count/ansi
429
		 */
430
		$cfgpath = 'mshop/media/manager/delete';
431
432
		foreach( map( $items ) as $item )
433
		{
434
			if( $item instanceof \Aimeos\MShop\Media\Item\Iface && $item->getFileSystem() === 'fs-media' )
435
			{
436
				$this->deletePreviews( $item, $item->getPreviews() );
437
				$this->deleteFile( $item->getUrl(), 'fs-media' );
438
			}
439
		}
440
441
		return $this->deleteItemsBase( $items, $cfgpath )->deleteRefItems( $items );
442
	}
443
444
445
	/**
446
	 * Creates a filter object.
447
	 *
448
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
449
	 * @param bool $site TRUE for adding site criteria to limit items by the site of related items
450
	 * @return \Aimeos\Base\Criteria\Iface Returns the filter object
451
	 */
452
	public function filter( ?bool $default = false, bool $site = false ) : \Aimeos\Base\Criteria\Iface
453
	{
454
		if( $default !== false )
455
		{
456
			$object = $this->filterBase( 'media', $default );
457
			$langid = $this->context()->locale()->getLanguageId();
458
459
			if( $langid !== null )
460
			{
461
				$temp = array(
462
					$object->compare( '==', 'media.languageid', $langid ),
463
					$object->compare( '==', 'media.languageid', null ),
464
				);
465
466
				$expr = array(
467
					$object->getConditions(),
468
					$object->or( $temp ),
469
				);
470
471
				$object->setConditions( $object->and( $expr ) );
472
			}
473
474
			return $object;
475
		}
476
477
		return parent::filter();
478
	}
479
480
481
	/**
482
	 * Returns an item for the given ID.
483
	 *
484
	 * @param string $id ID of the item that should be retrieved
485
	 * @param string[] $ref List of domains to fetch list items and referenced items for
486
	 * @param bool|null $default Add default criteria or NULL for relaxed default criteria
487
	 * @return \Aimeos\MShop\Media\Item\Iface Returns the media item of the given id
488
	 * @throws \Aimeos\MShop\Exception If item couldn't be found
489
	 */
490
	public function get( string $id, array $ref = [], ?bool $default = false ) : \Aimeos\MShop\Common\Item\Iface
491
	{
492
		return $this->getItemBase( 'media.id', $id, $ref, $default );
493
	}
494
495
496
	/**
497
	 * Returns the available manager types
498
	 *
499
	 * @param bool $withsub Return also the resource type of sub-managers if true
500
	 * @return string[] Type of the manager and submanagers, subtypes are separated by slashes
501
	 */
502
	public function getResourceType( bool $withsub = true ) : array
503
	{
504
		$path = 'mshop/media/manager/submanagers';
505
		$default = ['lists', 'property'];
506
507
		return $this->getResourceTypeBase( 'media', $path, $default, $withsub );
508
	}
509
510
511
	/**
512
	 * Returns the attributes that can be used for searching.
513
	 *
514
	 * @param bool $withsub Return also attributes of sub-managers if true
515
	 * @return \Aimeos\Base\Criteria\Attribute\Iface[] List of search attribute items
516
	 */
517
	public function getSearchAttributes( bool $withsub = true ) : array
518
	{
519
		/** mshop/media/manager/submanagers
520
		 * List of manager names that can be instantiated by the media manager
521
		 *
522
		 * Managers provide a generic interface to the underlying storage.
523
		 * Each manager has or can have sub-managers caring about particular
524
		 * aspects. Each of these sub-managers can be instantiated by its
525
		 * parent manager using the getSubManager() method.
526
		 *
527
		 * The search keys from sub-managers can be normally used in the
528
		 * manager as well. It allows you to search for items of the manager
529
		 * using the search keys of the sub-managers to further limit the
530
		 * retrieved list of items.
531
		 *
532
		 * @param array List of sub-manager names
533
		 * @since 2014.03
534
		 * @category Developer
535
		 */
536
		$path = 'mshop/media/manager/submanagers';
537
538
		return $this->getSearchAttributesBase( $this->searchConfig, $path, [], $withsub );
539
	}
540
541
542
	/**
543
	 * Returns a new manager for media extensions
544
	 *
545
	 * @param string $manager Name of the sub manager type in lower case
546
	 * @param string|null $name Name of the implementation, will be from configuration (or Default) if null
547
	 * @return \Aimeos\MShop\Common\Manager\Iface Manager for different extensions, e.g stock, tags, locations, etc.
548
	 */
549
	public function getSubManager( string $manager, string $name = null ) : \Aimeos\MShop\Common\Manager\Iface
550
	{
551
		return $this->getSubManagerBase( 'media', $manager, $name );
552
	}
553
554
555
	/**
556
	 * Adds a new item to the storage or updates an existing one.
557
	 *
558
	 * @param \Aimeos\MShop\Media\Item\Iface $item New item that should be saved to the storage
559
	 * @param bool $fetch True if the new ID should be returned in the item
560
	 * @return \Aimeos\MShop\Media\Item\Iface $item Updated item including the generated ID
561
	 */
562
	protected function saveItem( \Aimeos\MShop\Media\Item\Iface $item, bool $fetch = true ) : \Aimeos\MShop\Media\Item\Iface
563
	{
564
		if( !$item->isModified() )
565
		{
566
			$item = $this->savePropertyItems( $item, 'media', $fetch );
567
			return $this->saveListItems( $item, 'media', $fetch );
568
		}
569
570
		$context = $this->context();
571
		$conn = $context->db( $this->getResourceName() );
572
573
		$id = $item->getId();
574
		$date = date( 'Y-m-d H:i:s' );
575
		$columns = $this->object()->getSaveAttributes();
576
577
		if( $id === null )
578
		{
579
			/** mshop/media/manager/insert/mysql
580
			 * Inserts a new media record into the database table
581
			 *
582
			 * @see mshop/media/manager/insert/ansi
583
			 */
584
585
			/** mshop/media/manager/insert/ansi
586
			 * Inserts a new media record into the database table
587
			 *
588
			 * Items with no ID yet (i.e. the ID is NULL) will be created in
589
			 * the database and the newly created ID retrieved afterwards
590
			 * using the "newid" SQL statement.
591
			 *
592
			 * The SQL statement must be a string suitable for being used as
593
			 * prepared statement. It must include question marks for binding
594
			 * the values from the media item to the statement before they are
595
			 * sent to the database server. The number of question marks must
596
			 * be the same as the number of columns listed in the INSERT
597
			 * statement. The order of the columns must correspond to the
598
			 * order in the save() method, so the correct values are
599
			 * bound to the columns.
600
			 *
601
			 * The SQL statement should conform to the ANSI standard to be
602
			 * compatible with most relational database systems. This also
603
			 * includes using double quotes for table and column names.
604
			 *
605
			 * @param string SQL statement for inserting records
606
			 * @since 2014.03
607
			 * @category Developer
608
			 * @see mshop/media/manager/update/ansi
609
			 * @see mshop/media/manager/newid/ansi
610
			 * @see mshop/media/manager/delete/ansi
611
			 * @see mshop/media/manager/search/ansi
612
			 * @see mshop/media/manager/count/ansi
613
			 */
614
			$path = 'mshop/media/manager/insert';
615
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ) );
0 ignored issues
show
Bug introduced by
It seems like $this->getSqlConfig($path) can also be of type array; however, parameter $sql of Aimeos\MShop\Common\Manager\Base::addSqlColumns() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

615
			$sql = $this->addSqlColumns( array_keys( $columns ), /** @scrutinizer ignore-type */ $this->getSqlConfig( $path ) );
Loading history...
616
		}
617
		else
618
		{
619
			/** mshop/media/manager/update/mysql
620
			 * Updates an existing media record in the database
621
			 *
622
			 * @see mshop/media/manager/update/ansi
623
			 */
624
625
			/** mshop/media/manager/update/ansi
626
			 * Updates an existing media record in the database
627
			 *
628
			 * Items which already have an ID (i.e. the ID is not NULL) will
629
			 * be updated in the database.
630
			 *
631
			 * The SQL statement must be a string suitable for being used as
632
			 * prepared statement. It must include question marks for binding
633
			 * the values from the media item to the statement before they are
634
			 * sent to the database server. The order of the columns must
635
			 * correspond to the order in the save() method, so the
636
			 * correct values are bound to the columns.
637
			 *
638
			 * The SQL statement should conform to the ANSI standard to be
639
			 * compatible with most relational database systems. This also
640
			 * includes using double quotes for table and column names.
641
			 *
642
			 * @param string SQL statement for updating records
643
			 * @since 2014.03
644
			 * @category Developer
645
			 * @see mshop/media/manager/insert/ansi
646
			 * @see mshop/media/manager/newid/ansi
647
			 * @see mshop/media/manager/delete/ansi
648
			 * @see mshop/media/manager/search/ansi
649
			 * @see mshop/media/manager/count/ansi
650
			 */
651
			$path = 'mshop/media/manager/update';
652
			$sql = $this->addSqlColumns( array_keys( $columns ), $this->getSqlConfig( $path ), false );
653
		}
654
655
		$idx = 1;
656
		$stmt = $this->getCachedStatement( $conn, $path, $sql );
657
658
		foreach( $columns as $name => $entry ) {
659
			$stmt->bind( $idx++, $item->get( $name ), \Aimeos\Base\Criteria\SQL::type( $entry->getType() ) );
660
		}
661
662
		$stmt->bind( $idx++, $item->getLanguageId() );
663
		$stmt->bind( $idx++, $item->getType() );
664
		$stmt->bind( $idx++, $item->getLabel() );
665
		$stmt->bind( $idx++, $item->getMimeType() );
666
		$stmt->bind( $idx++, $item->getUrl() );
667
		$stmt->bind( $idx++, $item->getStatus(), \Aimeos\Base\DB\Statement\Base::PARAM_INT );
668
		$stmt->bind( $idx++, $item->getFileSystem() );
669
		$stmt->bind( $idx++, $item->getDomain() );
670
		$stmt->bind( $idx++, json_encode( $item->getPreviews(), JSON_FORCE_OBJECT ) );
671
		$stmt->bind( $idx++, $date ); // mtime
672
		$stmt->bind( $idx++, $context->editor() );
673
674
		if( $id !== null ) {
675
			$stmt->bind( $idx++, $context->locale()->getSiteId() . '%' );
676
			$stmt->bind( $idx++, $id, \Aimeos\Base\DB\Statement\Base::PARAM_INT );
677
		} else {
678
			$stmt->bind( $idx++, $this->siteId( $item->getSiteId(), \Aimeos\MShop\Locale\Manager\Base::SITE_SUBTREE ) );
679
			$stmt->bind( $idx++, $date ); // ctime
680
		}
681
682
		$stmt->execute()->finish();
683
684
		if( $id === null )
685
		{
686
			/** mshop/media/manager/newid/mysql
687
			 * Retrieves the ID generated by the database when inserting a new record
688
			 *
689
			 * @see mshop/media/manager/newid/ansi
690
			 */
691
692
			/** mshop/media/manager/newid/ansi
693
			 * Retrieves the ID generated by the database when inserting a new record
694
			 *
695
			 * As soon as a new record is inserted into the database table,
696
			 * the database server generates a new and unique identifier for
697
			 * that record. This ID can be used for retrieving, updating and
698
			 * deleting that specific record from the table again.
699
			 *
700
			 * For MySQL:
701
			 *  SELECT LAST_INSERT_ID()
702
			 * For PostgreSQL:
703
			 *  SELECT currval('seq_mmed_id')
704
			 * For SQL Server:
705
			 *  SELECT SCOPE_IDENTITY()
706
			 * For Oracle:
707
			 *  SELECT "seq_mmed_id".CURRVAL FROM DUAL
708
			 *
709
			 * There's no way to retrive the new ID by a SQL statements that
710
			 * fits for most database servers as they implement their own
711
			 * specific way.
712
			 *
713
			 * @param string SQL statement for retrieving the last inserted record ID
714
			 * @since 2014.03
715
			 * @category Developer
716
			 * @see mshop/media/manager/insert/ansi
717
			 * @see mshop/media/manager/update/ansi
718
			 * @see mshop/media/manager/delete/ansi
719
			 * @see mshop/media/manager/search/ansi
720
			 * @see mshop/media/manager/count/ansi
721
			 */
722
			$path = 'mshop/media/manager/newid';
723
			$id = $this->newId( $conn, $path );
724
		}
725
726
		$item->setId( $id );
727
728
		$item = $this->savePropertyItems( $item, 'media', $fetch );
729
		return $this->saveListItems( $item, 'media', $fetch );
730
	}
731
732
733
	/**
734
	 * Rescales the original file to preview files referenced by the media item
735
	 *
736
	 * The height/width configuration for scaling
737
	 * - mshop/media/<files|preview>/maxheight
738
	 * - mshop/media/<files|preview>/maxwidth
739
	 * - mshop/media/<files|preview>/force-size
740
	 *
741
	 * @param \Aimeos\MShop\Media\Item\Iface $item Media item whose files should be scaled
742
	 * @param bool $force True to enforce creating new preview images
743
	 * @return \Aimeos\MShop\Media\Item\Iface Rescaled media item
744
	 */
745
	public function scale( \Aimeos\MShop\Media\Item\Iface $item, bool $force = false ) : \Aimeos\MShop\Media\Item\Iface
746
	{
747
		$mime = $item->getMimeType();
748
749
		if( empty( $url = $item->getUrl() )
750
			|| $item->getFileSystem() === 'fs-mimeicon'
751
			|| strncmp( 'data:', $url, 5 ) === 0
752
			|| strncmp( 'image/svg', $mime, 9 ) === 0
753
			|| strncmp( 'image/', $mime, 6 ) !== 0
754
		) {
755
			return $item;
756
		}
757
758
		$fs = $this->context()->fs( $item->getFileSystem() );
759
		$is = ( $fs instanceof \Aimeos\Base\Filesystem\MetaIface ? true : false );
760
761
		if( !$force
762
			&& !empty( $item->getPreviews() )
763
			&& preg_match( '#^[a-zA-Z]{2,6}://#', $url ) !== 1
764
			&& ( $is && date( 'Y-m-d H:i:s', $fs->time( $url ) ) < $item->getTimeModified() || $fs->has( $url ) )
765
		) {
766
			return $item;
767
		}
768
769
		$previews = [];
770
		$old = $item->getPreviews();
771
		$domain = $item->getDomain() ?: '-';
772
		$image = $this->image( $fs, $url );
773
		$quality = $this->quality();
774
775
		foreach( $this->createPreviews( $image, $domain, $item->getType() ) as $width => $image )
776
		{
777
			$path = $old[$width] ?? $this->path( $url, 'image/webp', $domain );
778
			$fs->write( $path, (string) $image->toWebp( $quality ) );
779
			$previews[$width] = $path;
780
		}
781
782
		$item = $this->deletePreviews( $item, $old )->setPreviews( $previews );
783
784
		$this->call( 'scaled', $item, $image );
785
786
		return $item;
787
	}
788
789
790
	/**
791
	 * Returns the item objects matched by the given search criteria.
792
	 *
793
	 * @param \Aimeos\Base\Criteria\Iface $search Search criteria object
794
	 * @param string[] $ref List of domains to fetch list items and referenced items for
795
	 * @param int|null &$total Number of items that are available in total
796
	 * @return \Aimeos\Map List of items implementing \Aimeos\MShop\Media\Item\Iface with ids as keys
797
	 */
798
	public function search( \Aimeos\Base\Criteria\Iface $search, array $ref = [], int &$total = null ) : \Aimeos\Map
799
	{
800
		$map = [];
801
		$context = $this->context();
802
		$conn = $context->db( $this->getResourceName() );
803
804
			$required = array( 'media' );
805
806
			/** mshop/media/manager/sitemode
807
			 * Mode how items from levels below or above in the site tree are handled
808
			 *
809
			 * By default, only items from the current site are fetched from the
810
			 * storage. If the ai-sites extension is installed, you can create a
811
			 * tree of sites. Then, this setting allows you to define for the
812
			 * whole media domain if items from parent sites are inherited,
813
			 * sites from child sites are aggregated or both.
814
			 *
815
			 * Available constants for the site mode are:
816
			 * * 0 = only items from the current site
817
			 * * 1 = inherit items from parent sites
818
			 * * 2 = aggregate items from child sites
819
			 * * 3 = inherit and aggregate items at the same time
820
			 *
821
			 * You also need to set the mode in the locale manager
822
			 * (mshop/locale/manager/sitelevel) to one of the constants.
823
			 * If you set it to the same value, it will work as described but you
824
			 * can also use different modes. For example, if inheritance and
825
			 * aggregation is configured the locale manager but only inheritance
826
			 * in the domain manager because aggregating items makes no sense in
827
			 * this domain, then items wil be only inherited. Thus, you have full
828
			 * control over inheritance and aggregation in each domain.
829
			 *
830
			 * @param int Constant from Aimeos\MShop\Locale\Manager\Base class
831
			 * @category Developer
832
			 * @since 2018.01
833
			 * @see mshop/locale/manager/sitelevel
834
			 */
835
			$level = \Aimeos\MShop\Locale\Manager\Base::SITE_ALL;
836
			$level = $context->config()->get( 'mshop/media/manager/sitemode', $level );
837
838
			/** mshop/media/manager/search/mysql
839
			 * Retrieves the records matched by the given criteria in the database
840
			 *
841
			 * @see mshop/media/manager/search/ansi
842
			 */
843
844
			/** mshop/media/manager/search/ansi
845
			 * Retrieves the records matched by the given criteria in the database
846
			 *
847
			 * Fetches the records matched by the given criteria from the media
848
			 * database. The records must be from one of the sites that are
849
			 * configured via the context item. If the current site is part of
850
			 * a tree of sites, the SELECT statement can retrieve all records
851
			 * from the current site and the complete sub-tree of sites.
852
			 *
853
			 * As the records can normally be limited by criteria from sub-managers,
854
			 * their tables must be joined in the SQL context. This is done by
855
			 * using the "internaldeps" property from the definition of the ID
856
			 * column of the sub-managers. These internal dependencies specify
857
			 * the JOIN between the tables and the used columns for joining. The
858
			 * ":joins" placeholder is then replaced by the JOIN strings from
859
			 * the sub-managers.
860
			 *
861
			 * To limit the records matched, conditions can be added to the given
862
			 * criteria object. It can contain comparisons like column names that
863
			 * must match specific values which can be combined by AND, OR or NOT
864
			 * operators. The resulting string of SQL conditions replaces the
865
			 * ":cond" placeholder before the statement is sent to the database
866
			 * server.
867
			 *
868
			 * If the records that are retrieved should be ordered by one or more
869
			 * columns, the generated string of column / sort direction pairs
870
			 * replaces the ":order" placeholder. In case no ordering is required,
871
			 * the complete ORDER BY part including the "\/*-orderby*\/...\/*orderby-*\/"
872
			 * markers is removed to speed up retrieving the records. Columns of
873
			 * sub-managers can also be used for ordering the result set but then
874
			 * no index can be used.
875
			 *
876
			 * The number of returned records can be limited and can start at any
877
			 * number between the begining and the end of the result set. For that
878
			 * the ":size" and ":start" placeholders are replaced by the
879
			 * corresponding values from the criteria object. The default values
880
			 * are 0 for the start and 100 for the size value.
881
			 *
882
			 * The SQL statement should conform to the ANSI standard to be
883
			 * compatible with most relational database systems. This also
884
			 * includes using double quotes for table and column names.
885
			 *
886
			 * @param string SQL statement for searching items
887
			 * @since 2014.03
888
			 * @category Developer
889
			 * @see mshop/media/manager/insert/ansi
890
			 * @see mshop/media/manager/update/ansi
891
			 * @see mshop/media/manager/newid/ansi
892
			 * @see mshop/media/manager/delete/ansi
893
			 * @see mshop/media/manager/count/ansi
894
			 */
895
			$cfgPathSearch = 'mshop/media/manager/search';
896
897
			/** mshop/media/manager/count/mysql
898
			 * Counts the number of records matched by the given criteria in the database
899
			 *
900
			 * @see mshop/media/manager/count/ansi
901
			 */
902
903
			/** mshop/media/manager/count/ansi
904
			 * Counts the number of records matched by the given criteria in the database
905
			 *
906
			 * Counts all records matched by the given criteria from the media
907
			 * database. The records must be from one of the sites that are
908
			 * configured via the context item. If the current site is part of
909
			 * a tree of sites, the statement can count all records from the
910
			 * current site and the complete sub-tree of sites.
911
			 *
912
			 * As the records can normally be limited by criteria from sub-managers,
913
			 * their tables must be joined in the SQL context. This is done by
914
			 * using the "internaldeps" property from the definition of the ID
915
			 * column of the sub-managers. These internal dependencies specify
916
			 * the JOIN between the tables and the used columns for joining. The
917
			 * ":joins" placeholder is then replaced by the JOIN strings from
918
			 * the sub-managers.
919
			 *
920
			 * To limit the records matched, conditions can be added to the given
921
			 * criteria object. It can contain comparisons like column names that
922
			 * must match specific values which can be combined by AND, OR or NOT
923
			 * operators. The resulting string of SQL conditions replaces the
924
			 * ":cond" placeholder before the statement is sent to the database
925
			 * server.
926
			 *
927
			 * Both, the strings for ":joins" and for ":cond" are the same as for
928
			 * the "search" SQL statement.
929
			 *
930
			 * Contrary to the "search" statement, it doesn't return any records
931
			 * but instead the number of records that have been found. As counting
932
			 * thousands of records can be a long running task, the maximum number
933
			 * of counted records is limited for performance reasons.
934
			 *
935
			 * The SQL statement should conform to the ANSI standard to be
936
			 * compatible with most relational database systems. This also
937
			 * includes using double quotes for table and column names.
938
			 *
939
			 * @param string SQL statement for counting items
940
			 * @since 2014.03
941
			 * @category Developer
942
			 * @see mshop/media/manager/insert/ansi
943
			 * @see mshop/media/manager/update/ansi
944
			 * @see mshop/media/manager/newid/ansi
945
			 * @see mshop/media/manager/delete/ansi
946
			 * @see mshop/media/manager/search/ansi
947
			 */
948
			$cfgPathCount = 'mshop/media/manager/count';
949
950
			$results = $this->searchItemsBase( $conn, $search, $cfgPathSearch, $cfgPathCount, $required, $total, $level );
951
952
			while( $row = $results->fetch() )
953
			{
954
				if( ( $row['media.previews'] = json_decode( $config = $row['media.previews'], true ) ) === null ) {
955
					$row['media.previews'] = [];
956
				}
957
958
				$map[$row['media.id']] = $row;
959
			}
960
961
		$propItems = []; $name = 'media/property';
962
		if( isset( $ref[$name] ) || in_array( $name, $ref, true ) )
963
		{
964
			$propTypes = isset( $ref[$name] ) && is_array( $ref[$name] ) ? $ref[$name] : null;
965
			$propItems = $this->getPropertyItems( array_keys( $map ), 'media', $propTypes );
966
		}
967
968
		return $this->buildItems( $map, $ref, 'media', $propItems );
969
	}
970
971
972
	/**
973
	 * Stores the uploaded file and returns the updated item
974
	 *
975
	 * @param \Aimeos\MShop\Media\Item\Iface $item Media item for storing the file meta data, "domain" must be set
976
	 * @param \Psr\Http\Message\UploadedFileInterface|null $file Uploaded file object
977
	 * @param \Psr\Http\Message\UploadedFileInterface|null $preview Uploaded preview image
978
	 * @return \Aimeos\MShop\Media\Item\Iface Updated media item including file and preview paths
979
	 */
980
	public function upload( \Aimeos\MShop\Media\Item\Iface $item, ?UploadedFileInterface $file, UploadedFileInterface $preview = null ) : \Aimeos\MShop\Media\Item\Iface
981
	{
982
		$domain = $item->getDomain() ?: '-';
983
		$fsname = $item->getFileSystem() ?: 'fs-media';
984
985
		if( $file && $file->getError() !== UPLOAD_ERR_NO_FILE && $this->isAllowed( $mime = $this->mimetype( $file ) ) )
986
		{
987
			$path = $this->path( $file->getClientFilename(), $mime, $domain );
988
			$this->context()->fs( $fsname )->write( $path, $this->sanitize( (string) $file->getStream(), $mime ) );
989
990
			$item->setLabel( $file->getClientFilename() )
991
				->setMimetype( $mime )
992
				->setUrl( $path );
993
		}
994
995
		if( $preview && $preview->getError() !== UPLOAD_ERR_NO_FILE && $this->isAllowed( $mime = $this->mimetype( $preview ) ) )
996
		{
997
			$path = $this->path( $preview->getClientFilename(), $mime, $domain );
998
			$this->context()->fs( $fsname )->write( $path, $this->sanitize( (string) $preview->getStream(), $mime ) );
999
1000
			$item->setPreview( $path );
1001
		}
1002
1003
		return $this->scale( $item );
1004
	}
1005
1006
1007
	/**
1008
	 * Creates a new media item instance.
1009
	 *
1010
	 * @param array $values Associative list of key/value pairs
1011
	 * @param \Aimeos\MShop\Common\Item\Lists\Iface[] $listItems List of list items
1012
	 * @param \Aimeos\MShop\Common\Item\Iface[] $refItems List of items referenced
1013
	 * @param \Aimeos\MShop\Common\Item\Property\Iface[] $propItems List of property items
1014
	 * @return \Aimeos\MShop\Media\Item\Iface New media item
1015
	 */
1016
	protected function createItemBase( array $values = [], array $listItems = [], array $refItems = [],
1017
		array $propItems = [] ) : \Aimeos\MShop\Common\Item\Iface
1018
	{
1019
		$values['.languageid'] = $this->languageId;
1020
1021
		return new \Aimeos\MShop\Media\Item\Standard( $values, $listItems, $refItems, $propItems );
1022
	}
1023
1024
1025
	/**
1026
	 * Creates scaled images according to the configuration settings
1027
	 *
1028
	 * @param \Intervention\Image\Interfaces\ImageInterface $image Media object
0 ignored issues
show
Bug introduced by
The type Intervention\Image\Interfaces\ImageInterface 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...
1029
	 * @param string $domain Domain the item is from, e.g. product, catalog, etc.
1030
	 * @param string $type Type of the item within the given domain, e.g. default, stage, etc.
1031
	 * @return \Intervention\Image\Interfaces\ImageInterface[] Associative list of image width as keys and scaled media object as values
1032
	 */
1033
	protected function createPreviews( ImageInterface $image, string $domain, string $type ) : array
1034
	{
1035
		$list = [];
1036
		$config = $this->context()->config();
1037
1038
		/** mshop/media/manager/previews/common
1039
		 * Scaling options for preview images
1040
		 *
1041
		 * For responsive images, several preview images of different sizes are
1042
		 * generated. This setting controls how many preview images are generated,
1043
		 * what's their maximum width and height and if the given width/height is
1044
		 * enforced by cropping images that doesn't fit.
1045
		 *
1046
		 * The setting must consist of a list image size definitions like:
1047
		 *
1048
		 *  [
1049
		 *    ['maxwidth' => 240, 'maxheight' => 320, 'force-size' => true],
1050
		 *    ['maxwidth' => 720, 'maxheight' => 960, 'force-size' => false],
1051
		 *    ['maxwidth' => 2160, 'maxheight' => 2880, 'force-size' => false],
1052
		 *  ]
1053
		 *
1054
		 * "maxwidth" sets the maximum allowed width of the image whereas
1055
		 * "maxheight" does the same for the maximum allowed height. If both
1056
		 * values are given, the image is scaled proportionally so it fits into
1057
		 * the box defined by both values. In case the image has different
1058
		 * proportions than the specified ones and "force-size" is false, the
1059
		 * image is resized to fit entirely into the specified box. One side of
1060
		 * the image will be shorter than it would be possible by the specified
1061
		 * box.
1062
		 *
1063
		 * If "force-size" is true, scaled images that doesn't fit into the
1064
		 * given maximum width/height are centered and then cropped. By default,
1065
		 * images aren't cropped.
1066
		 *
1067
		 * The values for "maxwidth" and "maxheight" can also be null or not
1068
		 * used. In that case, the width or height or both is unbound. If none
1069
		 * of the values are given, the image won't be scaled at all. If only
1070
		 * one value is set, the image will be scaled exactly to the given width
1071
		 * or height and the other side is scaled proportionally.
1072
		 *
1073
		 * You can also define different preview sizes for different domains (e.g.
1074
		 * for catalog images) and for different types (e.g. catalog stage images).
1075
		 * Use configuration settings like
1076
		 *
1077
		 *  mshop/media/manager/previews/previews/<domain>/
1078
		 *  mshop/media/manager/previews/previews/<domain>/<type>/
1079
		 *
1080
		 * for example:
1081
		 *
1082
		 *  mshop/media/manager/previews/catalog/previews => [
1083
		 *    ['maxwidth' => 240, 'maxheight' => 320, 'force-size' => true],
1084
		 *  ]
1085
		 *  mshop/media/manager/previews/catalog/previews => [
1086
		 *    ['maxwidth' => 400, 'maxheight' => 300, 'force-size' => false]
1087
		 *  ]
1088
		 *  mshop/media/manager/previews/catalog/stage/previews => [
1089
		 *    ['maxwidth' => 360, 'maxheight' => 320, 'force-size' => true],
1090
		 *    ['maxwidth' => 720, 'maxheight' => 480, 'force-size' => true]
1091
		 *  ]
1092
		 *
1093
		 * These settings will create two preview images for catalog stage images,
1094
		 * one with a different size for all other catalog images and all images
1095
		 * from other domains will be sized to 240x320px. The available domains
1096
		 * which can have images are:
1097
		 *
1098
		 * * attribute
1099
		 * * catalog
1100
		 * * product
1101
		 * * service
1102
		 * * supplier
1103
		 *
1104
		 * There are a few image types included per domain ("default" is always
1105
		 * available). You can also add your own types in the admin backend and
1106
		 * extend the frontend to display them where you need them.
1107
		 *
1108
		 * @param array List of image size definitions
1109
		 * @category Developer
1110
		 * @category User
1111
		 * @since 2019.07
1112
		 */
1113
		$previews = $config->get( 'mshop/media/manager/previews/common', [] );
1114
		$previews = $config->get( 'mshop/media/manager/previews/' . $domain, $previews );
1115
		$previews = $config->get( 'mshop/media/manager/previews/' . $domain . '/' . $type, $previews );
1116
1117
		foreach( $previews as $entry )
1118
		{
1119
			$force = $entry['force-size'] ?? 0;
1120
			$maxwidth = $entry['maxwidth'] ?? null;
1121
			$maxheight = $entry['maxheight'] ?? null;
1122
			$bg = ltrim( $entry['background'] ?? 'ffffffff', '#' );
1123
1124
			if( $this->call( 'filterPreviews', $image, $domain, $type, $maxwidth, $maxheight, $force ) )
1125
			{
1126
				$file = match( $force ) {
1127
					0 => $image->scaleDown( $maxwidth, $maxheight ),
1128
					1 => $image->pad( $maxwidth, $maxheight, $bg, 'center' ),
1129
					2 => $image->cover( $maxwidth, $maxheight )
1130
				};
1131
1132
				$list[$file->width()] = $file;
1133
			}
1134
		}
1135
1136
		return $list;
1137
	}
1138
1139
1140
	/**
1141
	 * Removes the previes images from the storage
1142
	 *
1143
	 * @param \Aimeos\MShop\Media\Item\Iface $item Media item which will contains the image URLs afterwards
1144
	 * @param array List of preview paths to remove
1145
	 * @return \Aimeos\MShop\Media\Item\Iface Media item with preview images removed
1146
	 */
1147
	protected function deletePreviews( \Aimeos\MShop\Media\Item\Iface $item, array $paths ) : \Aimeos\MShop\Media\Item\Iface
1148
	{
1149
		if( !empty( $paths = $this->call( 'removePreviews', $item, $paths ) ) )
1150
		{
1151
			$fs = $this->context()->fs( $item->getFileSystem() );
1152
1153
			foreach( $paths as $preview )
1154
			{
1155
				if( $preview && $fs->has( $preview ) ) {
1156
					$fs->rm( $preview );
1157
				}
1158
			}
1159
		}
1160
1161
		return $item;
1162
	}
1163
1164
1165
	/**
1166
	 * Tests if the preview image should be created
1167
	 *
1168
	 * @param \Intervention\Image\Interfaces\ImageInterface $image Media object
1169
	 * @param string $domain Domain the item is from, e.g. product, catalog, etc.
1170
	 * @param string $type Type of the item within the given domain, e.g. default, stage, etc.
1171
	 * @param int|null $width New width of the image or null for automatic calculation
1172
	 * @param int|null $height New height of the image or null for automatic calculation
1173
	 * @param int $fit "0" keeps image ratio, "1" adds padding while "2" crops image to enforce image size
1174
	 */
1175
	protected function filterPreviews( ImageInterface $image, string $domain, string $type,
1176
		?int $maxwidth, ?int $maxheight, int $force ) : bool
1177
	{
1178
		return true;
1179
	}
1180
1181
1182
	/**
1183
	 * Checks if the mime type is allowed
1184
	 *
1185
	 * @param string Mime type
0 ignored issues
show
Bug introduced by
The type Aimeos\MShop\Media\Manager\Mime 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...
1186
	 * @return bool TRUE if mime type is allowed
1187
	 * @throws \Aimeos\MShop\Media\Exception If mime type is not allowed
1188
	 */
1189
	protected function isAllowed( string $mimetype ) : bool
1190
	{
1191
		$context = $this->context();
1192
1193
		/** mshop/media/manager/allowedtypes
1194
		 * A list of mime types that are allowed for uploaded files
1195
		 *
1196
		 * The list of allowed mime types must be explicitly configured for the
1197
		 * uploaded files. Trying to upload and store a file not available in
1198
		 * the list of allowed mime types will result in an exception.
1199
		 *
1200
		 * @param array List of image mime types
1201
		 * @since 2024.01
1202
		 */
1203
		$default = [
1204
			'image/webp', 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml',
1205
			'application/epub+zip', 'application/pdf', 'application/zip',
1206
			'video/mp4', 'video/webm',
1207
			'audio/mpeg', 'audio/ogg', 'audio/weba'
1208
		];
1209
		$allowed = $context->config()->get( 'mshop/media/manager/allowedtypes', $default );
1210
1211
		if( !in_array( $mimetype, $allowed ) )
1212
		{
1213
			$msg = sprintf( $context->translate( 'mshop', 'Uploading mimetype "%1$s" is not allowed' ), $mimetype );
1214
			throw new \Aimeos\MShop\Media\Exception( $msg, 406 );
1215
		}
1216
1217
		return true;
1218
	}
1219
1220
1221
	/**
1222
	 * Returns the media object for the given file name
1223
	 *
1224
	 * @param \Aimeos\Base\Filesystem\Iface $fs File system where the file is stored
1225
	 * @param string $file URL or relative path to the file
1226
	 * @return \Intervention\Image\Interfaces\ImageInterface Image object
1227
	 */
1228
	protected function image( \Aimeos\Base\Filesystem\Iface $fs, string $file ) : ImageInterface
1229
	{
1230
		if( !isset( $this->driver ) )
1231
		{
1232
			if( class_exists( '\Intervention\Image\Vips\Driver' ) ) {
1233
				$driver = new \Intervention\Image\Vips\Driver();
0 ignored issues
show
Bug introduced by
The type Intervention\Image\Vips\Driver 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...
1234
			} elseif( class_exists( 'Imagick' ) ) {
1235
				$driver = new \Intervention\Image\Drivers\Imagick\Driver();
0 ignored issues
show
Bug introduced by
The type Intervention\Image\Drivers\Imagick\Driver 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...
1236
			} else {
1237
				$driver = new Intervention\Image\Drivers\Gd\Driver();
0 ignored issues
show
Bug introduced by
The type Aimeos\MShop\Media\Manag...Image\Drivers\Gd\Driver 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...
1238
			}
1239
1240
			$this->driver = new \Intervention\Image\ImageManager( $driver );
0 ignored issues
show
Bug Best Practice introduced by
The property driver does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
Bug introduced by
The type Intervention\Image\ImageManager 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...
1241
		}
1242
1243
		if( preg_match( '#^[a-zA-Z]{1,10}://#', $file ) === 1 )
1244
		{
1245
			if( ( $fh = fopen( $file, 'r' ) ) === false )
1246
			{
1247
				$msg = $this->context()->translate( 'mshop', 'Unable to open file "%1$s"' );
1248
				throw new \Aimeos\MShop\Media\Exception( sprintf( $msg, $file ) );
1249
			}
1250
		}
1251
		else
1252
		{
1253
			$fh = $this->context()->fs( 'fs-media' )->reads( $file );
1254
		}
1255
1256
		$image = $this->driver->read( $fh );
1257
		fclose( $fh );
1258
1259
		return $image;
1260
	}
1261
1262
1263
	/**
1264
	 * Creates a new file path from the given arguments
1265
	 *
1266
	 * @param string $filepath Original file name, can contain the path as well
1267
	 * @param string $mimetype Mime type
1268
	 * @param string $domain data domain
1269
	 * @return string New file name including the file path
1270
	 */
1271
	protected function path( string $filepath, string $mimetype, string $domain ) : string
1272
	{
1273
		$context = $this->context();
1274
1275
		/** mshop/media/manager/extensions
1276
		 * Available files extensions for mime types of uploaded files
1277
		 *
1278
		 * Uploaded files should have the right file extension (e.g. ".jpg" for
1279
		 * JPEG images) so files are recognized correctly if downloaded by users.
1280
		 * The extension of the uploaded file can't be trusted and only its mime
1281
		 * type can be determined automatically. This configuration setting
1282
		 * provides the file extensions for the configured mime types. You can
1283
		 * add more mime type / file extension combinations if required.
1284
		 *
1285
		 * @param array Associative list of mime types as keys and file extensions as values
1286
		 * @since 2018.04
1287
		 * @category Developer
1288
		 */
1289
		$default = ['image/gif' => 'gif', 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
1290
		$list = $context->config()->get( 'mshop/media/manager/extensions', $default );
1291
1292
		$filename = basename( $filepath );
1293
		$filename = \Aimeos\Base\Str::slug( substr( $filename, 0, strrpos( $filename, '.' ) ?: null ) );
1294
		$filename = substr( md5( $filename . getmypid() . microtime( true ) ), -8 ) . '_' . $filename;
1295
1296
		$ext = isset( $list[$mimetype] ) ? '.' . $list[$mimetype] : '';
1297
		$siteid = $context->locale()->getSiteId();
1298
1299
		// the "d" after {siteid} is the required extension for Windows (no dots at the end allowed)
1300
		return "{$siteid}d/{$domain}/{$filename[0]}/{$filename[1]}/{$filename}{$ext}";
1301
	}
1302
1303
1304
	/**
1305
	 * Returns the quality level of the resized images
1306
	 *
1307
	 * @return int Quality level from 0 to 100
1308
	 */
1309
	protected function quality() : int
1310
	{
1311
		/** mshop/media/manager/quality
1312
		 * Quality level of saved images
1313
		 *
1314
		 * Qualitity level must be an integer from 0 (worst) to 100 (best).
1315
		 * The higher the quality, the bigger the file size.
1316
		 *
1317
		 * @param int Quality level from 0 to 100
1318
		 * @since 2024.01
1319
		 */
1320
		return $this->context()->config()->get( 'mshop/media/manager/quality', 75 );
1321
	}
1322
1323
1324
	/**
1325
	 * Returns the preview images to be deleted
1326
	 *
1327
	 * @param \Aimeos\MShop\Media\Item\Iface $item Media item with new preview URLs
1328
	 * @param array List of preview paths to remove
1329
	 * @return iterable List of preview URLs to remove
1330
	 */
1331
	protected function removePreviews( \Aimeos\MShop\Media\Item\Iface $item, array $paths ) : iterable
1332
	{
1333
		$previews = $item->getPreviews();
1334
1335
		// don't delete first (smallest) image because it may be referenced in past orders
1336
		if( $item->getDomain() === 'product' && in_array( key( $previews ), $paths ) ) {
1337
			return array_slice( $paths, 1 );
1338
		}
1339
1340
		return $paths;
1341
	}
1342
1343
1344
	/**
1345
	 * Sanitizes the uploaded file
1346
	 *
1347
	 * @param string $content File content
1348
	 * @param string $mimetype File mime type
1349
	 * @return string Sanitized content
1350
	 */
1351
	protected function sanitize( string $content, string $mimetype ) : string
1352
	{
1353
		if( strncmp( 'image/svg', $mimetype, 9 ) === 0 )
1354
		{
1355
			$sanitizer = new Sanitizer();
1356
			$sanitizer->removeRemoteReferences( true );
1357
1358
			if( ( $content = $sanitizer->sanitize( $content ) ) === false )
1359
			{
1360
				$msg = $this->context()->translate( 'mshop', 'Invalid SVG file: %1$s' );
1361
				throw new \Aimeos\MShop\Media\Exception\Exception( sprintf( $msg, print_r( $sanitizer->getXmlIssues(), true ) ) );
0 ignored issues
show
Bug introduced by
The type Aimeos\MShop\Media\Exception\Exception 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...
1362
			}
1363
		}
1364
1365
		if( $fcn = self::macro( 'sanitize' ) ) {
1366
			$content = $fcn( $content, $mimetype );
1367
		}
1368
1369
		return $content;
1370
	}
1371
1372
1373
	/**
1374
	 * Called after the image has been scaled
1375
	 * Can be used to update the media item with image information.
1376
	 *
1377
	 * @param \Aimeos\MShop\Media\Item\Iface $item Media item with new preview URLs
1378
	 * @param \Intervention\Image\Interfaces\ImageInterface $image Media object
1379
	 */
1380
	protected function scaled( \Aimeos\MShop\Media\Item\Iface $item, ImageInterface $image )
1381
	{
1382
	}
1383
}
1384