<?php
namespace Jackalope;
use PHPCR\Util\PathHelper;
use Exception;
use PHPCR\RepositoryInterface;
use PHPCR\SessionInterface;
use PHPCR\SimpleCredentials;
use PHPCR\CredentialsInterface;
use PHPCR\PathNotFoundException;
use PHPCR\ItemNotFoundException;
use PHPCR\ItemExistsException;
use PHPCR\RepositoryException;
use PHPCR\UnsupportedRepositoryOperationException;
use InvalidArgumentException;
use PHPCR\Security\AccessControlException;
use Jackalope\ImportExport\ImportExport;
use Jackalope\Transport\TransportInterface;
use Jackalope\Transport\TransactionInterface;
use Traversable;
/**
* {@inheritDoc}
*
* Jackalope adds the SessionOption concept to handle session specific tweaking
* and optimization. We distinguish between options that are purely
* optimization but do not affect the behaviour and those that are change the
* behaviour.
*
* @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
* @license http://opensource.org/licenses/MIT MIT License
*
* @api
*/
class Session implements SessionInterface
{
/**
* Constant for setSessionOption to manage the fetch depth.
*
* This option is used to set the depth with which nodes should be fetched from the backend to optimize
* performance when you know you will need the child nodes.
*/
const OPTION_FETCH_DEPTH = 'jackalope.fetch_depth';
/**
* Constant for setSessionOption to manage whether nodes having mix:lastModified should automatically be updated.
*
* Disable if you want to manually control this information, e.g. in a PHPCR-ODM listener.
*/
const OPTION_AUTO_LASTMODIFIED = 'jackalope.auto_lastmodified';
/**
* A registry for all created sessions to be able to reference them by id in
* the stream wrapper for lazy loading binary properties.
*
* Keys are spl_object_hash'es for the sessions which are the values
*
* @var array
*/
protected static $sessionRegistry = [];
/**
* The factory to instantiate objects
*
* @var FactoryInterface
*/
protected $factory;
/**
* @var Repository
*/
protected $repository;
/**
* @var Workspace
*/
protected $workspace;
/**
* @var ObjectManager
*/
protected $objectManager;
/**
* @var SimpleCredentials
*/
protected $credentials;
/**
* Whether this session is in logged out state and can not be used anymore
*
* @var bool
*/
protected $logout = false;
/**
* The namespace registry.
*
* It is only used to check prefixes and at setup. Session namespace remapping must be handled locally.
*
* @var NamespaceRegistry
*/
protected $namespaceRegistry;
/**
* List of local namespaces
*
* TODO: implement local namespace rewriting
* see jackrabbit-spi-commons/src/main/java/org/apache/jackrabbit/spi/commons/conversion/PathParser.java and friends
* for how this is done in jackrabbit
*/
//protected $localNamespaces;
/** Creates a session
*
* Builds the corresponding workspace instance
*
* @param FactoryInterface $factory the object factory
* @param Repository $repository
* @param string $workspaceName the workspace name that is used
* @param SimpleCredentials $credentials the credentials that where
* used to log in, in order to implement Session::getUserID()
* if they are null, getUserID returns null
* @param TransportInterface $transport the transport implementation
*/
public function __construct(FactoryInterface $factory, Repository $repository, $workspaceName, SimpleCredentials $credentials = null, TransportInterface $transport)
{
$this->factory = $factory;
$this->repository = $repository;
$this->objectManager = $this->factory->get(ObjectManager::class, [$transport, $this]);
$this->workspace = $this->factory->get(Workspace::class, [$this, $this->objectManager, $workspaceName]);
$this->credentials = $credentials;
$this->namespaceRegistry = $this->workspace->getNamespaceRegistry();
self::registerSession($this);
$transport->setNodeTypeManager($this->workspace->getNodeTypeManager());
}
/**
* {@inheritDoc}
*
* @api
*/
public function getRepository()
{
return $this->repository;
}
/**
* {@inheritDoc}
*
* @api
*/
public function getUserID()
{
if (null === $this->credentials) {
return null;
}
return $this->credentials->getUserID(); //TODO: what if its not simple credentials? what about anonymous login?
}
/**
* {@inheritDoc}
*
* @api
*/
public function getAttributeNames()
{
if (null === $this->credentials) {
return [];
}
return $this->credentials->getAttributeNames();
}
/**
* {@inheritDoc}
*
* @api
*/
public function getAttribute($name)
{
if (null === $this->credentials) {
return null;
}
return $this->credentials->getAttribute($name);
}
/**
* {@inheritDoc}
*
* @api
*/
public function getWorkspace()
{
return $this->workspace;
}
/**
* {@inheritDoc}
*
* @api
*/
public function getRootNode()
{
return $this->getNode('/');
}
/**
* {@inheritDoc}
*
* @api
*/
public function impersonate(CredentialsInterface $credentials)
{
throw new UnsupportedRepositoryOperationException('Not supported');
}
/**
* {@inheritDoc}
*
* @api
*/
public function getNodeByIdentifier($id)
{
return $this->objectManager->getNodeByIdentifier($id);
}
/**
* {@inheritDoc}
*
* @api
*/
public function getNodesByIdentifier($ids)
{
if (! is_array($ids) && ! $ids instanceof Traversable) {
$hint = is_object($ids) ? get_class($ids) : gettype($ids);
throw new InvalidArgumentException("Not a valid array or Traversable: $hint");
}
return $this->objectManager->getNodesByIdentifier($ids);
}
/**
* {@inheritDoc}
*
* @api
*/
public function getItem($absPath)
{
if (! is_string($absPath) || strlen($absPath) === 0 || '/' !== $absPath[0]) {
throw new PathNotFoundException('It is forbidden to call getItem on session with a relative path');
}
if ($this->nodeExists($absPath)) {
return $this->getNode($absPath);
}
return $this->getProperty($absPath);
}
/**
* {@inheritDoc}
*
* @api
*/
public function getNode($absPath, $depthHint = -1)
{
if (-1 !== $depthHint) {
$depth = $this->getSessionOption(self::OPTION_FETCH_DEPTH);
$this->setSessionOption(self::OPTION_FETCH_DEPTH, $depthHint);
}
try {
$node = $this->objectManager->getNodeByPath($absPath);
if (isset($depth)) {
$this->setSessionOption(self::OPTION_FETCH_DEPTH, $depth);
}
return $node;
} catch (ItemNotFoundException $e) {
if (isset($depth)) {
$this->setSessionOption(self::OPTION_FETCH_DEPTH, $depth);
}
throw new PathNotFoundException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritDoc}
*
* @api
*/
public function getNodes($absPaths)
{
if (! is_array($absPaths) && ! $absPaths instanceof Traversable) {
$hint = is_object($absPaths) ? get_class($absPaths) : gettype($absPaths);
throw new InvalidArgumentException("Not a valid array or Traversable: $hint");
}
return $this->objectManager->getNodesByPath($absPaths);
}
/**
* {@inheritDoc}
*
* @api
*/
public function getProperty($absPath)
{
try {
return $this->objectManager->getPropertyByPath($absPath);
} catch (ItemNotFoundException $e) {
throw new PathNotFoundException($e->getMessage(), $e->getCode(), $e);
}
}
public function getProperties($absPaths)
{
if (! is_array($absPaths) && ! $absPaths instanceof Traversable) {
$hint = is_object($absPaths) ? get_class($absPaths) : gettype($absPaths);
throw new InvalidArgumentException("Not a valid array or Traversable: $hint");
}
return $this->objectManager->getPropertiesByPath($absPaths);
}
/**
* {@inheritDoc}
*
* @api
*/
public function itemExists($absPath)
{
if ($absPath === '/') {
return true;
}
return $this->nodeExists($absPath) || $this->propertyExists($absPath);
}
/**
* {@inheritDoc}
*
* @api
*/
public function nodeExists($absPath)
{
if ($absPath === '/') {
return true;
}
try {
//OPTIMIZE: avoid throwing and catching errors would improve performance if many node exists calls are made
//would need to communicate to the lower layer that we do not want exceptions
$this->objectManager->getNodeByPath($absPath);
} catch (ItemNotFoundException $e) {
return false;
}
return true;
}
/**
* {@inheritDoc}
*
* @api
*/
public function propertyExists($absPath)
{
try {
//OPTIMIZE: avoid throwing and catching errors would improve performance if many node exists calls are made
//would need to communicate to the lower layer that we do not want exceptions
$this->getProperty($absPath);
} catch (PathNotFoundException $e) {
return false;
}
return true;
}
/**
* {@inheritDoc}
*
* @api
*/
public function move($srcAbsPath, $destAbsPath)
{
try {
$parent = $this->objectManager->getNodeByPath(PathHelper::getParentPath($destAbsPath));
} catch (ItemNotFoundException $e) {
throw new PathNotFoundException("Target path can not be found: $destAbsPath", $e->getCode(), $e);
}
if ($parent->hasNode(PathHelper::getNodeName($destAbsPath))) {
// TODO same-name siblings
throw new ItemExistsException('Target node already exists at '.$destAbsPath);
}
if ($parent->hasProperty(PathHelper::getNodeName($destAbsPath))) {
throw new ItemExistsException('Target property already exists at '.$destAbsPath);
}
$this->objectManager->moveNode($srcAbsPath, $destAbsPath);
}
/**
* {@inheritDoc}
*
* @api
*/
public function removeItem($absPath)
{
$item = $this->getItem($absPath);
$item->remove();
}
/**
* {@inheritDoc}
*
* Wraps the save operation into a transaction if transactions are enabled
* but we are not currently inside a transaction and rolls back on error.
*
* If transactions are disabled, errors on save can lead to partial saves
* and inconsistent data.
*
* @api
*/
public function save()
{
if ($this->getTransport() instanceof TransactionInterface) {
try {
$utx = $this->workspace->getTransactionManager();
} catch (UnsupportedRepositoryOperationException $e) {
// user transactions where disabled for this session, do no automatic transaction.
}
}
if (isset($utx) && !$utx->inTransaction()) {
// do the operation in a short transaction
$utx->begin();
try {
$this->objectManager->save();
$utx->commit();
} catch (Exception $e) {
// if anything goes wrong, rollback this mess
try {
$utx->rollback();
} catch (Exception $rollbackException) {
// ignore this exception
}
// but do not eat this exception
throw $e;
}
} else {
$this->objectManager->save();
}
}
/**
* {@inheritDoc}
*
* @api
*/
public function refresh($keepChanges)
{
$this->objectManager->refresh($keepChanges);
}
/**
* Jackalope specific hack to drop the state of the current session
*
* Removes all cached objects, planned changes etc without making the
* objects aware of it. Was done as a cheap replacement for refresh
* in testing.
*
* @deprecated: this will screw up major, as the user of the api can still have references to nodes. USE refresh instead!
*/
public function clear()
{
trigger_error('Use Session::refresh instead, this method is extremely unsafe', E_USER_DEPRECATED);
$this->objectManager->clear();
}
/**
* {@inheritDoc}
*
* @api
*/
public function hasPendingChanges()
{
return $this->objectManager->hasPendingChanges();
}
/**
* {@inheritDoc}
*
* @api
*/
public function hasPermission($absPath, $actions)
{
$actualPermissions = $this->objectManager->getPermissions($absPath);
$requestedPermissions = explode(',', $actions);
foreach ($requestedPermissions as $perm) {
if (! in_array(strtolower(trim($perm)), $actualPermissions)) {
return false;
}
}
return true;
}
/**
* {@inheritDoc}
*
* @api
*/
public function checkPermission($absPath, $actions)
{
if (! $this->hasPermission($absPath, $actions)) {
throw new AccessControlException($absPath);
}
}
/**
* {@inheritDoc}
*
* Jackalope does currently not check anything and always return true.
*
* @api
*/
public function hasCapability($methodName, $target, array $arguments)
{
//we never determine whether operation can be performed as it is optional ;-)
//TODO: could implement some
return true;
}
/**
* {@inheritDoc}
*
* @api
*/
public function importXML($parentAbsPath, $uri, $uuidBehavior)
{
ImportExport::importXML(
$this->getNode($parentAbsPath),
$this->workspace->getNamespaceRegistry(),
$uri,
$uuidBehavior
);
}
/**
* {@inheritDoc}
*
* @api
*/
public function exportSystemView($absPath, $stream, $skipBinary, $noRecurse)
{
ImportExport::exportSystemView(
$this->getNode($absPath),
$this->workspace->getNamespaceRegistry(),
$stream,
$skipBinary,
$noRecurse
);
}
/**
* {@inheritDoc}
*
* @api
*/
public function exportDocumentView($absPath, $stream, $skipBinary, $noRecurse)
{
ImportExport::exportDocumentView(
$this->getNode($absPath),
$this->workspace->getNamespaceRegistry(),
$stream,
$skipBinary,
$noRecurse
);
}
/**
* {@inheritDoc}
*
* @api
*/
public function setNamespacePrefix($prefix, $uri)
{
$this->namespaceRegistry->checkPrefix($prefix);
throw new NotImplementedException('TODO: implement session scope remapping of namespaces');
//this will lead to rewrite all names and paths in requests and replies. part of this can be done in ObjectManager::normalizePath
}
/**
* {@inheritDoc}
*
* @api
*/
public function getNamespacePrefixes()
{
//TODO: once setNamespacePrefix is implemented, must take session remaps into account
return $this->namespaceRegistry->getPrefixes();
}
/**
* {@inheritDoc}
*
* @api
*/
public function getNamespaceURI($prefix)
{
//TODO: once setNamespacePrefix is implemented, must take session remaps into account
return $this->namespaceRegistry->getURI($prefix);
}
/**
* {@inheritDoc}
*
* @api
*/
public function getNamespacePrefix($uri)
{
//TODO: once setNamespacePrefix is implemented, must take session remaps into account
return $this->namespaceRegistry->getPrefix($uri);
}
/**
* {@inheritDoc}
*
* @api
*/
public function logout()
{
//OPTIMIZATION: flush object manager to help garbage collector
$this->logout = true;
if ($this->getRepository()->getDescriptor(RepositoryInterface::OPTION_LOCKING_SUPPORTED)) {
$this->getWorkspace()->getLockManager()->logout();
}
self::unregisterSession($this);
$this->getTransport()->logout();
}
/**
* {@inheritDoc}
*
* @api
*/
public function isLive()
{
return ! $this->logout;
}
/**
* {@inheritDoc}
*
* @api
*/
public function getAccessControlManager()
{
throw new UnsupportedRepositoryOperationException();
}
/**
* {@inheritDoc}
*
* @api
*/
public function getRetentionManager()
{
throw new UnsupportedRepositoryOperationException();
}
/**
* Implementation specific: The object manager is also used by other components, i.e. the QueryManager.
*
* @return ObjectManager the object manager associated with this session
*
* @private
*/
public function getObjectManager()
{
return $this->objectManager;
}
/**
* Implementation specific: The transport implementation is also used by other components,
* i.e. the NamespaceRegistry
*
* @return TransportInterface the transport implementation associated with
* this session.
*
* @private
*/
public function getTransport()
{
return $this->objectManager->getTransport();
}
/**
* Implementation specific: register session in session registry for the stream wrapper.
*
* @param Session $session the session to register
*
* @private
*/
protected static function registerSession(Session $session)
{
$key = $session->getRegistryKey();
self::$sessionRegistry[$key] = $session;
}
/**
* Implementation specific: unregister session in session registry on logout.
*
* @param Session $session the session to unregister
*
* @private
*/
protected static function unregisterSession(Session $session)
{
$key = $session->getRegistryKey();
unset(self::$sessionRegistry[$key]);
}
/**
* Implementation specific: create an id for the session registry so that the stream wrapper can identify it.
*
* @private
*
* @return string an id for this session
*/
public function getRegistryKey()
{
return spl_object_hash($this);
}
/**
* Implementation specific: get a session from the session registry for the stream wrapper.
*
* @param string $key key for the session
*
* @return Session|null the session or null if none is registered with the given key
*
* @private
*/
public static function getSessionFromRegistry($key)
{
if (isset(self::$sessionRegistry[$key])) {
return self::$sessionRegistry[$key];
}
return null;
}
/**
* Sets a session specific option.
*
* @param string $key the key to be set
* @param mixed $value the value to be set
*
* @throws InvalidArgumentException if the option is unknown
* @throws RepositoryException if this option is not supported and is
* a behaviour relevant option
*
* @see BaseTransport::setFetchDepth($value);
*/
public function setSessionOption($key, $value)
{
switch ($key) {
case self::OPTION_FETCH_DEPTH:
$this->getTransport()->setFetchDepth($value);
break;
case self::OPTION_AUTO_LASTMODIFIED:
$this->getTransport()->setAutoLastModified($value);
break;
default:
throw new InvalidArgumentException("Unknown option: $key");
}
}
/**
* Gets a session specific option.
*
* @param string $key the key to be gotten
*
* @return bool
*
* @throws InvalidArgumentException if the option is unknown
*
* @see setSessionOption($key, $value);
*/
public function getSessionOption($key)
{
switch ($key) {
case self::OPTION_FETCH_DEPTH:
return $this->getTransport()->getFetchDepth();
case self::OPTION_AUTO_LASTMODIFIED:
return $this->getTransport()->getAutoLastModified();
}
throw new InvalidArgumentException("Unknown option: $key");
}
}