vendor/jackalope/jackalope/src/Jackalope/Session.php line 271

Open in your IDE?
  1. <?php
  2. namespace Jackalope;
  3. use PHPCR\Util\PathHelper;
  4. use Exception;
  5. use PHPCR\RepositoryInterface;
  6. use PHPCR\SessionInterface;
  7. use PHPCR\SimpleCredentials;
  8. use PHPCR\CredentialsInterface;
  9. use PHPCR\PathNotFoundException;
  10. use PHPCR\ItemNotFoundException;
  11. use PHPCR\ItemExistsException;
  12. use PHPCR\RepositoryException;
  13. use PHPCR\UnsupportedRepositoryOperationException;
  14. use InvalidArgumentException;
  15. use PHPCR\Security\AccessControlException;
  16. use Jackalope\ImportExport\ImportExport;
  17. use Jackalope\Transport\TransportInterface;
  18. use Jackalope\Transport\TransactionInterface;
  19. use Traversable;
  20. /**
  21. * {@inheritDoc}
  22. *
  23. * Jackalope adds the SessionOption concept to handle session specific tweaking
  24. * and optimization. We distinguish between options that are purely
  25. * optimization but do not affect the behaviour and those that are change the
  26. * behaviour.
  27. *
  28. * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
  29. * @license http://opensource.org/licenses/MIT MIT License
  30. *
  31. * @api
  32. */
  33. class Session implements SessionInterface
  34. {
  35. /**
  36. * Constant for setSessionOption to manage the fetch depth.
  37. *
  38. * This option is used to set the depth with which nodes should be fetched from the backend to optimize
  39. * performance when you know you will need the child nodes.
  40. */
  41. const OPTION_FETCH_DEPTH = 'jackalope.fetch_depth';
  42. /**
  43. * Constant for setSessionOption to manage whether nodes having mix:lastModified should automatically be updated.
  44. *
  45. * Disable if you want to manually control this information, e.g. in a PHPCR-ODM listener.
  46. */
  47. const OPTION_AUTO_LASTMODIFIED = 'jackalope.auto_lastmodified';
  48. /**
  49. * A registry for all created sessions to be able to reference them by id in
  50. * the stream wrapper for lazy loading binary properties.
  51. *
  52. * Keys are spl_object_hash'es for the sessions which are the values
  53. *
  54. * @var array
  55. */
  56. protected static $sessionRegistry = [];
  57. /**
  58. * The factory to instantiate objects
  59. *
  60. * @var FactoryInterface
  61. */
  62. protected $factory;
  63. /**
  64. * @var Repository
  65. */
  66. protected $repository;
  67. /**
  68. * @var Workspace
  69. */
  70. protected $workspace;
  71. /**
  72. * @var ObjectManager
  73. */
  74. protected $objectManager;
  75. /**
  76. * @var SimpleCredentials
  77. */
  78. protected $credentials;
  79. /**
  80. * Whether this session is in logged out state and can not be used anymore
  81. *
  82. * @var bool
  83. */
  84. protected $logout = false;
  85. /**
  86. * The namespace registry.
  87. *
  88. * It is only used to check prefixes and at setup. Session namespace remapping must be handled locally.
  89. *
  90. * @var NamespaceRegistry
  91. */
  92. protected $namespaceRegistry;
  93. /**
  94. * List of local namespaces
  95. *
  96. * TODO: implement local namespace rewriting
  97. * see jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/PathParser.java and friends
  98. * for how this is done in jackrabbit
  99. */
  100. //protected $localNamespaces;
  101. /** Creates a session
  102. *
  103. * Builds the corresponding workspace instance
  104. *
  105. * @param FactoryInterface $factory the object factory
  106. * @param Repository $repository
  107. * @param string $workspaceName the workspace name that is used
  108. * @param SimpleCredentials $credentials the credentials that where
  109. * used to log in, in order to implement Session::getUserID()
  110. * if they are null, getUserID returns null
  111. * @param TransportInterface $transport the transport implementation
  112. */
  113. public function __construct(FactoryInterface $factory, Repository $repository, $workspaceName, SimpleCredentials $credentials = null, TransportInterface $transport)
  114. {
  115. $this->factory = $factory;
  116. $this->repository = $repository;
  117. $this->objectManager = $this->factory->get(ObjectManager::class, [$transport, $this]);
  118. $this->workspace = $this->factory->get(Workspace::class, [$this, $this->objectManager, $workspaceName]);
  119. $this->credentials = $credentials;
  120. $this->namespaceRegistry = $this->workspace->getNamespaceRegistry();
  121. self::registerSession($this);
  122. $transport->setNodeTypeManager($this->workspace->getNodeTypeManager());
  123. }
  124. /**
  125. * {@inheritDoc}
  126. *
  127. * @api
  128. */
  129. public function getRepository()
  130. {
  131. return $this->repository;
  132. }
  133. /**
  134. * {@inheritDoc}
  135. *
  136. * @api
  137. */
  138. public function getUserID()
  139. {
  140. if (null === $this->credentials) {
  141. return null;
  142. }
  143. return $this->credentials->getUserID(); //TODO: what if its not simple credentials? what about anonymous login?
  144. }
  145. /**
  146. * {@inheritDoc}
  147. *
  148. * @api
  149. */
  150. public function getAttributeNames()
  151. {
  152. if (null === $this->credentials) {
  153. return [];
  154. }
  155. return $this->credentials->getAttributeNames();
  156. }
  157. /**
  158. * {@inheritDoc}
  159. *
  160. * @api
  161. */
  162. public function getAttribute($name)
  163. {
  164. if (null === $this->credentials) {
  165. return null;
  166. }
  167. return $this->credentials->getAttribute($name);
  168. }
  169. /**
  170. * {@inheritDoc}
  171. *
  172. * @api
  173. */
  174. public function getWorkspace()
  175. {
  176. return $this->workspace;
  177. }
  178. /**
  179. * {@inheritDoc}
  180. *
  181. * @api
  182. */
  183. public function getRootNode()
  184. {
  185. return $this->getNode('/');
  186. }
  187. /**
  188. * {@inheritDoc}
  189. *
  190. * @api
  191. */
  192. public function impersonate(CredentialsInterface $credentials)
  193. {
  194. throw new UnsupportedRepositoryOperationException('Not supported');
  195. }
  196. /**
  197. * {@inheritDoc}
  198. *
  199. * @api
  200. */
  201. public function getNodeByIdentifier($id)
  202. {
  203. return $this->objectManager->getNodeByIdentifier($id);
  204. }
  205. /**
  206. * {@inheritDoc}
  207. *
  208. * @api
  209. */
  210. public function getNodesByIdentifier($ids)
  211. {
  212. if (! is_array($ids) && ! $ids instanceof Traversable) {
  213. $hint = is_object($ids) ? get_class($ids) : gettype($ids);
  214. throw new InvalidArgumentException("Not a valid array or Traversable: $hint");
  215. }
  216. return $this->objectManager->getNodesByIdentifier($ids);
  217. }
  218. /**
  219. * {@inheritDoc}
  220. *
  221. * @api
  222. */
  223. public function getItem($absPath)
  224. {
  225. if (! is_string($absPath) || strlen($absPath) === 0 || '/' !== $absPath[0]) {
  226. throw new PathNotFoundException('It is forbidden to call getItem on session with a relative path');
  227. }
  228. if ($this->nodeExists($absPath)) {
  229. return $this->getNode($absPath);
  230. }
  231. return $this->getProperty($absPath);
  232. }
  233. /**
  234. * {@inheritDoc}
  235. *
  236. * @api
  237. */
  238. public function getNode($absPath, $depthHint = -1)
  239. {
  240. if (-1 !== $depthHint) {
  241. $depth = $this->getSessionOption(self::OPTION_FETCH_DEPTH);
  242. $this->setSessionOption(self::OPTION_FETCH_DEPTH, $depthHint);
  243. }
  244. try {
  245. $node = $this->objectManager->getNodeByPath($absPath);
  246. if (isset($depth)) {
  247. $this->setSessionOption(self::OPTION_FETCH_DEPTH, $depth);
  248. }
  249. return $node;
  250. } catch (ItemNotFoundException $e) {
  251. if (isset($depth)) {
  252. $this->setSessionOption(self::OPTION_FETCH_DEPTH, $depth);
  253. }
  254. throw new PathNotFoundException($e->getMessage(), $e->getCode(), $e);
  255. }
  256. }
  257. /**
  258. * {@inheritDoc}
  259. *
  260. * @api
  261. */
  262. public function getNodes($absPaths)
  263. {
  264. if (! is_array($absPaths) && ! $absPaths instanceof Traversable) {
  265. $hint = is_object($absPaths) ? get_class($absPaths) : gettype($absPaths);
  266. throw new InvalidArgumentException("Not a valid array or Traversable: $hint");
  267. }
  268. return $this->objectManager->getNodesByPath($absPaths);
  269. }
  270. /**
  271. * {@inheritDoc}
  272. *
  273. * @api
  274. */
  275. public function getProperty($absPath)
  276. {
  277. try {
  278. return $this->objectManager->getPropertyByPath($absPath);
  279. } catch (ItemNotFoundException $e) {
  280. throw new PathNotFoundException($e->getMessage(), $e->getCode(), $e);
  281. }
  282. }
  283. public function getProperties($absPaths)
  284. {
  285. if (! is_array($absPaths) && ! $absPaths instanceof Traversable) {
  286. $hint = is_object($absPaths) ? get_class($absPaths) : gettype($absPaths);
  287. throw new InvalidArgumentException("Not a valid array or Traversable: $hint");
  288. }
  289. return $this->objectManager->getPropertiesByPath($absPaths);
  290. }
  291. /**
  292. * {@inheritDoc}
  293. *
  294. * @api
  295. */
  296. public function itemExists($absPath)
  297. {
  298. if ($absPath === '/') {
  299. return true;
  300. }
  301. return $this->nodeExists($absPath) || $this->propertyExists($absPath);
  302. }
  303. /**
  304. * {@inheritDoc}
  305. *
  306. * @api
  307. */
  308. public function nodeExists($absPath)
  309. {
  310. if ($absPath === '/') {
  311. return true;
  312. }
  313. try {
  314. //OPTIMIZE: avoid throwing and catching errors would improve performance if many node exists calls are made
  315. //would need to communicate to the lower layer that we do not want exceptions
  316. $this->objectManager->getNodeByPath($absPath);
  317. } catch (ItemNotFoundException $e) {
  318. return false;
  319. }
  320. return true;
  321. }
  322. /**
  323. * {@inheritDoc}
  324. *
  325. * @api
  326. */
  327. public function propertyExists($absPath)
  328. {
  329. try {
  330. //OPTIMIZE: avoid throwing and catching errors would improve performance if many node exists calls are made
  331. //would need to communicate to the lower layer that we do not want exceptions
  332. $this->getProperty($absPath);
  333. } catch (PathNotFoundException $e) {
  334. return false;
  335. }
  336. return true;
  337. }
  338. /**
  339. * {@inheritDoc}
  340. *
  341. * @api
  342. */
  343. public function move($srcAbsPath, $destAbsPath)
  344. {
  345. try {
  346. $parent = $this->objectManager->getNodeByPath(PathHelper::getParentPath($destAbsPath));
  347. } catch (ItemNotFoundException $e) {
  348. throw new PathNotFoundException("Target path can not be found: $destAbsPath", $e->getCode(), $e);
  349. }
  350. if ($parent->hasNode(PathHelper::getNodeName($destAbsPath))) {
  351. // TODO same-name siblings
  352. throw new ItemExistsException('Target node already exists at '.$destAbsPath);
  353. }
  354. if ($parent->hasProperty(PathHelper::getNodeName($destAbsPath))) {
  355. throw new ItemExistsException('Target property already exists at '.$destAbsPath);
  356. }
  357. $this->objectManager->moveNode($srcAbsPath, $destAbsPath);
  358. }
  359. /**
  360. * {@inheritDoc}
  361. *
  362. * @api
  363. */
  364. public function removeItem($absPath)
  365. {
  366. $item = $this->getItem($absPath);
  367. $item->remove();
  368. }
  369. /**
  370. * {@inheritDoc}
  371. *
  372. * Wraps the save operation into a transaction if transactions are enabled
  373. * but we are not currently inside a transaction and rolls back on error.
  374. *
  375. * If transactions are disabled, errors on save can lead to partial saves
  376. * and inconsistent data.
  377. *
  378. * @api
  379. */
  380. public function save()
  381. {
  382. if ($this->getTransport() instanceof TransactionInterface) {
  383. try {
  384. $utx = $this->workspace->getTransactionManager();
  385. } catch (UnsupportedRepositoryOperationException $e) {
  386. // user transactions where disabled for this session, do no automatic transaction.
  387. }
  388. }
  389. if (isset($utx) && !$utx->inTransaction()) {
  390. // do the operation in a short transaction
  391. $utx->begin();
  392. try {
  393. $this->objectManager->save();
  394. $utx->commit();
  395. } catch (Exception $e) {
  396. // if anything goes wrong, rollback this mess
  397. try {
  398. $utx->rollback();
  399. } catch (Exception $rollbackException) {
  400. // ignore this exception
  401. }
  402. // but do not eat this exception
  403. throw $e;
  404. }
  405. } else {
  406. $this->objectManager->save();
  407. }
  408. }
  409. /**
  410. * {@inheritDoc}
  411. *
  412. * @api
  413. */
  414. public function refresh($keepChanges)
  415. {
  416. $this->objectManager->refresh($keepChanges);
  417. }
  418. /**
  419. * Jackalope specific hack to drop the state of the current session
  420. *
  421. * Removes all cached objects, planned changes etc without making the
  422. * objects aware of it. Was done as a cheap replacement for refresh
  423. * in testing.
  424. *
  425. * @deprecated: this will screw up major, as the user of the api can still have references to nodes. USE refresh instead!
  426. */
  427. public function clear()
  428. {
  429. trigger_error('Use Session::refresh instead, this method is extremely unsafe', E_USER_DEPRECATED);
  430. $this->objectManager->clear();
  431. }
  432. /**
  433. * {@inheritDoc}
  434. *
  435. * @api
  436. */
  437. public function hasPendingChanges()
  438. {
  439. return $this->objectManager->hasPendingChanges();
  440. }
  441. /**
  442. * {@inheritDoc}
  443. *
  444. * @api
  445. */
  446. public function hasPermission($absPath, $actions)
  447. {
  448. $actualPermissions = $this->objectManager->getPermissions($absPath);
  449. $requestedPermissions = explode(',', $actions);
  450. foreach ($requestedPermissions as $perm) {
  451. if (! in_array(strtolower(trim($perm)), $actualPermissions)) {
  452. return false;
  453. }
  454. }
  455. return true;
  456. }
  457. /**
  458. * {@inheritDoc}
  459. *
  460. * @api
  461. */
  462. public function checkPermission($absPath, $actions)
  463. {
  464. if (! $this->hasPermission($absPath, $actions)) {
  465. throw new AccessControlException($absPath);
  466. }
  467. }
  468. /**
  469. * {@inheritDoc}
  470. *
  471. * Jackalope does currently not check anything and always return true.
  472. *
  473. * @api
  474. */
  475. public function hasCapability($methodName, $target, array $arguments)
  476. {
  477. //we never determine whether operation can be performed as it is optional ;-)
  478. //TODO: could implement some
  479. return true;
  480. }
  481. /**
  482. * {@inheritDoc}
  483. *
  484. * @api
  485. */
  486. public function importXML($parentAbsPath, $uri, $uuidBehavior)
  487. {
  488. ImportExport::importXML(
  489. $this->getNode($parentAbsPath),
  490. $this->workspace->getNamespaceRegistry(),
  491. $uri,
  492. $uuidBehavior
  493. );
  494. }
  495. /**
  496. * {@inheritDoc}
  497. *
  498. * @api
  499. */
  500. public function exportSystemView($absPath, $stream, $skipBinary, $noRecurse)
  501. {
  502. ImportExport::exportSystemView(
  503. $this->getNode($absPath),
  504. $this->workspace->getNamespaceRegistry(),
  505. $stream,
  506. $skipBinary,
  507. $noRecurse
  508. );
  509. }
  510. /**
  511. * {@inheritDoc}
  512. *
  513. * @api
  514. */
  515. public function exportDocumentView($absPath, $stream, $skipBinary, $noRecurse)
  516. {
  517. ImportExport::exportDocumentView(
  518. $this->getNode($absPath),
  519. $this->workspace->getNamespaceRegistry(),
  520. $stream,
  521. $skipBinary,
  522. $noRecurse
  523. );
  524. }
  525. /**
  526. * {@inheritDoc}
  527. *
  528. * @api
  529. */
  530. public function setNamespacePrefix($prefix, $uri)
  531. {
  532. $this->namespaceRegistry->checkPrefix($prefix);
  533. throw new NotImplementedException('TODO: implement session scope remapping of namespaces');
  534. //this will lead to rewrite all names and paths in requests and replies. part of this can be done in ObjectManager::normalizePath
  535. }
  536. /**
  537. * {@inheritDoc}
  538. *
  539. * @api
  540. */
  541. public function getNamespacePrefixes()
  542. {
  543. //TODO: once setNamespacePrefix is implemented, must take session remaps into account
  544. return $this->namespaceRegistry->getPrefixes();
  545. }
  546. /**
  547. * {@inheritDoc}
  548. *
  549. * @api
  550. */
  551. public function getNamespaceURI($prefix)
  552. {
  553. //TODO: once setNamespacePrefix is implemented, must take session remaps into account
  554. return $this->namespaceRegistry->getURI($prefix);
  555. }
  556. /**
  557. * {@inheritDoc}
  558. *
  559. * @api
  560. */
  561. public function getNamespacePrefix($uri)
  562. {
  563. //TODO: once setNamespacePrefix is implemented, must take session remaps into account
  564. return $this->namespaceRegistry->getPrefix($uri);
  565. }
  566. /**
  567. * {@inheritDoc}
  568. *
  569. * @api
  570. */
  571. public function logout()
  572. {
  573. //OPTIMIZATION: flush object manager to help garbage collector
  574. $this->logout = true;
  575. if ($this->getRepository()->getDescriptor(RepositoryInterface::OPTION_LOCKING_SUPPORTED)) {
  576. $this->getWorkspace()->getLockManager()->logout();
  577. }
  578. self::unregisterSession($this);
  579. $this->getTransport()->logout();
  580. }
  581. /**
  582. * {@inheritDoc}
  583. *
  584. * @api
  585. */
  586. public function isLive()
  587. {
  588. return ! $this->logout;
  589. }
  590. /**
  591. * {@inheritDoc}
  592. *
  593. * @api
  594. */
  595. public function getAccessControlManager()
  596. {
  597. throw new UnsupportedRepositoryOperationException();
  598. }
  599. /**
  600. * {@inheritDoc}
  601. *
  602. * @api
  603. */
  604. public function getRetentionManager()
  605. {
  606. throw new UnsupportedRepositoryOperationException();
  607. }
  608. /**
  609. * Implementation specific: The object manager is also used by other components, i.e. the QueryManager.
  610. *
  611. * @return ObjectManager the object manager associated with this session
  612. *
  613. * @private
  614. */
  615. public function getObjectManager()
  616. {
  617. return $this->objectManager;
  618. }
  619. /**
  620. * Implementation specific: The transport implementation is also used by other components,
  621. * i.e. the NamespaceRegistry
  622. *
  623. * @return TransportInterface the transport implementation associated with
  624. * this session.
  625. *
  626. * @private
  627. */
  628. public function getTransport()
  629. {
  630. return $this->objectManager->getTransport();
  631. }
  632. /**
  633. * Implementation specific: register session in session registry for the stream wrapper.
  634. *
  635. * @param Session $session the session to register
  636. *
  637. * @private
  638. */
  639. protected static function registerSession(Session $session)
  640. {
  641. $key = $session->getRegistryKey();
  642. self::$sessionRegistry[$key] = $session;
  643. }
  644. /**
  645. * Implementation specific: unregister session in session registry on logout.
  646. *
  647. * @param Session $session the session to unregister
  648. *
  649. * @private
  650. */
  651. protected static function unregisterSession(Session $session)
  652. {
  653. $key = $session->getRegistryKey();
  654. unset(self::$sessionRegistry[$key]);
  655. }
  656. /**
  657. * Implementation specific: create an id for the session registry so that the stream wrapper can identify it.
  658. *
  659. * @private
  660. *
  661. * @return string an id for this session
  662. */
  663. public function getRegistryKey()
  664. {
  665. return spl_object_hash($this);
  666. }
  667. /**
  668. * Implementation specific: get a session from the session registry for the stream wrapper.
  669. *
  670. * @param string $key key for the session
  671. *
  672. * @return Session|null the session or null if none is registered with the given key
  673. *
  674. * @private
  675. */
  676. public static function getSessionFromRegistry($key)
  677. {
  678. if (isset(self::$sessionRegistry[$key])) {
  679. return self::$sessionRegistry[$key];
  680. }
  681. return null;
  682. }
  683. /**
  684. * Sets a session specific option.
  685. *
  686. * @param string $key the key to be set
  687. * @param mixed $value the value to be set
  688. *
  689. * @throws InvalidArgumentException if the option is unknown
  690. * @throws RepositoryException if this option is not supported and is
  691. * a behaviour relevant option
  692. *
  693. * @see BaseTransport::setFetchDepth($value);
  694. */
  695. public function setSessionOption($key, $value)
  696. {
  697. switch ($key) {
  698. case self::OPTION_FETCH_DEPTH:
  699. $this->getTransport()->setFetchDepth($value);
  700. break;
  701. case self::OPTION_AUTO_LASTMODIFIED:
  702. $this->getTransport()->setAutoLastModified($value);
  703. break;
  704. default:
  705. throw new InvalidArgumentException("Unknown option: $key");
  706. }
  707. }
  708. /**
  709. * Gets a session specific option.
  710. *
  711. * @param string $key the key to be gotten
  712. *
  713. * @return bool
  714. *
  715. * @throws InvalidArgumentException if the option is unknown
  716. *
  717. * @see setSessionOption($key, $value);
  718. */
  719. public function getSessionOption($key)
  720. {
  721. switch ($key) {
  722. case self::OPTION_FETCH_DEPTH:
  723. return $this->getTransport()->getFetchDepth();
  724. case self::OPTION_AUTO_LASTMODIFIED:
  725. return $this->getTransport()->getAutoLastModified();
  726. }
  727. throw new InvalidArgumentException("Unknown option: $key");
  728. }
  729. }