vendor/jackalope/jackalope/src/Jackalope/ObjectManager.php line 236

Open in your IDE?
  1. <?php
  2. namespace Jackalope;
  3. use ArrayIterator;
  4. use Exception;
  5. use InvalidArgumentException;
  6. use Jackalope\Transport\NodeTypeFilterInterface;
  7. use Jackalope\Version\Version;
  8. use PHPCR\NamespaceException;
  9. use PHPCR\NodeType\InvalidNodeTypeDefinitionException;
  10. use PHPCR\NodeType\NodeTypeExistsException;
  11. use PHPCR\NodeType\NodeTypeManagerInterface;
  12. use PHPCR\NoSuchWorkspaceException;
  13. use PHPCR\ReferentialIntegrityException;
  14. use PHPCR\SessionInterface;
  15. use PHPCR\NodeInterface;
  16. use PHPCR\PropertyInterface;
  17. use PHPCR\RepositoryException;
  18. use PHPCR\AccessDeniedException;
  19. use PHPCR\ItemNotFoundException;
  20. use PHPCR\ItemExistsException;
  21. use PHPCR\PathNotFoundException;
  22. use PHPCR\Transaction\RollbackException;
  23. use PHPCR\UnsupportedRepositoryOperationException;
  24. use PHPCR\Util\CND\Writer\CndWriter;
  25. use PHPCR\Version\VersionException;
  26. use PHPCR\Version\VersionInterface;
  27. use PHPCR\Util\PathHelper;
  28. use PHPCR\Util\CND\Parser\CndParser;
  29. use Jackalope\Transport\Operation;
  30. use Jackalope\Transport\TransportInterface;
  31. use Jackalope\Transport\PermissionInterface;
  32. use Jackalope\Transport\WritingInterface;
  33. use Jackalope\Transport\NodeTypeManagementInterface;
  34. use Jackalope\Transport\NodeTypeCndManagementInterface;
  35. use Jackalope\Transport\AddNodeOperation;
  36. use Jackalope\Transport\MoveNodeOperation;
  37. use Jackalope\Transport\RemoveNodeOperation;
  38. use Jackalope\Transport\RemovePropertyOperation;
  39. use Jackalope\Transport\VersioningInterface;
  40. use RuntimeException;
  41. /**
  42. * Implementation specific class that talks to the Transport layer to get nodes
  43. * and caches every node retrieved to improve performance.
  44. *
  45. * For write operations, the object manager acts as the Unit of Work handler:
  46. * it keeps track which nodes are dirty and updates them with the transport
  47. * interface.
  48. *
  49. * As not all transports have the same capabilities, we do some checks here,
  50. * but only if the check is not already done at the entry point. For
  51. * versioning, transactions, locking and so on, the check is done when the
  52. * respective manager is requested from the session or workspace. As those
  53. * managers are the only entry points we do not check here again.
  54. *
  55. * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
  56. * @license http://opensource.org/licenses/MIT MIT License
  57. *
  58. * @private
  59. */
  60. class ObjectManager
  61. {
  62. /**
  63. * The factory to instantiate objects
  64. * @var FactoryInterface
  65. */
  66. protected $factory;
  67. /**
  68. * @var SessionInterface
  69. */
  70. protected $session;
  71. /**
  72. * @var TransportInterface
  73. */
  74. protected $transport;
  75. /**
  76. * Mapping of typename => absolutePath => node or item object.
  77. *
  78. * There is no notion of order here. The order is defined by order in the
  79. * Node::nodes array.
  80. *
  81. * @var array
  82. */
  83. protected $objectsByPath = [Node::class => []];
  84. /**
  85. * Mapping of uuid => absolutePath.
  86. *
  87. * Take care never to put a path in here unless there is a node for that
  88. * path in objectsByPath.
  89. *
  90. * @var array
  91. */
  92. protected $objectsByUuid = [];
  93. /**
  94. * This is an ordered list of all operations to commit to the transport
  95. * during save. The values are the add, move and remove operation classes.
  96. *
  97. * Add, remove and move actions need to be saved in the correct order to avoid
  98. * i.e. adding something where a node has not yet been moved to.
  99. *
  100. * @var Operation[]
  101. */
  102. protected $operationsLog = [];
  103. /**
  104. * Contains the list of paths that have been added to the workspace in the
  105. * current session.
  106. *
  107. * Keys are the full paths to be added
  108. *
  109. * @var AddNodeOperation[]
  110. */
  111. protected $nodesAdd = [];
  112. /**
  113. * Contains the list of node remove operations for the current session.
  114. *
  115. * Keys are the full paths to be removed.
  116. *
  117. * Note: Keep in mind that a delete is recursive, but we only have the
  118. * explicitly deleted paths in this array. We check on deleted parents
  119. * whenever retrieving a non-cached node.
  120. *
  121. * @var RemoveNodeOperation[]
  122. */
  123. protected $nodesRemove = [];
  124. /**
  125. * Contains the list of property remove operations for the current session.
  126. *
  127. * Keys are the full paths of properties to be removed.
  128. *
  129. * @var RemovePropertyOperation[]
  130. */
  131. protected $propertiesRemove = [];
  132. /**
  133. * Contains a list of nodes that where moved during this session.
  134. *
  135. * Keys are the source paths, values the move operations containing the
  136. * target path.
  137. *
  138. * The objectsByPath array is updated immediately and any getItem and
  139. * similar requests are rewritten for the transport layer until save()
  140. *
  141. * Only nodes can be moved, not properties.
  142. *
  143. * Note: Keep in mind that moving also affects all children of the moved
  144. * node, but we only have the explicitly moved paths in this array. We
  145. * check on moved parents whenever retrieving a non-cached node.
  146. *
  147. * @var MoveNodeOperation[]
  148. */
  149. protected $nodesMove = [];
  150. /**
  151. * Create the ObjectManager instance with associated session and transport
  152. *
  153. * @param FactoryInterface $factory the object factory
  154. * @param TransportInterface $transport
  155. * @param SessionInterface $session
  156. */
  157. public function __construct(FactoryInterface $factory, TransportInterface $transport, SessionInterface $session)
  158. {
  159. $this->factory = $factory;
  160. $this->transport = $transport;
  161. $this->session = $session;
  162. }
  163. /**
  164. * Get the node identified by an absolute path.
  165. *
  166. * To prevent unnecessary work to be done a cache is filled to only fetch
  167. * nodes once. To reset a node with the data from the backend, use
  168. * Node::refresh()
  169. *
  170. * Uses the factory to create a Node object.
  171. *
  172. * @param string $absPath The absolute path of the node to fetch.
  173. * @param string $class The class of node to get. TODO: Is it sane to fetch
  174. * data separately for Version and normal Node?
  175. * @param object $object A (prefetched) object (de-serialized json) from the backend
  176. * only to be used if we get child nodes in one backend call
  177. *
  178. * @return NodeInterface
  179. *
  180. * @throws ItemNotFoundException If nothing is found at that
  181. * absolute path
  182. * @throws RepositoryException If the path is not absolute or not
  183. * well-formed
  184. *
  185. * @see Session::getNode()
  186. */
  187. public function getNodeByPath($absPath, $class = Node::class, $object = null)
  188. {
  189. $absPath = PathHelper::normalizePath($absPath);
  190. if (!empty($this->objectsByPath[$class][$absPath])) {
  191. // Return it from memory if we already have it
  192. return $this->objectsByPath[$class][$absPath];
  193. }
  194. // do this even if we have item in cache, will throw error if path is deleted - sanity check
  195. $fetchPath = $this->getFetchPath($absPath, $class);
  196. if (!$object) {
  197. // this is the first request, get data from transport
  198. $object = $this->transport->getNode($fetchPath);
  199. }
  200. // recursively create nodes for pre-fetched children if fetchDepth was > 1
  201. foreach ($object as $name => $properties) {
  202. if (is_object($properties)) {
  203. $objVars = get_object_vars($properties);
  204. $countObjVars = count($objVars);
  205. // if there's more than one objectvar or just one and this isn't jcr:uuid,
  206. // then we assume this child was pre-fetched from the backend completely
  207. if ($countObjVars > 1 || ($countObjVars === 1 && !isset($objVars['jcr:uuid']))) {
  208. try {
  209. $parentPath = ('/' === $absPath) ? '/' : $absPath . '/';
  210. $this->getNodeByPath($parentPath . $name, $class, $properties);
  211. } catch (ItemNotFoundException $ignore) {
  212. // we get here if the item was deleted or moved locally. just ignore
  213. }
  214. }
  215. }
  216. }
  217. /** @var $node NodeInterface */
  218. $node = $this->factory->get($class, [$object, $absPath, $this->session, $this]);
  219. if ($uuid = $node->getIdentifier()) {
  220. // Map even nodes that are not mix:referenceable, as long as they have a uuid
  221. $this->objectsByUuid[$uuid] = $absPath;
  222. }
  223. $this->objectsByPath[$class][$absPath] = $node;
  224. return $this->objectsByPath[$class][$absPath];
  225. }
  226. /**
  227. * Get multiple nodes identified by an absolute paths. Missing nodes are ignored.
  228. *
  229. * Note paths that cannot be found will be ignored and missing from the result.
  230. *
  231. * Uses the factory to create Node objects.
  232. *
  233. * @param array $absPaths Array containing the absolute paths of the nodes to
  234. * fetch.
  235. * @param string $class The class of node to get. TODO: Is it sane to
  236. * fetch data separately for Version and normal Node?
  237. * @param array|null $typeFilter Node type list to skip some nodes
  238. *
  239. * @return Node[] Iterator that contains all found NodeInterface instances keyed by their path
  240. *
  241. * @throws RepositoryException If the path is not absolute or not well-formed
  242. *
  243. * @see Session::getNodes()
  244. */
  245. public function getNodesByPath($absPaths, $class = Node::class, $typeFilter = null)
  246. {
  247. return new NodePathIterator($this, $absPaths, $class, $typeFilter);
  248. }
  249. public function getNodesByPathAsArray($paths, $class = Node::class, $typeFilter = null)
  250. {
  251. if (is_string($typeFilter)) {
  252. $typeFilter = [$typeFilter];
  253. }
  254. $nodes = $fetchPaths = [];
  255. foreach ($paths as $absPath) {
  256. if (!empty($this->objectsByPath[$class][$absPath])) {
  257. // Return it from memory if we already have it and type is correct
  258. if ($typeFilter
  259. && !$this->matchNodeType($this->objectsByPath[$class][$absPath], $typeFilter)
  260. ) {
  261. // skip this node if it did not match a type filter
  262. continue;
  263. }
  264. $nodes[$absPath] = $this->objectsByPath[$class][$absPath];
  265. } else {
  266. $nodes[$absPath] = '';
  267. $fetchPaths[$absPath] = $this->getFetchPath($absPath, $class);
  268. }
  269. }
  270. $userlandTypeFilter = false;
  271. if (!empty($fetchPaths)) {
  272. if ($typeFilter) {
  273. if ($this->transport instanceof NodeTypeFilterInterface) {
  274. $data = $this->transport->getNodesFiltered($fetchPaths, $typeFilter);
  275. } else {
  276. $data = $this->transport->getNodes($fetchPaths);
  277. $userlandTypeFilter = true;
  278. }
  279. } else {
  280. $data = $this->transport->getNodes($fetchPaths);
  281. }
  282. $inversePaths = array_flip($fetchPaths);
  283. foreach ($data as $fetchPath => $item) {
  284. // only add this node to the list if it was actually requested.
  285. if (isset($inversePaths[$fetchPath])) {
  286. // transform back to session paths from the fetch paths, in case of
  287. // a pending move operation
  288. $absPath = $inversePaths[$fetchPath];
  289. $node = $this->getNodeByPath($absPath, $class, $item);
  290. if ($userlandTypeFilter) {
  291. if (null !== $typeFilter && !$this->matchNodeType($node, $typeFilter)) {
  292. continue;
  293. }
  294. }
  295. $nodes[$absPath] = $node;
  296. unset($inversePaths[$fetchPath]);
  297. } else {
  298. // this is either a prefetched node that was not requested
  299. // or it falls through the type filter. cache it.
  300. // first undo eventual move operation
  301. $parent = $fetchPath;
  302. $relPath = '';
  303. while ($parent) {
  304. if (isset($inversePaths[$parent])) {
  305. break;
  306. }
  307. if ('/' === $parent) {
  308. $parent = false;
  309. } else {
  310. $parent = PathHelper::getParentPath($parent);
  311. $relPath = '/' . PathHelper::getNodeName($parent) . $relPath;
  312. }
  313. }
  314. if ($parent) {
  315. $this->getNodeByPath($parent . $relPath, $class, $item);
  316. }
  317. }
  318. }
  319. // clean away the not found paths from the final result
  320. foreach ($inversePaths as $absPath) {
  321. unset($nodes[$absPath]);
  322. }
  323. }
  324. return $nodes;
  325. }
  326. /**
  327. * Check if a node is of any of the types listed in typeFilter.
  328. *
  329. * @param NodeInterface $node
  330. * @param array $typeFilter
  331. *
  332. * @return boolean
  333. */
  334. private function matchNodeType(NodeInterface $node, array $typeFilter)
  335. {
  336. foreach ($typeFilter as $type) {
  337. if ($node->isNodeType($type)) {
  338. return true;
  339. }
  340. }
  341. return false;
  342. }
  343. /**
  344. * This method will either let the transport filter if that is possible or
  345. * forward to getNodes and return the names of the nodes found there.,
  346. *
  347. * @param NodeInterface $node
  348. * @param string|array $nameFilter
  349. * @param string|array $typeFilter
  350. *
  351. * @return ArrayIterator
  352. */
  353. public function filterChildNodeNamesByType(NodeInterface $node, $nameFilter, $typeFilter)
  354. {
  355. if ($this->transport instanceof NodeTypeFilterInterface) {
  356. return $this->transport->filterChildNodeNamesByType($node->getPath(), $node->getNodeNames($nameFilter), $typeFilter);
  357. }
  358. // fallback: get the actual nodes and let that filter. this is expensive.
  359. return new ArrayIterator(array_keys($node->getNodes($nameFilter, $typeFilter)->getArrayCopy()));
  360. }
  361. /**
  362. * Resolve the path through all pending operations and sanity check while
  363. * doing this.
  364. *
  365. * @param string $absPath The absolute path of the node to fetch.
  366. * @param string $class The class of node to get. TODO: Is it sane to fetch
  367. * data separately for Version and normal Node?
  368. *
  369. * @return string fetch path
  370. *
  371. * @throws ItemNotFoundException if while walking backwards through the
  372. * operations log we see this path was moved away or got deleted
  373. * @throws RepositoryException
  374. */
  375. protected function getFetchPath($absPath, $class)
  376. {
  377. $absPath = PathHelper::normalizePath($absPath);
  378. if (!isset($this->objectsByPath[$class])) {
  379. $this->objectsByPath[$class] = [];
  380. }
  381. $op = end($this->operationsLog);
  382. while ($op) {
  383. if ($op instanceof MoveNodeOperation) {
  384. if ($absPath === $op->srcPath) {
  385. throw new ItemNotFoundException("Path not found (moved in current session): $absPath");
  386. }
  387. if (strpos($absPath, $op->srcPath . '/') === 0) {
  388. throw new ItemNotFoundException("Path not found (parent node {$op->srcPath} moved in current session): $absPath");
  389. }
  390. if (strpos($absPath, $op->dstPath . '/') === 0 || $absPath == $op->dstPath) {
  391. $absPath= substr_replace($absPath, $op->srcPath, 0, strlen($op->dstPath));
  392. }
  393. } elseif ($op instanceof RemoveNodeOperation || $op instanceof RemovePropertyOperation) {
  394. if ($absPath === $op->srcPath) {
  395. throw new ItemNotFoundException("Path not found (node deleted in current session): $absPath");
  396. }
  397. if (strpos($absPath, $op->srcPath . '/') === 0) {
  398. throw new ItemNotFoundException("Path not found (parent node {$op->srcPath} deleted in current session): $absPath");
  399. }
  400. } elseif ($op instanceof AddNodeOperation) {
  401. if ($absPath === $op->srcPath) {
  402. // we added this node at this point so no more sanity checks needed.
  403. return $absPath;
  404. }
  405. }
  406. $op = prev($this->operationsLog);
  407. }
  408. return $absPath;
  409. }
  410. /**
  411. * Get the property identified by an absolute path.
  412. *
  413. * Uses the factory to instantiate a Property.
  414. *
  415. * Currently Jackalope just loads the containing node and then returns
  416. * the requested property of the node instance.
  417. *
  418. * @param string $absPath The absolute path of the property to create.
  419. * @return PropertyInterface
  420. *
  421. * @throws ItemNotFoundException if item is not found at this path
  422. * @throws InvalidArgumentException
  423. * @throws RepositoryException
  424. */
  425. public function getPropertyByPath($absPath)
  426. {
  427. list($name, $nodep) = $this->getNodePath($absPath);
  428. // OPTIMIZE: should use transport->getProperty - when we implement this, we must make sure only one instance of each property ever exists. and do the moved/deleted checks that are done in node
  429. $n = $this->getNodeByPath($nodep);
  430. try {
  431. return $n->getProperty($name); //throws PathNotFoundException if there is no such property
  432. } catch (PathNotFoundException $e) {
  433. throw new ItemNotFoundException($e->getMessage(), $e->getCode(), $e);
  434. }
  435. }
  436. /**
  437. * Get all nodes of those properties in one batch, then collect the
  438. * properties of them.
  439. *
  440. * @param $absPaths
  441. *
  442. * @return ArrayIterator that contains all found PropertyInterface
  443. * instances keyed by their path
  444. */
  445. public function getPropertiesByPath($absPaths)
  446. {
  447. // list of nodes to fetch
  448. $nodemap = [];
  449. // ordered list of what to return
  450. $returnmap = [];
  451. foreach ($absPaths as $path) {
  452. list($name, $nodep) = $this->getNodePath($path);
  453. if (! isset($nodemap[$nodep])) {
  454. $nodemap[$nodep] = $nodep;
  455. }
  456. $returnmap[$path] = ['name' => $name, 'path' => $nodep];
  457. }
  458. $nodes = $this->getNodesByPath($nodemap);
  459. $properties = [];
  460. foreach ($returnmap as $key => $data) {
  461. if (isset($nodes[$data['path']]) && $nodes[$data['path']]->hasProperty($data['name'])) {
  462. $properties[$key] = $nodes[$data['path']]->getProperty($data['name']);
  463. }
  464. }
  465. return new ArrayIterator($properties);
  466. }
  467. /**
  468. * Get the node path for a property, and the property name
  469. *
  470. * @param string $absPath
  471. *
  472. * @return array with name, node path
  473. *
  474. * @throws RepositoryException
  475. */
  476. protected function getNodePath($absPath)
  477. {
  478. $absPath = PathHelper::normalizePath($absPath);
  479. $name = PathHelper::getNodeName($absPath); //the property name
  480. $nodep = PathHelper::getParentPath($absPath, 0, strrpos($absPath, '/') + 1); //the node this property should be in
  481. return [$name, $nodep];
  482. }
  483. /**
  484. * Get the node identified by a relative path.
  485. *
  486. * If you have an absolute path use {@link getNodeByPath()} for better
  487. * performance.
  488. *
  489. * @param string $relPath relative path
  490. * @param string $context context path
  491. * @param string $class optional class name for the factory
  492. *
  493. * @return NodeInterface The specified Node. if not available,
  494. * ItemNotFoundException is thrown
  495. *
  496. * @throws ItemNotFoundException If the path was not found
  497. * @throws RepositoryException if another error occurs.
  498. *
  499. * @see Session::getNode()
  500. */
  501. public function getNode($relPath, $context, $class = Node::class)
  502. {
  503. $path = PathHelper::absolutizePath($relPath, $context);
  504. return $this->getNodeByPath($path, $class);
  505. }
  506. /**
  507. * Get the node identified by an uuid.
  508. *
  509. * @param string $identifier uuid
  510. * @param string $class optional class name for factory
  511. *
  512. * @return NodeInterface The specified Node. if not available,
  513. * ItemNotFoundException is thrown
  514. *
  515. * @throws ItemNotFoundException If the path was not found
  516. * @throws RepositoryException if another error occurs.
  517. * @throws NoSuchWorkspaceException if the workspace was not found
  518. *
  519. * @see Session::getNodeByIdentifier()
  520. */
  521. public function getNodeByIdentifier($identifier, $class = Node::class)
  522. {
  523. if (empty($this->objectsByUuid[$identifier])) {
  524. $data = $this->transport->getNodeByIdentifier($identifier);
  525. $path = $data->{':jcr:path'};
  526. unset($data->{':jcr:path'});
  527. // TODO: $path is a backend path. we should inverse the getFetchPath operation here
  528. $node = $this->getNodeByPath($path, $class, $data);
  529. $this->objectsByUuid[$identifier] = $path; //only do this once the getNodeByPath has worked
  530. return $node;
  531. }
  532. return $this->getNodeByPath($this->objectsByUuid[$identifier], $class);
  533. }
  534. /**
  535. * Get the nodes identified by the given UUIDs.
  536. *
  537. * Note UUIDs that are not found will be ignored. Also, duplicate IDs
  538. * will be eliminated by nature of using the IDs as keys.
  539. *
  540. * @param array $identifiers UUIDs of nodes to retrieve.
  541. * @param string $class Optional class name for the factory.
  542. *
  543. * @return ArrayIterator|Node[] Iterator of the specified nodes keyed by their unique ids
  544. *
  545. * @throws RepositoryException if another error occurs.
  546. *
  547. * @see Session::getNodesByIdentifier()
  548. */
  549. public function getNodesByIdentifier($identifiers, $class = Node::class)
  550. {
  551. $nodes = $fetchPaths = [];
  552. foreach ($identifiers as $uuid) {
  553. if (!empty($this->objectsByUuid[$uuid])
  554. && !empty($this->objectsByPath[$class][$this->objectsByUuid[$uuid]])
  555. ) {
  556. // Return it from memory if we already have it
  557. $nodes[$uuid] = $this->objectsByPath[$class][$this->objectsByUuid[$uuid]];
  558. } else {
  559. $fetchPaths[$uuid] = $uuid;
  560. $nodes[$uuid] = $uuid; // keep position
  561. }
  562. }
  563. if (!empty($fetchPaths)) {
  564. $data = $this->transport->getNodesByIdentifier($fetchPaths);
  565. foreach ($data as $absPath => $item) {
  566. // TODO: $absPath is the backend path. we should inverse the getFetchPath operation here
  567. // build the node from the received data
  568. $node = $this->getNodeByPath($absPath, $class, $item);
  569. $found[$node->getIdentifier()] = $node;
  570. }
  571. foreach ($nodes as $key => $node) {
  572. if (is_string($node)) {
  573. if (isset($found[$node])) {
  574. $nodes[$key] = $found[$node];
  575. } else {
  576. unset($nodes[$key]);
  577. }
  578. }
  579. }
  580. }
  581. reset($nodes);
  582. return new ArrayIterator($nodes);
  583. }
  584. /**
  585. * Retrieves the stream for a binary value.
  586. *
  587. * @param string $path The absolute path to the stream
  588. *
  589. * @return resource
  590. *
  591. * @throws ItemNotFoundException
  592. * @throws RepositoryException
  593. */
  594. public function getBinaryStream($path)
  595. {
  596. return $this->transport->getBinaryStream($this->getFetchPath($path, Node::class));
  597. }
  598. /**
  599. * Returns the node types specified by name in the array or all types if no filter is given.
  600. *
  601. * This is only a proxy to the transport
  602. *
  603. * @param array $nodeTypes Empty for all or specify node types by name
  604. *
  605. * @return array|\DOMDocument containing the nodetype information
  606. */
  607. public function getNodeTypes(array $nodeTypes = [])
  608. {
  609. return $this->transport->getNodeTypes($nodeTypes);
  610. }
  611. /**
  612. * Get a single nodetype.
  613. *
  614. * @param string $nodeType the name of nodetype to get from the transport
  615. *
  616. * @return \DOMDocument containing the nodetype information
  617. *
  618. * @see getNodeTypes()
  619. */
  620. public function getNodeType($nodeType)
  621. {
  622. return $this->getNodeTypes([$nodeType]);
  623. }
  624. /**
  625. * Register node types with the backend.
  626. *
  627. * This is only a proxy to the transport
  628. *
  629. * @param array $types an array of NodeTypeDefinitions
  630. * @param boolean $allowUpdate whether to fail if node already exists or to
  631. * update it
  632. *
  633. * @return bool true on success
  634. *
  635. * @throws InvalidNodeTypeDefinitionException
  636. * @throws NodeTypeExistsException
  637. * @throws RepositoryException
  638. * @throws UnsupportedRepositoryOperationException
  639. */
  640. public function registerNodeTypes($types, $allowUpdate)
  641. {
  642. if ($this->transport instanceof NodeTypeManagementInterface) {
  643. return $this->transport->registerNodeTypes($types, $allowUpdate);
  644. }
  645. if ($this->transport instanceof NodeTypeCndManagementInterface) {
  646. $writer = new CndWriter($this->session->getWorkspace()->getNamespaceRegistry());
  647. return $this->transport->registerNodeTypesCnd($writer->writeString($types), $allowUpdate);
  648. }
  649. throw new UnsupportedRepositoryOperationException('Transport does not support registering node types');
  650. }
  651. /**
  652. * Returns all accessible REFERENCE properties in the workspace that point
  653. * to the node
  654. *
  655. * @param string $path the path of the referenced node
  656. * @param string $name name of referring REFERENCE properties to be
  657. * returned; if null then all referring REFERENCEs are returned
  658. *
  659. * @return ArrayIterator
  660. *
  661. * @see Node::getReferences()
  662. */
  663. public function getReferences($path, $name = null)
  664. {
  665. $references = $this->transport->getReferences($this->getFetchPath($path, Node::class), $name);
  666. return $this->pathArrayToPropertiesIterator($references);
  667. }
  668. /**
  669. * Returns all accessible WEAKREFERENCE properties in the workspace that
  670. * point to the node
  671. *
  672. * @param string $path the path of the referenced node
  673. * @param string $name name of referring WEAKREFERENCE properties to be
  674. * returned; if null then all referring WEAKREFERENCEs are returned
  675. *
  676. * @return ArrayIterator
  677. *
  678. * @see Node::getWeakReferences()
  679. */
  680. public function getWeakReferences($path, $name = null)
  681. {
  682. $references = $this->transport->getWeakReferences($this->getFetchPath($path, Node::class), $name);
  683. return $this->pathArrayToPropertiesIterator($references);
  684. }
  685. /**
  686. * Transform an array containing properties paths to an ArrayIterator over
  687. * Property objects
  688. *
  689. * @param array $propertyPaths an array of properties paths
  690. *
  691. * @return ArrayIterator
  692. */
  693. protected function pathArrayToPropertiesIterator($propertyPaths)
  694. {
  695. //FIXME: this will break if we have non-persisted move
  696. return new ArrayIterator($this->getPropertiesByPath($propertyPaths));
  697. }
  698. /**
  699. * Register node types with compact node definition format
  700. *
  701. * This is only a proxy to the transport
  702. *
  703. * @param string $cnd a string with cnd information
  704. * @param boolean $allowUpdate whether to fail if node already exists or to update it
  705. * @return bool|\Iterator true on success or \Iterator over the registered node types if repository is not able to process
  706. * CND directly
  707. *
  708. * @throws UnsupportedRepositoryOperationException
  709. * @throws RepositoryException
  710. * @throws AccessDeniedException
  711. * @throws NamespaceException
  712. * @throws InvalidNodeTypeDefinitionException
  713. * @throws NodeTypeExistsException
  714. *
  715. * @see NodeTypeManagerInterface::registerNodeTypesCnd
  716. */
  717. public function registerNodeTypesCnd($cnd, $allowUpdate)
  718. {
  719. if ($this->transport instanceof NodeTypeCndManagementInterface) {
  720. return $this->transport->registerNodeTypesCnd($cnd, $allowUpdate);
  721. }
  722. if ($this->transport instanceof NodeTypeManagementInterface) {
  723. $workspace = $this->session->getWorkspace();
  724. $nsRegistry = $workspace->getNamespaceRegistry();
  725. $parser = new CndParser($workspace->getNodeTypeManager());
  726. $res = $parser->parseString($cnd);
  727. $ns = $res['namespaces'];
  728. $types = $res['nodeTypes'];
  729. foreach ($ns as $prefix => $uri) {
  730. $nsRegistry->registerNamespace($prefix, $uri);
  731. }
  732. return $workspace->getNodeTypeManager()->registerNodeTypes($types, $allowUpdate);
  733. }
  734. throw new UnsupportedRepositoryOperationException('Transport does not support registering node types');
  735. }
  736. /**
  737. * Push all recorded changes to the backend.
  738. *
  739. * The order is important to avoid conflicts
  740. * 1. operationsLog
  741. * 2. commit any other changes
  742. *
  743. * If transactions are enabled but we are not currently inside a
  744. * transaction, the session is responsible to start a transaction to make
  745. * sure the backend state does not get messed up in case of error.
  746. */
  747. public function save()
  748. {
  749. if (! $this->transport instanceof WritingInterface) {
  750. throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  751. }
  752. try {
  753. $this->transport->prepareSave();
  754. $this->executeOperations($this->operationsLog);
  755. // loop through cached nodes and commit all dirty and set them to clean.
  756. if (isset($this->objectsByPath[Node::class])) {
  757. foreach ($this->objectsByPath[Node::class] as $node) {
  758. /** @var $node Node */
  759. if ($node->isModified()) {
  760. if (! $node instanceof NodeInterface) {
  761. throw new RepositoryException('Internal Error: Unknown type '.get_class($node));
  762. }
  763. $this->transport->updateProperties($node);
  764. if ($node->needsChildReordering()) {
  765. $this->transport->reorderChildren($node);
  766. }
  767. }
  768. }
  769. }
  770. $this->transport->finishSave();
  771. } catch (Exception $e) {
  772. $this->transport->rollbackSave();
  773. if (! $e instanceof RepositoryException) {
  774. throw new RepositoryException('Error inside the transport layer: '.$e->getMessage(), 0, $e);
  775. }
  776. throw $e;
  777. }
  778. foreach ($this->operationsLog as $operation) {
  779. if ($operation instanceof MoveNodeOperation) {
  780. if (isset($this->objectsByPath[Node::class][$operation->dstPath])) {
  781. // might not be set if moved again afterwards
  782. // move is not treated as modified, need to confirm separately
  783. $this->objectsByPath[Node::class][$operation->dstPath]->confirmSaved();
  784. }
  785. }
  786. }
  787. //clear those lists before reloading the newly added nodes from backend, to avoid collisions
  788. $this->nodesRemove = [];
  789. $this->propertiesRemove = [];
  790. $this->nodesMove = [];
  791. foreach ($this->operationsLog as $operation) {
  792. if ($operation instanceof AddNodeOperation) {
  793. if (! $operation->node->isDeleted()) {
  794. $operation->node->confirmSaved();
  795. }
  796. }
  797. }
  798. if (isset($this->objectsByPath[Node::class])) {
  799. foreach ($this->objectsByPath[Node::class] as $item) {
  800. /** @var $item Item */
  801. if ($item->isModified() || $item->isMoved()) {
  802. $item->confirmSaved();
  803. }
  804. }
  805. }
  806. $this->nodesAdd = [];
  807. $this->operationsLog = [];
  808. }
  809. /**
  810. * Execute the recorded operations in the right order, skipping
  811. * stale data.
  812. *
  813. * @param Operation[] $operations
  814. *
  815. * @throws \Exception
  816. */
  817. protected function executeOperations(array $operations)
  818. {
  819. $lastType = null;
  820. $batch = [];
  821. foreach ($operations as $operation) {
  822. if ($operation->skip) {
  823. continue;
  824. }
  825. if (null === $lastType) {
  826. $lastType = $operation->type;
  827. }
  828. if ($operation->type !== $lastType) {
  829. $this->executeBatch($lastType, $batch);
  830. $lastType = $operation->type;
  831. $batch = [];
  832. }
  833. $batch[] = $operation;
  834. }
  835. // only execute last batch if not all was skipped
  836. if (! count($batch)) {
  837. return;
  838. }
  839. $this->executeBatch($lastType, $batch);
  840. }
  841. /**
  842. * Execute a batch of operations of one type.
  843. *
  844. * @param int $type type of the operations to be executed
  845. * @param Operation[] $operations list of same type operations
  846. *
  847. * @throws \Exception
  848. */
  849. protected function executeBatch($type, $operations)
  850. {
  851. switch ($type) {
  852. case Operation::ADD_NODE:
  853. $this->transport->storeNodes($operations);
  854. break;
  855. case Operation::MOVE_NODE:
  856. $this->transport->moveNodes($operations);
  857. break;
  858. case Operation::REMOVE_NODE:
  859. $this->transport->deleteNodes($operations);
  860. break;
  861. case Operation::REMOVE_PROPERTY:
  862. $this->transport->deleteProperties($operations);
  863. break;
  864. default:
  865. throw new Exception("internal error: unknown operation '$type'");
  866. }
  867. }
  868. /**
  869. * Removes the cache of the predecessor version after the node has been checked in.
  870. *
  871. * TODO: document more clearly
  872. *
  873. * @see VersionManager::checkin
  874. *
  875. * @param string $absPath
  876. * @return VersionInterface node version
  877. *
  878. * @throws ItemNotFoundException
  879. * @throws RepositoryException
  880. */
  881. public function checkin($absPath)
  882. {
  883. $path = $this->transport->checkinItem($absPath); //FIXME: what about pending move operations?
  884. return $this->getNodeByPath($path, Version::class);
  885. }
  886. /**
  887. * Removes the cache of the predecessor version after the node has been checked in.
  888. *
  889. * TODO: document more clearly. This looks like copy-paste from checkin
  890. *
  891. * @see VersionManager::checkout
  892. */
  893. public function checkout($absPath)
  894. {
  895. $this->transport->checkoutItem($absPath); //FIXME: what about pending move operations?
  896. }
  897. /**
  898. * @see VersioningInterface::addVersionLabel
  899. */
  900. public function addVersionLabel($path, $label, $moveLabel)
  901. {
  902. $this->transport->addVersionLabel($path, $label, $moveLabel);
  903. }
  904. /**
  905. * @see VersioningInterface::addVersionLabel
  906. */
  907. public function removeVersionLabel($path, $label)
  908. {
  909. $this->transport->removeVersionLabel($path, $label);
  910. }
  911. /**
  912. * Restore the node at $nodePath to the version at $versionPath
  913. *
  914. * Clears the node's cache after it has been restored.
  915. *
  916. * TODO: This is incomplete. Needs batch processing to implement restoring an array of versions
  917. *
  918. * @param bool $removeExisting whether to remove the existing current
  919. * version or create a new version after that version
  920. * @param string $versionPath
  921. * @param string $nodePath absolute path to the node
  922. */
  923. public function restore($removeExisting, $versionPath, $nodePath)
  924. {
  925. // TODO: handle pending move operations?
  926. if (isset($this->objectsByPath[Node::class][$nodePath])) {
  927. $this->objectsByPath[Node::class][$nodePath]->setChildrenDirty();
  928. $this->objectsByPath[Node::class][$nodePath]->setDirty();
  929. }
  930. if (isset($this->objectsByPath[Version::class][$versionPath])) {
  931. $this->objectsByPath[Version::class][$versionPath]->setChildrenDirty();
  932. $this->objectsByPath[Version::class][$versionPath]->setDirty();
  933. }
  934. $this->transport->restoreItem($removeExisting, $versionPath, $nodePath);
  935. }
  936. /**
  937. * Remove a version given the path to the version node and the version name.
  938. *
  939. * @param string $versionPath The path to the version node
  940. * @param string $versionName The name of the version to remove
  941. *
  942. * @throws UnsupportedRepositoryOperationException
  943. * @throws ReferentialIntegrityException
  944. * @throws VersionException
  945. */
  946. public function removeVersion($versionPath, $versionName)
  947. {
  948. $this->transport->removeVersion($versionPath, $versionName);
  949. // Adjust the in memory state
  950. $absPath = $versionPath . '/' . $versionName;
  951. if (isset($this->objectsByPath[Node::class][$absPath])) {
  952. /** @var $node Node */
  953. $node = $this->objectsByPath[Node::class][$absPath];
  954. unset($this->objectsByUuid[$node->getIdentifier()]);
  955. $node->setDeleted();
  956. }
  957. if (isset($this->objectsByPath[Version::class][$absPath])) {
  958. /** @var $version Version */
  959. $version = $this->objectsByPath[Version::class][$absPath];
  960. unset($this->objectsByUuid[$version->getIdentifier()]);
  961. $version->setDeleted();
  962. }
  963. unset(
  964. $this->objectsByPath[Node::class][$absPath],
  965. $this->objectsByPath[Version::class][$absPath]
  966. );
  967. $this->cascadeDelete($absPath, false);
  968. $this->cascadeDeleteVersion($absPath);
  969. }
  970. /**
  971. * Refresh cached items from the backend.
  972. *
  973. * @param boolean $keepChanges whether to keep local changes or discard
  974. * them.
  975. *
  976. * @see Session::refresh()
  977. */
  978. public function refresh($keepChanges)
  979. {
  980. if (! $keepChanges) {
  981. // revert all scheduled add, remove and move operations
  982. $this->operationsLog = [];
  983. foreach ($this->nodesAdd as $path => $operation) {
  984. if (! $operation->skip) {
  985. $operation->node->setDeleted();
  986. unset($this->objectsByPath[Node::class][$path]); // did you see anything? it never existed
  987. }
  988. }
  989. $this->nodesAdd = [];
  990. // the code below will set this to dirty again. but it must not
  991. // be in state deleted or we will fail the sanity checks
  992. foreach ($this->propertiesRemove as $path => $operation) {
  993. $operation->property->setClean();
  994. }
  995. $this->propertiesRemove = [];
  996. foreach ($this->nodesRemove as $path => $operation) {
  997. $operation->node->setClean();
  998. $this->objectsByPath[Node::class][$path] = $operation->node; // back in glory
  999. $parentPath = PathHelper::getParentPath($path);
  1000. if (array_key_exists($parentPath, $this->objectsByPath[Node::class])) {
  1001. // tell the parent about its restored child
  1002. $this->objectsByPath[Node::class][$parentPath]->addChildNode($operation->node, false);
  1003. }
  1004. }
  1005. $this->nodesRemove = [];
  1006. foreach (array_reverse($this->nodesMove) as $operation) {
  1007. if (isset($this->objectsByPath[Node::class][$operation->dstPath])) {
  1008. // not set if we moved twice
  1009. $item = $this->objectsByPath[Node::class][$operation->dstPath];
  1010. $item->setPath($operation->srcPath);
  1011. }
  1012. $parentPath = PathHelper::getParentPath($operation->dstPath);
  1013. if (array_key_exists($parentPath, $this->objectsByPath[Node::class])) {
  1014. // tell the parent about its restored child
  1015. $this->objectsByPath[Node::class][$parentPath]->unsetChildNode(PathHelper::getNodeName($operation->dstPath), false);
  1016. }
  1017. // TODO: from in a two step move might fail. we should merge consecutive moves
  1018. $parentPath = PathHelper::getParentPath($operation->srcPath);
  1019. if (array_key_exists($parentPath, $this->objectsByPath[Node::class]) && isset($item) && $item instanceof Node) {
  1020. // tell the parent about its restored child
  1021. $this->objectsByPath[Node::class][$parentPath]->addChildNode($item, false);
  1022. }
  1023. // move item to old location
  1024. $this->objectsByPath[Node::class][$operation->srcPath] = $this->objectsByPath[Node::class][$operation->dstPath];
  1025. unset($this->objectsByPath[Node::class][$operation->dstPath]);
  1026. }
  1027. $this->nodesMove = [];
  1028. }
  1029. $this->objectsByUuid = [];
  1030. /** @var $node Node */
  1031. foreach ($this->objectsByPath[Node::class] as $node) {
  1032. if (! $keepChanges || ! ($node->isDeleted() || $node->isNew())) {
  1033. // if we keep changes, do not restore a deleted item
  1034. $this->objectsByUuid[$node->getIdentifier()] = $node->getPath();
  1035. $node->setDirty($keepChanges);
  1036. }
  1037. }
  1038. }
  1039. /**
  1040. * Determine if any object is modified and not saved to storage.
  1041. *
  1042. * @return boolean true if this session has any pending changes.
  1043. *
  1044. * @see Session::hasPendingChanges()
  1045. */
  1046. public function hasPendingChanges()
  1047. {
  1048. if (count($this->operationsLog)) {
  1049. return true;
  1050. }
  1051. foreach ($this->objectsByPath[Node::class] as $item) {
  1052. if ($item->isModified()) {
  1053. return true;
  1054. }
  1055. }
  1056. return false;
  1057. }
  1058. /**
  1059. * Remove the item at absPath from local cache and keep information for undo.
  1060. *
  1061. * @param string $absPath The absolute path of the item that is being
  1062. * removed. Note that contrary to removeItem(), this path is the full
  1063. * path for a property too.
  1064. * @param PropertyInterface $property The item that is being removed
  1065. * @param bool $sessionOperation whether the property removal should be
  1066. * dispatched immediately or needs to be scheduled in the operations log
  1067. *
  1068. * @see ObjectManager::removeItem()
  1069. */
  1070. protected function performPropertyRemove($absPath, PropertyInterface $property, $sessionOperation = true)
  1071. {
  1072. if ($sessionOperation) {
  1073. if ($property->isNew()) {
  1074. return;
  1075. }
  1076. // keep reference to object in case of refresh
  1077. $operation = new RemovePropertyOperation($absPath, $property);
  1078. $this->propertiesRemove[$absPath] = $operation;
  1079. $this->operationsLog[] = $operation;
  1080. return;
  1081. }
  1082. // this is no session operation
  1083. $this->transport->deletePropertyImmediately($absPath);
  1084. }
  1085. /**
  1086. * Remove the item at absPath from local cache and keep information for undo.
  1087. *
  1088. * @param string $absPath The absolute path of the item that is being
  1089. * removed. Note that contrary to removeItem(), this path is the full
  1090. * path for a property too.
  1091. * @param NodeInterface $node The item that is being removed
  1092. * @param bool $sessionOperation whether the node removal should be
  1093. * dispatched immediately or needs to be scheduled in the operations log
  1094. *
  1095. * @see ObjectManager::removeItem()
  1096. */
  1097. protected function performNodeRemove($absPath, NodeInterface $node, $sessionOperation = true, $cascading = false)
  1098. {
  1099. if (! $sessionOperation && ! $cascading) {
  1100. $this->transport->deleteNodeImmediately($absPath);
  1101. }
  1102. unset(
  1103. $this->objectsByUuid[$node->getIdentifier()],
  1104. $this->objectsByPath[Node::class][$absPath]
  1105. );
  1106. if ($sessionOperation) {
  1107. // keep reference to object in case of refresh
  1108. $operation = new RemoveNodeOperation($absPath, $node);
  1109. $this->nodesRemove[$absPath] = $operation;
  1110. if (! $cascading) {
  1111. $this->operationsLog[] = $operation;
  1112. }
  1113. }
  1114. }
  1115. /**
  1116. * Notify all cached children that they are deleted as well and clean up
  1117. * internal state
  1118. *
  1119. * @param string $absPath parent node that was removed
  1120. * @param bool $sessionOperation to carry over the session operation information
  1121. */
  1122. protected function cascadeDelete($absPath, $sessionOperation = true)
  1123. {
  1124. foreach ($this->objectsByPath[Node::class] as $path => $node) {
  1125. if (strpos($path, "$absPath/") === 0) {
  1126. // notify item and let it call removeItem again. save()
  1127. // makes sure no children of already deleted items are
  1128. // deleted again.
  1129. $this->performNodeRemove($path, $node, $sessionOperation, true);
  1130. if (!$node->isDeleted()) {
  1131. $node->setDeleted();
  1132. }
  1133. }
  1134. }
  1135. }
  1136. /**
  1137. * Notify all cached version children that they are deleted as well and clean up
  1138. * internal state
  1139. *
  1140. * @param string $absPath parent version node that was removed
  1141. */
  1142. protected function cascadeDeleteVersion($absPath)
  1143. {
  1144. // delete all versions, similar to cascadeDelete
  1145. foreach ($this->objectsByPath[Version::class] as $path => $node) {
  1146. if (strpos($path, "$absPath/") === 0) {
  1147. // versions are read only, we simple unset them
  1148. unset(
  1149. $this->objectsByUuid[$node->getIdentifier()],
  1150. $this->objectsByPath[Version::class][$absPath]
  1151. );
  1152. if (!$node->isDeleted()) {
  1153. $node->setDeleted();
  1154. }
  1155. }
  1156. }
  1157. }
  1158. /**
  1159. * Remove a node or a property.
  1160. *
  1161. * If this is a node, sets all cached items below this node to deleted as
  1162. * well.
  1163. *
  1164. * If property is set, the path denotes the node containing the property,
  1165. * otherwise the node at path is removed.
  1166. *
  1167. * @param string $absPath The absolute path to the node to be removed,
  1168. * including the node name.
  1169. * @param PropertyInterface $property optional, property instance to delete from the
  1170. * given node path. If set, absPath is the path to the node containing
  1171. * this property.
  1172. *
  1173. * @throws RepositoryException If node cannot be found at given path
  1174. *
  1175. * @see Item::remove()
  1176. */
  1177. public function removeItem($absPath, PropertyInterface $property = null)
  1178. {
  1179. if (! $this->transport instanceof WritingInterface) {
  1180. throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1181. }
  1182. // the object is always cached as invocation flow goes through Item::remove() without exception
  1183. if (!isset($this->objectsByPath[Node::class][$absPath])) {
  1184. throw new RepositoryException("Internal error: Item not found in local cache at $absPath");
  1185. }
  1186. if ($property) {
  1187. $absPath = PathHelper::absolutizePath($property->getName(), $absPath);
  1188. $this->performPropertyRemove($absPath, $property);
  1189. } else {
  1190. $node = $this->objectsByPath[Node::class][$absPath];
  1191. $this->performNodeRemove($absPath, $node);
  1192. $this->cascadeDelete($absPath);
  1193. }
  1194. }
  1195. /**
  1196. * Rewrites the path of a node for the movement operation, also updating
  1197. * all cached children.
  1198. *
  1199. * This applies both to the cache and to the items themselves so
  1200. * they return the correct value on getPath calls.
  1201. *
  1202. * @param string $curPath Absolute path of the node to rewrite
  1203. * @param string $newPath The new absolute path
  1204. */
  1205. protected function rewriteItemPaths($curPath, $newPath)
  1206. {
  1207. // update internal references in parent
  1208. $parentCurPath = PathHelper::getParentPath($curPath);
  1209. $parentNewPath = PathHelper::getParentPath($newPath);
  1210. if (isset($this->objectsByPath[Node::class][$parentCurPath])) {
  1211. /** @var $node Node */
  1212. $node = $this->objectsByPath[Node::class][$parentCurPath];
  1213. if (! $node->hasNode(PathHelper::getNodeName($curPath))) {
  1214. throw new PathNotFoundException("Source path can not be found: $curPath");
  1215. }
  1216. $node->unsetChildNode(PathHelper::getNodeName($curPath), true);
  1217. }
  1218. if (isset($this->objectsByPath[Node::class][$parentNewPath])) {
  1219. /** @var $node Node */
  1220. $node = $this->objectsByPath[Node::class][$parentNewPath];
  1221. $node->addChildNode($this->getNodeByPath($curPath), true, PathHelper::getNodeName($newPath));
  1222. }
  1223. // propagate to current and children items of $curPath, updating internal path
  1224. /** @var $node Node */
  1225. foreach ($this->objectsByPath[Node::class] as $path => $node) {
  1226. // is it current or child?
  1227. if ((strpos($path, $curPath . '/') === 0)||($path == $curPath)) {
  1228. // curPath = /foo
  1229. // newPath = /mo
  1230. // path = /foo/bar
  1231. // newItemPath= /mo/bar
  1232. $newItemPath = substr_replace($path, $newPath, 0, strlen($curPath));
  1233. if (isset($this->objectsByPath[Node::class][$path])) {
  1234. $node = $this->objectsByPath[Node::class][$path];
  1235. $this->objectsByPath[Node::class][$newItemPath] = $node;
  1236. unset($this->objectsByPath[Node::class][$path]);
  1237. $node->setPath($newItemPath, true);
  1238. }
  1239. // update uuid cache
  1240. $this->objectsByUuid[$node->getIdentifier()] = $node->getPath();
  1241. }
  1242. }
  1243. }
  1244. /**
  1245. * WRITE: move node from source path to destination path
  1246. *
  1247. * @param string $srcAbsPath Absolute path to the source node.
  1248. * @param string $destAbsPath Absolute path to the destination where the node shall be moved to.
  1249. *
  1250. * @throws RepositoryException If node cannot be found at given path
  1251. *
  1252. * @see Session::move()
  1253. */
  1254. public function moveNode($srcAbsPath, $destAbsPath)
  1255. {
  1256. if (! $this->transport instanceof WritingInterface) {
  1257. throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1258. }
  1259. $srcAbsPath = PathHelper::normalizePath($srcAbsPath);
  1260. $destAbsPath = PathHelper::normalizePath($destAbsPath, true);
  1261. $this->rewriteItemPaths($srcAbsPath, $destAbsPath, true);
  1262. // record every single move in case we have intermediary operations
  1263. $operation = new MoveNodeOperation($srcAbsPath, $destAbsPath);
  1264. $this->operationsLog[] = $operation;
  1265. // update local cache state information
  1266. if ($original = $this->getMoveSrcPath($srcAbsPath)) {
  1267. $srcAbsPath = $original;
  1268. }
  1269. $this->nodesMove[$srcAbsPath] = $operation;
  1270. }
  1271. /**
  1272. * Implement the workspace move method. It is dispatched to transport
  1273. * immediately.
  1274. *
  1275. * @param string $srcAbsPath the path of the node to be moved.
  1276. * @param string $destAbsPath the location to which the node at srcAbsPath
  1277. * is to be moved.
  1278. *
  1279. * @throws RepositoryException If node cannot be found at given path
  1280. *
  1281. * @see Workspace::move()
  1282. */
  1283. public function moveNodeImmediately($srcAbsPath, $destAbsPath)
  1284. {
  1285. if (! $this->transport instanceof WritingInterface) {
  1286. throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1287. }
  1288. $srcAbsPath = PathHelper::normalizePath($srcAbsPath);
  1289. $destAbsPath = PathHelper::normalizePath($destAbsPath, true);
  1290. $this->transport->moveNodeImmediately($srcAbsPath, $destAbsPath, true); // should throw the right exceptions
  1291. $this->rewriteItemPaths($srcAbsPath, $destAbsPath); // update local cache
  1292. }
  1293. /**
  1294. * Implement the workspace removeItem method.
  1295. *
  1296. * @param string $absPath the absolute path of the item to be removed
  1297. *
  1298. * @see Workspace::removeItem
  1299. */
  1300. public function removeItemImmediately($absPath)
  1301. {
  1302. if (! $this->transport instanceof WritingInterface) {
  1303. throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1304. }
  1305. $absPath = PathHelper::normalizePath($absPath);
  1306. $item = $this->session->getItem($absPath);
  1307. // update local state and cached objects about disappeared nodes
  1308. if ($item instanceof NodeInterface) {
  1309. $this->performNodeRemove($absPath, $item, false);
  1310. $this->cascadeDelete($absPath, false);
  1311. } else {
  1312. $this->performPropertyRemove($absPath, $item, false);
  1313. }
  1314. $item->setDeleted();
  1315. }
  1316. /**
  1317. * Implement the workspace copy method. It is dispatched immediately.
  1318. *
  1319. * @param string $srcAbsPath the path of the node to be copied.
  1320. * @param string $destAbsPath the location to which the node at srcAbsPath
  1321. * is to be copied in this workspace.
  1322. * @param string $srcWorkspace the name of the workspace from which the
  1323. * copy is to be made.
  1324. *
  1325. * @throws UnsupportedRepositoryOperationException
  1326. * @throws RepositoryException
  1327. * @throws ItemExistsException
  1328. *
  1329. * @see Workspace::copy()
  1330. */
  1331. public function copyNodeImmediately($srcAbsPath, $destAbsPath, $srcWorkspace)
  1332. {
  1333. if (! $this->transport instanceof WritingInterface) {
  1334. throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1335. }
  1336. $srcAbsPath = PathHelper::normalizePath($srcAbsPath);
  1337. $destAbsPath = PathHelper::normalizePath($destAbsPath, true);
  1338. if ($this->session->nodeExists($destAbsPath)) {
  1339. throw new ItemExistsException('Node already exists at destination (update-on-copy is currently not supported)');
  1340. // to support this, we would have to update the local cache of nodes as well
  1341. }
  1342. $this->transport->copyNode($srcAbsPath, $destAbsPath, $srcWorkspace);
  1343. }
  1344. /**
  1345. * Implement the workspace clone method. It is dispatched immediately.
  1346. * http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.10%20Corresponding%20Nodes
  1347. * http://www.day.com/specs/jcr/2.0/10_Writing.html#10.8%20Cloning%20and%20Updating%20Nodes
  1348. *
  1349. * @param string $srcWorkspace the name of the workspace from which the copy is to be made.
  1350. * @param string $srcAbsPath the path of the node to be cloned.
  1351. * @param string $destAbsPath the location to which the node at srcAbsPath is to be cloned in this workspace.
  1352. * @param boolean $removeExisting
  1353. *
  1354. * @throws UnsupportedRepositoryOperationException
  1355. * @throws RepositoryException
  1356. * @throws ItemExistsException
  1357. *
  1358. * @see Workspace::cloneFrom()
  1359. */
  1360. public function cloneFromImmediately($srcWorkspace, $srcAbsPath, $destAbsPath, $removeExisting)
  1361. {
  1362. if (! $this->transport instanceof WritingInterface) {
  1363. throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1364. }
  1365. $srcAbsPath = PathHelper::normalizePath($srcAbsPath);
  1366. $destAbsPath = PathHelper::normalizePath($destAbsPath, true);
  1367. if (! $removeExisting && $this->session->nodeExists($destAbsPath)) {
  1368. throw new ItemExistsException('Node already exists at destination and removeExisting is false');
  1369. }
  1370. $this->transport->cloneFrom($srcWorkspace, $srcAbsPath, $destAbsPath, $removeExisting);
  1371. }
  1372. /**
  1373. * WRITE: add a node at the specified path. Schedules an add operation
  1374. * for the next save() and caches the node.
  1375. *
  1376. * @param string $absPath the path to the node or property, including the item name
  1377. * @param NodeInterface $node The item instance that is added.
  1378. *
  1379. * @throws UnsupportedRepositoryOperationException
  1380. * @throws ItemExistsException if a node already exists at that path
  1381. */
  1382. public function addNode($absPath, NodeInterface $node)
  1383. {
  1384. if (! $this->transport instanceof WritingInterface) {
  1385. throw new UnsupportedRepositoryOperationException('Transport does not support writing');
  1386. }
  1387. if (isset($this->objectsByPath[Node::class][$absPath])) {
  1388. throw new ItemExistsException($absPath); //FIXME: same-name-siblings...
  1389. }
  1390. $this->objectsByPath[Node::class][$absPath] = $node;
  1391. // a new item never has a uuid, no need to add to objectsByUuid
  1392. $operation = new AddNodeOperation($absPath, $node);
  1393. $this->nodesAdd[$absPath] = $operation;
  1394. $this->operationsLog[] = $operation;
  1395. }
  1396. /**
  1397. * Return the permissions of the current session on the node given by path.
  1398. * Permission can be of 4 types:
  1399. *
  1400. * - add_node
  1401. * - read
  1402. * - remove
  1403. * - set_property
  1404. *
  1405. * This function will return an array containing zero, one or more of the
  1406. * above strings.
  1407. *
  1408. * @param string $absPath absolute path to node to get permissions for it
  1409. *
  1410. * @return array of string
  1411. *
  1412. * @throws UnsupportedRepositoryOperationException
  1413. */
  1414. public function getPermissions($absPath)
  1415. {
  1416. if (! $this->transport instanceof PermissionInterface) {
  1417. throw new UnsupportedRepositoryOperationException('Transport does not support permissions');
  1418. }
  1419. return $this->transport->getPermissions($absPath);
  1420. }
  1421. /**
  1422. * Clears the state of the current session
  1423. *
  1424. * Removes all cached objects, planned changes etc. Mostly useful for
  1425. * testing purposes.
  1426. *
  1427. * @deprecated: this will screw up major, as the user of the api can still have references to nodes. USE refresh instead!
  1428. */
  1429. public function clear()
  1430. {
  1431. $this->objectsByPath = [Node::class => []];
  1432. $this->objectsByUuid = [];
  1433. $this->nodesAdd = [];
  1434. $this->nodesRemove = [];
  1435. $this->propertiesRemove = [];
  1436. $this->nodesMove = [];
  1437. }
  1438. /**
  1439. * Implementation specific: Transport is used elsewhere, provide it here
  1440. * for Session
  1441. *
  1442. * @return TransportInterface
  1443. */
  1444. public function getTransport()
  1445. {
  1446. return $this->transport;
  1447. }
  1448. /**
  1449. * Begin new transaction associated with current session.
  1450. *
  1451. * @throws RepositoryException if the transaction implementation
  1452. * encounters an unexpected error condition.
  1453. * @throws InvalidArgumentException
  1454. */
  1455. public function beginTransaction()
  1456. {
  1457. $this->notifyItems('beginTransaction');
  1458. $this->transport->beginTransaction();
  1459. }
  1460. /**
  1461. * Complete the transaction associated with the current session.
  1462. *
  1463. * TODO: Make sure RollbackException and AccessDeniedException are thrown
  1464. * by the transport if corresponding problems occur.
  1465. *
  1466. * @throws RollbackException if the transaction failed
  1467. * and was rolled back rather than committed.
  1468. * @throws AccessDeniedException if the session is not allowed to
  1469. * commit the transaction.
  1470. * @throws RepositoryException if the transaction implementation
  1471. * encounters an unexpected error condition.
  1472. */
  1473. public function commitTransaction()
  1474. {
  1475. $this->notifyItems('commitTransaction');
  1476. $this->transport->commitTransaction();
  1477. }
  1478. /**
  1479. * Roll back the transaction associated with the current session.
  1480. *
  1481. * TODO: Make sure AccessDeniedException is thrown by the transport
  1482. * if corresponding problems occur
  1483. * TODO: restore the in-memory state as it would be if save() was never
  1484. * called during the transaction. The save() method will need to track some
  1485. * undo information for this to be possible.
  1486. *
  1487. * @throws AccessDeniedException if the session is not allowed to
  1488. * roll back the transaction.
  1489. * @throws RepositoryException if the transaction implementation
  1490. * encounters an unexpected error condition.
  1491. */
  1492. public function rollbackTransaction()
  1493. {
  1494. $this->transport->rollbackTransaction();
  1495. $this->notifyItems('rollbackTransaction');
  1496. }
  1497. /**
  1498. * Notifies the given node and all of its children and properties that a
  1499. * transaction has begun, was committed or rolled back so that the item has
  1500. * a chance to save or restore his internal state.
  1501. *
  1502. * @param string $method The method to call on each item for the
  1503. * notification (must be beginTransaction, commitTransaction or
  1504. * rollbackTransaction)
  1505. *
  1506. * @throws InvalidArgumentException if the passed $method is not valid
  1507. */
  1508. protected function notifyItems($method)
  1509. {
  1510. if (! in_array($method, ['beginTransaction', 'commitTransaction', 'rollbackTransaction'])) {
  1511. throw new InvalidArgumentException("Unknown notification method '$method'");
  1512. }
  1513. // Notify the loaded nodes
  1514. foreach ($this->objectsByPath[Node::class] as $node) {
  1515. $node->$method();
  1516. }
  1517. // Notify the deleted nodes
  1518. foreach ($this->nodesRemove as $op) {
  1519. $op->node->$method();
  1520. }
  1521. // Notify the deleted properties
  1522. foreach ($this->propertiesRemove as $op) {
  1523. $op->property->$method();
  1524. }
  1525. }
  1526. /**
  1527. * Check whether a node path has an unpersisted move operation.
  1528. *
  1529. * This is a simplistic check to be used by the Node to determine if it
  1530. * should not show one of the children the backend told it would exist.
  1531. *
  1532. * @param string $absPath The absolute path of the node
  1533. *
  1534. * @return boolean true if the node has an unsaved move operation, false
  1535. * otherwise
  1536. *
  1537. * @see Node::__construct
  1538. */
  1539. public function isNodeMoved($absPath)
  1540. {
  1541. return array_key_exists($absPath, $this->nodesMove);
  1542. }
  1543. /**
  1544. * Get the src path of a move operation knowing the target path.
  1545. *
  1546. * @param string $dstPath
  1547. *
  1548. * @return string|bool the source path if found, false otherwise
  1549. */
  1550. private function getMoveSrcPath($dstPath)
  1551. {
  1552. foreach ($this->nodesMove as $operation) {
  1553. if ($operation->dstPath === $dstPath) {
  1554. return $operation->srcPath;
  1555. }
  1556. }
  1557. return false;
  1558. }
  1559. /**
  1560. * Check whether the node at path has an unpersisted delete operation and
  1561. * there is no other node moved or added there.
  1562. *
  1563. * This is a simplistic check to be used by the Node to determine if it
  1564. * should not show one of the children the backend told it would exist.
  1565. *
  1566. * @param string $absPath The absolute path of the node
  1567. *
  1568. * @return boolean true if the current changed state has no node at this place
  1569. *
  1570. * @see Node::__construct
  1571. */
  1572. public function isNodeDeleted($absPath)
  1573. {
  1574. return array_key_exists($absPath, $this->nodesRemove)
  1575. && !(array_key_exists($absPath, $this->nodesAdd) && !$this->nodesAdd[$absPath]->skip
  1576. || $this->getMoveSrcPath($absPath));
  1577. }
  1578. /**
  1579. * Get a node if it is already in cache or null otherwise.
  1580. *
  1581. * Note that this method will also return deleted node objects so you can
  1582. * use them in refresh operations.
  1583. *
  1584. * @param string $absPath the absolute path to the node to fetch from cache
  1585. *
  1586. * @return NodeInterface or null
  1587. *
  1588. * @see Node::refresh()
  1589. */
  1590. public function getCachedNode($absPath, $class = Node::class)
  1591. {
  1592. if (isset($this->objectsByPath[$class][$absPath])) {
  1593. return $this->objectsByPath[$class][$absPath];
  1594. }
  1595. if (array_key_exists($absPath, $this->nodesRemove)) {
  1596. return $this->nodesRemove[$absPath]->node;
  1597. }
  1598. return null;
  1599. }
  1600. /**
  1601. * Return an ArrayIterator containing all the cached children of the given node.
  1602. * It makes no difference whether or not the node itself is cached.
  1603. *
  1604. * Note that this method will also return deleted node objects so you can
  1605. * use them in refresh operations.
  1606. *
  1607. * @param string $absPath
  1608. * @param string $class
  1609. *
  1610. * @return ArrayIterator
  1611. */
  1612. public function getCachedDescendants($absPath, $class = Node::class)
  1613. {
  1614. $descendants = [];
  1615. foreach ($this->objectsByPath[$class] as $path => $node) {
  1616. if (0 === strpos($path, "$absPath/")) {
  1617. $descendants[$path] = $node;
  1618. }
  1619. }
  1620. return new ArrayIterator(array_values($descendants));
  1621. }
  1622. /**
  1623. * Get a node if it is already in cache or null otherwise.
  1624. *
  1625. * As getCachedNode but looking up the node by uuid.
  1626. *
  1627. * Note that this will never return you a removed node because the uuid is
  1628. * removed from the map.
  1629. *
  1630. * @see getCachedNode
  1631. *
  1632. * @param $uuid
  1633. * @param string $class
  1634. *
  1635. * @return NodeInterface or null
  1636. */
  1637. public function getCachedNodeByUuid($uuid, $class = Node::class)
  1638. {
  1639. if (array_key_exists($uuid, $this->objectsByUuid)) {
  1640. return $this->getCachedNode($this->objectsByUuid[$uuid], $class);
  1641. }
  1642. return null;
  1643. }
  1644. /**
  1645. * Purge an item given by path from the cache and return whether the node
  1646. * should forget it or keep it.
  1647. *
  1648. * This is used by Node::refresh() to let the object manager notify
  1649. * deleted nodes or detect cases when not to delete.
  1650. *
  1651. * @param string $absPath The absolute path of the item
  1652. * @param boolean $keepChanges Whether to keep local changes or forget
  1653. * them
  1654. *
  1655. * @return bool true if the node is to be forgotten by its parent (deleted or
  1656. * moved away), false if child should be kept
  1657. */
  1658. public function purgeDisappearedNode($absPath, $keepChanges)
  1659. {
  1660. if (array_key_exists($absPath, $this->objectsByPath[Node::class])) {
  1661. $item = $this->objectsByPath[Node::class][$absPath];
  1662. if ($keepChanges &&
  1663. ($item->isNew() || $this->getMoveSrcPath($absPath))
  1664. ) {
  1665. // we keep changes and this is a new node or it moved here
  1666. return false;
  1667. }
  1668. // may not use $item->getIdentifier here - leads to endless loop if node purges itself
  1669. $uuid = array_search($absPath, $this->objectsByUuid);
  1670. if (false !== $uuid) {
  1671. unset($this->objectsByUuid[$uuid]);
  1672. }
  1673. unset($this->objectsByPath[Node::class][$absPath]);
  1674. $item->setDeleted();
  1675. }
  1676. // if the node moved away from this node, we did not find it in
  1677. // objectsByPath and the calling parent node can forget it
  1678. return true;
  1679. }
  1680. /**
  1681. * Register a given node path against a UUID.
  1682. *
  1683. * This is called when setting the UUID property of a node to ensure that
  1684. * it can be subsequently referenced by the UUID.
  1685. *
  1686. * @param string $uuid
  1687. * @param string $absPath
  1688. */
  1689. public function registerUuid($uuid, $absPath)
  1690. {
  1691. if (array_key_exists($uuid, $this->objectsByUuid)) {
  1692. throw new RuntimeException(sprintf(
  1693. 'Object path for UUID "%s" has already been registered to "%s"',
  1694. $uuid,
  1695. $this->objectsByUuid[$uuid]
  1696. ));
  1697. }
  1698. $this->objectsByUuid[$uuid] = $absPath;
  1699. }
  1700. }