vendor/pimcore/pimcore/models/Asset.php line 1778

Open in your IDE?
  1. <?php
  2. /**
  3.  * Pimcore
  4.  *
  5.  * This source file is available under two different licenses:
  6.  * - GNU General Public License version 3 (GPLv3)
  7.  * - Pimcore Commercial License (PCL)
  8.  * Full copyright and license information is available in
  9.  * LICENSE.md which is distributed with this source code.
  10.  *
  11.  *  @copyright  Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  12.  *  @license    http://www.pimcore.org/license     GPLv3 and PCL
  13.  */
  14. namespace Pimcore\Model;
  15. use Doctrine\DBAL\Exception\DeadlockException;
  16. use League\Flysystem\FilesystemOperator;
  17. use League\Flysystem\StorageAttributes;
  18. use League\Flysystem\UnableToMoveFile;
  19. use Pimcore\Event\AssetEvents;
  20. use Pimcore\Event\FrontendEvents;
  21. use Pimcore\Event\Model\AssetEvent;
  22. use Pimcore\File;
  23. use Pimcore\Helper\TemporaryFileHelperTrait;
  24. use Pimcore\Loader\ImplementationLoader\Exception\UnsupportedException;
  25. use Pimcore\Localization\LocaleServiceInterface;
  26. use Pimcore\Logger;
  27. use Pimcore\Messenger\AssetUpdateTasksMessage;
  28. use Pimcore\Messenger\VersionDeleteMessage;
  29. use Pimcore\Model\Asset\Listing;
  30. use Pimcore\Model\Asset\MetaData\ClassDefinition\Data\Data;
  31. use Pimcore\Model\Asset\MetaData\ClassDefinition\Data\DataDefinitionInterface;
  32. use Pimcore\Model\Element\ElementInterface;
  33. use Pimcore\Model\Element\Service;
  34. use Pimcore\Model\Element\Traits\ScheduledTasksTrait;
  35. use Pimcore\Model\Element\ValidationException;
  36. use Pimcore\Model\Exception\NotFoundException;
  37. use Pimcore\Tool;
  38. use Pimcore\Tool\Storage;
  39. use Symfony\Component\EventDispatcher\GenericEvent;
  40. use Symfony\Component\Messenger\MessageBusInterface;
  41. use Symfony\Component\Mime\MimeTypes;
  42. /**
  43.  * @method \Pimcore\Model\Asset\Dao getDao()
  44.  * @method bool __isBasedOnLatestData()
  45.  * @method int getChildAmount($user = null)
  46.  * @method string|null getCurrentFullPath()
  47.  */
  48. class Asset extends Element\AbstractElement
  49. {
  50.     use ScheduledTasksTrait;
  51.     use TemporaryFileHelperTrait;
  52.     /**
  53.      * all possible types of assets
  54.      *
  55.      * @internal
  56.      *
  57.      * @var array
  58.      */
  59.     public static $types = ['folder''image''text''audio''video''document''archive''unknown'];
  60.     /**
  61.      * @internal
  62.      *
  63.      * @var int|null
  64.      */
  65.     protected $id;
  66.     /**
  67.      * @internal
  68.      *
  69.      * @var int|null
  70.      */
  71.     protected $parentId;
  72.     /**
  73.      * @internal
  74.      *
  75.      * @var self|null
  76.      */
  77.     protected $parent;
  78.     /**
  79.      * @internal
  80.      *
  81.      * @var string
  82.      */
  83.     protected $type '';
  84.     /**
  85.      * @internal
  86.      *
  87.      * @var string|null
  88.      */
  89.     protected $filename;
  90.     /**
  91.      * @internal
  92.      *
  93.      * @var string|null
  94.      */
  95.     protected $path;
  96.     /**
  97.      * @internal
  98.      *
  99.      * @var string|null
  100.      */
  101.     protected $mimetype;
  102.     /**
  103.      * @internal
  104.      *
  105.      * @var int|null
  106.      */
  107.     protected $creationDate;
  108.     /**
  109.      * @internal
  110.      *
  111.      * @var int|null
  112.      */
  113.     protected $modificationDate;
  114.     /**
  115.      * @internal
  116.      *
  117.      * @var resource|null
  118.      */
  119.     protected $stream;
  120.     /**
  121.      * @internal
  122.      *
  123.      * @var int|null
  124.      */
  125.     protected ?int $userOwner null;
  126.     /**
  127.      * @internal
  128.      *
  129.      * @var int|null
  130.      */
  131.     protected ?int $userModification null;
  132.     /**
  133.      * @internal
  134.      *
  135.      * @var array
  136.      */
  137.     protected $properties null;
  138.     /**
  139.      * @internal
  140.      *
  141.      * @var array|null
  142.      */
  143.     protected $versions null;
  144.     /**
  145.      * @internal
  146.      *
  147.      * @var array
  148.      */
  149.     protected $metadata = [];
  150.     /**
  151.      * @internal
  152.      *
  153.      * enum('self','propagate') nullable
  154.      *
  155.      * @var string|null
  156.      */
  157.     protected $locked;
  158.     /**
  159.      * List of some custom settings  [key] => value
  160.      * Here there can be stored some data, eg. the video thumbnail files, ...  of the asset, ...
  161.      *
  162.      * @internal
  163.      *
  164.      * @var array
  165.      */
  166.     protected $customSettings = [];
  167.     /**
  168.      * @internal
  169.      *
  170.      * @var bool
  171.      */
  172.     protected $hasMetaData false;
  173.     /**
  174.      * @internal
  175.      *
  176.      * @var array|null
  177.      */
  178.     protected $siblings;
  179.     /**
  180.      * @internal
  181.      *
  182.      * @var bool|null
  183.      */
  184.     protected $hasSiblings;
  185.     /**
  186.      * @internal
  187.      *
  188.      * @var bool
  189.      */
  190.     protected $dataChanged false;
  191.     /**
  192.      * @internal
  193.      *
  194.      * @var int
  195.      */
  196.     protected $versionCount 0;
  197.     /**
  198.      *
  199.      * @return array
  200.      */
  201.     public static function getTypes()
  202.     {
  203.         return self::$types;
  204.     }
  205.     /**
  206.      * Static helper to get an asset by the passed path
  207.      *
  208.      * @param string $path
  209.      * @param bool $force
  210.      *
  211.      * @return static|null
  212.      */
  213.     public static function getByPath($path$force false)
  214.     {
  215.         if (!$path) {
  216.             return null;
  217.         }
  218.         $path Element\Service::correctPath($path);
  219.         try {
  220.             $asset = new Asset();
  221.             $asset->getDao()->getByPath($path);
  222.             return static::getById($asset->getId(), $force);
  223.         } catch (NotFoundException $e) {
  224.             return null;
  225.         }
  226.     }
  227.     /**
  228.      * @internal
  229.      *
  230.      * @param Asset $asset
  231.      *
  232.      * @return bool
  233.      */
  234.     protected static function typeMatch(Asset $asset)
  235.     {
  236.         $staticType get_called_class();
  237.         if ($staticType != Asset::class) {
  238.             if (!$asset instanceof $staticType) {
  239.                 return false;
  240.             }
  241.         }
  242.         return true;
  243.     }
  244.     /**
  245.      * @param int $id
  246.      * @param bool $force
  247.      *
  248.      * @return static|null
  249.      */
  250.     public static function getById($id$force false)
  251.     {
  252.         if (!is_numeric($id) || $id 1) {
  253.             return null;
  254.         }
  255.         $id = (int)$id;
  256.         $cacheKey self::getCacheKey($id);
  257.         if (!$force && \Pimcore\Cache\Runtime::isRegistered($cacheKey)) {
  258.             $asset \Pimcore\Cache\Runtime::get($cacheKey);
  259.             if ($asset && static::typeMatch($asset)) {
  260.                 return $asset;
  261.             }
  262.         }
  263.         if ($force || !($asset \Pimcore\Cache::load($cacheKey))) {
  264.             $asset = new Asset();
  265.             try {
  266.                 $asset->getDao()->getById($id);
  267.                 $className 'Pimcore\\Model\\Asset\\' ucfirst($asset->getType());
  268.                 /** @var Asset $asset */
  269.                 $asset self::getModelFactory()->build($className);
  270.                 \Pimcore\Cache\Runtime::set($cacheKey$asset);
  271.                 $asset->getDao()->getById($id);
  272.                 $asset->__setDataVersionTimestamp($asset->getModificationDate());
  273.                 $asset->resetDirtyMap();
  274.                 \Pimcore\Cache::save($asset$cacheKey);
  275.             } catch (NotFoundException $e) {
  276.                 return null;
  277.             }
  278.         } else {
  279.             \Pimcore\Cache\Runtime::set($cacheKey$asset);
  280.         }
  281.         if (!$asset || !static::typeMatch($asset)) {
  282.             return null;
  283.         }
  284.         return $asset;
  285.     }
  286.     /**
  287.      * @param int $parentId
  288.      * @param array $data
  289.      * @param bool $save
  290.      *
  291.      * @return Asset
  292.      */
  293.     public static function create($parentId$data = [], $save true)
  294.     {
  295.         // create already the real class for the asset type, this is especially for images, because a system-thumbnail
  296.         // (tree) is generated immediately after creating an image
  297.         $class Asset::class;
  298.         if (array_key_exists('filename'$data) && (array_key_exists('data'$data) || array_key_exists('sourcePath'$data) || array_key_exists('stream'$data))) {
  299.             if (array_key_exists('data'$data) || array_key_exists('stream'$data)) {
  300.                 $tmpFile PIMCORE_SYSTEM_TEMP_DIRECTORY '/asset-create-tmp-file-' uniqid() . '.' File::getFileExtension($data['filename']);
  301.                 if (array_key_exists('data'$data)) {
  302.                     File::put($tmpFile$data['data']);
  303.                     self::checkMaxPixels($tmpFile$data);
  304.                     $mimeType MimeTypes::getDefault()->guessMimeType($tmpFile);
  305.                     unlink($tmpFile);
  306.                 } else {
  307.                     $streamMeta stream_get_meta_data($data['stream']);
  308.                     if (file_exists($streamMeta['uri'])) {
  309.                         // stream is a local file, so we don't have to write a tmp file
  310.                         self::checkMaxPixels($streamMeta['uri'], $data);
  311.                         $mimeType MimeTypes::getDefault()->guessMimeType($streamMeta['uri']);
  312.                     } else {
  313.                         // write a tmp file because the stream isn't a pointer to the local filesystem
  314.                         $isRewindable = @rewind($data['stream']);
  315.                         $dest fopen($tmpFile'w+'falseFile::getContext());
  316.                         stream_copy_to_stream($data['stream'], $dest);
  317.                         self::checkMaxPixels($tmpFile$data);
  318.                         $mimeType MimeTypes::getDefault()->guessMimeType($tmpFile);
  319.                         if (!$isRewindable) {
  320.                             $data['stream'] = $dest;
  321.                         } else {
  322.                             fclose($dest);
  323.                             unlink($tmpFile);
  324.                         }
  325.                     }
  326.                 }
  327.             } else {
  328.                 if (is_dir($data['sourcePath'])) {
  329.                     $mimeType 'directory';
  330.                 } else {
  331.                     self::checkMaxPixels($data['sourcePath'], $data);
  332.                     $mimeType MimeTypes::getDefault()->guessMimeType($data['sourcePath']);
  333.                     if (is_file($data['sourcePath'])) {
  334.                         $data['stream'] = fopen($data['sourcePath'], 'rb'falseFile::getContext());
  335.                     }
  336.                 }
  337.                 unset($data['sourcePath']);
  338.             }
  339.             $type self::getTypeFromMimeMapping($mimeType$data['filename']);
  340.             $class '\\Pimcore\\Model\\Asset\\' ucfirst($type);
  341.             if (array_key_exists('type'$data)) {
  342.                 unset($data['type']);
  343.             }
  344.         }
  345.         /** @var Asset $asset */
  346.         $asset self::getModelFactory()->build($class);
  347.         $asset->setParentId($parentId);
  348.         self::checkCreateData($data);
  349.         $asset->setValues($data);
  350.         if ($save) {
  351.             $asset->save();
  352.         }
  353.         return $asset;
  354.     }
  355.     private static function checkMaxPixels(string $localPath, array $data): void
  356.     {
  357.         // this check is intentionally done in Asset::create() because in Asset::update() it would result
  358.         // in an additional download from remote storage if configured, so in terms of performance
  359.         // this is the more efficient way
  360.         $maxPixels = (int) \Pimcore::getContainer()->getParameter('pimcore.config')['assets']['image']['max_pixels'];
  361.         if ($size = @getimagesize($localPath)) {
  362.             $imagePixels = (int) ($size[0] * $size[1]);
  363.             if ($imagePixels $maxPixels) {
  364.                 Logger::error("Image to be created {$localPath} (temp. path) exceeds max pixel size of {$maxPixels}, you can change the value in config pimcore.assets.image.max_pixels");
  365.                 $diff sqrt(+ ($maxPixels $imagePixels));
  366.                 $suggestion_0 = (int) round($size[0] / $diff, -2PHP_ROUND_HALF_DOWN);
  367.                 $suggestion_1 = (int) round($size[1] / $diff, -2PHP_ROUND_HALF_DOWN);
  368.                 $mp $maxPixels 1_000_000;
  369.                 throw new ValidationException("<p>Image dimensions of <em>{$data['filename']}</em> are too large.</p>
  370. <p>Max size: <code>{$mp}</code> <abbr title='Million pixels'>Megapixels</abbr></p>
  371. <p>Suggestion: resize to <code>{$suggestion_0}&times;{$suggestion_1}</code> pixels or smaller.</p>");
  372.             }
  373.         }
  374.     }
  375.     /**
  376.      * @param array $config
  377.      *
  378.      * @return mixed
  379.      *
  380.      * @throws \Exception
  381.      */
  382.     public static function getList($config = [])
  383.     {
  384.         if (!\is_array($config)) {
  385.             throw new \Exception('Unable to initiate list class - please provide valid configuration array');
  386.         }
  387.         $listClass Listing::class;
  388.         $list self::getModelFactory()->build($listClass);
  389.         $list->setValues($config);
  390.         return $list;
  391.     }
  392.     /**
  393.      * @deprecated will be removed in Pimcore 11
  394.      *
  395.      * @param array $config
  396.      *
  397.      * @return int total count
  398.      */
  399.     public static function getTotalCount($config = [])
  400.     {
  401.         $list = static::getList($config);
  402.         $count $list->getTotalCount();
  403.         return $count;
  404.     }
  405.     /**
  406.      * @internal
  407.      *
  408.      * @param string $mimeType
  409.      * @param string $filename
  410.      *
  411.      * @return string
  412.      */
  413.     public static function getTypeFromMimeMapping($mimeType$filename)
  414.     {
  415.         if ($mimeType == 'directory') {
  416.             return 'folder';
  417.         }
  418.         $type null;
  419.         $mappings = [
  420.             'unknown' => ["/\.stp$/"],
  421.             'image' => ['/image/'"/\.eps$/""/\.ai$/""/\.svgz$/""/\.pcx$/""/\.iff$/""/\.pct$/""/\.wmf$/"],
  422.             'text' => ['/text/''/xml$/''/\.json$/'],
  423.             'audio' => ['/audio/'],
  424.             'video' => ['/video/'],
  425.             'document' => ['/msword/''/pdf/''/powerpoint/''/office/''/excel/''/opendocument/'],
  426.             'archive' => ['/zip/''/tar/'],
  427.         ];
  428.         foreach ($mappings as $assetType => $patterns) {
  429.             foreach ($patterns as $pattern) {
  430.                 if (preg_match($pattern$mimeType ' .' File::getFileExtension($filename))) {
  431.                     $type $assetType;
  432.                     break;
  433.                 }
  434.             }
  435.             // break at first match
  436.             if ($type) {
  437.                 break;
  438.             }
  439.         }
  440.         if (!$type) {
  441.             $type 'unknown';
  442.         }
  443.         return $type;
  444.     }
  445.     /**
  446.      * {@inheritdoc}
  447.      */
  448.     public function save()
  449.     {
  450.         // additional parameters (e.g. "versionNote" for the version note)
  451.         $params = [];
  452.         if (func_num_args() && is_array(func_get_arg(0))) {
  453.             $params func_get_arg(0);
  454.         }
  455.         $isUpdate false;
  456.         $differentOldPath null;
  457.         try {
  458.             $preEvent = new AssetEvent($this$params);
  459.             if ($this->getId()) {
  460.                 $isUpdate true;
  461.                 \Pimcore::getEventDispatcher()->dispatch($preEventAssetEvents::PRE_UPDATE);
  462.             } else {
  463.                 \Pimcore::getEventDispatcher()->dispatch($preEventAssetEvents::PRE_ADD);
  464.             }
  465.             $params $preEvent->getArguments();
  466.             $this->correctPath();
  467.             // we wrap the save actions in a loop here, so that we can restart the database transactions in the case it fails
  468.             // if a transaction fails it gets restarted $maxRetries times, then the exception is thrown out
  469.             // this is especially useful to avoid problems with deadlocks in multi-threaded environments (forked workers, ...)
  470.             $maxRetries 5;
  471.             for ($retries 0$retries $maxRetries$retries++) {
  472.                 $this->beginTransaction();
  473.                 try {
  474.                     if (!$isUpdate) {
  475.                         $this->getDao()->create();
  476.                     }
  477.                     // get the old path from the database before the update is done
  478.                     $oldPath null;
  479.                     if ($isUpdate) {
  480.                         $oldPath $this->getDao()->getCurrentFullPath();
  481.                     }
  482.                     $this->update($params);
  483.                     $storage Storage::get('asset');
  484.                     // if the old path is different from the new path, update all children
  485.                     $updatedChildren = [];
  486.                     if ($oldPath && $oldPath != $this->getRealFullPath()) {
  487.                         $differentOldPath $oldPath;
  488.                         try {
  489.                             $storage->move($oldPath$this->getRealFullPath());
  490.                         } catch (UnableToMoveFile $e) {
  491.                             //update children, if unable to move parent
  492.                             $this->updateChildPaths($storage$oldPath);
  493.                         }
  494.                         $this->getDao()->updateWorkspaces();
  495.                         $updatedChildren $this->getDao()->updateChildPaths($oldPath);
  496.                         $this->relocateThumbnails($oldPath);
  497.                     }
  498.                     // lastly create a new version if necessary
  499.                     // this has to be after the registry update and the DB update, otherwise this would cause problem in the
  500.                     // $this->__wakeUp() method which is called by $version->save(); (path correction for version restore)
  501.                     if ($this->getType() != 'folder') {
  502.                         $this->saveVersion(falsefalse, isset($params['versionNote']) ? $params['versionNote'] : null);
  503.                     }
  504.                     $this->commit();
  505.                     break; // transaction was successfully completed, so we cancel the loop here -> no restart required
  506.                 } catch (\Exception $e) {
  507.                     try {
  508.                         $this->rollBack();
  509.                     } catch (\Exception $er) {
  510.                         // PDO adapter throws exceptions if rollback fails
  511.                         Logger::error($er);
  512.                     }
  513.                     // we try to start the transaction $maxRetries times again (deadlocks, ...)
  514.                     if ($e instanceof DeadlockException && $retries < ($maxRetries 1)) {
  515.                         $run $retries 1;
  516.                         $waitTime rand(15) * 100000// microseconds
  517.                         Logger::warn('Unable to finish transaction (' $run ". run) because of the following reason '" $e->getMessage() . "'. --> Retrying in " $waitTime ' microseconds ... (' . ($run 1) . ' of ' $maxRetries ')');
  518.                         usleep($waitTime); // wait specified time until we restart the transaction
  519.                     } else {
  520.                         // if the transaction still fail after $maxRetries retries, we throw out the exception
  521.                         throw $e;
  522.                     }
  523.                 }
  524.             }
  525.             $additionalTags = [];
  526.             if (isset($updatedChildren) && is_array($updatedChildren)) {
  527.                 foreach ($updatedChildren as $assetId) {
  528.                     $tag 'asset_' $assetId;
  529.                     $additionalTags[] = $tag;
  530.                     // remove the child also from registry (internal cache) to avoid path inconsistencies during long running scripts, such as CLI
  531.                     \Pimcore\Cache\Runtime::set($tagnull);
  532.                 }
  533.             }
  534.             $this->clearDependentCache($additionalTags);
  535.             if ($this->getDataChanged()) {
  536.                 if (in_array($this->getType(), ['image''video''document'])) {
  537.                     $this->addToUpdateTaskQueue();
  538.                 }
  539.             }
  540.             $this->setDataChanged(false);
  541.             $postEvent = new AssetEvent($this$params);
  542.             if ($isUpdate) {
  543.                 if ($differentOldPath) {
  544.                     $postEvent->setArgument('oldPath'$differentOldPath);
  545.                 }
  546.                 \Pimcore::getEventDispatcher()->dispatch($postEventAssetEvents::POST_UPDATE);
  547.             } else {
  548.                 \Pimcore::getEventDispatcher()->dispatch($postEventAssetEvents::POST_ADD);
  549.             }
  550.             return $this;
  551.         } catch (\Exception $e) {
  552.             $failureEvent = new AssetEvent($this$params);
  553.             $failureEvent->setArgument('exception'$e);
  554.             if ($isUpdate) {
  555.                 \Pimcore::getEventDispatcher()->dispatch($failureEventAssetEvents::POST_UPDATE_FAILURE);
  556.             } else {
  557.                 \Pimcore::getEventDispatcher()->dispatch($failureEventAssetEvents::POST_ADD_FAILURE);
  558.             }
  559.             throw $e;
  560.         }
  561.     }
  562.     /**
  563.      * @internal
  564.      *
  565.      * @throws \Exception
  566.      */
  567.     public function correctPath()
  568.     {
  569.         // set path
  570.         if ($this->getId() != 1) { // not for the root node
  571.             if (!Element\Service::isValidKey($this->getKey(), 'asset')) {
  572.                 throw new \Exception("invalid filename '" $this->getKey() . "' for asset with id [ " $this->getId() . ' ]');
  573.             }
  574.             if ($this->getParentId() == $this->getId()) {
  575.                 throw new \Exception("ParentID and ID is identical, an element can't be the parent of itself.");
  576.             }
  577.             if ($this->getFilename() === '..' || $this->getFilename() === '.') {
  578.                 throw new \Exception('Cannot create asset called ".." or "."');
  579.             }
  580.             $parent Asset::getById($this->getParentId());
  581.             if ($parent) {
  582.                 // use the parent's path from the database here (getCurrentFullPath), to ensure the path really exists and does not rely on the path
  583.                 // that is currently in the parent asset (in memory), because this might have changed but wasn't not saved
  584.                 $this->setPath(str_replace('//''/'$parent->getCurrentFullPath() . '/'));
  585.             } else {
  586.                 // parent document doesn't exist anymore, set the parent to to root
  587.                 $this->setParentId(1);
  588.                 $this->setPath('/');
  589.             }
  590.         } elseif ($this->getId() == 1) {
  591.             // some data in root node should always be the same
  592.             $this->setParentId(0);
  593.             $this->setPath('/');
  594.             $this->setFilename('');
  595.             $this->setType('folder');
  596.         }
  597.         // do not allow PHP and .htaccess files
  598.         if (preg_match("@\.ph(p[\d+]?|t|tml|ps|ar)$@i"$this->getFilename()) || $this->getFilename() == '.htaccess') {
  599.             $this->setFilename($this->getFilename() . '.txt');
  600.         }
  601.         if (mb_strlen($this->getFilename()) > 255) {
  602.             throw new \Exception('Filenames longer than 255 characters are not allowed');
  603.         }
  604.         if (Asset\Service::pathExists($this->getRealFullPath())) {
  605.             $duplicate Asset::getByPath($this->getRealFullPath());
  606.             if ($duplicate instanceof Asset && $duplicate->getId() != $this->getId()) {
  607.                 throw new \Exception('Duplicate full path [ ' $this->getRealFullPath() . ' ] - cannot save asset');
  608.             }
  609.         }
  610.         $this->validatePathLength();
  611.     }
  612.     /**
  613.      * @internal
  614.      *
  615.      * @param array $params additional parameters (e.g. "versionNote" for the version note)
  616.      *
  617.      * @throws \Exception
  618.      */
  619.     protected function update($params = [])
  620.     {
  621.         $storage Storage::get('asset');
  622.         $this->updateModificationInfos();
  623.         $path $this->getRealFullPath();
  624.         $typeChanged false;
  625.         if ($this->getType() != 'folder') {
  626.             if ($this->getDataChanged()) {
  627.                 $src $this->getStream();
  628.                 // Write original data to temp path for writing stream
  629.                 // as original file will be deleted before overwrite
  630.                 $pathInfo pathinfo($this->getFilename());
  631.                 $tempFilePath $this->getRealPath() . uniqid('temp_');
  632.                 if ($pathInfo['extension'] ?? false) {
  633.                     $tempFilePath .= '.' $pathInfo['extension'];
  634.                 }
  635.                 $storage->writeStream($tempFilePath$src);
  636.                 $dbPath $this->getDao()->getCurrentFullPath();
  637.                 if ($dbPath !== $path && $storage->fileExists($dbPath)) {
  638.                     $storage->delete($dbPath);
  639.                 }
  640.                 if ($storage->fileExists($path)) {
  641.                     // We don't open a stream on existing files, because they could be possibly used by versions
  642.                     // using hardlinks, so it's safer to delete them first, so the inode and therefore also the
  643.                     // versioning information persists. Using the stream on the existing file would overwrite the
  644.                     // contents of the inode and therefore leads to wrong version data
  645.                     $storage->delete($path);
  646.                 }
  647.                 $storage->move($tempFilePath$path);
  648.                 $this->closeStream(); // set stream to null, so that the source stream isn't used anymore after saving
  649.                 $mimeType $storage->mimeType($path);
  650.                 $this->setMimeType($mimeType);
  651.                 // set type
  652.                 $type self::getTypeFromMimeMapping($mimeType$this->getFilename());
  653.                 if ($type != $this->getType()) {
  654.                     $this->setType($type);
  655.                     $typeChanged true;
  656.                 }
  657.                 // not only check if the type is set but also if the implementation can be found
  658.                 $className 'Pimcore\\Model\\Asset\\' ucfirst($this->getType());
  659.                 if (!self::getModelFactory()->supports($className)) {
  660.                     throw new \Exception('unable to resolve asset implementation with type: ' $this->getType());
  661.                 }
  662.             }
  663.         } else {
  664.             $storage->createDirectory($path);
  665.         }
  666.         if (!$this->getType()) {
  667.             $this->setType('unknown');
  668.         }
  669.         $this->postPersistData();
  670.         // save properties
  671.         $this->getProperties();
  672.         $this->getDao()->deleteAllProperties();
  673.         if (is_array($this->getProperties()) && count($this->getProperties()) > 0) {
  674.             foreach ($this->getProperties() as $property) {
  675.                 if (!$property->getInherited()) {
  676.                     $property->setDao(null);
  677.                     $property->setCid($this->getId());
  678.                     $property->setCtype('asset');
  679.                     $property->setCpath($this->getRealFullPath());
  680.                     $property->save();
  681.                 }
  682.             }
  683.         }
  684.         // save dependencies
  685.         $d = new Dependency();
  686.         $d->setSourceType('asset');
  687.         $d->setSourceId($this->getId());
  688.         foreach ($this->resolveDependencies() as $requirement) {
  689.             if ($requirement['id'] == $this->getId() && $requirement['type'] == 'asset') {
  690.                 // dont't add a reference to yourself
  691.                 continue;
  692.             } else {
  693.                 $d->addRequirement($requirement['id'], $requirement['type']);
  694.             }
  695.         }
  696.         $d->save();
  697.         $this->getDao()->update();
  698.         //set asset to registry
  699.         $cacheKey self::getCacheKey($this->getId());
  700.         \Pimcore\Cache\Runtime::set($cacheKey$this);
  701.         if (get_class($this) == 'Asset' || $typeChanged) {
  702.             // get concrete type of asset
  703.             // this is important because at the time of creating an asset it's not clear which type (resp. class) it will have
  704.             // the type (image, document, ...) depends on the mime-type
  705.             \Pimcore\Cache\Runtime::set($cacheKeynull);
  706.             Asset::getById($this->getId()); // call it to load it to the runtime cache again
  707.         }
  708.         $this->closeStream();
  709.     }
  710.     /**
  711.      * @internal
  712.      */
  713.     protected function postPersistData()
  714.     {
  715.         // hook for the save process, can be overwritten in implementations, such as Image
  716.     }
  717.     /**
  718.      * @param bool $setModificationDate
  719.      * @param bool $saveOnlyVersion
  720.      * @param string $versionNote version note
  721.      *
  722.      * @return null|Version
  723.      *
  724.      * @throws \Exception
  725.      */
  726.     public function saveVersion($setModificationDate true$saveOnlyVersion true$versionNote null)
  727.     {
  728.         try {
  729.             // hook should be also called if "save only new version" is selected
  730.             if ($saveOnlyVersion) {
  731.                 $event = new AssetEvent($this, [
  732.                     'saveVersionOnly' => true,
  733.                 ]);
  734.                 \Pimcore::getEventDispatcher()->dispatch($eventAssetEvents::PRE_UPDATE);
  735.             }
  736.             // set date
  737.             if ($setModificationDate) {
  738.                 $this->setModificationDate(time());
  739.             }
  740.             // scheduled tasks are saved always, they are not versioned!
  741.             $this->saveScheduledTasks();
  742.             // create version
  743.             $version null;
  744.             // only create a new version if there is at least 1 allowed
  745.             // or if saveVersion() was called directly (it's a newer version of the asset)
  746.             $assetsConfig \Pimcore\Config::getSystemConfiguration('assets');
  747.             if ((is_null($assetsConfig['versions']['days'] ?? null) && is_null($assetsConfig['versions']['steps'] ?? null))
  748.                 || (!empty($assetsConfig['versions']['steps']))
  749.                 || !empty($assetsConfig['versions']['days'])
  750.                 || $setModificationDate) {
  751.                 $saveStackTrace = !($assetsConfig['versions']['disable_stack_trace'] ?? false);
  752.                 $version $this->doSaveVersion($versionNote$saveOnlyVersion$saveStackTrace);
  753.             }
  754.             // hook should be also called if "save only new version" is selected
  755.             if ($saveOnlyVersion) {
  756.                 $event = new AssetEvent($this, [
  757.                     'saveVersionOnly' => true,
  758.                 ]);
  759.                 \Pimcore::getEventDispatcher()->dispatch($eventAssetEvents::POST_UPDATE);
  760.             }
  761.             return $version;
  762.         } catch (\Exception $e) {
  763.             $event = new AssetEvent($this, [
  764.                 'saveVersionOnly' => true,
  765.                 'exception' => $e,
  766.             ]);
  767.             \Pimcore::getEventDispatcher()->dispatch($eventAssetEvents::POST_UPDATE_FAILURE);
  768.             throw $e;
  769.         }
  770.     }
  771.     /**
  772.      * {@inheritdoc}
  773.      */
  774.     public function getFullPath()
  775.     {
  776.         $path $this->getPath() . $this->getFilename();
  777.         if (Tool::isFrontend()) {
  778.             return $this->getFrontendFullPath();
  779.         }
  780.         return $path;
  781.     }
  782.     /**
  783.      * Returns the full path of the asset (listener aware)
  784.      *
  785.      * @return string
  786.      *
  787.      * @internal
  788.      */
  789.     public function getFrontendFullPath()
  790.     {
  791.         $path $this->getPath() . $this->getFilename();
  792.         $path urlencode_ignore_slash($path);
  793.         $prefix \Pimcore::getContainer()->getParameter('pimcore.config')['assets']['frontend_prefixes']['source'];
  794.         $path $prefix $path;
  795.         $event = new GenericEvent($this, [
  796.             'frontendPath' => $path,
  797.         ]);
  798.         \Pimcore::getEventDispatcher()->dispatch($eventFrontendEvents::ASSET_PATH);
  799.         return $event->getArgument('frontendPath');
  800.     }
  801.     /**
  802.      * {@inheritdoc}
  803.      */
  804.     public function getRealPath()
  805.     {
  806.         return $this->path;
  807.     }
  808.     /**
  809.      * {@inheritdoc}
  810.      */
  811.     public function getRealFullPath()
  812.     {
  813.         $path $this->getRealPath() . $this->getFilename();
  814.         return $path;
  815.     }
  816.     /**
  817.      * @return array
  818.      */
  819.     public function getSiblings()
  820.     {
  821.         if ($this->siblings === null) {
  822.             if ($this->getParentId()) {
  823.                 $list = new Asset\Listing();
  824.                 $list->addConditionParam('parentId = ?'$this->getParentId());
  825.                 if ($this->getId()) {
  826.                     $list->addConditionParam('id != ?'$this->getId());
  827.                 }
  828.                 $list->setOrderKey('filename');
  829.                 $list->setOrder('asc');
  830.                 $this->siblings $list->getAssets();
  831.             } else {
  832.                 $this->siblings = [];
  833.             }
  834.         }
  835.         return $this->siblings;
  836.     }
  837.     /**
  838.      * @return bool
  839.      */
  840.     public function hasSiblings()
  841.     {
  842.         if (is_bool($this->hasSiblings)) {
  843.             if (($this->hasSiblings && empty($this->siblings)) || (!$this->hasSiblings && !empty($this->siblings))) {
  844.                 return $this->getDao()->hasSiblings();
  845.             } else {
  846.                 return $this->hasSiblings;
  847.             }
  848.         }
  849.         return $this->getDao()->hasSiblings();
  850.     }
  851.     /**
  852.      * @return bool
  853.      */
  854.     public function hasChildren()
  855.     {
  856.         return false;
  857.     }
  858.     /**
  859.      * @return Asset[]
  860.      */
  861.     public function getChildren()
  862.     {
  863.         return [];
  864.     }
  865.     /**
  866.      * {@inheritdoc}
  867.      */
  868.     public function getLocked()
  869.     {
  870.         return $this->locked;
  871.     }
  872.     /**
  873.      * {@inheritdoc}
  874.      */
  875.     public function setLocked($locked)
  876.     {
  877.         $this->locked $locked;
  878.         return $this;
  879.     }
  880.     /**
  881.      * @throws \League\Flysystem\FilesystemException
  882.      */
  883.     private function deletePhysicalFile()
  884.     {
  885.         $storage Storage::get('asset');
  886.         if ($this->getType() != 'folder') {
  887.             $storage->delete($this->getRealFullPath());
  888.         } else {
  889.             $storage->deleteDirectory($this->getRealFullPath());
  890.         }
  891.     }
  892.     /**
  893.      * {@inheritdoc}
  894.      */
  895.     public function delete(bool $isNested false)
  896.     {
  897.         if ($this->getId() == 1) {
  898.             throw new \Exception('root-node cannot be deleted');
  899.         }
  900.         \Pimcore::getEventDispatcher()->dispatch(new AssetEvent($this), AssetEvents::PRE_DELETE);
  901.         $this->beginTransaction();
  902.         try {
  903.             $this->closeStream();
  904.             // remove children
  905.             if ($this->hasChildren()) {
  906.                 foreach ($this->getChildren() as $child) {
  907.                     $child->delete(true);
  908.                 }
  909.             }
  910.             // Dispatch Symfony Message Bus to delete versions
  911.             \Pimcore::getContainer()->get(MessageBusInterface::class)->dispatch(
  912.                 new VersionDeleteMessage(Service::getElementType($this), $this->getId())
  913.             );
  914.             // remove permissions
  915.             $this->getDao()->deleteAllPermissions();
  916.             // remove all properties
  917.             $this->getDao()->deleteAllProperties();
  918.             // remove all metadata
  919.             $this->getDao()->deleteAllMetadata();
  920.             // remove all tasks
  921.             $this->getDao()->deleteAllTasks();
  922.             // remove dependencies
  923.             $d $this->getDependencies();
  924.             $d->cleanAllForElement($this);
  925.             // remove from resource
  926.             $this->getDao()->delete();
  927.             $this->commit();
  928.             // remove file on filesystem
  929.             if (!$isNested) {
  930.                 $fullPath $this->getRealFullPath();
  931.                 if ($fullPath != '/..' && !strpos($fullPath,
  932.                         '/../') && $this->getKey() !== '.' && $this->getKey() !== '..') {
  933.                     $this->deletePhysicalFile();
  934.                 }
  935.             }
  936.             $this->clearThumbnails(true);
  937.         } catch (\Exception $e) {
  938.             $this->rollBack();
  939.             $failureEvent = new AssetEvent($this);
  940.             $failureEvent->setArgument('exception'$e);
  941.             \Pimcore::getEventDispatcher()->dispatch($failureEventAssetEvents::POST_DELETE_FAILURE);
  942.             Logger::crit($e);
  943.             throw $e;
  944.         }
  945.         // empty asset cache
  946.         $this->clearDependentCache();
  947.         // clear asset from registry
  948.         \Pimcore\Cache\Runtime::set(self::getCacheKey($this->getId()), null);
  949.         \Pimcore::getEventDispatcher()->dispatch(new AssetEvent($this), AssetEvents::POST_DELETE);
  950.     }
  951.     /**
  952.      * {@inheritdoc}
  953.      */
  954.     public function clearDependentCache($additionalTags = [])
  955.     {
  956.         try {
  957.             $tags = [$this->getCacheTag(), 'asset_properties''output'];
  958.             $tags array_merge($tags$additionalTags);
  959.             \Pimcore\Cache::clearTags($tags);
  960.         } catch (\Exception $e) {
  961.             Logger::crit($e);
  962.         }
  963.     }
  964.     /**
  965.      * {@inheritdoc}
  966.      */
  967.     public function getCreationDate()
  968.     {
  969.         return $this->creationDate;
  970.     }
  971.     /**
  972.      * {@inheritdoc}
  973.      */
  974.     public function getId()
  975.     {
  976.         return $this->id;
  977.     }
  978.     /**
  979.      * @return string|null
  980.      */
  981.     public function getFilename()
  982.     {
  983.         return $this->filename;
  984.     }
  985.     /**
  986.      * {@inheritdoc}
  987.      */
  988.     public function getKey()
  989.     {
  990.         return $this->getFilename();
  991.     }
  992.     /**
  993.      * {@inheritdoc}
  994.      */
  995.     public function getModificationDate()
  996.     {
  997.         return $this->modificationDate;
  998.     }
  999.     /**
  1000.      * {@inheritdoc}
  1001.      */
  1002.     public function getParentId()
  1003.     {
  1004.         return $this->parentId;
  1005.     }
  1006.     /**
  1007.      * {@inheritdoc}
  1008.      */
  1009.     public function getPath()
  1010.     {
  1011.         return $this->path;
  1012.     }
  1013.     /**
  1014.      * {@inheritdoc}
  1015.      */
  1016.     public function getType()
  1017.     {
  1018.         return $this->type;
  1019.     }
  1020.     /**
  1021.      * {@inheritdoc}
  1022.      */
  1023.     public function setCreationDate($creationDate)
  1024.     {
  1025.         $this->creationDate = (int)$creationDate;
  1026.         return $this;
  1027.     }
  1028.     /**
  1029.      * {@inheritdoc}
  1030.      */
  1031.     public function setId($id)
  1032.     {
  1033.         $this->id = (int)$id;
  1034.         return $this;
  1035.     }
  1036.     /**
  1037.      * @param string $filename
  1038.      *
  1039.      * @return $this
  1040.      */
  1041.     public function setFilename($filename)
  1042.     {
  1043.         $this->filename = (string)$filename;
  1044.         return $this;
  1045.     }
  1046.     /**
  1047.      * {@inheritdoc}
  1048.      */
  1049.     public function setKey($key)
  1050.     {
  1051.         return $this->setFilename($key);
  1052.     }
  1053.     /**
  1054.      * {@inheritdoc}
  1055.      */
  1056.     public function setModificationDate($modificationDate)
  1057.     {
  1058.         $this->markFieldDirty('modificationDate');
  1059.         $this->modificationDate = (int)$modificationDate;
  1060.         return $this;
  1061.     }
  1062.     /**
  1063.      * @param int $parentId
  1064.      *
  1065.      * @return $this
  1066.      */
  1067.     public function setParentId($parentId)
  1068.     {
  1069.         $this->parentId = (int)$parentId;
  1070.         $this->parent null;
  1071.         return $this;
  1072.     }
  1073.     /**
  1074.      * {@inheritdoc}
  1075.      */
  1076.     public function setPath($path)
  1077.     {
  1078.         $this->path = (string)$path;
  1079.         return $this;
  1080.     }
  1081.     /**
  1082.      * @param string $type
  1083.      *
  1084.      * @return $this
  1085.      */
  1086.     public function setType($type)
  1087.     {
  1088.         $this->type = (string)$type;
  1089.         return $this;
  1090.     }
  1091.     /**
  1092.      * @return mixed
  1093.      */
  1094.     public function getData()
  1095.     {
  1096.         $stream $this->getStream();
  1097.         if ($stream) {
  1098.             return stream_get_contents($stream);
  1099.         }
  1100.         return '';
  1101.     }
  1102.     /**
  1103.      * @param mixed $data
  1104.      *
  1105.      * @return $this
  1106.      */
  1107.     public function setData($data)
  1108.     {
  1109.         $handle tmpfile();
  1110.         fwrite($handle$data);
  1111.         $this->setStream($handle);
  1112.         return $this;
  1113.     }
  1114.     /**
  1115.      * @return resource|null
  1116.      */
  1117.     public function getStream()
  1118.     {
  1119.         if ($this->stream) {
  1120.             if (get_resource_type($this->stream) !== 'stream') {
  1121.                 $this->stream null;
  1122.             } elseif (!@rewind($this->stream)) {
  1123.                 $this->stream null;
  1124.             }
  1125.         }
  1126.         if (!$this->stream && $this->getType() !== 'folder') {
  1127.             try {
  1128.                 $this->stream Storage::get('asset')->readStream($this->getRealFullPath());
  1129.             } catch (\Exception $e) {
  1130.                 $this->stream tmpfile();
  1131.             }
  1132.         }
  1133.         return $this->stream;
  1134.     }
  1135.     /**
  1136.      * @param resource|null $stream
  1137.      *
  1138.      * @return $this
  1139.      */
  1140.     public function setStream($stream)
  1141.     {
  1142.         // close existing stream
  1143.         if ($stream !== $this->stream) {
  1144.             $this->closeStream();
  1145.         }
  1146.         if (is_resource($stream)) {
  1147.             $this->setDataChanged(true);
  1148.             $this->stream $stream;
  1149.             $isRewindable = @rewind($this->stream);
  1150.             if (!$isRewindable) {
  1151.                 $tempFile $this->getLocalFileFromStream($this->stream);
  1152.                 $dest fopen($tempFile'rb'falseFile::getContext());
  1153.                 $this->stream $dest;
  1154.             }
  1155.         } elseif (is_null($stream)) {
  1156.             $this->stream null;
  1157.         }
  1158.         return $this;
  1159.     }
  1160.     private function closeStream()
  1161.     {
  1162.         if (is_resource($this->stream)) {
  1163.             @fclose($this->stream);
  1164.             $this->stream null;
  1165.         }
  1166.     }
  1167.     /**
  1168.      * @return bool
  1169.      */
  1170.     public function getDataChanged()
  1171.     {
  1172.         return $this->dataChanged;
  1173.     }
  1174.     /**
  1175.      * @param bool $changed
  1176.      *
  1177.      * @return $this
  1178.      */
  1179.     public function setDataChanged($changed true)
  1180.     {
  1181.         $this->dataChanged $changed;
  1182.         return $this;
  1183.     }
  1184.     /**
  1185.      * {@inheritdoc}
  1186.      */
  1187.     public function getProperties()
  1188.     {
  1189.         if ($this->properties === null) {
  1190.             // try to get from cache
  1191.             $cacheKey 'asset_properties_' $this->getId();
  1192.             $properties \Pimcore\Cache::load($cacheKey);
  1193.             if (!is_array($properties)) {
  1194.                 $properties $this->getDao()->getProperties();
  1195.                 $elementCacheTag $this->getCacheTag();
  1196.                 $cacheTags = ['asset_properties' => 'asset_properties'$elementCacheTag => $elementCacheTag];
  1197.                 \Pimcore\Cache::save($properties$cacheKey$cacheTags);
  1198.             }
  1199.             $this->setProperties($properties);
  1200.         }
  1201.         return $this->properties;
  1202.     }
  1203.     /**
  1204.      * {@inheritdoc}
  1205.      */
  1206.     public function setProperties(?array $properties)
  1207.     {
  1208.         $this->properties $properties;
  1209.         return $this;
  1210.     }
  1211.     /**
  1212.      * {@inheritdoc}
  1213.      */
  1214.     public function setProperty($name$type$data$inherited false$inheritable false)
  1215.     {
  1216.         $this->getProperties();
  1217.         $property = new Property();
  1218.         $property->setType($type);
  1219.         $property->setCid($this->getId());
  1220.         $property->setName($name);
  1221.         $property->setCtype('asset');
  1222.         $property->setData($data);
  1223.         $property->setInherited($inherited);
  1224.         $property->setInheritable($inheritable);
  1225.         $this->properties[$name] = $property;
  1226.         return $this;
  1227.     }
  1228.     /**
  1229.      * {@inheritdoc}
  1230.      */
  1231.     public function getUserOwner()
  1232.     {
  1233.         return $this->userOwner;
  1234.     }
  1235.     /**
  1236.      * {@inheritdoc}
  1237.      */
  1238.     public function getUserModification()
  1239.     {
  1240.         return $this->userModification;
  1241.     }
  1242.     /**
  1243.      * {@inheritdoc}
  1244.      */
  1245.     public function setUserOwner($userOwner)
  1246.     {
  1247.         $this->userOwner = (int)$userOwner;
  1248.         return $this;
  1249.     }
  1250.     /**
  1251.      * {@inheritdoc}
  1252.      */
  1253.     public function setUserModification($userModification)
  1254.     {
  1255.         $this->markFieldDirty('userModification');
  1256.         $this->userModification = (int)$userModification;
  1257.         return $this;
  1258.     }
  1259.     /**
  1260.      * {@inheritdoc}
  1261.      */
  1262.     public function getVersions()
  1263.     {
  1264.         if ($this->versions === null) {
  1265.             $this->setVersions($this->getDao()->getVersions());
  1266.         }
  1267.         return $this->versions;
  1268.     }
  1269.     /**
  1270.      * @param Version[] $versions
  1271.      *
  1272.      * @return $this
  1273.      */
  1274.     public function setVersions($versions)
  1275.     {
  1276.         $this->versions $versions;
  1277.         return $this;
  1278.     }
  1279.     /**
  1280.      * @internal
  1281.      *
  1282.      * @param bool $keep whether to delete this file on shutdown or not
  1283.      *
  1284.      * @return string
  1285.      *
  1286.      * @throws \Exception
  1287.      */
  1288.     public function getTemporaryFile(bool $keep false)
  1289.     {
  1290.         return self::getTemporaryFileFromStream($this->getStream(), $keep);
  1291.     }
  1292.     /**
  1293.      * @internal
  1294.      *
  1295.      * @return string
  1296.      *
  1297.      * @throws \Exception
  1298.      */
  1299.     public function getLocalFile()
  1300.     {
  1301.         return self::getLocalFileFromStream($this->getStream());
  1302.     }
  1303.     /**
  1304.      * @param string $key
  1305.      * @param mixed $value
  1306.      *
  1307.      * @return $this
  1308.      */
  1309.     public function setCustomSetting($key$value)
  1310.     {
  1311.         $this->customSettings[$key] = $value;
  1312.         return $this;
  1313.     }
  1314.     /**
  1315.      * @param string $key
  1316.      *
  1317.      * @return mixed
  1318.      */
  1319.     public function getCustomSetting($key)
  1320.     {
  1321.         if (is_array($this->customSettings) && array_key_exists($key$this->customSettings)) {
  1322.             return $this->customSettings[$key];
  1323.         }
  1324.         return null;
  1325.     }
  1326.     /**
  1327.      * @param string $key
  1328.      */
  1329.     public function removeCustomSetting($key)
  1330.     {
  1331.         if (is_array($this->customSettings) && array_key_exists($key$this->customSettings)) {
  1332.             unset($this->customSettings[$key]);
  1333.         }
  1334.     }
  1335.     /**
  1336.      * @return array
  1337.      */
  1338.     public function getCustomSettings()
  1339.     {
  1340.         return $this->customSettings;
  1341.     }
  1342.     /**
  1343.      * @param mixed $customSettings
  1344.      *
  1345.      * @return $this
  1346.      */
  1347.     public function setCustomSettings($customSettings)
  1348.     {
  1349.         if (is_string($customSettings)) {
  1350.             $customSettings \Pimcore\Tool\Serialize::unserialize($customSettings);
  1351.         }
  1352.         if ($customSettings instanceof \stdClass) {
  1353.             $customSettings = (array)$customSettings;
  1354.         }
  1355.         if (!is_array($customSettings)) {
  1356.             $customSettings = [];
  1357.         }
  1358.         $this->customSettings $customSettings;
  1359.         return $this;
  1360.     }
  1361.     /**
  1362.      * @return string|null
  1363.      */
  1364.     public function getMimeType()
  1365.     {
  1366.         return $this->mimetype;
  1367.     }
  1368.     /**
  1369.      * @param string $mimetype
  1370.      *
  1371.      * @return $this
  1372.      */
  1373.     public function setMimeType($mimetype)
  1374.     {
  1375.         $this->mimetype = (string)$mimetype;
  1376.         return $this;
  1377.     }
  1378.     /**
  1379.      * @param array $metadata for each array item: mandatory keys: name, type - optional keys: data, language
  1380.      *
  1381.      * @return self
  1382.      *
  1383.      * @internal
  1384.      *
  1385.      */
  1386.     public function setMetadataRaw($metadata)
  1387.     {
  1388.         $this->metadata $metadata;
  1389.         if ($this->metadata) {
  1390.             $this->setHasMetaData(true);
  1391.         }
  1392.         return $this;
  1393.     }
  1394.     /**
  1395.      * @param array|\stdClass[] $metadata for each array item: mandatory keys: name, type - optional keys: data, language
  1396.      *
  1397.      * @return self
  1398.      */
  1399.     public function setMetadata($metadata)
  1400.     {
  1401.         $this->metadata = [];
  1402.         $this->setHasMetaData(false);
  1403.         if (!empty($metadata)) {
  1404.             foreach ((array)$metadata as $metaItem) {
  1405.                 $metaItem = (array)$metaItem// also allow object with appropriate keys
  1406.                 $this->addMetadata($metaItem['name'], $metaItem['type'], $metaItem['data'] ?? null$metaItem['language'] ?? null);
  1407.             }
  1408.         }
  1409.         return $this;
  1410.     }
  1411.     /**
  1412.      * @return bool
  1413.      */
  1414.     public function getHasMetaData()
  1415.     {
  1416.         return $this->hasMetaData;
  1417.     }
  1418.     /**
  1419.      * @param bool $hasMetaData
  1420.      *
  1421.      * @return self
  1422.      */
  1423.     public function setHasMetaData($hasMetaData)
  1424.     {
  1425.         $this->hasMetaData = (bool)$hasMetaData;
  1426.         return $this;
  1427.     }
  1428.     /**
  1429.      * @param string $name
  1430.      * @param string $type can be "asset", "checkbox", "date", "document", "input", "object", "select" or "textarea"
  1431.      * @param mixed $data
  1432.      * @param string|null $language
  1433.      *
  1434.      * @return self
  1435.      */
  1436.     public function addMetadata($name$type$data null$language null)
  1437.     {
  1438.         if ($name && $type) {
  1439.             $tmp = [];
  1440.             $name str_replace('~''---'$name);
  1441.             if (!is_array($this->metadata)) {
  1442.                 $this->metadata = [];
  1443.             }
  1444.             foreach ($this->metadata as $item) {
  1445.                 if ($item['name'] != $name || $language != $item['language']) {
  1446.                     $tmp[] = $item;
  1447.                 }
  1448.             }
  1449.             $item = [
  1450.                 'name' => $name,
  1451.                 'type' => $type,
  1452.                 'data' => $data,
  1453.                 'language' => $language,
  1454.             ];
  1455.             $loader \Pimcore::getContainer()->get('pimcore.implementation_loader.asset.metadata.data');
  1456.             try {
  1457.                 /** @var Data $instance */
  1458.                 $instance $loader->build($item['type']);
  1459.                 $transformedData $instance->transformSetterData($data$item);
  1460.                 $item['data'] = $transformedData;
  1461.             } catch (UnsupportedException $e) {
  1462.             }
  1463.             $tmp[] = $item;
  1464.             $this->metadata $tmp;
  1465.             $this->setHasMetaData(true);
  1466.         }
  1467.         return $this;
  1468.     }
  1469.     /**
  1470.      * @param string $name
  1471.      * @param string|null $language
  1472.      *
  1473.      * @return self
  1474.      */
  1475.     public function removeMetadata(string $name, ?string $language null)
  1476.     {
  1477.         if ($name) {
  1478.             $tmp = [];
  1479.             $name str_replace('~''---'$name);
  1480.             if (!is_array($this->metadata)) {
  1481.                 $this->metadata = [];
  1482.             }
  1483.             foreach ($this->metadata as $item) {
  1484.                 if ($item['name'] === $name && ($language == $item['language'] || $language === '*')) {
  1485.                     continue;
  1486.                 }
  1487.                 $tmp[] = $item;
  1488.             }
  1489.             $this->metadata $tmp;
  1490.             $this->setHasMetaData(!empty($this->metadata));
  1491.         }
  1492.         return $this;
  1493.     }
  1494.     /**
  1495.      * @param string|null $name
  1496.      * @param string|null $language
  1497.      * @param bool $strictMatch
  1498.      * @param bool $raw
  1499.      *
  1500.      * @return array|string|null
  1501.      */
  1502.     public function getMetadata($name null$language null$strictMatch false$raw false)
  1503.     {
  1504.         $preEvent = new AssetEvent($this);
  1505.         $preEvent->setArgument('metadata'$this->metadata);
  1506.         \Pimcore::getEventDispatcher()->dispatch($preEventAssetEvents::PRE_GET_METADATA);
  1507.         $this->metadata $preEvent->getArgument('metadata');
  1508.         $convert = function ($metaData) {
  1509.             $loader \Pimcore::getContainer()->get('pimcore.implementation_loader.asset.metadata.data');
  1510.             $transformedData $metaData['data'];
  1511.             try {
  1512.                 /** @var Data $instance */
  1513.                 $instance $loader->build($metaData['type']);
  1514.                 $transformedData $instance->transformGetterData($metaData['data'], $metaData);
  1515.             } catch (UnsupportedException $e) {
  1516.             }
  1517.             return $transformedData;
  1518.         };
  1519.         if ($name) {
  1520.             if ($language === null) {
  1521.                 $language \Pimcore::getContainer()->get(LocaleServiceInterface::class)->findLocale();
  1522.             }
  1523.             $data null;
  1524.             foreach ($this->metadata as $md) {
  1525.                 if ($md['name'] == $name) {
  1526.                     if ($language == $md['language']) {
  1527.                         if ($raw) {
  1528.                             return $md;
  1529.                         }
  1530.                         return $convert($md);
  1531.                     }
  1532.                     if (empty($md['language']) && !$strictMatch) {
  1533.                         if ($raw) {
  1534.                             return $md;
  1535.                         }
  1536.                         $data $md;
  1537.                     }
  1538.                 }
  1539.             }
  1540.             if ($data) {
  1541.                 if ($raw) {
  1542.                     return $data;
  1543.                 }
  1544.                 return $convert($data);
  1545.             }
  1546.             return null;
  1547.         }
  1548.         $metaData $this->getObjectVar('metadata');
  1549.         $result = [];
  1550.         if (is_array($metaData)) {
  1551.             foreach ($metaData as $md) {
  1552.                 $md = (array)$md;
  1553.                 if (!$raw) {
  1554.                     $md['data'] = $convert($md);
  1555.                 }
  1556.                 $result[] = $md;
  1557.             }
  1558.         }
  1559.         return $result;
  1560.     }
  1561.     /**
  1562.      * @param bool $formatted
  1563.      * @param int $precision
  1564.      *
  1565.      * @return string|int
  1566.      */
  1567.     public function getFileSize($formatted false$precision 2)
  1568.     {
  1569.         try {
  1570.             $bytes Storage::get('asset')->fileSize($this->getRealFullPath());
  1571.         } catch (\Exception $e) {
  1572.             $bytes 0;
  1573.         }
  1574.         if ($formatted) {
  1575.             return formatBytes($bytes$precision);
  1576.         }
  1577.         return $bytes;
  1578.     }
  1579.     /**
  1580.      * {@inheritdoc}
  1581.      */
  1582.     public function getParent()
  1583.     {
  1584.         if ($this->parent === null) {
  1585.             $this->setParent(Asset::getById($this->getParentId()));
  1586.         }
  1587.         return $this->parent;
  1588.     }
  1589.     /**
  1590.      * @param Asset|null $parent
  1591.      *
  1592.      * @return $this
  1593.      */
  1594.     public function setParent($parent)
  1595.     {
  1596.         $this->parent $parent;
  1597.         if ($parent instanceof Asset) {
  1598.             $this->parentId $parent->getId();
  1599.         }
  1600.         return $this;
  1601.     }
  1602.     public function __sleep()
  1603.     {
  1604.         $parentVars parent::__sleep();
  1605.         $blockedVars = ['scheduledTasks''hasChildren''versions''parent''stream'];
  1606.         if ($this->isInDumpState()) {
  1607.             // this is if we want to make a full dump of the asset (eg. for a new version), including children for recyclebin
  1608.             $this->removeInheritedProperties();
  1609.         } else {
  1610.             // this is if we want to cache the asset
  1611.             $blockedVars array_merge($blockedVars, ['children''properties']);
  1612.         }
  1613.         return array_diff($parentVars$blockedVars);
  1614.     }
  1615.     public function __wakeup()
  1616.     {
  1617.         if ($this->isInDumpState()) {
  1618.             // set current parent and path, this is necessary because the serialized data can have a different path than the original element (element was moved)
  1619.             $originalElement Asset::getById($this->getId());
  1620.             if ($originalElement) {
  1621.                 $this->setParentId($originalElement->getParentId());
  1622.                 $this->setPath($originalElement->getRealPath());
  1623.             }
  1624.         }
  1625.         if ($this->isInDumpState() && $this->properties !== null) {
  1626.             $this->renewInheritedProperties();
  1627.         }
  1628.         $this->setInDumpState(false);
  1629.     }
  1630.     public function __destruct()
  1631.     {
  1632.         // close open streams
  1633.         $this->closeStream();
  1634.     }
  1635.     /**
  1636.      * {@inheritdoc}
  1637.      */
  1638.     public function getVersionCount(): int
  1639.     {
  1640.         return $this->versionCount $this->versionCount 0;
  1641.     }
  1642.     /**
  1643.      * {@inheritdoc}
  1644.      */
  1645.     public function setVersionCount(?int $versionCount): ElementInterface
  1646.     {
  1647.         $this->versionCount = (int)$versionCount;
  1648.         return $this;
  1649.     }
  1650.     /**
  1651.      * {@inheritdoc}
  1652.      */
  1653.     protected function resolveDependencies(): array
  1654.     {
  1655.         $dependencies = [parent::resolveDependencies()];
  1656.         if ($this->hasMetaData) {
  1657.             $loader \Pimcore::getContainer()->get('pimcore.implementation_loader.asset.metadata.data');
  1658.             foreach ($this->getMetadata() as $metaData) {
  1659.                 if (!empty($metaData['data'])) {
  1660.                     /** @var ElementInterface $elementData */
  1661.                     $elementData $metaData['data'];
  1662.                     $elementType $metaData['type'];
  1663.                     try {
  1664.                         /** @var DataDefinitionInterface $implementation */
  1665.                         $implementation $loader->build($elementType);
  1666.                         $dependencies[] = $implementation->resolveDependencies($elementData$metaData);
  1667.                     } catch (UnsupportedException $e) {
  1668.                     }
  1669.                 }
  1670.             }
  1671.         }
  1672.         return array_merge(...$dependencies);
  1673.     }
  1674.     public function __clone()
  1675.     {
  1676.         parent::__clone();
  1677.         $this->parent null;
  1678.         $this->versions null;
  1679.         $this->hasSiblings null;
  1680.         $this->siblings null;
  1681.         $this->scheduledTasks null;
  1682.         $this->closeStream();
  1683.     }
  1684.     /**
  1685.      * @param bool $force
  1686.      */
  1687.     public function clearThumbnails($force false)
  1688.     {
  1689.         if ($this->getDataChanged() || $force) {
  1690.             foreach (['thumbnail''asset_cache'] as $storageName) {
  1691.                 $storage Storage::get($storageName);
  1692.                 $contents $storage->listContents($this->getRealPath());
  1693.                 /** @var StorageAttributes $item */
  1694.                 foreach ($contents as $item) {
  1695.                     if (preg_match('@(image|video|pdf)\-thumb__' $this->getId() . '__@'$item->path())) {
  1696.                         if ($item->isDir()) {
  1697.                             $storage->deleteDirectory($item->path());
  1698.                         } elseif ($item->isFile()) {
  1699.                             $storage->delete($item->path());
  1700.                         }
  1701.                     }
  1702.                 }
  1703.             }
  1704.         }
  1705.     }
  1706.     /**
  1707.      * @param FilesystemOperator $storage
  1708.      * @param string $oldPath
  1709.      *
  1710.      * @throws \League\Flysystem\FilesystemException
  1711.      */
  1712.     private function updateChildPaths(FilesystemOperator $storagestring $oldPath)
  1713.     {
  1714.         try {
  1715.             $children $storage->listContents($oldPathtrue);
  1716.             foreach ($children as $child) {
  1717.                 if ($child['type'] === 'file') {
  1718.                     $src  $child['path'];
  1719.                     $dest str_replace($oldPath$this->getRealFullPath(), '/' $src);
  1720.                     $storage->move($src$dest);
  1721.                 }
  1722.             }
  1723.             $storage->deleteDirectory($oldPath);
  1724.         } catch (UnableToMoveFile $e) {
  1725.             // noting to do
  1726.         }
  1727.     }
  1728.     /**
  1729.      * @param string $oldPath
  1730.      *
  1731.      * @throws \League\Flysystem\FilesystemException
  1732.      */
  1733.     private function relocateThumbnails(string $oldPath)
  1734.     {
  1735.         $oldParent dirname($oldPath);
  1736.         $newParent dirname($this->getRealFullPath());
  1737.         $storage Storage::get('thumbnail');
  1738.         try {
  1739.             //remove source parent folder thumbnails
  1740.             $contents $storage->listContents($oldParent)->filter(fn (StorageAttributes $attributes) => ($attributes->isFile() && strstr($attributes['path'], 'image-thumb_')));
  1741.             /** @var StorageAttributes $item */
  1742.             foreach ($contents as $item) {
  1743.                 $storage->delete($item['path']);
  1744.             }
  1745.             //remove destination parent folder thumbnails
  1746.             $contents $storage->listContents($newParent)->filter(fn (StorageAttributes $attributes) => ($attributes->isFile() && strstr($attributes['path'], 'image-thumb_')));
  1747.             /** @var StorageAttributes $item */
  1748.             foreach ($contents as $item) {
  1749.                 $storage->delete($item['path']);
  1750.             }
  1751.             $contents $storage->listContents($oldParent);
  1752.             /** @var StorageAttributes $item */
  1753.             foreach ($contents as $item) {
  1754.                 if (preg_match('@(image|video|pdf)\-thumb__' $this->getId() . '__@'$item->path())) {
  1755.                     $replacePath ltrim($newParent'/') .'/' basename($item->path());
  1756.                     if (!$storage->fileExists($replacePath)) {
  1757.                         $storage->move($item->path(), $replacePath);
  1758.                     }
  1759.                 }
  1760.             }
  1761.             //required in case if renaming or moving parent folder
  1762.             try {
  1763.                 $storage->move($oldPath$this->getRealFullPath());
  1764.             } catch (UnableToMoveFile $e) {
  1765.                 //update children, if unable to move parent
  1766.                 $this->updateChildPaths($storage$oldPath);
  1767.             }
  1768.         } catch (UnableToMoveFile $e) {
  1769.             // noting to do
  1770.         }
  1771.     }
  1772.     /**
  1773.      * @param string $name
  1774.      */
  1775.     public function clearThumbnail($name)
  1776.     {
  1777.         try {
  1778.             Storage::get('thumbnail')->deleteDirectory($this->getRealPath() . 'image-thumb__' $this->getId() . '__' $name);
  1779.         } catch (\Exception $e) {
  1780.             // noting to do
  1781.         }
  1782.     }
  1783.     /**
  1784.      * @internal
  1785.      */
  1786.     protected function addToUpdateTaskQueue(): void
  1787.     {
  1788.         \Pimcore::getContainer()->get(MessageBusInterface::class)->dispatch(
  1789.             new AssetUpdateTasksMessage($this->getId())
  1790.         );
  1791.     }
  1792. }