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

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