vendor/pimcore/pimcore/models/Version.php line 170

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 Pimcore\Cache\Runtime;
  16. use Pimcore\Event\Model\VersionEvent;
  17. use Pimcore\Event\VersionEvents;
  18. use Pimcore\File;
  19. use Pimcore\Logger;
  20. use Pimcore\Model\DataObject\ClassDefinition\Data;
  21. use Pimcore\Model\DataObject\Concrete;
  22. use Pimcore\Model\DataObject\Data\GeoCoordinates;
  23. use Pimcore\Model\Element\DeepCopy\PimcoreClassDefinitionMatcher;
  24. use Pimcore\Model\Element\DeepCopy\PimcoreClassDefinitionReplaceFilter;
  25. use Pimcore\Model\Element\ElementDumpStateInterface;
  26. use Pimcore\Model\Element\ElementInterface;
  27. use Pimcore\Model\Element\Service;
  28. use Pimcore\Model\Version\SetDumpStateFilter;
  29. use Pimcore\Tool\Serialize;
  30. use Pimcore\Tool\Storage;
  31. /**
  32. * @method \Pimcore\Model\Version\Dao getDao()
  33. */
  34. final class Version extends AbstractModel
  35. {
  36. /**
  37. * @var int
  38. */
  39. protected $id;
  40. /**
  41. * @var int
  42. */
  43. protected $cid;
  44. /**
  45. * @var string
  46. */
  47. protected $ctype;
  48. /**
  49. * @var int
  50. */
  51. protected $userId;
  52. /**
  53. * @var User|null
  54. */
  55. protected ?User $user = null;
  56. /**
  57. * @var string
  58. */
  59. protected $note;
  60. /**
  61. * @var int
  62. */
  63. protected $date;
  64. /**
  65. * @var mixed
  66. */
  67. protected $data;
  68. /**
  69. * @var bool
  70. */
  71. protected $public = false;
  72. /**
  73. * @var bool
  74. */
  75. protected $serialized = false;
  76. /**
  77. * @var string|null
  78. */
  79. protected $stackTrace = '';
  80. /**
  81. * @var bool
  82. */
  83. protected $generateStackTrace = true;
  84. /**
  85. * @var int
  86. */
  87. protected $versionCount = 0;
  88. /**
  89. * @var string|null
  90. */
  91. protected $binaryFileHash;
  92. /**
  93. * @var int|null
  94. */
  95. protected $binaryFileId;
  96. /**
  97. * @var bool
  98. */
  99. public static $disabled = false;
  100. /**
  101. * @var bool
  102. */
  103. protected bool $autoSave = false;
  104. /**
  105. * @param int $id
  106. *
  107. * @return Version|null
  108. */
  109. public static function getById($id)
  110. {
  111. try {
  112. /**
  113. * @var self $version
  114. */
  115. $version = self::getModelFactory()->build(Version::class);
  116. $version->getDao()->getById($id);
  117. return $version;
  118. } catch (\Exception $e) {
  119. return null;
  120. }
  121. }
  122. /**
  123. * disables the versioning for the current process, this is useful for importers, ...
  124. * There are no new versions created, the read continues to operate normally
  125. *
  126. * @static
  127. */
  128. public static function disable()
  129. {
  130. self::$disabled = true;
  131. }
  132. /**
  133. * see @ self::disable()
  134. * just enabled the creation of versioning in the current process
  135. *
  136. * @static
  137. */
  138. public static function enable()
  139. {
  140. self::$disabled = false;
  141. }
  142. /**
  143. * @throws \Exception
  144. */
  145. public function save()
  146. {
  147. \Pimcore::getEventDispatcher()->dispatch(new VersionEvent($this), VersionEvents::PRE_SAVE);
  148. // check if versioning is disabled for this process
  149. if (self::$disabled) {
  150. return;
  151. }
  152. $storage = Storage::get('version');
  153. if (!$this->date) {
  154. $this->setDate(time());
  155. }
  156. // get stack trace, if enabled
  157. if ($this->getGenerateStackTrace()) {
  158. try {
  159. throw new \Exception('not a real exception ... ;-)');
  160. } catch (\Exception $e) {
  161. $this->stackTrace = $e->getTraceAsString();
  162. }
  163. }
  164. $data = $this->getData();
  165. // if necessary convert the data to save it to filesystem
  166. if (is_object($data) || is_array($data)) {
  167. // this is because of lazy loaded element inside documents and objects (eg: relational data-types, fieldcollections, ...)
  168. $fromRuntime = null;
  169. $cacheKey = null;
  170. if ($data instanceof Element\ElementInterface) {
  171. Element\Service::loadAllFields($data);
  172. }
  173. $this->setSerialized(true);
  174. $condensedData = $this->marshalData($data);
  175. $dataString = Serialize::serialize($condensedData);
  176. // revert all changed made by __sleep()
  177. if (method_exists($data, '__wakeup')) {
  178. $data->__wakeup();
  179. }
  180. } else {
  181. $dataString = $data;
  182. }
  183. $isAssetFile = false;
  184. if ($data instanceof Asset && $data->getType() != 'folder') {
  185. $isAssetFile = true;
  186. $ctx = hash_init('sha3-512');
  187. hash_update_stream($ctx, $data->getStream());
  188. $this->binaryFileHash = hash_final($ctx);
  189. $this->binaryFileId = $this->getDao()->getBinaryFileIdForHash($this->binaryFileHash);
  190. }
  191. $id = $this->getDao()->save();
  192. $this->setId($id);
  193. $storage->write($this->getStoragePath(), $dataString);
  194. // assets are kinda special because they can contain massive amount of binary data which isn't serialized, we append it to the data file
  195. if ($isAssetFile && !$storage->fileExists($this->getBinaryStoragePath())) {
  196. $linked = false;
  197. // we always try to create a hardlink onto the original file, the asset ensures that not the actual
  198. // inodes get overwritten but creates new inodes if the content changes. This is done by deleting the
  199. // old file first before opening a new stream -> see Asset::update()
  200. $useHardlinks = \Pimcore::getContainer()->getParameter('pimcore.config')['assets']['versions']['use_hardlinks'];
  201. $storage->write($this->getBinaryStoragePath(), '1'); // temp file to determine if stream is local or not
  202. if ($useHardlinks && stream_is_local($this->getBinaryFileStream()) && stream_is_local($data->getStream())) {
  203. $linkPath = stream_get_meta_data($this->getBinaryFileStream())['uri'];
  204. $storage->delete($this->getBinaryStoragePath());
  205. $linked = @link(stream_get_meta_data($data->getStream())['uri'], $linkPath);
  206. }
  207. if (!$linked) {
  208. $storage->writeStream($this->getBinaryStoragePath(), $data->getStream());
  209. }
  210. }
  211. \Pimcore::getEventDispatcher()->dispatch(new VersionEvent($this), VersionEvents::POST_SAVE);
  212. }
  213. /**
  214. * @param ElementInterface $data
  215. *
  216. * @return mixed
  217. */
  218. private function marshalData($data)
  219. {
  220. $context = [
  221. 'source' => __METHOD__,
  222. 'conversion' => 'marshal',
  223. 'defaultFilters' => true,
  224. ];
  225. $copier = Service::getDeepCopyInstance($data, $context);
  226. if ($data instanceof Concrete) {
  227. $copier->addFilter(
  228. new PimcoreClassDefinitionReplaceFilter(
  229. function (Concrete $object, Data $fieldDefinition, $property, $currentValue) {
  230. if ($fieldDefinition instanceof Data\CustomVersionMarshalInterface) {
  231. return $fieldDefinition->marshalVersion($object, $currentValue);
  232. }
  233. return $currentValue;
  234. }
  235. ),
  236. new PimcoreClassDefinitionMatcher(Data\CustomVersionMarshalInterface::class)
  237. );
  238. }
  239. $copier->addFilter(new SetDumpStateFilter(true), new \DeepCopy\Matcher\PropertyMatcher(ElementDumpStateInterface::class, ElementDumpStateInterface::DUMP_STATE_PROPERTY_NAME));
  240. $newData = $copier->copy($data);
  241. return $newData;
  242. }
  243. /**
  244. * @param ElementInterface $data
  245. *
  246. * @return mixed
  247. */
  248. private function unmarshalData($data)
  249. {
  250. $context = [
  251. 'source' => __METHOD__,
  252. 'conversion' => 'unmarshal',
  253. 'defaultFilters' => false,
  254. ];
  255. $copier = Service::getDeepCopyInstance($data, $context);
  256. if ($data instanceof Concrete) {
  257. $copier->addFilter(
  258. new PimcoreClassDefinitionReplaceFilter(
  259. function (Concrete $object, Data $fieldDefinition, $property, $currentValue) {
  260. if ($fieldDefinition instanceof Data\CustomVersionMarshalInterface) {
  261. return $fieldDefinition->unmarshalVersion($object, $currentValue);
  262. }
  263. return $currentValue;
  264. }
  265. ),
  266. new PimcoreClassDefinitionMatcher(Data\CustomVersionMarshalInterface::class)
  267. );
  268. }
  269. return $copier->copy($data);
  270. }
  271. /**
  272. * Delete this Version
  273. */
  274. public function delete()
  275. {
  276. \Pimcore::getEventDispatcher()->dispatch(new VersionEvent($this), VersionEvents::PRE_DELETE);
  277. $storage = Storage::get('version');
  278. if ($storage->fileExists($this->getStoragePath())) {
  279. $storage->delete($this->getStoragePath());
  280. }
  281. if ($storage->fileExists($this->getBinaryStoragePath()) && !$this->getDao()->isBinaryHashInUse($this->getBinaryFileHash())) {
  282. $storage->delete($this->getBinaryStoragePath());
  283. }
  284. $this->getDao()->delete();
  285. \Pimcore::getEventDispatcher()->dispatch(new VersionEvent($this), VersionEvents::POST_DELETE);
  286. }
  287. /**
  288. * @internal
  289. *
  290. * @param bool $renewReferences
  291. *
  292. * @return mixed
  293. */
  294. public function loadData($renewReferences = true)
  295. {
  296. $storage = Storage::get('version');
  297. $data = $storage->read($this->getStoragePath());
  298. if (!$data) {
  299. Logger::err('Version: cannot read version data from file system.');
  300. $this->delete();
  301. return null;
  302. }
  303. if ($this->getSerialized()) {
  304. // this makes it possible to restore data object versions from older Pimcore versions
  305. @class_alias(GeoCoordinates::class, 'Pimcore\Model\DataObject\Data\Geopoint');
  306. $data = Serialize::unserialize($data);
  307. //clear runtime cache to avoid dealing with marshalled data
  308. Runtime::clear();
  309. if ($data instanceof \__PHP_Incomplete_Class) {
  310. Logger::err('Version: cannot read version data from file system because of incompatible class.');
  311. return null;
  312. }
  313. $data = $this->unmarshalData($data);
  314. }
  315. if ($data instanceof Concrete) {
  316. $data->markAllLazyLoadedKeysAsLoaded();
  317. }
  318. if ($data instanceof Asset && $storage->fileExists($this->getBinaryStoragePath())) {
  319. $data->setStream($this->getBinaryFileStream());
  320. }
  321. if ($renewReferences) {
  322. $data = Element\Service::renewReferences($data);
  323. }
  324. $this->setData($data);
  325. return $data;
  326. }
  327. /**
  328. * @param int|null $id
  329. *
  330. * @return string
  331. */
  332. private function getStoragePath(?int $id = null): string
  333. {
  334. if (!$id) {
  335. $id = $this->getId();
  336. }
  337. $group = floor($this->getCid() / 10000) * 10000;
  338. return $this->getCtype() . '/g' . $group . '/' . $this->getCid() . '/' . $id;
  339. }
  340. /**
  341. * @return resource
  342. *
  343. * @throws \League\Flysystem\FilesystemException
  344. */
  345. public function getFileStream()
  346. {
  347. return Storage::get('version')->readStream($this->getStoragePath());
  348. }
  349. /**
  350. * @return string
  351. */
  352. private function getBinaryStoragePath(): string
  353. {
  354. return $this->getStoragePath($this->getBinaryFileId()) . '.bin';
  355. }
  356. /**
  357. * @return resource
  358. *
  359. * @throws \League\Flysystem\FilesystemException
  360. */
  361. public function getBinaryFileStream()
  362. {
  363. return Storage::get('version')->readStream($this->getBinaryStoragePath());
  364. }
  365. /**
  366. * @return int
  367. */
  368. public function getCid()
  369. {
  370. return $this->cid;
  371. }
  372. /**
  373. * @return int
  374. */
  375. public function getDate()
  376. {
  377. return $this->date;
  378. }
  379. /**
  380. * @return int
  381. */
  382. public function getId()
  383. {
  384. return $this->id;
  385. }
  386. /**
  387. * @return string
  388. */
  389. public function getNote()
  390. {
  391. return $this->note;
  392. }
  393. /**
  394. * @return int
  395. */
  396. public function getUserId()
  397. {
  398. return $this->userId;
  399. }
  400. /**
  401. * @param int $cid
  402. *
  403. * @return $this
  404. */
  405. public function setCid($cid)
  406. {
  407. $this->cid = (int) $cid;
  408. return $this;
  409. }
  410. /**
  411. * @param int $date
  412. *
  413. * @return $this
  414. */
  415. public function setDate($date)
  416. {
  417. $this->date = (int) $date;
  418. return $this;
  419. }
  420. /**
  421. * @param int $id
  422. *
  423. * @return $this
  424. */
  425. public function setId($id)
  426. {
  427. $this->id = (int) $id;
  428. return $this;
  429. }
  430. /**
  431. * @param string $note
  432. *
  433. * @return $this
  434. */
  435. public function setNote($note)
  436. {
  437. $this->note = (string) $note;
  438. return $this;
  439. }
  440. /**
  441. * @param int $userId
  442. *
  443. * @return $this
  444. */
  445. public function setUserId($userId)
  446. {
  447. if (is_numeric($userId)) {
  448. if ($user = User::getById($userId)) {
  449. $this->userId = (int) $userId;
  450. $this->setUser($user);
  451. }
  452. }
  453. return $this;
  454. }
  455. /**
  456. * @return mixed
  457. */
  458. public function getData()
  459. {
  460. if (!$this->data) {
  461. $this->loadData();
  462. }
  463. return $this->data;
  464. }
  465. /**
  466. * @param mixed $data
  467. *
  468. * @return $this
  469. */
  470. public function setData($data)
  471. {
  472. $this->data = $data;
  473. return $this;
  474. }
  475. /**
  476. * @return bool
  477. */
  478. public function getSerialized()
  479. {
  480. return $this->serialized;
  481. }
  482. /**
  483. * @param bool $serialized
  484. *
  485. * @return $this
  486. */
  487. public function setSerialized($serialized)
  488. {
  489. $this->serialized = (bool) $serialized;
  490. return $this;
  491. }
  492. /**
  493. * @return string
  494. */
  495. public function getCtype()
  496. {
  497. return $this->ctype;
  498. }
  499. /**
  500. * @param string $ctype
  501. *
  502. * @return $this
  503. */
  504. public function setCtype($ctype)
  505. {
  506. $this->ctype = (string) $ctype;
  507. return $this;
  508. }
  509. /**
  510. * @return User|null
  511. */
  512. public function getUser(): ?User
  513. {
  514. return $this->user;
  515. }
  516. /**
  517. * @param User|null $user
  518. *
  519. * @return $this
  520. */
  521. public function setUser(?User $user)
  522. {
  523. $this->user = $user;
  524. return $this;
  525. }
  526. /**
  527. * @return bool
  528. */
  529. public function getPublic()
  530. {
  531. return $this->public;
  532. }
  533. /**
  534. * @return bool
  535. */
  536. public function isPublic()
  537. {
  538. return $this->public;
  539. }
  540. /**
  541. * @param bool $public
  542. *
  543. * @return $this
  544. */
  545. public function setPublic($public)
  546. {
  547. $this->public = (bool) $public;
  548. return $this;
  549. }
  550. /**
  551. * @return int
  552. */
  553. public function getVersionCount(): int
  554. {
  555. return $this->versionCount ? $this->versionCount : 0;
  556. }
  557. /**
  558. * @param int $versionCount
  559. */
  560. public function setVersionCount($versionCount): void
  561. {
  562. $this->versionCount = (int) $versionCount;
  563. }
  564. /**
  565. * @return string|null
  566. */
  567. public function getBinaryFileHash(): ?string
  568. {
  569. return $this->binaryFileHash;
  570. }
  571. /**
  572. * @param string|null $binaryFileHash
  573. */
  574. public function setBinaryFileHash(?string $binaryFileHash): void
  575. {
  576. $this->binaryFileHash = $binaryFileHash;
  577. }
  578. /**
  579. * @return int|null
  580. */
  581. public function getBinaryFileId(): ?int
  582. {
  583. return $this->binaryFileId;
  584. }
  585. /**
  586. * @param int|null $binaryFileId
  587. */
  588. public function setBinaryFileId(?int $binaryFileId): void
  589. {
  590. $this->binaryFileId = $binaryFileId;
  591. }
  592. /**
  593. * @return bool
  594. */
  595. public function getGenerateStackTrace()
  596. {
  597. return (bool) $this->generateStackTrace;
  598. }
  599. /**
  600. * @param bool $generateStackTrace
  601. */
  602. public function setGenerateStackTrace(bool $generateStackTrace): void
  603. {
  604. $this->generateStackTrace = $generateStackTrace;
  605. }
  606. /**
  607. * @param string|null $stackTrace
  608. */
  609. public function setStackTrace(?string $stackTrace): void
  610. {
  611. $this->stackTrace = $stackTrace;
  612. }
  613. /**
  614. * @return string
  615. */
  616. public function getStackTrace(): ?string
  617. {
  618. return $this->stackTrace;
  619. }
  620. /**
  621. * @return bool
  622. */
  623. public function isAutoSave(): bool
  624. {
  625. return $this->autoSave;
  626. }
  627. /**
  628. * @param bool $autoSave
  629. */
  630. public function setAutoSave(bool $autoSave): self
  631. {
  632. $this->autoSave = $autoSave;
  633. return $this;
  634. }
  635. }