vendor/pimcore/pimcore/models/Element/Service.php line 856

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\Element;
  15. use DeepCopy\DeepCopy;
  16. use DeepCopy\Filter\Doctrine\DoctrineCollectionFilter;
  17. use DeepCopy\Filter\SetNullFilter;
  18. use DeepCopy\Matcher\PropertyNameMatcher;
  19. use DeepCopy\Matcher\PropertyTypeMatcher;
  20. use Doctrine\Common\Collections\Collection;
  21. use Doctrine\DBAL\Query\QueryBuilder as DoctrineQueryBuilder;
  22. use League\Csv\EscapeFormula;
  23. use Pimcore\Db;
  24. use Pimcore\Event\SystemEvents;
  25. use Pimcore\File;
  26. use Pimcore\Logger;
  27. use Pimcore\Model;
  28. use Pimcore\Model\Asset;
  29. use Pimcore\Model\DataObject;
  30. use Pimcore\Model\DataObject\AbstractObject;
  31. use Pimcore\Model\DataObject\ClassDefinition\Data;
  32. use Pimcore\Model\DataObject\Concrete;
  33. use Pimcore\Model\Dependency;
  34. use Pimcore\Model\Document;
  35. use Pimcore\Model\Element\DeepCopy\MarshalMatcher;
  36. use Pimcore\Model\Element\DeepCopy\PimcoreClassDefinitionMatcher;
  37. use Pimcore\Model\Element\DeepCopy\PimcoreClassDefinitionReplaceFilter;
  38. use Pimcore\Model\Element\DeepCopy\UnmarshalMatcher;
  39. use Pimcore\Model\Tool\TmpStore;
  40. use Pimcore\Tool;
  41. use Pimcore\Tool\Serialize;
  42. use Pimcore\Tool\Session;
  43. use Symfony\Component\EventDispatcher\GenericEvent;
  44. /**
  45. * @method \Pimcore\Model\Element\Dao getDao()
  46. */
  47. class Service extends Model\AbstractModel
  48. {
  49. /**
  50. * @var EscapeFormula|null
  51. */
  52. private static ?EscapeFormula $formatter = null;
  53. /**
  54. * @internal
  55. *
  56. * @param ElementInterface $element
  57. *
  58. * @return string
  59. */
  60. public static function getIdPath(ElementInterface $element): string
  61. {
  62. $path = '';
  63. $elementType = self::getElementType($element);
  64. $parentId = $element->getParentId();
  65. $parentElement = self::getElementById($elementType, $parentId);
  66. if ($parentElement) {
  67. $path = self::getIdPath($parentElement);
  68. }
  69. $path .= '/' . $element->getId();
  70. return $path;
  71. }
  72. /**
  73. * @internal
  74. *
  75. * @param ElementInterface $element
  76. *
  77. * @return string
  78. *
  79. * @throws \Exception
  80. */
  81. public static function getTypePath(ElementInterface $element): string
  82. {
  83. $path = '';
  84. $elementType = self::getElementType($element);
  85. $parentId = $element->getParentId();
  86. $parentElement = self::getElementById($elementType, $parentId);
  87. if ($parentElement) {
  88. $path = self::getTypePath($parentElement);
  89. }
  90. $type = $element->getType();
  91. if ($type !== DataObject::OBJECT_TYPE_FOLDER) {
  92. if ($element instanceof Document) {
  93. $type = 'document';
  94. } elseif ($element instanceof DataObject\AbstractObject) {
  95. $type = 'object';
  96. } elseif ($element instanceof Asset) {
  97. $type = 'asset';
  98. } else {
  99. throw new \Exception('unknown type');
  100. }
  101. }
  102. $path .= '/' . $type;
  103. return $path;
  104. }
  105. /**
  106. * @internal
  107. *
  108. * @param ElementInterface $element
  109. *
  110. * @return string
  111. *
  112. * @throws \Exception
  113. */
  114. public static function getSortIndexPath(ElementInterface $element): string
  115. {
  116. $path = '';
  117. $elementType = self::getElementType($element);
  118. $parentId = $element->getParentId();
  119. $parentElement = self::getElementById($elementType, $parentId);
  120. if ($parentElement) {
  121. $path = self::getSortIndexPath($parentElement);
  122. }
  123. $sortIndex = method_exists($element, 'getIndex') ? (int) $element->getIndex() : 0;
  124. $path .= '/' . $sortIndex;
  125. return $path;
  126. }
  127. /**
  128. * @internal
  129. *
  130. * @param array|Model\Listing\AbstractListing $list
  131. * @param string $idGetter
  132. *
  133. * @return int[]
  134. */
  135. public static function getIdList($list, $idGetter = 'getId')
  136. {
  137. $ids = [];
  138. if (is_array($list)) {
  139. foreach ($list as $entry) {
  140. if (is_object($entry) && method_exists($entry, $idGetter)) {
  141. $ids[] = $entry->$idGetter();
  142. } elseif (is_scalar($entry)) {
  143. $ids[] = $entry;
  144. }
  145. }
  146. }
  147. if ($list instanceof Model\Listing\AbstractListing && method_exists($list, 'loadIdList')) {
  148. $ids = $list->loadIdList();
  149. }
  150. $ids = array_unique($ids);
  151. return $ids;
  152. }
  153. /**
  154. * @internal
  155. *
  156. * @param Dependency $d
  157. *
  158. * @return array
  159. */
  160. public static function getRequiredByDependenciesForFrontend(Dependency $d, $offset, $limit)
  161. {
  162. $dependencies['hasHidden'] = false;
  163. $dependencies['requiredBy'] = [];
  164. // requiredBy
  165. foreach ($d->getRequiredBy($offset, $limit) as $r) {
  166. if ($e = self::getDependedElement($r)) {
  167. if ($e->isAllowed('list')) {
  168. $dependencies['requiredBy'][] = self::getDependencyForFrontend($e);
  169. } else {
  170. $dependencies['hasHidden'] = true;
  171. }
  172. }
  173. }
  174. return $dependencies;
  175. }
  176. /**
  177. * @internal
  178. *
  179. * @param Dependency $d
  180. *
  181. * @return array
  182. */
  183. public static function getRequiresDependenciesForFrontend(Dependency $d, $offset, $limit)
  184. {
  185. $dependencies['hasHidden'] = false;
  186. $dependencies['requires'] = [];
  187. // requires
  188. foreach ($d->getRequires($offset, $limit) as $r) {
  189. if ($e = self::getDependedElement($r)) {
  190. if ($e->isAllowed('list')) {
  191. $dependencies['requires'][] = self::getDependencyForFrontend($e);
  192. } else {
  193. $dependencies['hasHidden'] = true;
  194. }
  195. }
  196. }
  197. return $dependencies;
  198. }
  199. /**
  200. * @param Document|Asset|DataObject\AbstractObject $element
  201. *
  202. * @return array
  203. */
  204. private static function getDependencyForFrontend($element)
  205. {
  206. if ($element instanceof ElementInterface) {
  207. return [
  208. 'id' => $element->getId(),
  209. 'path' => $element->getRealFullPath(),
  210. 'type' => self::getElementType($element),
  211. 'subtype' => $element->getType(),
  212. ];
  213. }
  214. }
  215. /**
  216. * @param array $config
  217. *
  218. * @return DataObject\AbstractObject|Document|Asset|null
  219. */
  220. private static function getDependedElement($config)
  221. {
  222. if ($config['type'] == 'object') {
  223. return DataObject::getById($config['id']);
  224. } elseif ($config['type'] == 'asset') {
  225. return Asset::getById($config['id']);
  226. } elseif ($config['type'] == 'document') {
  227. return Document::getById($config['id']);
  228. }
  229. return null;
  230. }
  231. /**
  232. * @static
  233. *
  234. * @return bool
  235. */
  236. public static function doHideUnpublished($element)
  237. {
  238. return ($element instanceof AbstractObject && DataObject::doHideUnpublished())
  239. || ($element instanceof Document && Document::doHideUnpublished());
  240. }
  241. /**
  242. * determines whether an element is published
  243. *
  244. * @internal
  245. *
  246. * @param ElementInterface $element
  247. *
  248. * @return bool
  249. */
  250. public static function isPublished($element = null)
  251. {
  252. if ($element instanceof ElementInterface) {
  253. if (method_exists($element, 'isPublished')) {
  254. return $element->isPublished();
  255. } else {
  256. return true;
  257. }
  258. }
  259. return false;
  260. }
  261. /**
  262. * @internal
  263. *
  264. * @param array|null $data
  265. *
  266. * @return array
  267. *
  268. * @throws \Exception
  269. */
  270. public static function filterUnpublishedAdvancedElements($data): array
  271. {
  272. if (DataObject::doHideUnpublished() && is_array($data)) {
  273. $publishedList = [];
  274. $mapping = [];
  275. foreach ($data as $advancedElement) {
  276. if (!$advancedElement instanceof DataObject\Data\ObjectMetadata
  277. && !$advancedElement instanceof DataObject\Data\ElementMetadata) {
  278. throw new \Exception('only supported for advanced many-to-many (+object) relations');
  279. }
  280. $elementId = null;
  281. if ($advancedElement instanceof DataObject\Data\ObjectMetadata) {
  282. $elementId = $advancedElement->getObjectId();
  283. $elementType = 'object';
  284. } else {
  285. $elementId = $advancedElement->getElementId();
  286. $elementType = $advancedElement->getElementType();
  287. }
  288. if (!$elementId) {
  289. continue;
  290. }
  291. if ($elementType == 'asset') {
  292. // there is no published flag for assets
  293. continue;
  294. }
  295. $mapping[$elementType][$elementId] = true;
  296. }
  297. $db = Db::get();
  298. $publishedMapping = [];
  299. // now do the query;
  300. foreach ($mapping as $elementType => $idList) {
  301. $idList = array_keys($mapping[$elementType]);
  302. switch ($elementType) {
  303. case 'document':
  304. $idColumn = 'id';
  305. $publishedColumn = 'published';
  306. break;
  307. case 'object':
  308. $idColumn = 'o_id';
  309. $publishedColumn = 'o_published';
  310. break;
  311. default:
  312. throw new \Exception('unknown type');
  313. }
  314. $query = 'SELECT ' . $idColumn . ' FROM ' . $elementType . 's WHERE ' . $publishedColumn . '=1 AND ' . $idColumn . ' IN (' . implode(',', $idList) . ');';
  315. $publishedIds = $db->fetchCol($query);
  316. $publishedMapping[$elementType] = $publishedIds;
  317. }
  318. foreach ($data as $advancedElement) {
  319. $elementId = null;
  320. if ($advancedElement instanceof DataObject\Data\ObjectMetadata) {
  321. $elementId = $advancedElement->getObjectId();
  322. $elementType = 'object';
  323. } else {
  324. $elementId = $advancedElement->getElementId();
  325. $elementType = $advancedElement->getElementType();
  326. }
  327. if ($elementType == 'asset') {
  328. $publishedList[] = $advancedElement;
  329. }
  330. if (isset($publishedMapping[$elementType]) && in_array($elementId, $publishedMapping[$elementType])) {
  331. $publishedList[] = $advancedElement;
  332. }
  333. }
  334. return $publishedList;
  335. }
  336. return is_array($data) ? $data : [];
  337. }
  338. /**
  339. * @param string $type
  340. * @param string $path
  341. *
  342. * @return ElementInterface|null
  343. */
  344. public static function getElementByPath($type, $path)
  345. {
  346. $element = null;
  347. if ($type == 'asset') {
  348. $element = Asset::getByPath($path);
  349. } elseif ($type == 'object') {
  350. $element = DataObject::getByPath($path);
  351. } elseif ($type == 'document') {
  352. $element = Document::getByPath($path);
  353. }
  354. return $element;
  355. }
  356. /**
  357. * @internal
  358. *
  359. * @param string|ElementInterface $element
  360. *
  361. * @return string
  362. *
  363. * @throws \Exception
  364. */
  365. public static function getBaseClassNameForElement($element)
  366. {
  367. if ($element instanceof ElementInterface) {
  368. $elementType = self::getElementType($element);
  369. } elseif (is_string($element)) {
  370. $elementType = $element;
  371. } else {
  372. throw new \Exception('Wrong type given for getBaseClassNameForElement(), ElementInterface and string are allowed');
  373. }
  374. $baseClass = ucfirst($elementType);
  375. if ($elementType == 'object') {
  376. $baseClass = 'DataObject';
  377. }
  378. return $baseClass;
  379. }
  380. /**
  381. * @deprecated will be removed in Pimcore 11, use getSafeCopyName() instead
  382. *
  383. * @param string $type
  384. * @param string $sourceKey
  385. * @param ElementInterface $target
  386. *
  387. * @return string
  388. */
  389. public static function getSaveCopyName($type, $sourceKey, $target)
  390. {
  391. return self::getSafeCopyName($sourceKey, $target);
  392. }
  393. /**
  394. * Returns a uniqe key for the element in the $target-Path (recursive)
  395. *
  396. * @return string
  397. *
  398. * @param string $sourceKey
  399. * @param ElementInterface $target
  400. */
  401. public static function getSafeCopyName(string $sourceKey, ElementInterface $target)
  402. {
  403. $type = self::getElementType($target);
  404. if (self::pathExists($target->getRealFullPath() . '/' . $sourceKey, $type)) {
  405. // only for assets: add the prefix _copy before the file extension (if exist) not after to that source.jpg will be source_copy.jpg and not source.jpg_copy
  406. if ($type == 'asset' && $fileExtension = File::getFileExtension($sourceKey)) {
  407. $sourceKey = preg_replace('/\.' . $fileExtension . '$/i', '_copy.' . $fileExtension, $sourceKey);
  408. } elseif (preg_match("/_copy(|_\d*)$/", $sourceKey) === 1) {
  409. // If key already ends with _copy or copy_N, append a digit to avoid _copy_copy_copy naming
  410. $keyParts = explode('_', $sourceKey);
  411. $counterKey = array_key_last($keyParts);
  412. if ((int)$keyParts[$counterKey] > 0) {
  413. $keyParts[$counterKey] = (int)$keyParts[$counterKey] + 1;
  414. } else {
  415. $keyParts[] = 1;
  416. }
  417. $sourceKey = implode('_', $keyParts);
  418. } else {
  419. $sourceKey .= '_copy';
  420. }
  421. return self::getSafeCopyName($sourceKey, $target);
  422. }
  423. return $sourceKey;
  424. }
  425. /**
  426. * @param string $path
  427. * @param string|null $type
  428. *
  429. * @return bool
  430. */
  431. public static function pathExists($path, $type = null)
  432. {
  433. if ($type == 'asset') {
  434. return Asset\Service::pathExists($path);
  435. } elseif ($type == 'document') {
  436. return Document\Service::pathExists($path);
  437. } elseif ($type == 'object') {
  438. return DataObject\Service::pathExists($path);
  439. }
  440. return false;
  441. }
  442. /**
  443. * @param string $type
  444. * @param int $id
  445. * @param bool $force
  446. *
  447. * @return Asset|AbstractObject|Document|null
  448. */
  449. public static function getElementById($type, $id, $force = false)
  450. {
  451. $element = null;
  452. if ($type === 'asset') {
  453. $element = Asset::getById($id, $force);
  454. } elseif ($type === 'object') {
  455. $element = DataObject::getById($id, $force);
  456. } elseif ($type === 'document') {
  457. $element = Document::getById($id, $force);
  458. }
  459. return $element;
  460. }
  461. /**
  462. * @static
  463. *
  464. * @param ElementInterface $element
  465. *
  466. * @return string|null
  467. */
  468. public static function getElementType($element): ?string
  469. {
  470. if ($element instanceof DataObject\AbstractObject) {
  471. return 'object';
  472. }
  473. if ($element instanceof Document) {
  474. return 'document';
  475. }
  476. if ($element instanceof Asset) {
  477. return 'asset';
  478. }
  479. return null;
  480. }
  481. /**
  482. * @internal
  483. *
  484. * @param string $className
  485. *
  486. * @return string|null
  487. */
  488. public static function getElementTypeByClassName(string $className): ?string
  489. {
  490. $className = trim($className, '\\');
  491. if (is_a($className, AbstractObject::class, true)) {
  492. return 'object';
  493. }
  494. if (is_a($className, Asset::class, true)) {
  495. return 'asset';
  496. }
  497. if (is_a($className, Document::class, true)) {
  498. return 'document';
  499. }
  500. return null;
  501. }
  502. /**
  503. * @internal
  504. *
  505. * @param ElementInterface $element
  506. *
  507. * @return string|null
  508. */
  509. public static function getElementHash(ElementInterface $element): ?string
  510. {
  511. $elementType = self::getElementType($element);
  512. if ($elementType === null) {
  513. return null;
  514. }
  515. return $elementType . '-' . $element->getId();
  516. }
  517. /**
  518. * determines the type of an element (object,asset,document)
  519. *
  520. * @deprecated use getElementType() instead, will be removed in Pimcore 11
  521. *
  522. * @param ElementInterface $element
  523. *
  524. * @return string
  525. */
  526. public static function getType($element)
  527. {
  528. return self::getElementType($element);
  529. }
  530. /**
  531. * @internal
  532. *
  533. * @param array $props
  534. *
  535. * @return array
  536. */
  537. public static function minimizePropertiesForEditmode($props)
  538. {
  539. $properties = [];
  540. foreach ($props as $key => $p) {
  541. //$p = object2array($p);
  542. $allowedProperties = [
  543. 'key',
  544. 'o_key',
  545. 'filename',
  546. 'path',
  547. 'o_path',
  548. 'id',
  549. 'o_id',
  550. 'o_type',
  551. 'type',
  552. ];
  553. if ($p->getData() instanceof Document || $p->getData() instanceof Asset || $p->getData() instanceof DataObject\AbstractObject) {
  554. $pa = [];
  555. $vars = $p->getData()->getObjectVars();
  556. foreach ($vars as $k => $value) {
  557. if (in_array($k, $allowedProperties)) {
  558. $pa[$k] = $value;
  559. }
  560. }
  561. // clone it because of caching
  562. $tmp = clone $p;
  563. $tmp->setData($pa);
  564. $properties[$key] = $tmp->getObjectVars();
  565. } else {
  566. $properties[$key] = $p->getObjectVars();
  567. }
  568. // add config from predefined properties
  569. if ($p->getName() && $p->getType()) {
  570. $predefined = Model\Property\Predefined::getByKey($p->getName());
  571. if ($predefined && $predefined->getType() == $p->getType()) {
  572. $properties[$key]['config'] = $predefined->getConfig();
  573. $properties[$key]['description'] = $predefined->getDescription();
  574. }
  575. }
  576. }
  577. return $properties;
  578. }
  579. /**
  580. * @internal
  581. *
  582. * @param DataObject\AbstractObject|Document|Asset\Folder $target the parent element
  583. * @param ElementInterface $new the newly inserted child
  584. */
  585. protected function updateChildren($target, $new)
  586. {
  587. //check in case of recursion
  588. $found = false;
  589. foreach ($target->getChildren() as $child) {
  590. /**
  591. * @var ElementInterface $child
  592. */
  593. if ($child->getId() == $new->getId()) {
  594. $found = true;
  595. break;
  596. }
  597. }
  598. if (!$found) {
  599. $target->setChildren(array_merge($target->getChildren(), [$new]));
  600. }
  601. }
  602. /**
  603. * @internal
  604. *
  605. * @param ElementInterface $element
  606. *
  607. * @return array
  608. */
  609. public static function gridElementData(ElementInterface $element)
  610. {
  611. $data = [
  612. 'id' => $element->getId(),
  613. 'fullpath' => $element->getRealFullPath(),
  614. 'type' => self::getType($element),
  615. 'subtype' => $element->getType(),
  616. 'filename' => $element->getKey(),
  617. 'creationDate' => $element->getCreationDate(),
  618. 'modificationDate' => $element->getModificationDate(),
  619. ];
  620. if (method_exists($element, 'isPublished')) {
  621. $data['published'] = $element->isPublished();
  622. } else {
  623. $data['published'] = true;
  624. }
  625. return $data;
  626. }
  627. /**
  628. * find all elements which the user may not list and therefore may never be shown to the user
  629. *
  630. * @internal
  631. *
  632. * @param string $type asset|object|document
  633. * @param Model\User $user
  634. *
  635. * @return array
  636. */
  637. public static function findForbiddenPaths($type, $user)
  638. {
  639. if ($user->isAdmin()) {
  640. return [];
  641. }
  642. // get workspaces
  643. $workspaces = $user->{'getWorkspaces' . ucfirst($type)}();
  644. foreach ($user->getRoles() as $roleId) {
  645. $role = Model\User\Role::getById($roleId);
  646. $workspaces = array_merge($workspaces, $role->{'getWorkspaces' . ucfirst($type)}());
  647. }
  648. $forbidden = [];
  649. if (count($workspaces) > 0) {
  650. foreach ($workspaces as $workspace) {
  651. if (!$workspace->getList()) {
  652. $forbidden[] = $workspace->getCpath();
  653. }
  654. }
  655. } else {
  656. $forbidden[] = '/';
  657. }
  658. return $forbidden;
  659. }
  660. /**
  661. * renews all references, for example after unserializing an ElementInterface
  662. *
  663. * @internal
  664. *
  665. * @param mixed $data
  666. * @param bool $initial
  667. * @param string $key
  668. *
  669. * @return mixed
  670. */
  671. public static function renewReferences($data, $initial = true, $key = null)
  672. {
  673. if ($data instanceof \__PHP_Incomplete_Class) {
  674. Logger::err(sprintf('Renew References: Cannot read data (%s) of incomplete class.', is_null($key) ? 'not available' : $key));
  675. return null;
  676. }
  677. if (is_array($data)) {
  678. foreach ($data as $dataKey => &$value) {
  679. $value = self::renewReferences($value, false, $dataKey);
  680. }
  681. return $data;
  682. }
  683. if (is_object($data)) {
  684. if ($data instanceof ElementInterface && !$initial) {
  685. return self::getElementById(self::getElementType($data), $data->getId());
  686. }
  687. // if this is the initial element set the correct path and key
  688. if ($data instanceof ElementInterface && !DataObject\AbstractObject::doNotRestoreKeyAndPath()) {
  689. $originalElement = self::getElementById(self::getElementType($data), $data->getId());
  690. if ($originalElement) {
  691. //do not override filename for Assets https://github.com/pimcore/pimcore/issues/8316
  692. // if ($data instanceof Asset) {
  693. // /** @var Asset $originalElement */
  694. // $data->setFilename($originalElement->getFilename());
  695. // } else
  696. if ($data instanceof Document) {
  697. /** @var Document $originalElement */
  698. $data->setKey($originalElement->getKey());
  699. } elseif ($data instanceof DataObject\AbstractObject) {
  700. /** @var AbstractObject $originalElement */
  701. $data->setKey($originalElement->getKey());
  702. }
  703. $data->setPath($originalElement->getRealPath());
  704. }
  705. }
  706. if ($data instanceof Model\AbstractModel) {
  707. $properties = $data->getObjectVars();
  708. foreach ($properties as $name => $value) {
  709. $data->setObjectVar($name, self::renewReferences($value, false, $name), true);
  710. }
  711. } else {
  712. $properties = method_exists($data, 'getObjectVars') ? $data->getObjectVars() : get_object_vars($data);
  713. foreach ($properties as $name => $value) {
  714. if (method_exists($data, 'setObjectVar')) {
  715. $data->setObjectVar($name, self::renewReferences($value, false, $name), true);
  716. } else {
  717. $data->$name = self::renewReferences($value, false, $name);
  718. }
  719. }
  720. }
  721. return $data;
  722. }
  723. return $data;
  724. }
  725. /**
  726. * @internal
  727. *
  728. * @param string $path
  729. *
  730. * @return string
  731. */
  732. public static function correctPath(string $path): string
  733. {
  734. // remove trailing slash
  735. if ($path !== '/') {
  736. $path = rtrim($path, '/ ');
  737. }
  738. // correct wrong path (root-node problem)
  739. $path = str_replace('//', '/', $path);
  740. if (str_contains($path, '%')) {
  741. $path = rawurldecode($path);
  742. }
  743. return $path;
  744. }
  745. /**
  746. * @internal
  747. *
  748. * @param ElementInterface $element
  749. *
  750. * @return ElementInterface
  751. */
  752. public static function loadAllFields(ElementInterface $element): ElementInterface
  753. {
  754. if ($element instanceof Document) {
  755. Document\Service::loadAllDocumentFields($element);
  756. } elseif ($element instanceof DataObject\Concrete) {
  757. DataObject\Service::loadAllObjectFields($element);
  758. } elseif ($element instanceof Asset) {
  759. Asset\Service::loadAllFields($element);
  760. }
  761. return $element;
  762. }
  763. /** Callback for array_filter function.
  764. * @param string $var value
  765. *
  766. * @return bool true if value is accepted
  767. */
  768. private static function filterNullValues($var)
  769. {
  770. return strlen($var) > 0;
  771. }
  772. /**
  773. * @param string $path
  774. * @param array $options
  775. *
  776. * @return Asset\Folder|Document\Folder|DataObject\Folder
  777. *
  778. * @throws \Exception
  779. */
  780. public static function createFolderByPath($path, $options = [])
  781. {
  782. $calledClass = get_called_class();
  783. if ($calledClass == __CLASS__) {
  784. throw new \Exception('This method must be called from a extended class. e.g Asset\\Service, DataObject\\Service, Document\\Service');
  785. }
  786. $type = str_replace('\Service', '', $calledClass);
  787. $type = '\\' . ltrim($type, '\\');
  788. $folderType = $type . '\Folder';
  789. $lastFolder = null;
  790. $pathsArray = [];
  791. $parts = explode('/', $path);
  792. $parts = array_filter($parts, '\\Pimcore\\Model\\Element\\Service::filterNullValues');
  793. $sanitizedPath = '/';
  794. $itemType = self::getElementType(new $type);
  795. foreach ($parts as $part) {
  796. $sanitizedPath = $sanitizedPath . self::getValidKey($part, $itemType) . '/';
  797. }
  798. if (self::pathExists($sanitizedPath, $itemType)) {
  799. return $type::getByPath($sanitizedPath);
  800. }
  801. foreach ($parts as $part) {
  802. $pathPart = $pathsArray[count($pathsArray) - 1] ?? '';
  803. $pathsArray[] = $pathPart . '/' . self::getValidKey($part, $itemType);
  804. }
  805. for ($i = 0; $i < count($pathsArray); $i++) {
  806. $currentPath = $pathsArray[$i];
  807. if (!self::pathExists($currentPath, $itemType)) {
  808. $parentFolderPath = ($i == 0) ? '/' : $pathsArray[$i - 1];
  809. $parentFolder = $type::getByPath($parentFolderPath);
  810. $folder = new $folderType();
  811. $folder->setParent($parentFolder);
  812. if ($parentFolder) {
  813. $folder->setParentId($parentFolder->getId());
  814. } else {
  815. $folder->setParentId(1);
  816. }
  817. $key = substr($currentPath, strrpos($currentPath, '/') + 1, strlen($currentPath));
  818. if (method_exists($folder, 'setKey')) {
  819. $folder->setKey($key);
  820. }
  821. if (method_exists($folder, 'setFilename')) {
  822. $folder->setFilename($key);
  823. }
  824. if (method_exists($folder, 'setType')) {
  825. $folder->setType('folder');
  826. }
  827. $folder->setPath($currentPath);
  828. $folder->setUserModification(0);
  829. $folder->setUserOwner(1);
  830. $folder->setCreationDate(time());
  831. $folder->setModificationDate(time());
  832. $folder->setValues($options);
  833. $folder->save();
  834. $lastFolder = $folder;
  835. }
  836. }
  837. return $lastFolder;
  838. }
  839. /**
  840. * Changes the query according to the custom view config
  841. *
  842. * @internal
  843. *
  844. * @param array $cv
  845. * @param Model\Asset\Listing|Model\DataObject\Listing|Model\Document\Listing $childsList
  846. */
  847. public static function addTreeFilterJoins($cv, $childsList)
  848. {
  849. if ($cv) {
  850. $childsList->onCreateQueryBuilder(static function (DoctrineQueryBuilder $select) use ($cv) {
  851. $where = $cv['where'] ?? null;
  852. if ($where) {
  853. $select->andWhere($where);
  854. }
  855. $fromAlias = $select->getQueryPart('from')[0]['alias'] ?? $select->getQueryPart('from')[0]['table'] ;
  856. $customViewJoins = $cv['joins'] ?? null;
  857. if ($customViewJoins) {
  858. foreach ($customViewJoins as $joinConfig) {
  859. $type = $joinConfig['type'];
  860. $method = $type == 'left' || $type == 'right' ? $method = $type . 'Join' : 'join';
  861. $joinAlias = array_keys($joinConfig['name']);
  862. $joinAlias = reset($joinAlias);
  863. $joinTable = $joinConfig['name'][$joinAlias];
  864. $condition = $joinConfig['condition'];
  865. $columns = $joinConfig['columns'];
  866. $select->addSelect($columns);
  867. $select->$method($fromAlias, $joinTable, $joinAlias, $condition);
  868. }
  869. }
  870. if (!empty($cv['having'])) {
  871. $select->having($cv['having']);
  872. }
  873. });
  874. }
  875. }
  876. /**
  877. * @internal
  878. *
  879. * @param string $id
  880. *
  881. * @return array|null
  882. */
  883. public static function getCustomViewById($id)
  884. {
  885. $customViews = Tool::getCustomViewConfig();
  886. if ($customViews) {
  887. foreach ($customViews as $customView) {
  888. if ($customView['id'] == $id) {
  889. return $customView;
  890. }
  891. }
  892. }
  893. return null;
  894. }
  895. /**
  896. * @param string $key
  897. * @param string $type
  898. *
  899. * @return string
  900. */
  901. public static function getValidKey($key, $type)
  902. {
  903. $event = new GenericEvent(null, [
  904. 'key' => $key,
  905. 'type' => $type,
  906. ]);
  907. \Pimcore::getEventDispatcher()->dispatch($event, SystemEvents::SERVICE_PRE_GET_VALID_KEY);
  908. $key = $event->getArgument('key');
  909. $key = trim($key);
  910. // replace all 4 byte unicode characters
  911. $key = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '-', $key);
  912. // replace slashes with a hyphen
  913. $key = str_replace('/', '-', $key);
  914. if ($type === 'object') {
  915. $key = preg_replace('/[<>]/', '-', $key);
  916. } elseif ($type === 'document') {
  917. // replace URL reserved characters with a hyphen
  918. $key = preg_replace('/[#\?\*\:\\\\<\>\|"%&@=;\+]/', '-', $key);
  919. } elseif ($type === 'asset') {
  920. // keys shouldn't start with a "." (=hidden file) *nix operating systems
  921. // keys shouldn't end with a "." - Windows issue: filesystem API trims automatically . at the end of a folder name (no warning ... et al)
  922. $key = trim($key, '. ');
  923. // windows forbidden filenames + URL reserved characters (at least the ones which are problematic)
  924. $key = preg_replace('/[#\?\*\:\\\\<\>\|"%\+]/', '-', $key);
  925. } else {
  926. $key = ltrim($key, '. ');
  927. }
  928. $key = mb_substr($key, 0, 255);
  929. return $key;
  930. }
  931. /**
  932. * @param string $key
  933. * @param string $type
  934. *
  935. * @return bool
  936. */
  937. public static function isValidKey($key, $type)
  938. {
  939. return self::getValidKey($key, $type) == $key;
  940. }
  941. /**
  942. * @param string $path
  943. * @param string $type
  944. *
  945. * @return bool
  946. */
  947. public static function isValidPath($path, $type)
  948. {
  949. $parts = explode('/', $path);
  950. foreach ($parts as $part) {
  951. if (!self::isValidKey($part, $type)) {
  952. return false;
  953. }
  954. }
  955. return true;
  956. }
  957. /**
  958. * returns a unique key for an element
  959. *
  960. * @param ElementInterface $element
  961. *
  962. * @return string|null
  963. */
  964. public static function getUniqueKey($element)
  965. {
  966. if ($element instanceof DataObject\AbstractObject) {
  967. return DataObject\Service::getUniqueKey($element);
  968. }
  969. if ($element instanceof Document) {
  970. return Document\Service::getUniqueKey($element);
  971. }
  972. if ($element instanceof Asset) {
  973. return Asset\Service::getUniqueKey($element);
  974. }
  975. return null;
  976. }
  977. /**
  978. * @internal
  979. *
  980. * @param array $data
  981. * @param string $type
  982. *
  983. * @return array
  984. */
  985. public static function fixAllowedTypes($data, $type)
  986. {
  987. // this is the new method with Ext.form.MultiSelect
  988. if (is_array($data) && count($data)) {
  989. $first = reset($data);
  990. if (!is_array($first)) {
  991. $parts = $data;
  992. $data = [];
  993. foreach ($parts as $elementType) {
  994. $data[] = [$type => $elementType];
  995. }
  996. } else {
  997. $newList = [];
  998. foreach ($data as $key => $item) {
  999. if ($item) {
  1000. if (is_array($item)) {
  1001. foreach ($item as $itemKey => $itemValue) {
  1002. if ($itemValue) {
  1003. $newList[$key][$itemKey] = $itemValue;
  1004. }
  1005. }
  1006. } else {
  1007. $newList[$key] = $item;
  1008. }
  1009. }
  1010. }
  1011. $data = $newList;
  1012. }
  1013. }
  1014. return $data ? $data : [];
  1015. }
  1016. /**
  1017. * @internal
  1018. *
  1019. * @param Model\Version[] $versions
  1020. *
  1021. * @return array
  1022. */
  1023. public static function getSafeVersionInfo($versions)
  1024. {
  1025. $indexMap = [];
  1026. $result = [];
  1027. if (is_array($versions)) {
  1028. foreach ($versions as $versionObj) {
  1029. $version = [
  1030. 'id' => $versionObj->getId(),
  1031. 'cid' => $versionObj->getCid(),
  1032. 'ctype' => $versionObj->getCtype(),
  1033. 'note' => $versionObj->getNote(),
  1034. 'date' => $versionObj->getDate(),
  1035. 'public' => $versionObj->getPublic(),
  1036. 'versionCount' => $versionObj->getVersionCount(),
  1037. 'autoSave' => $versionObj->isAutoSave(),
  1038. ];
  1039. $version['user'] = ['name' => '', 'id' => ''];
  1040. if ($user = $versionObj->getUser()) {
  1041. $version['user'] = [
  1042. 'name' => $user->getName(),
  1043. 'id' => $user->getId(),
  1044. ];
  1045. }
  1046. $versionKey = $versionObj->getDate() . '-' . $versionObj->getVersionCount();
  1047. if (!isset($indexMap[$versionKey])) {
  1048. $indexMap[$versionKey] = 0;
  1049. }
  1050. $version['index'] = $indexMap[$versionKey];
  1051. $indexMap[$versionKey] = $indexMap[$versionKey] + 1;
  1052. $result[] = $version;
  1053. }
  1054. }
  1055. return $result;
  1056. }
  1057. /**
  1058. * @param ElementInterface $element
  1059. *
  1060. * @return ElementInterface
  1061. */
  1062. public static function cloneMe(ElementInterface $element)
  1063. {
  1064. $deepCopy = new \DeepCopy\DeepCopy();
  1065. $deepCopy->addFilter(new \DeepCopy\Filter\KeepFilter(), new class($element) implements \DeepCopy\Matcher\Matcher {
  1066. /**
  1067. * The element to be cloned
  1068. *
  1069. * @var ElementInterface
  1070. */
  1071. private $element;
  1072. /**
  1073. * @param ElementInterface $element
  1074. */
  1075. public function __construct($element)
  1076. {
  1077. $this->element = $element;
  1078. }
  1079. /**
  1080. * {@inheritdoc}
  1081. */
  1082. public function matches($object, $property)
  1083. {
  1084. try {
  1085. $reflectionProperty = new \ReflectionProperty($object, $property);
  1086. } catch (\Exception $e) {
  1087. return false;
  1088. }
  1089. $reflectionProperty->setAccessible(true);
  1090. $myValue = $reflectionProperty->getValue($object);
  1091. return $myValue instanceof ElementInterface;
  1092. }
  1093. });
  1094. if ($element instanceof Concrete) {
  1095. $deepCopy->addFilter(
  1096. new PimcoreClassDefinitionReplaceFilter(
  1097. function (Concrete $object, Data $fieldDefinition, $property, $currentValue) {
  1098. if ($fieldDefinition instanceof Data\CustomDataCopyInterface) {
  1099. return $fieldDefinition->createDataCopy($object, $currentValue);
  1100. }
  1101. return $currentValue;
  1102. }
  1103. ),
  1104. new PimcoreClassDefinitionMatcher(Data\CustomDataCopyInterface::class)
  1105. );
  1106. }
  1107. $deepCopy->addFilter(new SetNullFilter(), new PropertyNameMatcher('dao'));
  1108. $deepCopy->addFilter(new SetNullFilter(), new PropertyNameMatcher('resource'));
  1109. $deepCopy->addFilter(new SetNullFilter(), new PropertyNameMatcher('writeResource'));
  1110. $deepCopy->addFilter(new \DeepCopy\Filter\Doctrine\DoctrineCollectionFilter(), new \DeepCopy\Matcher\PropertyTypeMatcher(
  1111. Collection::class
  1112. ));
  1113. if ($element instanceof DataObject\Concrete) {
  1114. DataObject\Service::loadAllObjectFields($element);
  1115. }
  1116. $theCopy = $deepCopy->copy($element);
  1117. $theCopy->setId(null);
  1118. $theCopy->setParent(null);
  1119. return $theCopy;
  1120. }
  1121. /**
  1122. * @internal
  1123. *
  1124. * @param Note $note
  1125. *
  1126. * @return array
  1127. */
  1128. public static function getNoteData(Note $note)
  1129. {
  1130. $cpath = '';
  1131. if ($note->getCid() && $note->getCtype()) {
  1132. if ($element = Service::getElementById($note->getCtype(), $note->getCid())) {
  1133. $cpath = $element->getRealFullPath();
  1134. }
  1135. }
  1136. $e = [
  1137. 'id' => $note->getId(),
  1138. 'type' => $note->getType(),
  1139. 'cid' => $note->getCid(),
  1140. 'ctype' => $note->getCtype(),
  1141. 'cpath' => $cpath,
  1142. 'date' => $note->getDate(),
  1143. 'title' => $note->getTitle(),
  1144. 'description' => $note->getDescription(),
  1145. ];
  1146. // prepare key-values
  1147. $keyValues = [];
  1148. if (is_array($note->getData())) {
  1149. foreach ($note->getData() as $name => $d) {
  1150. $type = $d['type'];
  1151. $data = $d['data'];
  1152. if ($type == 'document' || $type == 'object' || $type == 'asset') {
  1153. if ($d['data'] instanceof ElementInterface) {
  1154. $data = [
  1155. 'id' => $d['data']->getId(),
  1156. 'path' => $d['data']->getRealFullPath(),
  1157. 'type' => $d['data']->getType(),
  1158. ];
  1159. }
  1160. } elseif ($type == 'date') {
  1161. if (is_object($d['data'])) {
  1162. $data = $d['data']->getTimestamp();
  1163. }
  1164. }
  1165. $keyValue = [
  1166. 'type' => $type,
  1167. 'name' => $name,
  1168. 'data' => $data,
  1169. ];
  1170. $keyValues[] = $keyValue;
  1171. }
  1172. }
  1173. $e['data'] = $keyValues;
  1174. // prepare user data
  1175. if ($note->getUser()) {
  1176. $user = Model\User::getById($note->getUser());
  1177. if ($user) {
  1178. $e['user'] = [
  1179. 'id' => $user->getId(),
  1180. 'name' => $user->getName(),
  1181. ];
  1182. } else {
  1183. $e['user'] = '';
  1184. }
  1185. }
  1186. return $e;
  1187. }
  1188. /**
  1189. * @internal
  1190. *
  1191. * @param string $type
  1192. * @param int $elementId
  1193. * @param null|string $postfix
  1194. *
  1195. * @return string
  1196. */
  1197. public static function getSessionKey($type, $elementId, $postfix = '')
  1198. {
  1199. $sessionId = Session::getSessionId();
  1200. $tmpStoreKey = $type . '_session_' . $elementId . '_' . $sessionId . $postfix;
  1201. return $tmpStoreKey;
  1202. }
  1203. /**
  1204. *
  1205. * @param string $type
  1206. * @param int $elementId
  1207. * @param null|string $postfix
  1208. *
  1209. * @return AbstractObject|Document|Asset|null
  1210. */
  1211. public static function getElementFromSession($type, $elementId, $postfix = '')
  1212. {
  1213. $element = null;
  1214. $tmpStoreKey = self::getSessionKey($type, $elementId, $postfix);
  1215. $tmpStore = TmpStore::get($tmpStoreKey);
  1216. if ($tmpStore) {
  1217. $data = $tmpStore->getData();
  1218. if ($data) {
  1219. $element = Serialize::unserialize($data);
  1220. $context = [
  1221. 'source' => __METHOD__,
  1222. 'conversion' => 'unmarshal',
  1223. ];
  1224. $copier = Self::getDeepCopyInstance($element, $context);
  1225. if ($element instanceof Concrete) {
  1226. $copier->addFilter(
  1227. new PimcoreClassDefinitionReplaceFilter(
  1228. function (Concrete $object, Data $fieldDefinition, $property, $currentValue) {
  1229. if ($fieldDefinition instanceof Data\CustomVersionMarshalInterface) {
  1230. return $fieldDefinition->unmarshalVersion($object, $currentValue);
  1231. }
  1232. return $currentValue;
  1233. }
  1234. ),
  1235. new PimcoreClassDefinitionMatcher(Data\CustomVersionMarshalInterface::class)
  1236. );
  1237. }
  1238. return $copier->copy($element);
  1239. }
  1240. }
  1241. return $element;
  1242. }
  1243. /**
  1244. * @internal
  1245. *
  1246. * @param ElementInterface $element
  1247. * @param string $postfix
  1248. * @param bool $clone save a copy
  1249. */
  1250. public static function saveElementToSession($element, $postfix = '', $clone = true)
  1251. {
  1252. if ($clone) {
  1253. $context = [
  1254. 'source' => __METHOD__,
  1255. 'conversion' => 'marshal',
  1256. ];
  1257. $copier = self::getDeepCopyInstance($element, $context);
  1258. if ($element instanceof Concrete) {
  1259. $copier->addFilter(
  1260. new PimcoreClassDefinitionReplaceFilter(
  1261. function (Concrete $object, Data $fieldDefinition, $property, $currentValue) {
  1262. if ($fieldDefinition instanceof Data\CustomVersionMarshalInterface) {
  1263. return $fieldDefinition->marshalVersion($object, $currentValue);
  1264. }
  1265. return $currentValue;
  1266. }
  1267. ),
  1268. new PimcoreClassDefinitionMatcher(Data\CustomVersionMarshalInterface::class)
  1269. );
  1270. }
  1271. $element = $copier->copy($element);
  1272. }
  1273. $elementType = Service::getElementType($element);
  1274. $tmpStoreKey = self::getSessionKey($elementType, $element->getId(), $postfix);
  1275. $tag = $elementType . '-session' . $postfix;
  1276. if ($element instanceof ElementDumpStateInterface) {
  1277. self::loadAllFields($element);
  1278. $element->setInDumpState(true);
  1279. }
  1280. $serializedData = Serialize::serialize($element);
  1281. TmpStore::set($tmpStoreKey, $serializedData, $tag);
  1282. }
  1283. /**
  1284. * @internal
  1285. *
  1286. * @param string $type
  1287. * @param int $elementId
  1288. * @param string $postfix
  1289. */
  1290. public static function removeElementFromSession($type, $elementId, $postfix = '')
  1291. {
  1292. $tmpStoreKey = self::getSessionKey($type, $elementId, $postfix);
  1293. TmpStore::delete($tmpStoreKey);
  1294. }
  1295. /**
  1296. * @internal
  1297. *
  1298. * @param mixed|null $element
  1299. * @param array|null $context
  1300. *
  1301. * @return DeepCopy
  1302. */
  1303. public static function getDeepCopyInstance($element, ?array $context = []): DeepCopy
  1304. {
  1305. $copier = new DeepCopy();
  1306. $copier->skipUncloneable(true);
  1307. if ($element instanceof ElementInterface) {
  1308. if (($context['conversion'] ?? false) === 'marshal') {
  1309. $sourceType = Service::getType($element);
  1310. $sourceId = $element->getId();
  1311. $copier->addTypeFilter(
  1312. new \DeepCopy\TypeFilter\ReplaceFilter(
  1313. function ($currentValue) {
  1314. if ($currentValue instanceof ElementInterface) {
  1315. $elementType = Service::getType($currentValue);
  1316. $descriptor = new ElementDescriptor($elementType, $currentValue->getId());
  1317. return $descriptor;
  1318. }
  1319. return $currentValue;
  1320. }
  1321. ),
  1322. new MarshalMatcher($sourceType, $sourceId)
  1323. );
  1324. } elseif (($context['conversion'] ?? false) === 'unmarshal') {
  1325. $copier->addTypeFilter(
  1326. new \DeepCopy\TypeFilter\ReplaceFilter(
  1327. function ($currentValue) {
  1328. if ($currentValue instanceof ElementDescriptor) {
  1329. $value = Service::getElementById($currentValue->getType(), $currentValue->getId());
  1330. return $value;
  1331. }
  1332. return $currentValue;
  1333. }
  1334. ),
  1335. new UnmarshalMatcher()
  1336. );
  1337. }
  1338. }
  1339. if ($context['defaultFilters'] ?? false) {
  1340. $copier->addFilter(new DoctrineCollectionFilter(), new PropertyTypeMatcher('Doctrine\Common\Collections\Collection'));
  1341. $copier->addFilter(new SetNullFilter(), new PropertyTypeMatcher('Psr\Container\ContainerInterface'));
  1342. $copier->addFilter(new SetNullFilter(), new PropertyTypeMatcher('Pimcore\Model\DataObject\ClassDefinition'));
  1343. }
  1344. $event = new GenericEvent(null, [
  1345. 'copier' => $copier,
  1346. 'element' => $element,
  1347. 'context' => $context,
  1348. ]);
  1349. \Pimcore::getEventDispatcher()->dispatch($event, SystemEvents::SERVICE_PRE_GET_DEEP_COPY);
  1350. return $event->getArgument('copier');
  1351. }
  1352. /**
  1353. * @internal
  1354. *
  1355. * @param array $rowData
  1356. *
  1357. * @return array
  1358. */
  1359. public static function escapeCsvRecord(array $rowData): array
  1360. {
  1361. if (self::$formatter === null) {
  1362. self::$formatter = new EscapeFormula("'", ['=', '-', '+', '@']);
  1363. }
  1364. $rowData = self::$formatter->escapeRecord($rowData);
  1365. return $rowData;
  1366. }
  1367. }