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

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