vendor/pimcore/pimcore/models/DataObject/AbstractObject/Dao.php line 236

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\DataObject\AbstractObject;
  15. use Pimcore\Db;
  16. use Pimcore\Logger;
  17. use Pimcore\Model;
  18. use Pimcore\Model\DataObject;
  19. /**
  20. * @internal
  21. *
  22. * @property \Pimcore\Model\DataObject\AbstractObject $model
  23. */
  24. class Dao extends Model\Element\Dao
  25. {
  26. /**
  27. * Get the data for the object from database for the given id
  28. *
  29. * @param int $id
  30. *
  31. * @throws \Exception
  32. */
  33. public function getById($id)
  34. {
  35. $data = $this->db->fetchRow("SELECT objects.*, tree_locks.locked as o_locked FROM objects
  36. LEFT JOIN tree_locks ON objects.o_id = tree_locks.id AND tree_locks.type = 'object'
  37. WHERE o_id = ?", $id);
  38. if (!empty($data['o_id'])) {
  39. $this->assignVariablesToModel($data);
  40. } else {
  41. throw new \Exception('Object with the ID ' . $id . " doesn't exists");
  42. }
  43. }
  44. /**
  45. * Get the data for the object from database for the given path
  46. *
  47. * @param string $path
  48. *
  49. * @throws Model\Exception\NotFoundException
  50. */
  51. public function getByPath($path)
  52. {
  53. $params = $this->extractKeyAndPath($path);
  54. $data = $this->db->fetchRow('SELECT o_id FROM objects WHERE o_path = :path AND `o_key` = :key', $params);
  55. if (!empty($data['o_id'])) {
  56. $this->assignVariablesToModel($data);
  57. } else {
  58. throw new Model\Exception\NotFoundException("object doesn't exist");
  59. }
  60. }
  61. /**
  62. * Create a new record for the object in database
  63. */
  64. public function create()
  65. {
  66. $this->db->insert('objects', [
  67. 'o_key' => $this->model->getKey(),
  68. 'o_path' => $this->model->getRealPath(),
  69. ]);
  70. $this->model->setId($this->db->lastInsertId());
  71. if (!$this->model->getKey() && !is_numeric($this->model->getKey())) {
  72. $this->model->setKey($this->db->lastInsertId());
  73. }
  74. }
  75. /**
  76. * @param bool|null $isUpdate
  77. *
  78. * @throws \Exception
  79. */
  80. public function update($isUpdate = null)
  81. {
  82. $object = $this->model->getObjectVars();
  83. $data = [];
  84. $validTableColumns = $this->getValidTableColumns('objects');
  85. foreach ($object as $key => $value) {
  86. if (in_array($key, $validTableColumns)) {
  87. if (is_bool($value)) {
  88. $value = (int)$value;
  89. }
  90. $data[$key] = $value;
  91. }
  92. }
  93. // check the type before updating, changing the type or class of an object is not possible
  94. $checkColumns = ['o_type', 'o_classId', 'o_className'];
  95. $existingData = $this->db->fetchRow('SELECT ' . implode(',', $checkColumns) . ' FROM objects WHERE o_id = ?', [$this->model->getId()]);
  96. foreach ($checkColumns as $column) {
  97. if ($column == 'o_type' && in_array($data[$column], [DataObject::OBJECT_TYPE_VARIANT, DataObject::OBJECT_TYPE_OBJECT]) && (isset($existingData[$column]) && in_array($existingData[$column], [DataObject::OBJECT_TYPE_VARIANT, DataObject::OBJECT_TYPE_OBJECT]))) {
  98. // type conversion variant <=> object should be possible
  99. continue;
  100. }
  101. if (!empty($existingData[$column]) && $data[$column] != $existingData[$column]) {
  102. throw new \Exception('Unable to save object: type, classId or className mismatch');
  103. }
  104. }
  105. $this->db->insertOrUpdate('objects', $data);
  106. // tree_locks
  107. $this->db->delete('tree_locks', ['id' => $this->model->getId(), 'type' => 'object']);
  108. if ($this->model->getLocked()) {
  109. $this->db->insert('tree_locks', [
  110. 'id' => $this->model->getId(),
  111. 'type' => 'object',
  112. 'locked' => $this->model->getLocked(),
  113. ]);
  114. }
  115. }
  116. /**
  117. * Deletes object from database
  118. *
  119. * @return void
  120. */
  121. public function delete()
  122. {
  123. $this->db->delete('objects', ['o_id' => $this->model->getId()]);
  124. }
  125. public function updateWorkspaces()
  126. {
  127. $this->db->update('users_workspaces_object', [
  128. 'cpath' => $this->model->getRealFullPath(),
  129. ], [
  130. 'cid' => $this->model->getId(),
  131. ]);
  132. }
  133. /**
  134. * Updates the paths for children, children's properties and children's permissions in the database
  135. *
  136. * @internal
  137. *
  138. * @param string $oldPath
  139. *
  140. * @return null|array
  141. */
  142. public function updateChildPaths($oldPath)
  143. {
  144. if ($this->hasChildren(DataObject::$types)) {
  145. //get objects to empty their cache
  146. $objects = $this->db->fetchCol('SELECT o_id FROM objects WHERE o_path LIKE ?', $this->db->escapeLike($oldPath) . '%');
  147. $userId = '0';
  148. if ($user = \Pimcore\Tool\Admin::getCurrentUser()) {
  149. $userId = $user->getId();
  150. }
  151. //update object child paths
  152. // we don't update the modification date here, as this can have side-effects when there's an unpublished version for an element
  153. $this->db->query('update objects set o_path = replace(o_path,' . $this->db->quote($oldPath . '/') . ',' . $this->db->quote($this->model->getRealFullPath() . '/') . "), o_userModification = '" . $userId . "' where o_path like " . $this->db->quote($this->db->escapeLike($oldPath) . '/%') . ';');
  154. //update object child permission paths
  155. $this->db->query('update users_workspaces_object set cpath = replace(cpath,' . $this->db->quote($oldPath . '/') . ',' . $this->db->quote($this->model->getRealFullPath() . '/') . ') where cpath like ' . $this->db->quote($this->db->escapeLike($oldPath) . '/%') . ';');
  156. //update object child properties paths
  157. $this->db->query('update properties set cpath = replace(cpath,' . $this->db->quote($oldPath . '/') . ',' . $this->db->quote($this->model->getRealFullPath() . '/') . ') where cpath like ' . $this->db->quote($this->db->escapeLike($oldPath) . '/%') . ';');
  158. return $objects;
  159. }
  160. return null;
  161. }
  162. /**
  163. * deletes all properties for the object from database
  164. *
  165. * @return void
  166. */
  167. public function deleteAllProperties()
  168. {
  169. $this->db->delete('properties', ['cid' => $this->model->getId(), 'ctype' => 'object']);
  170. }
  171. /**
  172. * @return string retrieves the current full object path from DB
  173. */
  174. public function getCurrentFullPath()
  175. {
  176. $path = null;
  177. try {
  178. $path = $this->db->fetchOne('SELECT CONCAT(o_path,`o_key`) as o_path FROM objects WHERE o_id = ?', $this->model->getId());
  179. } catch (\Exception $e) {
  180. Logger::error('could not get current object path from DB');
  181. }
  182. return $path;
  183. }
  184. /**
  185. * @return int
  186. */
  187. public function getVersionCountForUpdate(): int
  188. {
  189. $versionCount = (int) $this->db->fetchOne('SELECT o_versionCount FROM objects WHERE o_id = ? FOR UPDATE', $this->model->getId());
  190. if ($this->model instanceof DataObject\Concrete) {
  191. $versionCount2 = (int) $this->db->fetchOne("SELECT MAX(versionCount) FROM versions WHERE cid = ? AND ctype = 'object'", $this->model->getId());
  192. $versionCount = max($versionCount, $versionCount2);
  193. }
  194. return (int) $versionCount;
  195. }
  196. /**
  197. * Get the properties for the object from database and assign it
  198. *
  199. * @param bool $onlyInherited
  200. *
  201. * @return array
  202. */
  203. public function getProperties($onlyInherited = false)
  204. {
  205. $properties = [];
  206. // collect properties via parent - ids
  207. $parentIds = $this->getParentIds();
  208. $propertiesRaw = $this->db->fetchAll('SELECT name, type, data, cid, inheritable, cpath FROM properties WHERE ((cid IN (' . implode(',', $parentIds) . ") AND inheritable = 1) OR cid = ? ) AND ctype='object'", [$this->model->getId()]);
  209. // because this should be faster than mysql
  210. usort($propertiesRaw, function ($left, $right) {
  211. return strcmp($left['cpath'], $right['cpath']);
  212. });
  213. foreach ($propertiesRaw as $propertyRaw) {
  214. try {
  215. $property = new Model\Property();
  216. $property->setType($propertyRaw['type']);
  217. $property->setCid($this->model->getId());
  218. $property->setName($propertyRaw['name']);
  219. $property->setCtype('object');
  220. $property->setDataFromResource($propertyRaw['data']);
  221. $property->setInherited(true);
  222. if ($propertyRaw['cid'] == $this->model->getId()) {
  223. $property->setInherited(false);
  224. }
  225. $property->setInheritable(false);
  226. if ($propertyRaw['inheritable']) {
  227. $property->setInheritable(true);
  228. }
  229. if ($onlyInherited && !$property->getInherited()) {
  230. continue;
  231. }
  232. $properties[$propertyRaw['name']] = $property;
  233. } catch (\Exception $e) {
  234. Logger::error("can't add property " . $propertyRaw['name'] . ' to object ' . $this->model->getRealFullPath());
  235. }
  236. }
  237. // if only inherited then only return it and dont call the setter in the model
  238. if ($onlyInherited) {
  239. return $properties;
  240. }
  241. $this->model->setProperties($properties);
  242. return $properties;
  243. }
  244. public function deleteAllPermissions()
  245. {
  246. $this->db->delete('users_workspaces_object', ['cid' => $this->model->getId()]);
  247. }
  248. /**
  249. * Quick test if there are children
  250. *
  251. * @param array $objectTypes
  252. * @param bool|null $includingUnpublished
  253. *
  254. * @return bool
  255. */
  256. public function hasChildren($objectTypes = [DataObject::OBJECT_TYPE_OBJECT, DataObject::OBJECT_TYPE_FOLDER], $includingUnpublished = null)
  257. {
  258. $sql = 'SELECT 1 FROM objects WHERE o_parentId = ?';
  259. if ((isset($includingUnpublished) && !$includingUnpublished) || (!isset($includingUnpublished) && Model\Document::doHideUnpublished())) {
  260. $sql .= ' AND o_published = 1';
  261. }
  262. $sql .= " AND o_type IN ('" . implode("','", $objectTypes) . "') LIMIT 1";
  263. $c = $this->db->fetchOne($sql, $this->model->getId());
  264. return (bool)$c;
  265. }
  266. /**
  267. * Quick test if there are siblings
  268. *
  269. * @param array $objectTypes
  270. * @param bool|null $includingUnpublished
  271. *
  272. * @return bool
  273. */
  274. public function hasSiblings($objectTypes = [DataObject::OBJECT_TYPE_OBJECT, DataObject::OBJECT_TYPE_FOLDER], $includingUnpublished = null)
  275. {
  276. $sql = 'SELECT 1 FROM objects WHERE o_parentId = ? and o_id != ?';
  277. if ((isset($includingUnpublished) && !$includingUnpublished) || (!isset($includingUnpublished) && Model\Document::doHideUnpublished())) {
  278. $sql .= ' AND o_published = 1';
  279. }
  280. $sql .= " AND o_type IN ('" . implode("','", $objectTypes) . "') LIMIT 1";
  281. $c = $this->db->fetchOne($sql, [$this->model->getParentId(), $this->model->getId()]);
  282. return (bool)$c;
  283. }
  284. /**
  285. * returns the amount of directly children (not recursivly)
  286. *
  287. * @param array|null $objectTypes
  288. * @param Model\User $user
  289. *
  290. * @return int
  291. */
  292. public function getChildAmount($objectTypes = [DataObject::OBJECT_TYPE_OBJECT, DataObject::OBJECT_TYPE_FOLDER], $user = null)
  293. {
  294. $query = 'SELECT COUNT(*) AS count FROM objects o WHERE o_parentId = ?';
  295. if (!empty($objectTypes)) {
  296. $query .= sprintf(' AND o_type IN (\'%s\')', implode("','", $objectTypes));
  297. }
  298. if ($user && !$user->isAdmin()) {
  299. $userIds = $user->getRoles();
  300. $userIds[] = $user->getId();
  301. $query .= ' AND (select list as locate from users_workspaces_object where userId in (' . implode(',', $userIds) . ') and LOCATE(cpath,CONCAT(o.o_path,o.o_key))=1 ORDER BY LENGTH(cpath) DESC LIMIT 1)=1;';
  302. }
  303. $c = $this->db->fetchOne($query, $this->model->getId());
  304. return $c;
  305. }
  306. /**
  307. * @param int $id
  308. *
  309. * @return array
  310. *
  311. * @throws Model\Exception\NotFoundException
  312. */
  313. public function getTypeById($id)
  314. {
  315. $t = $this->db->fetchRow('SELECT o_type,o_className,o_classId FROM objects WHERE o_id = ?', $id);
  316. if (!$t) {
  317. throw new Model\Exception\NotFoundException('object with ID ' . $id . ' not found');
  318. }
  319. return $t;
  320. }
  321. /**
  322. * @return bool
  323. */
  324. public function isLocked()
  325. {
  326. // check for an locked element below this element
  327. $belowLocks = $this->db->fetchOne("SELECT tree_locks.id FROM tree_locks INNER JOIN objects ON tree_locks.id = objects.o_id WHERE objects.o_path LIKE ? AND tree_locks.type = 'object' AND tree_locks.locked IS NOT NULL AND tree_locks.locked != '' LIMIT 1", $this->db->escapeLike($this->model->getRealFullPath()) . '/%');
  328. if ($belowLocks > 0) {
  329. return true;
  330. }
  331. $parentIds = $this->getParentIds();
  332. $inhertitedLocks = $this->db->fetchOne('SELECT id FROM tree_locks WHERE id IN (' . implode(',', $parentIds) . ") AND type='object' AND locked = 'propagate' LIMIT 1");
  333. if ($inhertitedLocks > 0) {
  334. return true;
  335. }
  336. return false;
  337. }
  338. /**
  339. * @return array
  340. */
  341. public function unlockPropagate()
  342. {
  343. $lockIds = $this->db->fetchCol('SELECT o_id from objects WHERE o_path LIKE ' . $this->db->quote($this->db->escapeLike($this->model->getRealFullPath()) . '/%') . ' OR o_id = ' . $this->model->getId());
  344. $this->db->deleteWhere('tree_locks', "type = 'object' AND id IN (" . implode(',', $lockIds) . ')');
  345. return $lockIds;
  346. }
  347. /**
  348. * @return array
  349. */
  350. public function getClasses()
  351. {
  352. if ($this->getChildAmount()) {
  353. $path = $this->model->getRealFullPath();
  354. if (!$this->model->getId() || $this->model->getId() == 1) {
  355. $path = '';
  356. }
  357. $classIds = $this->db->fetchCol("SELECT o_classId FROM objects WHERE o_path LIKE ? AND o_type = 'object' GROUP BY o_classId", $this->db->escapeLike($path) . '/%');
  358. $classes = [];
  359. foreach ($classIds as $classId) {
  360. $classes[] = DataObject\ClassDefinition::getById($classId);
  361. }
  362. return $classes;
  363. }
  364. return [];
  365. }
  366. /**
  367. * @return int[]
  368. */
  369. protected function collectParentIds()
  370. {
  371. $parentIds = $this->getParentIds();
  372. $parentIds[] = $this->model->getId();
  373. return $parentIds;
  374. }
  375. /**
  376. * @param string $type
  377. * @param Model\User $user
  378. *
  379. * @return bool
  380. */
  381. public function isAllowed($type, $user)
  382. {
  383. $parentIds = $this->collectParentIds();
  384. $userIds = $user->getRoles();
  385. $userIds[] = $user->getId();
  386. try {
  387. $permissionsParent = $this->db->fetchOne('SELECT ' . $this->db->quoteIdentifier($type) . ' FROM users_workspaces_object WHERE cid IN (' . implode(',', $parentIds) . ') AND userId IN (' . implode(',', $userIds) . ') ORDER BY LENGTH(cpath) DESC, FIELD(userId, ' . $user->getId() . ') DESC, ' . $this->db->quoteIdentifier($type) . ' DESC LIMIT 1');
  388. if ($permissionsParent) {
  389. return true;
  390. }
  391. // exception for list permission
  392. if (empty($permissionsParent) && $type === 'list') {
  393. // check for children with permissions
  394. $path = $this->model->getRealFullPath() . '/';
  395. if ($this->model->getId() == 1) {
  396. $path = '/';
  397. }
  398. $permissionsChildren = $this->db->fetchOne('SELECT list FROM users_workspaces_object WHERE cpath LIKE ? AND userId IN (' . implode(',', $userIds) . ') AND list = 1 LIMIT 1', $this->db->escapeLike($path) . '%');
  399. if ($permissionsChildren) {
  400. return true;
  401. }
  402. }
  403. } catch (\Exception $e) {
  404. Logger::warn('Unable to get permission ' . $type . ' for object ' . $this->model->getId());
  405. }
  406. return false;
  407. }
  408. /**
  409. * @param string $type
  410. * @param Model\User $user
  411. * @param bool $quote
  412. *
  413. * @return array|null
  414. */
  415. public function getPermissions($type, $user, $quote = true)
  416. {
  417. $parentIds = $this->collectParentIds();
  418. $userIds = $user->getRoles();
  419. $userIds[] = $user->getId();
  420. try {
  421. if ($type && $quote) {
  422. $queryType = '`' . $type . '`';
  423. } else {
  424. $queryType = '*';
  425. }
  426. $commaSeparated = in_array($type, ['lView', 'lEdit', 'layouts']);
  427. if ($commaSeparated) {
  428. $allPermissions = $this->db->fetchAll('SELECT ' . $queryType . ',cid,cpath FROM users_workspaces_object WHERE cid IN (' . implode(',', $parentIds) . ') AND userId IN (' . implode(',', $userIds) . ') ORDER BY LENGTH(cpath) DESC, FIELD(userId, ' . $user->getId() . ') DESC, `' . $type . '` DESC');
  429. if (!$allPermissions) {
  430. return null;
  431. }
  432. if (count($allPermissions) == 1) {
  433. return $allPermissions[0];
  434. }
  435. $firstPermission = $allPermissions[0];
  436. $firstPermissionCid = $firstPermission['cid'];
  437. $mergedPermissions = [];
  438. foreach ($allPermissions as $permission) {
  439. $cid = $permission['cid'];
  440. if ($cid != $firstPermissionCid) {
  441. break;
  442. }
  443. $permissionValues = $permission[$type];
  444. if (!$permissionValues) {
  445. $firstPermission[$type] = null;
  446. return $firstPermission;
  447. }
  448. $permissionValues = explode(',', $permissionValues);
  449. foreach ($permissionValues as $permissionValue) {
  450. $mergedPermissions[$permissionValue] = $permissionValue;
  451. }
  452. }
  453. $firstPermission[$type] = implode(',', $mergedPermissions);
  454. return $firstPermission;
  455. }
  456. $orderByType = $type ? ', `' . $type . '` DESC' : '';
  457. $permissions = $this->db->fetchRow('SELECT ' . $queryType . ' FROM users_workspaces_object WHERE cid IN (' . implode(',', $parentIds) . ') AND userId IN (' . implode(',', $userIds) . ') ORDER BY LENGTH(cpath) DESC, FIELD(userId, ' . $user->getId() . ') DESC' . $orderByType . ' LIMIT 1');
  458. return $permissions;
  459. } catch (\Exception $e) {
  460. Logger::warn('Unable to get permission ' . $type . ' for object ' . $this->model->getId());
  461. }
  462. return null;
  463. }
  464. /**
  465. * @param string $type
  466. * @param Model\User $user
  467. * @param bool $quote
  468. *
  469. * @return array
  470. */
  471. public function getChildPermissions($type, $user, $quote = true)
  472. {
  473. $userIds = $user->getRoles();
  474. $userIds[] = $user->getId();
  475. $permissions = [];
  476. try {
  477. if ($type && $quote) {
  478. $type = '`' . $type . '`';
  479. } else {
  480. $type = '*';
  481. }
  482. $cid = $this->model->getId();
  483. $sql = 'SELECT ' . $type . ' FROM users_workspaces_object WHERE cid != ' . $cid . ' AND cpath LIKE ' . $this->db->quote($this->db->escapeLike($this->model->getRealFullPath()) . '%') . ' AND userId IN (' . implode(',', $userIds) . ') ORDER BY LENGTH(cpath) DESC';
  484. $permissions = $this->db->fetchAll($sql);
  485. } catch (\Exception $e) {
  486. Logger::warn('Unable to get permission ' . $type . ' for object ' . $this->model->getId());
  487. }
  488. return $permissions;
  489. }
  490. /**
  491. * @param int $index
  492. */
  493. public function saveIndex($index)
  494. {
  495. $this->db->update('objects', [
  496. 'o_index' => $index,
  497. ], [
  498. 'o_id' => $this->model->getId(),
  499. ]);
  500. }
  501. /**
  502. * @return bool
  503. */
  504. public function __isBasedOnLatestData()
  505. {
  506. $data = $this->db->fetchRow('SELECT o_modificationDate, o_versionCount from objects WHERE o_id = ?', $this->model->getId());
  507. return $data
  508. && $data['o_modificationDate'] == $this->model->__getDataVersionTimestamp()
  509. && $data['o_versionCount'] == $this->model->getVersionCount();
  510. }
  511. }