vendor/jackalope/jackalope-doctrine-dbal/src/Jackalope/Transport/DoctrineDBAL/Client.php line 621

Open in your IDE?
  1. <?php
  2. namespace Jackalope\Transport\DoctrineDBAL;
  3. use ArrayObject;
  4. use Closure;
  5. use DateTime;
  6. use DateTimeZone;
  7. use Doctrine\DBAL\Connection;
  8. use Doctrine\DBAL\DBALException;
  9. use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
  10. use Doctrine\DBAL\Platforms\SqlitePlatform;
  11. use DOMDocument;
  12. use DOMElement;
  13. use DOMXPath;
  14. use Exception;
  15. use InvalidArgumentException;
  16. use Jackalope\FactoryInterface;
  17. use Jackalope\Node;
  18. use Jackalope\NodeType\NodeProcessor;
  19. use Jackalope\NodeType\NodeTypeDefinition;
  20. use Jackalope\NodeType\NodeTypeManager;
  21. use Jackalope\NotImplementedException;
  22. use Jackalope\Property;
  23. use Jackalope\Query\QOM\QueryObjectModelFactory;
  24. use Jackalope\Query\Query;
  25. use Jackalope\Transport\BaseTransport;
  26. use Jackalope\Transport\DoctrineDBAL\Query\QOMWalker;
  27. use Jackalope\Transport\MoveNodeOperation;
  28. use Jackalope\Transport\NodeTypeManagementInterface;
  29. use Jackalope\Transport\QueryInterface as QueryTransport;
  30. use Jackalope\Transport\StandardNodeTypes;
  31. use Jackalope\Transport\TransactionInterface;
  32. use Jackalope\Transport\WorkspaceManagementInterface;
  33. use Jackalope\Transport\WritingInterface;
  34. use PDO;
  35. use PDOException;
  36. use PHPCR\AccessDeniedException;
  37. use PHPCR\CredentialsInterface;
  38. use PHPCR\ItemExistsException;
  39. use PHPCR\ItemNotFoundException;
  40. use PHPCR\LoginException;
  41. use PHPCR\NamespaceException;
  42. use PHPCR\NamespaceRegistryInterface as NS;
  43. use PHPCR\NodeType\ConstraintViolationException;
  44. use PHPCR\NodeType\NodeDefinitionInterface;
  45. use PHPCR\NodeType\NodeTypeExistsException;
  46. use PHPCR\NodeType\NoSuchNodeTypeException;
  47. use PHPCR\NodeType\PropertyDefinitionInterface;
  48. use PHPCR\NoSuchWorkspaceException;
  49. use PHPCR\PathNotFoundException;
  50. use PHPCR\PropertyType;
  51. use PHPCR\Query\InvalidQueryException;
  52. use PHPCR\Query\QOM\QueryObjectModelConstantsInterface as QOM;
  53. use PHPCR\Query\QOM\QueryObjectModelInterface;
  54. use PHPCR\Query\QOM\SelectorInterface;
  55. use PHPCR\Query\QueryInterface;
  56. use PHPCR\ReferentialIntegrityException;
  57. use PHPCR\RepositoryException;
  58. use PHPCR\RepositoryInterface;
  59. use PHPCR\SimpleCredentials;
  60. use PHPCR\UnsupportedRepositoryOperationException;
  61. use PHPCR\Util\PathHelper;
  62. use PHPCR\Util\QOM\Sql2ToQomQueryConverter;
  63. use PHPCR\Util\UUIDHelper;
  64. use PHPCR\Util\ValueConverter;
  65. use stdClass;
  66. /**
  67.  * Class to handle the communication between Jackalope and RDBMS via Doctrine DBAL.
  68.  *
  69.  * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
  70.  * @license http://opensource.org/licenses/MIT MIT License
  71.  *
  72.  * @author Benjamin Eberlei <kontakt@beberlei.de>
  73.  * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  74.  */
  75. class Client extends BaseTransport implements QueryTransportWritingInterfaceWorkspaceManagementInterfaceNodeTypeManagementInterfaceTransactionInterface
  76. {
  77.     /**
  78.      * SQlite can only handle a maximum of 999 parameters inside an IN statement
  79.      * see https://github.com/jackalope/jackalope-doctrine-dbal/pull/149/files#diff-a3a0165ed79ca1ba3513ec5ecd59ec56R707
  80.      */
  81.     const SQLITE_MAXIMUM_IN_PARAM_COUNT 999;
  82.     /**
  83.      * The factory to instantiate objects
  84.      *
  85.      * @var FactoryInterface
  86.      */
  87.     protected $factory;
  88.     /**
  89.      * @var ValueConverter
  90.      */
  91.     protected $valueConverter;
  92.     /**
  93.      * @var Connection
  94.      */
  95.     private $conn;
  96.     /**
  97.      * @var Closure
  98.      */
  99.     private $uuidGenerator;
  100.     /**
  101.      * @var bool
  102.      */
  103.     private $loggedIn false;
  104.     /**
  105.      * @var SimpleCredentials
  106.      */
  107.     private $credentials;
  108.     /**
  109.      * @var string
  110.      */
  111.     protected $workspaceName;
  112.     /**
  113.      * @var array
  114.      */
  115.     private $nodeIdentifiers = [];
  116.     /**
  117.      * @var NodeTypeManager
  118.      */
  119.     private $nodeTypeManager;
  120.     /**
  121.      * @var bool
  122.      */
  123.     protected $inTransaction false;
  124.     /**
  125.      * Check if an initial request on login should be send to check if repository exists
  126.      * This is according to the JCR specifications and set to true by default
  127.      *
  128.      * @see setCheckLoginOnServer
  129.      *
  130.      * @var bool
  131.      */
  132.     private $checkLoginOnServer true;
  133.     /**
  134.      * Using an ArrayObject here so that we can pass this into the NodeProcessor by reference more elegantly
  135.      *
  136.      * @var null|ArrayObject
  137.      */
  138.     protected $namespaces;
  139.     /**
  140.      * @var null|array The namespaces at initial state when making changes to the namespaces, in case of rollback.
  141.      */
  142.     private $originalNamespaces;
  143.     /**
  144.      * The core namespaces defined in JCR
  145.      *
  146.      * @var array
  147.      */
  148.     private $coreNamespaces = [
  149.         NS::PREFIX_EMPTY => NS::NAMESPACE_EMPTY,
  150.         NS::PREFIX_JCR => NS::NAMESPACE_JCR,
  151.         NS::PREFIX_NT => NS::NAMESPACE_NT,
  152.         NS::PREFIX_MIX => NS::NAMESPACE_MIX,
  153.         NS::PREFIX_XML => NS::NAMESPACE_XML,
  154.         NS::PREFIX_SV => NS::NAMESPACE_SV,
  155.     ];
  156.     /**
  157.      * @var string|null
  158.      */
  159.     private $sequenceNodeName;
  160.     /**
  161.      * @var string|null
  162.      */
  163.     private $sequenceTypeName;
  164.     /**
  165.      * @var array
  166.      */
  167.     private $referencesToUpdate = [];
  168.     /**
  169.      * @var array
  170.      */
  171.     private $referenceTables = [
  172.         PropertyType::REFERENCE => 'phpcr_nodes_references',
  173.         PropertyType::WEAKREFERENCE => 'phpcr_nodes_weakreferences',
  174.     ];
  175.     /**
  176.      * @var array
  177.      */
  178.     private $referencesToDelete = [];
  179.     /**
  180.      * @var boolean
  181.      */
  182.     private $connectionInitialized false;
  183.     /**
  184.      * @var NodeProcessor
  185.      */
  186.     private $nodeProcessor;
  187.     /**
  188.      * @var string
  189.      */
  190.     private $caseSensitiveEncoding null;
  191.     /**
  192.      * @param FactoryInterface $factory
  193.      * @param Connection $conn
  194.      */
  195.     public function __construct(FactoryInterface $factoryConnection $conn)
  196.     {
  197.         $this->factory $factory;
  198.         $this->valueConverter $this->factory->get(ValueConverter::class);
  199.         $this->conn $conn;
  200.     }
  201.     /**
  202.      * @TODO: move to "SqlitePlatform" and rename to "registerExtraFunctions"?
  203.      *
  204.      * @param PDO $sqliteConnection
  205.      *
  206.      * @return Client
  207.      */
  208.     private function registerSqliteFunctions(PDO $sqliteConnection)
  209.     {
  210.         $sqliteConnection->sqliteCreateFunction(
  211.             'EXTRACTVALUE',
  212.             function ($string$expression) {
  213.                 if (null === $string) {
  214.                     return null;
  215.                 }
  216.                 $dom = new DOMDocument('1.0''UTF-8');
  217.                 $dom->loadXML($string);
  218.                 $xpath = new DOMXPath($dom);
  219.                 $list $xpath->evaluate($expression);
  220.                 if (!is_object($list)) {
  221.                     return $list;
  222.                 }
  223.                 // @TODO: don't know if there are expressions returning more then one row
  224.                 if ($list->length 0) {
  225.                     // @TODO: why it can happen that we do not have a type? https://github.com/phpcr/phpcr-api-tests/pull/132
  226.                     $type is_object($list->item(0)->parentNode->attributes->getNamedItem('type')) ? $list->item(
  227.                         0
  228.                     )->parentNode->attributes->getNamedItem('type')->value null;
  229.                     $content $list->item(0)->textContent;
  230.                     switch ($type) {
  231.                         case 'long':
  232.                             return (int)$content;
  233.                             break;
  234.                         case 'double':
  235.                             return (double)$content;
  236.                             break;
  237.                         default:
  238.                             return $content;
  239.                     }
  240.                 }
  241.                 // @TODO: don't know if return value is right
  242.                 return null;
  243.             },
  244.             2
  245.         );
  246.         $sqliteConnection->sqliteCreateFunction(
  247.             'CONCAT',
  248.             function () {
  249.                 return implode(''func_get_args());
  250.             }
  251.         );
  252.         return $this;
  253.     }
  254.     /**
  255.      * @return Connection
  256.      */
  257.     public function getConnection()
  258.     {
  259.         $this->initConnection();
  260.         return $this->conn;
  261.     }
  262.     /**
  263.      * Set the UUID generator to use. If not set, the phpcr-utils UUIDHelper
  264.      * will be used.
  265.      *
  266.      * @param Closure $generator
  267.      */
  268.     public function setUuidGenerator(Closure $generator)
  269.     {
  270.         $this->uuidGenerator $generator;
  271.     }
  272.     /**
  273.      * @return callable a uuid generator function.
  274.      */
  275.     protected function getUuidGenerator()
  276.     {
  277.         if (!$this->uuidGenerator) {
  278.             $this->uuidGenerator = function () {
  279.                 return UUIDHelper::generateUUID();
  280.             };
  281.         }
  282.         return $this->uuidGenerator;
  283.     }
  284.     /**
  285.      * @return string a universally unique id.
  286.      */
  287.     protected function generateUuid()
  288.     {
  289.         // php 5.3 compatibility, no direct execution of this function.
  290.         $g $this->getUuidGenerator();
  291.         return $g();
  292.     }
  293.     /**
  294.      * {@inheritDoc}
  295.      *
  296.      * @throws NotImplementedException
  297.      */
  298.     public function createWorkspace($name$srcWorkspace null)
  299.     {
  300.         if (null !== $srcWorkspace) {
  301.             throw new NotImplementedException('Creating workspace as clone of existing workspace not supported');
  302.         }
  303.         if ($this->workspaceExists($name)) {
  304.             throw new RepositoryException("Workspace '$name' already exists");
  305.         }
  306.         try {
  307.             $this->getConnection()->insert('phpcr_workspaces', ['name' => $name]);
  308.         } catch (Exception $e) {
  309.             throw new RepositoryException("Couldn't create Workspace '$name': " $e->getMessage(), 0$e);
  310.         }
  311.         $this->getConnection()->insert(
  312.             'phpcr_nodes',
  313.             [
  314.                 'path' => '/',
  315.                 'parent' => '',
  316.                 'workspace_name' => $name,
  317.                 'identifier' => $this->generateUuid(),
  318.                 'type' => 'nt:unstructured',
  319.                 'local_name' => '',
  320.                 'namespace' => '',
  321.                 'props' => '<?xml version="1.0" encoding="UTF-8"?>
  322. <sv:node xmlns:' NS::PREFIX_MIX '="' NS::NAMESPACE_MIX '" xmlns:' NS::PREFIX_NT '="' NS::NAMESPACE_NT '" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:' NS::PREFIX_JCR '="' NS::NAMESPACE_JCR '" xmlns:' NS::PREFIX_SV '="' NS::NAMESPACE_SV '" xmlns:rep="internal" />',
  323.                 // TODO compute proper value
  324.                 'depth' => 0,
  325.             ]
  326.         );
  327.     }
  328.     /**
  329.      * {@inheritDoc}
  330.      */
  331.     public function deleteWorkspace($name)
  332.     {
  333.         if (!$this->workspaceExists($name)) {
  334.             throw new RepositoryException("Workspace '$name' cannot be deleted as it does not exist");
  335.         }
  336.         try {
  337.             $this->getConnection()->delete('phpcr_workspaces', ['name' => $name]);
  338.         } catch (Exception $e) {
  339.             throw new RepositoryException("Couldn't delete workspace '$name': " $e->getMessage(), 0$e);
  340.         }
  341.         try {
  342.             $this->getConnection()->delete('phpcr_nodes', ['workspace_name' => $name]);
  343.         } catch (Exception $e) {
  344.             throw new RepositoryException("Couldn't delete nodes in workspace '$name': " $e->getMessage(), 0$e);
  345.         }
  346.         try {
  347.             $this->getConnection()->delete('phpcr_binarydata', ['workspace_name' => $name]);
  348.         } catch (Exception $e) {
  349.             throw new RepositoryException(
  350.                 "Couldn't delete binary data in workspace '$name': " $e->getMessage(), 0$e
  351.             );
  352.         }
  353.     }
  354.     /**
  355.      * {@inheritDoc}
  356.      *
  357.      * @throws NotImplementedException
  358.      * @throws AccessDeniedException
  359.      * @throws UnsupportedRepositoryOperationException
  360.      */
  361.     public function login(CredentialsInterface $credentials null$workspaceName null)
  362.     {
  363.         $this->credentials $credentials;
  364.         $this->workspaceName $workspaceName ?: 'default';
  365.         if (!$this->checkLoginOnServer) {
  366.             return $this->workspaceName;
  367.         }
  368.         if (!$this->workspaceExists($this->workspaceName)) {
  369.             if ('default' !== $this->workspaceName) {
  370.                 throw new NoSuchWorkspaceException("Requested workspace: '{$this->workspaceName}'");
  371.             }
  372.             // Create default workspace if it not exists
  373.             $this->createWorkspace($this->workspaceName);
  374.         }
  375.         $this->loggedIn true;
  376.         return $this->workspaceName;
  377.     }
  378.     /**
  379.      * {@inheritDoc}
  380.      */
  381.     public function logout()
  382.     {
  383.         if ($this->loggedIn) {
  384.             $this->loggedIn false;
  385.             $this->conn->close();
  386.             $this->conn null;
  387.         }
  388.     }
  389.     /**
  390.      * Configure whether to check if we are logged in before doing a request.
  391.      *
  392.      * Will improve error reporting at the cost of some round trips.
  393.      */
  394.     public function setCheckLoginOnServer($bool)
  395.     {
  396.         $this->checkLoginOnServer $bool;
  397.     }
  398.     /**
  399.      * This will control the collate which is being used on MySQL when querying nodes. It will be autodetected by just
  400.      * appending _bin to the current charset, which is good enough in most cases.
  401.      *
  402.      * @param string $encoding
  403.      */
  404.     public function setCaseSensitiveEncoding($encoding)
  405.     {
  406.         $this->caseSensitiveEncoding $encoding;
  407.     }
  408.     /**
  409.      * Returns the collate which is being used on MySQL when querying nodes.
  410.      *
  411.      * @return string
  412.      */
  413.     private function getCaseSensitiveEncoding()
  414.     {
  415.         if (null !== $this->caseSensitiveEncoding) {
  416.             return $this->caseSensitiveEncoding;
  417.         }
  418.         $params $this->conn->getParams();
  419.         $charset = isset($params['charset']) ? $params['charset'] : 'utf8';
  420.         if (isset($params['defaultTableOptions']) && isset($params['defaultTableOptions']['collate'])) {
  421.             return $this->caseSensitiveEncoding $params['defaultTableOptions']['collate'];
  422.         }
  423.         return $this->caseSensitiveEncoding $charset === 'binary' $charset $charset '_bin';
  424.     }
  425.     protected function workspaceExists($workspaceName)
  426.     {
  427.         try {
  428.             $query 'SELECT 1 FROM phpcr_workspaces WHERE name = ?';
  429.             $result $this->getConnection()->fetchColumn($query, [$workspaceName]);
  430.         } catch (Exception $e) {
  431.             if ($e instanceof DBALException || $e instanceof PDOException) {
  432.                 if (1045 === $e->getCode()) {
  433.                     throw new LoginException('Access denied with your credentials: ' $e->getMessage());
  434.                 }
  435.                 if ('42S02' === $e->getCode()) {
  436.                     throw new RepositoryException(
  437.                         'You did not properly set up the database for the repository. See README.md for more information. Message from backend: ' $e->getMessage(
  438.                         )
  439.                     );
  440.                 }
  441.                 throw new RepositoryException('Unexpected error talking to the backend: ' $e->getMessage(), 0$e);
  442.             }
  443.             throw $e;
  444.         }
  445.         return $result;
  446.     }
  447.     /**
  448.      * Ensure that we are currently logged in, executing the login in case we
  449.      * did lazy login.
  450.      *
  451.      * @throws NotImplementedException
  452.      * @throws AccessDeniedException
  453.      * @throws LoginException
  454.      * @throws NoSuchWorkspaceException
  455.      * @throws UnsupportedRepositoryOperationException
  456.      * @throws RepositoryException if this transport is not logged in.
  457.      */
  458.     protected function assertLoggedIn()
  459.     {
  460.         if (!$this->loggedIn) {
  461.             if (!$this->checkLoginOnServer && $this->workspaceName) {
  462.                 $this->checkLoginOnServer true;
  463.                 if ($this->login($this->credentials$this->workspaceName)) {
  464.                     return;
  465.                 }
  466.             }
  467.             throw new RepositoryException('You need to be logged in for this operation');
  468.         }
  469.     }
  470.     /**
  471.      * {@inheritDoc}
  472.      */
  473.     public function getRepositoryDescriptors()
  474.     {
  475.         return [
  476.             RepositoryInterface::IDENTIFIER_STABILITY => RepositoryInterface::IDENTIFIER_STABILITY_INDEFINITE_DURATION,
  477.             RepositoryInterface::REP_NAME_DESC => 'jackalope_doctrine_dbal',
  478.             RepositoryInterface::REP_VENDOR_DESC => 'Jackalope Community',
  479.             RepositoryInterface::REP_VENDOR_URL_DESC => 'http://github.com/jackalope',
  480.             RepositoryInterface::REP_VERSION_DESC => '1.1.0-DEV',
  481.             RepositoryInterface::SPEC_NAME_DESC => 'Content Repository for PHP',
  482.             RepositoryInterface::SPEC_VERSION_DESC => '2.1',
  483.             RepositoryInterface::NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED => true,
  484.             RepositoryInterface::NODE_TYPE_MANAGEMENT_INHERITANCE => RepositoryInterface::NODE_TYPE_MANAGEMENT_INHERITANCE_SINGLE,
  485.             RepositoryInterface::NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED => true,
  486.             RepositoryInterface::NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED => true,
  487.             RepositoryInterface::NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED => true,
  488.             RepositoryInterface::NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED => false,
  489.             RepositoryInterface::NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED => true,
  490.             RepositoryInterface::NODE_TYPE_MANAGEMENT_PROPERTY_TYPES => true,
  491.             RepositoryInterface::NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED => false,
  492.             RepositoryInterface::NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED => false,
  493.             RepositoryInterface::NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPPORTED => false,
  494.             RepositoryInterface::NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED => false,
  495.             RepositoryInterface::OPTION_ACCESS_CONTROL_SUPPORTED => false,
  496.             RepositoryInterface::OPTION_ACTIVITIES_SUPPORTED => false,
  497.             RepositoryInterface::OPTION_BASELINES_SUPPORTED => false,
  498.             RepositoryInterface::OPTION_JOURNALED_OBSERVATION_SUPPORTED => false,
  499.             RepositoryInterface::OPTION_LIFECYCLE_SUPPORTED => false,
  500.             RepositoryInterface::OPTION_LOCKING_SUPPORTED => false,
  501.             RepositoryInterface::OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED => true,
  502.             RepositoryInterface::OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED => true,
  503.             RepositoryInterface::OPTION_OBSERVATION_SUPPORTED => false,
  504.             RepositoryInterface::OPTION_RETENTION_SUPPORTED => false,
  505.             RepositoryInterface::OPTION_SHAREABLE_NODES_SUPPORTED => false,
  506.             RepositoryInterface::OPTION_SIMPLE_VERSIONING_SUPPORTED => false,
  507.             RepositoryInterface::OPTION_TRANSACTIONS_SUPPORTED => true,
  508.             RepositoryInterface::OPTION_UNFILED_CONTENT_SUPPORTED => true,
  509.             RepositoryInterface::OPTION_UPDATE_MIXIN_NODETYPES_SUPPORTED => true,
  510.             RepositoryInterface::OPTION_UPDATE_PRIMARY_NODETYPE_SUPPORTED => true,
  511.             RepositoryInterface::OPTION_VERSIONING_SUPPORTED => false,
  512.             RepositoryInterface::OPTION_WORKSPACE_MANAGEMENT_SUPPORTED => true,
  513.             RepositoryInterface::OPTION_XML_EXPORT_SUPPORTED => true,
  514.             RepositoryInterface::OPTION_XML_IMPORT_SUPPORTED => true,
  515.             RepositoryInterface::QUERY_FULL_TEXT_SEARCH_SUPPORTED => true,
  516.             RepositoryInterface::QUERY_CANCEL_SUPPORTED => false,
  517.             RepositoryInterface::QUERY_JOINS => RepositoryInterface::QUERY_JOINS_NONE,
  518.             RepositoryInterface::QUERY_LANGUAGES => [QueryInterface::JCR_SQL2QueryInterface::JCR_JQOM],
  519.             RepositoryInterface::QUERY_STORED_QUERIES_SUPPORTED => false,
  520.             RepositoryInterface::WRITE_SUPPORTED => true,
  521.         ];
  522.     }
  523.     /**
  524.      * Get the registered namespace prefixes
  525.      *
  526.      * @return array
  527.      */
  528.     private function getNamespacePrefixes()
  529.     {
  530.         return array_keys($this->getNamespaces());
  531.     }
  532.     /**
  533.      * {@inheritDoc}
  534.      */
  535.     public function getNamespaces()
  536.     {
  537.         return (array)$this->getNamespacesObject();
  538.     }
  539.     /**
  540.      * Return the namespaces of the current session as a referenceable ArrayObject.
  541.      *
  542.      * @return ArrayObject
  543.      */
  544.     protected function getNamespacesObject()
  545.     {
  546.         if (null === $this->namespaces) {
  547.             $query 'SELECT prefix, uri FROM phpcr_namespaces';
  548.             $result $this->getConnection()->query($query);
  549.             $namespaces = (array)$result->fetchAll(\PDO::FETCH_KEY_PAIR);
  550.             $namespaces += $this->coreNamespaces;
  551.             $this->setNamespaces($namespaces);
  552.         }
  553.         return $this->namespaces;
  554.     }
  555.     /**
  556.      * Set the namespaces property to an \ArrayObject instance
  557.      *
  558.      * @param array|ArrayObject $namespaces
  559.      */
  560.     protected function setNamespaces($namespaces)
  561.     {
  562.         if ($this->namespaces instanceof ArrayObject) {
  563.             $this->namespaces->exchangeArray($namespaces);
  564.         } else {
  565.             $this->namespaces = new ArrayObject($namespaces);
  566.         }
  567.     }
  568.     /**
  569.      * Executes an UPDATE on DBAL while ensuring that we never try to send more than 999 parameters to SQLite
  570.      *
  571.      * @param $query
  572.      * @param array $params
  573.      *
  574.      * @throws DBALException
  575.      */
  576.     private function executeChunkedUpdate($query, array $params)
  577.     {
  578.         $types = [Connection::PARAM_INT_ARRAY];
  579.         if ($this->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) {
  580.             foreach (array_chunk($paramsself::SQLITE_MAXIMUM_IN_PARAM_COUNT) as $chunk) {
  581.                 $this->getConnection()->executeUpdate($query, [$chunk], $types);
  582.             }
  583.         } else {
  584.             $this->getConnection()->executeUpdate($query, [$params], $types);
  585.         }
  586.     }
  587.     /**
  588.      * {@inheritDoc}
  589.      *
  590.      * @throws NoSuchWorkspaceException
  591.      * @throws RepositoryException
  592.      * @throws PathNotFoundException
  593.      * @throws ItemExistsException
  594.      * @throws DBALException
  595.      * @throws InvalidArgumentException
  596.      */
  597.     public function copyNode($srcAbsPath$dstAbsPath$srcWorkspace null)
  598.     {
  599.         $this->assertLoggedIn();
  600.         if (null !== $srcWorkspace && !$this->workspaceExists($srcWorkspace)) {
  601.             throw new NoSuchWorkspaceException("Source workspace '$srcWorkspace' does not exist.");
  602.         }
  603.         $srcWorkspace $srcWorkspace ?: $this->workspaceName;
  604.         PathHelper::assertValidAbsolutePath($dstAbsPathtruetrue$this->getNamespacePrefixes());
  605.         $srcNodeId $this->getSystemIdForNode($srcAbsPath$srcWorkspace);
  606.         if (!$srcNodeId) {
  607.             throw new PathNotFoundException("Source path '$srcAbsPath' not found");
  608.         }
  609.         if ($this->getSystemIdForNode($dstAbsPath)) {
  610.             throw new ItemExistsException("Cannot copy to destination path '$dstAbsPath' that already exists.");
  611.         }
  612.         if (!$this->getSystemIdForNode(PathHelper::getParentPath($dstAbsPath))) {
  613.             throw new PathNotFoundException("Parent of the destination path '" $dstAbsPath "' has to exist.");
  614.         }
  615.         // Algorithm:
  616.         // 1. Select all nodes with path $srcAbsPath."%" and iterate them
  617.         // 2. create a new node with path $dstAbsPath + leftovers, with a new uuid. Save old => new uuid
  618.         // 3. copy all properties from old node to new node
  619.         // 4. if a reference is in the properties, either update the uuid based on the map if its inside the copied graph or keep it.
  620.         // 5. "May drop mixin types"
  621.         $query 'SELECT * FROM phpcr_nodes WHERE (path = ? OR path LIKE ?) AND workspace_name = ? ORDER BY depth, sort_order';
  622.         $stmt $this->getConnection()->executeQuery($query, [$srcAbsPath$srcAbsPath '/%'$srcWorkspace]);
  623.         $rows $stmt->fetchAll(PDO::FETCH_ASSOC);
  624.         $uuidMap = [];
  625.         $resultSetUuids = [];
  626.         // first iterate and build up an array of all the UUIDs in the result set
  627.         foreach ($rows as $row) {
  628.             $resultSetUuids[$row['identifier']] = $row['path'];
  629.         }
  630.         // array of references to remap within the copied tree
  631.         $referenceElsToRemap = [];
  632.         // array references that will need updating in the database
  633.         $referencesToUpdate = [];
  634.         foreach ($rows as $row) {
  635.             $newPath str_replace($srcAbsPath$dstAbsPath$row['path']);
  636.             $stringDom = new DOMDocument('1.0''UTF-8');
  637.             $stringDom->loadXML($row['props']);
  638.             $numericalDom null;
  639.             if ($row['numerical_props']) {
  640.                 $numericalDom = new DOMDocument('1.0''UTF-8');
  641.                 $numericalDom->loadXML($row['numerical_props']);
  642.             }
  643.             $propsData = [
  644.                 'stringDom' => $stringDom,
  645.                 'numericalDom' => $numericalDom,
  646.                 'references' => [],
  647.             ];
  648.             $xpath = new DOMXpath($stringDom);
  649.             $referenceEls $xpath->query(
  650.                 './/sv:property[@sv:type="reference" or @sv:type="Reference" or @sv:type="weakreference" or @sv:type="WeakReference"]'
  651.             );
  652.             $references = [];
  653.             foreach ($referenceEls as $referenceEl) {
  654.                 $propName $referenceEl->getAttribute('sv:name');
  655.                 $values = [];
  656.                 foreach ($xpath->query('./sv:value'$referenceEl) as $valueEl) {
  657.                     $values[] = $valueEl->nodeValue;
  658.                 }
  659.                 $references[$propName] = [
  660.                     'type' => PropertyType::valueFromName($referenceEl->getAttribute('sv:type')),
  661.                     'values' => $values,
  662.                 ];
  663.                 if (isset($resultSetUuids[$referenceEl->nodeValue])) {
  664.                     $referenceElsToRemap[] = [$referenceEl$newPath$row['type'], $propsData];
  665.                 }
  666.             }
  667.             $originalUuid $row['identifier'];
  668.             // when copying a node, the copy is always a new node. set $isNewNode to true
  669.             $newNodeId $this->syncNode(null$newPath$row['type'], true, [], $propsData);
  670.             if ($references) {
  671.                 $referencesToUpdate[$newNodeId] = [
  672.                     'path' => $row['path'],
  673.                     'properties' => $references,
  674.                 ];
  675.             }
  676.             $newUuid $this->nodeIdentifiers[$newPath];
  677.             $uuidMap[$originalUuid] = $newUuid;
  678.             $query 'INSERT INTO phpcr_binarydata (node_id, property_name, workspace_name, idx, data)' '   SELECT ?, b.property_name, ?, b.idx, b.data FROM phpcr_binarydata b WHERE b.node_id = ?';
  679.             try {
  680.                 $this->getConnection()->executeUpdate($query, [$newNodeId$this->workspaceName$row['id']]);
  681.             } catch (DBALException $e) {
  682.                 throw new RepositoryException(
  683.                     "Unexpected exception while copying node from $srcAbsPath to $dstAbsPath",
  684.                     $e->getCode(),
  685.                     $e
  686.                 );
  687.             }
  688.         }
  689.         foreach ($referenceElsToRemap as $data) {
  690.             list($referenceEl$newPath$type$propsData) = $data;
  691.             $referenceEl->nodeValue $uuidMap[$referenceEl->nodeValue];
  692.             $this->syncNode($this->nodeIdentifiers[$newPath], $newPath$typefalse, [], $propsData);
  693.         }
  694.         $this->syncReferences($referencesToUpdate);
  695.     }
  696.     /**
  697.      * @param string $path
  698.      *
  699.      * @return array
  700.      *
  701.      * @throws NamespaceException
  702.      */
  703.     private function getJcrName($path)
  704.     {
  705.         $name implode(''array_slice(explode('/'$path), -11));
  706.         $alias '';
  707.         if (($aliasLength strpos($name':')) !== false) {
  708.             $alias substr($name0$aliasLength);
  709.             $name substr($name$aliasLength 1);
  710.         }
  711.         $namespaces $this->getNamespaces();
  712.         if (!isset($namespaces[$alias])) {
  713.             throw new NamespaceException("the namespace $alias was not registered.");
  714.         }
  715.         return [$namespaces[$alias], $name];
  716.     }
  717.     /**
  718.      * Actually write the node into the database
  719.      *
  720.      * @param string $uuid node uuid
  721.      * @param string $path absolute path of the node
  722.      * @param string $type node type name
  723.      * @param boolean $isNewNode new nodes to insert (true) or existing node to update (false)
  724.      * @param Property[] $props
  725.      * @param array $propsData
  726.      *
  727.      * @return boolean|string
  728.      *
  729.      * @throws ItemExistsException
  730.      * @throws RepositoryException
  731.      * @throws NamespaceException
  732.      * @throws Exception
  733.      */
  734.     private function syncNode($uuid$path$type$isNewNode$props = [], $propsData = [])
  735.     {
  736.         // TODO: Not sure if there are always ALL props in $props, should we grab the online data here?
  737.         // TODO: PERFORMANCE Binary data is handled very inefficiently here, UPSERT will really be necessary here as well as lazy handling
  738.         if (!$propsData) {
  739.             $propsData $this->propsToXML($props);
  740.         }
  741.         if (null === $uuid) {
  742.             $uuid $this->generateUuid();
  743.         }
  744.         if ($isNewNode) {
  745.             list($namespace$localName) = $this->getJcrName($path);
  746.             $qb $this->getConnection()->createQueryBuilder();
  747.             $qb->select(
  748.                 ':identifier, :type, :path, :local_name, :namespace, :parent, :workspace_name, :props, :numerical_props, :depth, COALESCE(MAX(n.sort_order), 0) + 1'
  749.             )->from('phpcr_nodes''n')->where('n.parent = :parent_a');
  750.             $sql $qb->getSQL();
  751.             try {
  752.                 $insert "INSERT INTO phpcr_nodes (identifier, type, path, local_name, namespace, parent, workspace_name, props, numerical_props, depth, sort_order) " $sql;
  753.                 $this->getConnection()->executeUpdate(
  754.                     $insert,
  755.                     $data = [
  756.                         'identifier' => $uuid,
  757.                         'type' => $type,
  758.                         'path' => $path,
  759.                         'local_name' => $localName,
  760.                         'namespace' => $namespace,
  761.                         'parent' => PathHelper::getParentPath($path),
  762.                         'workspace_name' => $this->workspaceName,
  763.                         'props' => $propsData['stringDom']->saveXML(),
  764.                         'numerical_props' => $propsData['numericalDom'] ? $propsData['numericalDom']->saveXML() : null,
  765.                         'depth' => PathHelper::getPathDepth($path),
  766.                         'parent_a' => PathHelper::getParentPath($path),
  767.                     ]
  768.                 );
  769.             } catch (Exception $e) {
  770.                 if ($e instanceof PDOException || $e instanceof DBALException) {
  771.                     if (strpos($e->getMessage(), 'SQLSTATE[23') !== false) {
  772.                         throw new ItemExistsException('Item ' $path ' already exists in the database');
  773.                     } else {
  774.                         throw new RepositoryException(
  775.                             'Unknown database error while inserting item ' $path ': ' $e->getMessage(), 0$e
  776.                         );
  777.                     }
  778.                 } else {
  779.                     throw $e;
  780.                 }
  781.             }
  782.             $nodeId $this->getConnection()->lastInsertId($this->sequenceNodeName);
  783.         } else {
  784.             $nodeId $this->getSystemIdForNode($path);
  785.             if (!$nodeId) {
  786.                 throw new RepositoryException("nodeId for $path not found");
  787.             }
  788.             $this->getConnection()->update(
  789.                 'phpcr_nodes',
  790.                 [
  791.                     'props' => $propsData['stringDom']->saveXML(),
  792.                     'numerical_props' => $propsData['numericalDom'] ? $propsData['numericalDom']->saveXML() : null,
  793.                 ],
  794.                 ['id' => $nodeId]
  795.             );
  796.         }
  797.         $this->nodeIdentifiers[$path] = $uuid;
  798.         if (!empty($propsData['binaryData'])) {
  799.             $this->syncBinaryData($nodeId$propsData['binaryData']);
  800.         }
  801.         $this->referencesToUpdate[$nodeId] = ['path' => $path'properties' => $propsData['references']];
  802.         return $nodeId;
  803.     }
  804.     /**
  805.      * @param $nodeId
  806.      * @param $binaryData
  807.      */
  808.     private function syncBinaryData($nodeId$binaryData)
  809.     {
  810.         foreach ($binaryData as $propertyName => $binaryValues) {
  811.             foreach ($binaryValues as $idx => $data) {
  812.                 // TODO verify in which cases we can just update
  813.                 $params = [
  814.                     'node_id' => $nodeId,
  815.                     'property_name' => $propertyName,
  816.                     'workspace_name' => $this->workspaceName,
  817.                 ];
  818.                 $this->getConnection()->delete('phpcr_binarydata'$params);
  819.                 $params['idx'] = $idx;
  820.                 $params['data'] = $data;
  821.                 $types = [
  822.                     PDO::PARAM_INT,
  823.                     PDO::PARAM_STR,
  824.                     PDO::PARAM_STR,
  825.                     PDO::PARAM_INT,
  826.                     PDO::PARAM_LOB
  827.                 ];
  828.                 $this->getConnection()->insert('phpcr_binarydata'$params$types);
  829.             }
  830.         }
  831.     }
  832.     /**
  833.      * @param array $referencesToUpdate
  834.      *
  835.      * @throws RepositoryException
  836.      * @throws ReferentialIntegrityException
  837.      */
  838.     private function syncReferences(array $referencesToUpdate)
  839.     {
  840.         if ($referencesToUpdate) {
  841.             // do not update references that are going to be deleted anyways
  842.             $toUpdate array_diff(array_keys($referencesToUpdate), array_keys($this->referencesToDelete));
  843.             try {
  844.                 foreach ($this->referenceTables as $table) {
  845.                     $query "DELETE FROM $table WHERE source_id IN (?)";
  846.                     $this->executeChunkedUpdate($query$toUpdate);
  847.                 }
  848.             } catch (DBALException $e) {
  849.                 throw new RepositoryException('Unexpected exception while cleaning up after saving'$e->getCode(), $e);
  850.             }
  851.             $updates = [];
  852.             foreach ($toUpdate as $nodeId) {
  853.                 $references $referencesToUpdate[$nodeId];
  854.                 foreach ($references['properties'] as $name => $data) {
  855.                     foreach ($data['values'] as $value) {
  856.                         $targetId $this->getSystemIdForNode($value);
  857.                         if (false === $targetId) {
  858.                             if (PropertyType::REFERENCE === $data['type']) {
  859.                                 throw new ReferentialIntegrityException(
  860.                                     sprintf(
  861.                                         'Trying to store reference to non-existant node with path "%s" in node "%s" "%s"',
  862.                                         $value,
  863.                                         $references['path'],
  864.                                         $name
  865.                                     )
  866.                                 );
  867.                             }
  868.                             continue;
  869.                         }
  870.                         $key $targetId '-' $nodeId '-' $name;
  871.                         // it is valid to have multiple references to the same node in a multivalue
  872.                         // but it is not desired to store duplicates in the database
  873.                         $updates[$key] = [
  874.                             'type' => $data['type'],
  875.                             'data' => [
  876.                                 'source_id' => $nodeId,
  877.                                 'source_property_name' => $name,
  878.                                 'target_id' => $targetId,
  879.                             ],
  880.                         ];
  881.                     }
  882.                 }
  883.             }
  884.             foreach ($updates as $update) {
  885.                 $this->getConnection()->insert($this->referenceTables[$update['type']], $update['data']);
  886.             }
  887.         }
  888.         // TODO on RDBMS that support deferred FKs we could skip this step
  889.         if ($this->referencesToDelete) {
  890.             $params array_keys($this->referencesToDelete);
  891.             // remove all PropertyType::REFERENCE with a source_id on a deleted node
  892.             try {
  893.                 $query "DELETE FROM phpcr_nodes_references WHERE source_id IN (?)";
  894.                 $this->executeChunkedUpdate($query$params);
  895.             } catch (DBALException $e) {
  896.                 throw new RepositoryException(
  897.                     'Unexpected exception while cleaning up deleted nodes'$e->getCode(), $e
  898.                 );
  899.             }
  900.             // ensure that there are no PropertyType::REFERENCE pointing to nodes that will be deleted
  901.             // Note: due to the outer join we cannot filter on workspace_name, but this is ok
  902.             // since within a transaction there can never be missing referenced nodes within the current workspace
  903.             // make sure the target node is not in the list of nodes being deleted, to allow deletion in same request
  904.             $query 'SELECT DISTINCT r.target_id
  905.             FROM phpcr_nodes_references r
  906.                 LEFT OUTER JOIN phpcr_nodes n ON r.target_id = n.id
  907.             WHERE r.target_id IN (?)';
  908.             if ($this->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) {
  909.                 $missingTargets = [];
  910.                 foreach (array_chunk($paramsself::SQLITE_MAXIMUM_IN_PARAM_COUNT) as $chunk) {
  911.                     $stmt $this->getConnection()->executeQuery($query, [$chunk], [Connection::PARAM_INT_ARRAY]);
  912.                     $missingTargets array_merge($missingTargets$stmt->fetchAll(\PDO::FETCH_COLUMN));
  913.                 }
  914.             } else {
  915.                 $stmt $this->getConnection()->executeQuery($query, [$params], [Connection::PARAM_INT_ARRAY]);
  916.                 $missingTargets $stmt->fetchAll(PDO::FETCH_COLUMN);
  917.             }
  918.             if ($missingTargets) {
  919.                 $paths = [];
  920.                 foreach ($missingTargets as $id) {
  921.                     if (isset($this->referencesToDelete[$id])) {
  922.                         $paths[] = $this->referencesToDelete[$id];
  923.                     }
  924.                 }
  925.                 throw new ReferentialIntegrityException(
  926.                     "Cannot delete '" implode("', '"$paths) . "': A reference points to this node or a subnode"
  927.                 );
  928.             }
  929.             // clean up all references
  930.             try {
  931.                 foreach ($this->referenceTables as $table) {
  932.                     $query "DELETE FROM $table WHERE target_id IN (?)";
  933.                     $this->executeChunkedUpdate($query$params);
  934.                 }
  935.             } catch (DBALException $e) {
  936.                 throw new RepositoryException(
  937.                     'Unexpected exception while cleaning up deleted nodes'$e->getCode(), $e
  938.                 );
  939.             }
  940.         }
  941.     }
  942.     /**
  943.      * Convert the node XML to stdClass node representation containing all properties
  944.      *
  945.      * @param string $xml
  946.      *
  947.      * @return stdClass
  948.      *
  949.      * @throws InvalidArgumentException
  950.      */
  951.     protected function xmlToProps($xml)
  952.     {
  953.         $data = new stdClass();
  954.         $dom = new DOMDocument('1.0''UTF-8');
  955.         $dom->loadXML($xml);
  956.         foreach ($dom->getElementsByTagNameNS('http://www.jcp.org/jcr/sv/1.0''property') as $propertyNode) {
  957.             $this->mapPropertyFromElement($propertyNode$data);
  958.         }
  959.         return $data;
  960.     }
  961.     /**
  962.      * Convert the node XML to stdClass node representation containing only the given properties
  963.      *
  964.      * @param string $xml
  965.      * @param array $propertyNames
  966.      *
  967.      * @return stdClass
  968.      *
  969.      * @throws InvalidArgumentException
  970.      */
  971.     protected function xmlToColumns($xml$propertyNames)
  972.     {
  973.         $data = new stdClass();
  974.         $dom = new DOMDocument('1.0');
  975.         $dom->loadXML($xml);
  976.         $dom->formatOutput true;
  977.         $xpath = new DOMXPath($dom);
  978.         foreach ($propertyNames as $propertyName) {
  979.             $els $xpath->query(sprintf('.//sv:property[@sv:name="%s"]'$propertyName));
  980.             if ($els->length === 0) {
  981.                 continue;
  982.             }
  983.             $propertyEl $els->item(0);
  984.             $this->mapPropertyFromElement($propertyEl$data);
  985.         }
  986.         return $data;
  987.     }
  988.     /**
  989.      * Map a property from the given property XML node to the given \stdClass node representation
  990.      *
  991.      * @param DOMElement $propertyNode
  992.      * @param stdClass $data
  993.      *
  994.      * @throws InvalidArgumentException
  995.      */
  996.     protected function mapPropertyFromElement(DOMElement $propertyNodestdClass $data)
  997.     {
  998.         $name $propertyNode->getAttribute('sv:name');
  999.         $values = [];
  1000.         $type PropertyType::valueFromName($propertyNode->getAttribute('sv:type'));
  1001.         foreach ($propertyNode->childNodes as $valueNode) {
  1002.             switch ($type) {
  1003.                 case PropertyType::NAME:
  1004.                 case PropertyType::URI:
  1005.                 case PropertyType::WEAKREFERENCE:
  1006.                 case PropertyType::REFERENCE:
  1007.                 case PropertyType::PATH:
  1008.                 case PropertyType::DECIMAL:
  1009.                 case PropertyType::STRING:
  1010.                     $values[] = $valueNode->nodeValue;
  1011.                     break;
  1012.                 case PropertyType::BOOLEAN:
  1013.                     $values[] = (bool)$valueNode->nodeValue;
  1014.                     break;
  1015.                 case PropertyType::LONG:
  1016.                     $values[] = (int)$valueNode->nodeValue;
  1017.                     break;
  1018.                 case PropertyType::BINARY:
  1019.                     $values[] = (int)$valueNode->nodeValue;
  1020.                     break;
  1021.                 case PropertyType::DATE:
  1022.                     $date $valueNode->nodeValue;
  1023.                     if ($date) {
  1024.                         $date = new DateTime($date);
  1025.                         $date->setTimezone(new DateTimeZone(date_default_timezone_get()));
  1026.                         // Jackalope expects a string, might make sense to refactor to allow DateTime instances too
  1027.                         $date $this->valueConverter->convertType($datePropertyType::STRING);
  1028.                     }
  1029.                     $values[] = $date;
  1030.                     break;
  1031.                 case PropertyType::DOUBLE:
  1032.                     $values[] = (double)$valueNode->nodeValue;
  1033.                     break;
  1034.                 default:
  1035.                     throw new InvalidArgumentException("Type with constant $type not found.");
  1036.             }
  1037.         }
  1038.         switch ($type) {
  1039.             case PropertyType::BINARY:
  1040.                 $data->{':' $name} = $propertyNode->getAttribute('sv:multi-valued') ? $values $values[0];
  1041.                 break;
  1042.             default:
  1043.                 $data->{$name} = $propertyNode->getAttribute('sv:multi-valued') ? $values $values[0];
  1044.                 $data->{':' $name} = $type;
  1045.                 break;
  1046.         }
  1047.     }
  1048.     /**
  1049.      * Seperate properties array into an xml and binary data.
  1050.      *
  1051.      * @param array $properties
  1052.      *
  1053.      * @return array (
  1054.      *     'stringDom' => $stringDom,
  1055.      *     'numericalDom' => $numericalDom',
  1056.      *     'binaryData' => streams,
  1057.      *     'references' => ['type' => INT, 'values' => [UUIDs])
  1058.      * )
  1059.      *
  1060.      * @throws RepositoryException
  1061.      */
  1062.     private function propsToXML($properties)
  1063.     {
  1064.         $namespaces = [
  1065.             NS::PREFIX_MIX => NS::NAMESPACE_MIX,
  1066.             NS::PREFIX_NT => NS::NAMESPACE_NT,
  1067.             'xs' => 'http://www.w3.org/2001/XMLSchema',
  1068.             NS::PREFIX_JCR => NS::NAMESPACE_JCR,
  1069.             NS::PREFIX_SV => NS::NAMESPACE_SV,
  1070.             'rep' => 'internal',
  1071.         ];
  1072.         $doms = [
  1073.             'stringDom' => [],
  1074.             'numericalDom' => [],
  1075.         ];
  1076.         $binaryData $references = [];
  1077.         foreach ($properties as $property) {
  1078.             $targetDoms = ['stringDom'];
  1079.             switch ($property->getType()) {
  1080.                 case PropertyType::WEAKREFERENCE:
  1081.                 case PropertyType::REFERENCE:
  1082.                     $references[$property->getName()] = [
  1083.                         'type' => $property->getType(),
  1084.                         'values' => $property->isMultiple() ? $property->getString() : [$property->getString()],
  1085.                     ];
  1086.                 case PropertyType::NAME:
  1087.                 case PropertyType::URI:
  1088.                 case PropertyType::PATH:
  1089.                 case PropertyType::STRING:
  1090.                     $values $property->getString();
  1091.                     break;
  1092.                 case PropertyType::DECIMAL:
  1093.                     $values $property->getDecimal();
  1094.                     $targetDoms[] = 'numericalDom';
  1095.                     break;
  1096.                 case PropertyType::BOOLEAN:
  1097.                     $values array_map('intval', (array)$property->getBoolean());
  1098.                     break;
  1099.                 case PropertyType::LONG:
  1100.                     $values $property->getLong();
  1101.                     $targetDoms[] = 'numericalDom';
  1102.                     break;
  1103.                 case PropertyType::BINARY:
  1104.                     if ($property->isNew() || $property->isModified()) {
  1105.                         $values = [];
  1106.                         foreach ((array)$property->getValueForStorage() as $stream) {
  1107.                             if (null === $stream) {
  1108.                                 $binary '';
  1109.                             } else {
  1110.                                 $binary stream_get_contents($stream);
  1111.                                 fclose($stream);
  1112.                             }
  1113.                             $binaryData[$property->getName()][] = $binary;
  1114.                             $length strlen($binary);
  1115.                             $values[] = $length;
  1116.                         }
  1117.                     } else {
  1118.                         $values $property->getLength();
  1119.                         if (!$property->isMultiple() && empty($values)) {
  1120.                             $values = [0];
  1121.                         }
  1122.                     }
  1123.                     break;
  1124.                 case PropertyType::DATE:
  1125.                     $values $property->getDate();
  1126.                     if ($values instanceof DateTime) {
  1127.                         $values = [$values];
  1128.                     }
  1129.                     foreach ((array)$values as $key => $date) {
  1130.                         if ($date instanceof DateTime) {
  1131.                             // do not modify the instance which is associated with the node.
  1132.                             $date = clone $date;
  1133.                             // normalize to UTC for storage.
  1134.                             $date->setTimezone(new DateTimeZone('UTC'));
  1135.                         }
  1136.                         $values[$key] = $date;
  1137.                     }
  1138.                     $values $this->valueConverter->convertType($valuesPropertyType::STRING);
  1139.                     break;
  1140.                 case PropertyType::DOUBLE:
  1141.                     $values $property->getDouble();
  1142.                     $targetDoms[] = 'numericalDom';
  1143.                     break;
  1144.                 default:
  1145.                     throw new RepositoryException('unknown type ' $property->getType());
  1146.             }
  1147.             foreach ($targetDoms as $targetDom) {
  1148.                 $doms[$targetDom][] = [
  1149.                     'name' => $property->getName(),
  1150.                     'type' => PropertyType::nameFromValue($property->getType()),
  1151.                     'multiple' => $property->isMultiple(),
  1152.                     'lengths' => (array)$property->getLength(),
  1153.                     'values' => $values,
  1154.                 ];
  1155.             }
  1156.         }
  1157.         $ret = [
  1158.             'stringDom' => null,
  1159.             'numericalDom' => null,
  1160.             'binaryData' => $binaryData,
  1161.             'references' => $references
  1162.         ];
  1163.         foreach ($doms as $targetDom => $properties) {
  1164.             $dom = new DOMDocument('1.0''UTF-8');
  1165.             $rootNode $dom->createElement('sv:node');
  1166.             foreach ($namespaces as $namespace => $uri) {
  1167.                 $rootNode->setAttribute('xmlns:' $namespace$uri);
  1168.             }
  1169.             $dom->appendChild($rootNode);
  1170.             foreach ($properties as $property) {
  1171.                 /* @var $property Property */
  1172.                 $propertyNode $dom->createElement('sv:property');
  1173.                 $propertyNode->setAttribute('sv:name'$property['name']);
  1174.                 $propertyNode->setAttribute('sv:type'$property['type']);
  1175.                 $propertyNode->setAttribute('sv:multi-valued'$property['multiple'] ? '1' '0');
  1176.                 $lengths = (array)$property['lengths'];
  1177.                 foreach ((array)$property['values'] as $key => $value) {
  1178.                     $element $propertyNode->appendChild($dom->createElement('sv:value'));
  1179.                     $element->appendChild($dom->createTextNode($value));
  1180.                     if (isset($lengths[$key])) {
  1181.                         $lengthAttribute $dom->createAttribute('length');
  1182.                         $lengthAttribute->value $lengths[$key];
  1183.                         $element->appendChild($lengthAttribute);
  1184.                     }
  1185.                 }
  1186.                 $rootNode->appendChild($propertyNode);
  1187.             }
  1188.             if (count($properties)) {
  1189.                 $ret[$targetDom] = $dom;
  1190.             }
  1191.         }
  1192.         return $ret;
  1193.     }
  1194.     /**
  1195.      * {@inheritDoc}
  1196.      *
  1197.      * @throws DBALException
  1198.      */
  1199.     public function getAccessibleWorkspaceNames()
  1200.     {
  1201.         $query "SELECT DISTINCT name FROM phpcr_workspaces";
  1202.         $stmt $this->getConnection()->executeQuery($query);
  1203.         return $stmt->fetchAll(PDO::FETCH_COLUMN);
  1204.     }
  1205.     /**
  1206.      * {@inheritDoc}
  1207.      *
  1208.      * @throws RepositoryException
  1209.      * @throws DBALException
  1210.      */
  1211.     public function getNode($path)
  1212.     {
  1213.         $this->assertLoggedIn();
  1214.         PathHelper::assertValidAbsolutePath($pathfalsetrue$this->getNamespacePrefixes());
  1215.         $values[':path'] = $path;
  1216.         $values[':pathd'] = rtrim($path'/') . '/%';
  1217.         $values[':workspace'] = $this->workspaceName;
  1218.         if ($this->fetchDepth 0) {
  1219.             $values[':fetchDepth'] = $this->fetchDepth;
  1220.             $query '
  1221.               SELECT * FROM phpcr_nodes
  1222.               WHERE (path LIKE :pathd OR path = :path)
  1223.                 AND workspace_name = :workspace
  1224.                 AND depth <= ((SELECT depth FROM phpcr_nodes WHERE path = :path AND workspace_name = :workspace) + :fetchDepth)
  1225.               ORDER BY depth, sort_order ASC';
  1226.         } else {
  1227.             $query '
  1228.               SELECT * FROM phpcr_nodes
  1229.               WHERE path = :path
  1230.                 AND workspace_name = :workspace
  1231.               ORDER BY depth, sort_order ASC';
  1232.         }
  1233.         $stmt $this->getConnection()->executeQuery($query$values);
  1234.         $rows $stmt->fetchAll(PDO::FETCH_ASSOC);
  1235.         if (empty($rows)) {
  1236.             throw new ItemNotFoundException("Item $path not found in workspace " $this->workspaceName);
  1237.         }
  1238.         $nestedNodes $this->getNodesData($rows);
  1239.         $node array_shift($nestedNodes);
  1240.         foreach ($nestedNodes as $nestedPath => $nested) {
  1241.             $relativePath PathHelper::relativizePath($nestedPath$path);
  1242.             $this->nestNode($node$nestedexplode('/'$relativePath));
  1243.         }
  1244.         return $node;
  1245.     }
  1246.     /**
  1247.      * Attach a node at a subpath under the ancestor node.
  1248.      *
  1249.      * @param stdClass $ancestor The root node
  1250.      * @param stdClass $node The node to add
  1251.      * @param array $nodeNames Breadcrumb of child nodes from parentNode to the node itself
  1252.      */
  1253.     private function nestNode($ancestor$node, array $nodeNames)
  1254.     {
  1255.         while ($name array_shift($nodeNames)) {
  1256.             if (empty($nodeNames)) {
  1257.                 $ancestor->{$name} = $node;
  1258.                 return;
  1259.             }
  1260.             $ancestor $ancestor->{$name};
  1261.         }
  1262.     }
  1263.     /**
  1264.      * Convert a node result row to the stdClass representing all raw data.
  1265.      *
  1266.      * @param array $row
  1267.      *
  1268.      * @return stdClass raw node data
  1269.      */
  1270.     private function getNodeData($row)
  1271.     {
  1272.         $data $this->getNodesData([$row]);
  1273.         return array_shift($data);
  1274.     }
  1275.     /**
  1276.      * Build the raw data for a list of database result rows, fetching the
  1277.      * additional information in one single query.
  1278.      *
  1279.      * @param array $rows
  1280.      *
  1281.      * @return stdClass[]
  1282.      *
  1283.      * @throws NoSuchNodeTypeException
  1284.      * @throws RepositoryException
  1285.      */
  1286.     private function getNodesData($rows)
  1287.     {
  1288.         $data = [];
  1289.         $paths = [];
  1290.         foreach ($rows as $row) {
  1291.             $this->nodeIdentifiers[$row['path']] = $row['identifier'];
  1292.             $data[$row['path']] = $this->xmlToProps($row['props']);
  1293.             $data[$row['path']]->{'jcr:primaryType'} = $row['type'];
  1294.             $paths[] = $row['path'];
  1295.         }
  1296.         $query 'SELECT path, parent FROM phpcr_nodes WHERE parent IN (?) AND workspace_name = ? ORDER BY sort_order ASC';
  1297.         if ($this->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) {
  1298.             $childrenRows = [];
  1299.             foreach (array_chunk($pathsself::SQLITE_MAXIMUM_IN_PARAM_COUNT) as $chunk) {
  1300.                 $childrenRows += $this->getConnection()->fetchAll(
  1301.                     $query,
  1302.                     [$chunk$this->workspaceName],
  1303.                     [Connection::PARAM_STR_ARRAYnull]
  1304.                 );
  1305.             }
  1306.         } else {
  1307.             $childrenRows $this->getConnection()->fetchAll(
  1308.                 $query,
  1309.                 [$paths$this->workspaceName],
  1310.                 [Connection::PARAM_STR_ARRAYnull]
  1311.             );
  1312.         }
  1313.         foreach ($childrenRows as $child) {
  1314.             $childName explode('/'$child['path']);
  1315.             $childName end($childName);
  1316.             if (!isset($data[$child['parent']]->{$childName})) {
  1317.                 $data[$child['parent']]->{$childName} = new stdClass();
  1318.             }
  1319.         }
  1320.         foreach (array_keys($data) as $path) {
  1321.             // If the node is referenceable, return jcr:uuid.
  1322.             if (isset($data[$path]->{'jcr:mixinTypes'})) {
  1323.                 foreach ((array)$data[$path]->{'jcr:mixinTypes'} as $mixin) {
  1324.                     if ($this->nodeTypeManager->getNodeType($mixin)->isNodeType('mix:referenceable')) {
  1325.                         $data[$path]->{'jcr:uuid'} = $this->nodeIdentifiers[$path];
  1326.                         break;
  1327.                     }
  1328.                 }
  1329.             }
  1330.         }
  1331.         return $data;
  1332.     }
  1333.     /**
  1334.      * {@inheritDoc}
  1335.      *
  1336.      * @throws RepositoryException
  1337.      */
  1338.     public function getNodes($paths)
  1339.     {
  1340.         $this->assertLoggedIn();
  1341.         if (empty($paths)) {
  1342.             return [];
  1343.         }
  1344.         foreach ($paths as $path) {
  1345.             PathHelper::assertValidAbsolutePath($pathfalsetrue$this->getNamespacePrefixes());
  1346.         }
  1347.         $params[':workspace'] = $this->workspaceName;
  1348.         if ($this->fetchDepth 0) {
  1349.             $params[':fetchDepth'] = $this->fetchDepth;
  1350.             $query '
  1351.               SELECT path AS arraykey, id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  1352.               FROM phpcr_nodes
  1353.               WHERE workspace_name = :workspace
  1354.                 AND (';
  1355.             $i 0;
  1356.             foreach ($paths as $path) {
  1357.                 $params[':path' $i] = $path;
  1358.                 $params[':pathd' $i] = rtrim($path'/') . '/%';
  1359.                 $subquery 'SELECT depth FROM phpcr_nodes WHERE path = :path' $i ' AND workspace_name = :workspace';
  1360.                 $query .= '(path LIKE :pathd' $i ' OR path = :path' $i ') AND depth <= ((' $subquery ') + :fetchDepth) OR ';
  1361.                 $i++;
  1362.             }
  1363.         } else {
  1364.             $query 'SELECT path AS arraykey, id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  1365.                 FROM phpcr_nodes WHERE workspace_name = :workspace AND (';
  1366.             $i 0;
  1367.             foreach ($paths as $path) {
  1368.                 $params[':path' $i] = $path;
  1369.                 $query .= 'path = :path' $i ' OR ';
  1370.                 $i++;
  1371.             }
  1372.         }
  1373.         $query rtrim($query'OR ');
  1374.         $query .= ') ORDER BY sort_order ASC';
  1375.         $stmt $this->getConnection()->executeQuery($query$params);
  1376.         $all $stmt->fetchAll(PDO::FETCH_UNIQUE PDO::FETCH_GROUP);
  1377.         $nodes = [];
  1378.         if ($all) {
  1379.             $nodes $this->getNodesData($all);
  1380.         }
  1381.         return $nodes;
  1382.     }
  1383.     /**
  1384.      * Determine if a path exists already.
  1385.      *
  1386.      * @param string $path Path of the node
  1387.      * @param string $workspaceName To overwrite the current workspace
  1388.      *
  1389.      * @return bool
  1390.      */
  1391.     private function pathExists($path$workspaceName null)
  1392.     {
  1393.         return (boolean)$this->getSystemIdForNode($path$workspaceName);
  1394.     }
  1395.     /**
  1396.      * Get the database primary key for node.
  1397.      *
  1398.      * @param string $identifier Path of the identifier
  1399.      * @param string $workspaceName To overwrite the current workspace
  1400.      *
  1401.      * @return bool|string The database id
  1402.      */
  1403.     private function getSystemIdForNode($identifier$workspaceName null)
  1404.     {
  1405.         if (null === $workspaceName) {
  1406.             $workspaceName $this->workspaceName;
  1407.         }
  1408.         if (UUIDHelper::isUUID($identifier)) {
  1409.             $query 'SELECT id FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?';
  1410.         } else {
  1411.             if ($this->getConnection()->getDriver() instanceof \Doctrine\DBAL\Driver\PDOMySql\Driver) {
  1412.                 $query 'SELECT id FROM phpcr_nodes WHERE path COLLATE ' $this->getCaseSensitiveEncoding(
  1413.                     ) . ' = ? AND workspace_name = ?';
  1414.             } else {
  1415.                 $query 'SELECT id FROM phpcr_nodes WHERE path = ? AND workspace_name = ?';
  1416.             }
  1417.         }
  1418.         $nodeId $this->getConnection()->fetchColumn($query, [$identifier$workspaceName]);
  1419.         return $nodeId ?: false;
  1420.     }
  1421.     /**
  1422.      * {@inheritDoc}
  1423.      */
  1424.     public function getNodeByIdentifier($uuid)
  1425.     {
  1426.         $this->assertLoggedIn();
  1427.         $query 'SELECT * FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?';
  1428.         $row $this->getConnection()->fetchAssoc($query, [$uuid$this->workspaceName]);
  1429.         if (!$row) {
  1430.             throw new ItemNotFoundException("Item $uuid not found in workspace " $this->workspaceName);
  1431.         }
  1432.         $path $row['path'];
  1433.         $data $this->getNodeData($row);
  1434.         $data->{':jcr:path'} = $path;
  1435.         return $data;
  1436.     }
  1437.     /**
  1438.      * {@inheritDoc}
  1439.      */
  1440.     public function getNodesByIdentifier($identifiers)
  1441.     {
  1442.         $this->assertLoggedIn();
  1443.         if (empty($identifiers)) {
  1444.             return [];
  1445.         }
  1446.         $query 'SELECT id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  1447.             FROM phpcr_nodes WHERE workspace_name = ? AND identifier IN (?)';
  1448.         if ($this->getConnection()->getDatabasePlatform() instanceof SqlitePlatform) {
  1449.             $all = [];
  1450.             foreach (array_chunk($identifiersself::SQLITE_MAXIMUM_IN_PARAM_COUNT) as $chunk) {
  1451.                 $all += $this->getConnection()->fetchAll(
  1452.                     $query,
  1453.                     [$this->workspaceName$chunk],
  1454.                     [PDO::PARAM_STRConnection::PARAM_STR_ARRAY]
  1455.                 );
  1456.             }
  1457.         } else {
  1458.             $all $this->getConnection()->fetchAll(
  1459.                 $query,
  1460.                 [$this->workspaceName$identifiers],
  1461.                 [PDO::PARAM_STRConnection::PARAM_STR_ARRAY]
  1462.             );
  1463.         }
  1464.         $nodes = [];
  1465.         if ($all) {
  1466.             $nodesData $this->getNodesData($all);
  1467.             // ensure that the nodes are returned in the order if how the identifiers were passed in
  1468.             $pathByUuid = [];
  1469.             foreach ($nodesData as $path => $node) {
  1470.                 $pathByUuid[$node->{'jcr:uuid'}] = $path;
  1471.             }
  1472.             foreach ($identifiers as $identifier) {
  1473.                 if (isset($pathByUuid[$identifier])) {
  1474.                     $nodes[$pathByUuid[$identifier]] = $nodesData[$pathByUuid[$identifier]];
  1475.                 }
  1476.             }
  1477.         }
  1478.         return $nodes;
  1479.     }
  1480.     /**
  1481.      * {@inheritDoc}
  1482.      *
  1483.      * @throws NotImplementedException
  1484.      * @throws ItemNotFoundException
  1485.      */
  1486.     public function getNodePathForIdentifier($uuid$workspace null)
  1487.     {
  1488.         if (null !== $workspace) {
  1489.             throw new NotImplementedException('Specifying the workspace is not yet supported.');
  1490.         }
  1491.         $this->assertLoggedIn();
  1492.         $query "SELECT path FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?";
  1493.         $path $this->getConnection()->fetchColumn($query, [$uuid$this->workspaceName]);
  1494.         if (!$path) {
  1495.             throw new ItemNotFoundException("no item found with uuid " $uuid);
  1496.         }
  1497.         return $path;
  1498.     }
  1499.     /**
  1500.      * {@inheritDoc}
  1501.      */
  1502.     public function deleteNodes(array $operations)
  1503.     {
  1504.         $this->assertLoggedIn();
  1505.         foreach ($operations as $op) {
  1506.             $this->deleteNode($op->srcPath);
  1507.         }
  1508.         return true;
  1509.     }
  1510.     /**
  1511.      * {@inheritDoc}
  1512.      */
  1513.     public function deleteNodeImmediately($path)
  1514.     {
  1515.         $this->prepareSave();
  1516.         $this->deleteNode($path);
  1517.         $this->finishSave();
  1518.         return true;
  1519.     }
  1520.     /**
  1521.      * TODO instead of calling the deletes separately, we should batch the delete query
  1522.      * but careful with the caching!
  1523.      *
  1524.      * @param string $path node path to delete
  1525.      *
  1526.      * @throws ConstraintViolationException
  1527.      * @throws ItemNotFoundException
  1528.      * @throws RepositoryException
  1529.      */
  1530.     protected function deleteNode($path)
  1531.     {
  1532.         if ('/' === $path) {
  1533.             throw new ConstraintViolationException('You can not delete the root node of a repository');
  1534.         }
  1535.         if (!$this->pathExists($path)) {
  1536.             throw new ItemNotFoundException("No node found at " $path);
  1537.         }
  1538.         $params = [$path$path "/%"$this->workspaceName];
  1539.         // TODO on RDBMS that support deferred FKs we could skip this step
  1540.         $query 'SELECT id, path FROM phpcr_nodes WHERE (path = ? OR path LIKE ?) AND workspace_name = ?';
  1541.         $stmt $this->getConnection()->executeQuery($query$params);
  1542.         $this->referencesToDelete += $stmt->fetchAll(PDO::FETCH_UNIQUE PDO::FETCH_COLUMN);
  1543.         try {
  1544.             $query 'DELETE FROM phpcr_nodes WHERE (path = ? OR path LIKE ?) AND workspace_name = ?';
  1545.             $this->getConnection()->executeUpdate($query$params);
  1546.             $this->cleanIdentifierCache($path);
  1547.         } catch (DBALException $e) {
  1548.             throw new RepositoryException('Unexpected exception while deleting node ' $path$e->getCode(), $e);
  1549.         }
  1550.     }
  1551.     /**
  1552.      * Clean all identifiers under path $root.
  1553.      *
  1554.      * @param string $root Path to the root node to be cleared
  1555.      */
  1556.     private function cleanIdentifierCache($root)
  1557.     {
  1558.         unset($this->nodeIdentifiers[$root]);
  1559.         foreach (array_keys($this->nodeIdentifiers) as $path) {
  1560.             if (strpos($path"$root/") === 0) {
  1561.                 unset($this->nodeIdentifiers[$path]);
  1562.             }
  1563.         }
  1564.     }
  1565.     /**
  1566.      * {@inheritDoc}
  1567.      */
  1568.     public function deleteProperties(array $operations)
  1569.     {
  1570.         $this->assertLoggedIn();
  1571.         foreach ($operations as $op) {
  1572.             $this->deleteProperty($op->srcPath);
  1573.         }
  1574.         return true;
  1575.     }
  1576.     /**
  1577.      * {@inheritDoc}
  1578.      */
  1579.     public function deletePropertyImmediately($path)
  1580.     {
  1581.         $this->prepareSave();
  1582.         $this->deleteProperty($path);
  1583.         $this->finishSave();
  1584.         return true;
  1585.     }
  1586.     /**
  1587.      * {@inheritDoc}
  1588.      *
  1589.      * @throws ItemNotFoundException
  1590.      * @throws RepositoryException
  1591.      */
  1592.     protected function deleteProperty($path)
  1593.     {
  1594.         $this->assertLoggedIn();
  1595.         $nodePath PathHelper::getParentPath($path);
  1596.         $nodeId $this->getSystemIdForNode($nodePath);
  1597.         if (!$nodeId) {
  1598.             // no we really don't know that path
  1599.             throw new ItemNotFoundException("No item found at " $path);
  1600.         }
  1601.         $query 'SELECT props FROM phpcr_nodes WHERE id = ?';
  1602.         $xml $this->getConnection()->fetchColumn($query, [$nodeId]);
  1603.         $dom = new DOMDocument('1.0''UTF-8');
  1604.         $dom->loadXML($xml);
  1605.         $found false;
  1606.         $propertyName PathHelper::getNodeName($path);
  1607.         foreach ($dom->getElementsByTagNameNS('http://www.jcp.org/jcr/sv/1.0''property') as $propertyNode) {
  1608.             if ($propertyName == $propertyNode->getAttribute('sv:name')) {
  1609.                 $found true;
  1610.                 // would be nice to have the property object to ask for type
  1611.                 // but its in state deleted, would mean lots of refactoring
  1612.                 if ($propertyNode->hasAttribute('sv:type')) {
  1613.                     $type strtolower($propertyNode->getAttribute('sv:type'));
  1614.                     if (in_array($type, ['reference''weakreference'])) {
  1615.                         $table $this->referenceTables['reference' === $type PropertyType::REFERENCE PropertyType::WEAKREFERENCE];
  1616.                         try {
  1617.                             $query "DELETE FROM $table WHERE source_id = ? AND source_property_name = ?";
  1618.                             $this->getConnection()->executeUpdate($query, [$nodeId$propertyName]);
  1619.                         } catch (DBALException $e) {
  1620.                             throw new RepositoryException(
  1621.                                 'Unexpected exception while cleaning up deleted nodes',
  1622.                                 $e->getCode(),
  1623.                                 $e
  1624.                             );
  1625.                         }
  1626.                     }
  1627.                 }
  1628.                 $propertyNode->parentNode->removeChild($propertyNode);
  1629.                 break;
  1630.             }
  1631.         }
  1632.         if (!$found) {
  1633.             throw new ItemNotFoundException("Node $nodePath has no property $propertyName");
  1634.         }
  1635.         $xml $dom->saveXML();
  1636.         $query 'UPDATE phpcr_nodes SET props = ? WHERE id = ?';
  1637.         $params = [$xml$nodeId];
  1638.         try {
  1639.             $this->getConnection()->executeUpdate($query$params);
  1640.         } catch (DBALException $e) {
  1641.             throw new RepositoryException("Unexpected exception while updating properties of $path"$e->getCode(), $e);
  1642.         }
  1643.     }
  1644.     /**
  1645.      * {@inheritDoc}
  1646.      */
  1647.     public function moveNodes(array $operations)
  1648.     {
  1649.         /** @var $op MoveNodeOperation */
  1650.         foreach ($operations as $op) {
  1651.             $this->moveNode($op->srcPath$op->dstPath);
  1652.         }
  1653.         return true;
  1654.     }
  1655.     /**
  1656.      * {@inheritDoc}
  1657.      */
  1658.     public function moveNodeImmediately($srcAbsPath$dstAbspath)
  1659.     {
  1660.         $this->prepareSave();
  1661.         $this->moveNode($srcAbsPath$dstAbspath);
  1662.         $this->finishSave();
  1663.         return true;
  1664.     }
  1665.     /**
  1666.      * Execute moving a single node
  1667.      *
  1668.      * @throws PathNotFoundException
  1669.      * @throws ItemExistsException
  1670.      * @throws RepositoryException
  1671.      */
  1672.     protected function moveNode($srcAbsPath$dstAbsPath)
  1673.     {
  1674.         $this->assertLoggedIn();
  1675.         PathHelper::assertValidAbsolutePath($srcAbsPathfalsetrue$this->getNamespacePrefixes());
  1676.         PathHelper::assertValidAbsolutePath($dstAbsPathtruetrue$this->getNamespacePrefixes());
  1677.         if (!$this->pathExists($srcAbsPath)) {
  1678.             throw new PathNotFoundException("Source path '$srcAbsPath' not found");
  1679.         }
  1680.         if ($this->getSystemIdForNode($dstAbsPath)) {
  1681.             throw new ItemExistsException(
  1682.                 "Cannot move '$srcAbsPath' to '$dstAbsPath' because destination node already exists."
  1683.             );
  1684.         }
  1685.         if (!$this->getSystemIdForNode(PathHelper::getParentPath($dstAbsPath))) {
  1686.             throw new PathNotFoundException("Parent of the destination path '" $dstAbsPath "' has to exist.");
  1687.         }
  1688.         $query 'SELECT path, id FROM phpcr_nodes WHERE path LIKE ? OR path = ? AND workspace_name = ? ' $this->getConnection(
  1689.             )->getDatabasePlatform()->getForUpdateSQL();
  1690.         $stmt $this->getConnection()->executeQuery($query, [$srcAbsPath '/%'$srcAbsPath$this->workspaceName]);
  1691.         /*
  1692.          * TODO: https://github.com/jackalope/jackalope-doctrine-dbal/pull/26/files#L0R1057
  1693.          * the other thing i wonder: can't you do the replacement inside sql instead of loading and then storing
  1694.          * the node? this will be extremely slow for a large set of nodes. i think you should use query builder here
  1695.          * rather than raw sql, to make it work on a maximum of platforms.
  1696.          *
  1697.          * can you try to do this please? if we don't figure out how to do it, at least fix the where criteria, and
  1698.          * we can ask the doctrine community how to do the substring operation.
  1699.          * http://stackoverflow.com/questions/8619421/correct-syntax-for-doctrine2s-query-builder-substring-helper-method
  1700.          */
  1701.         $query "UPDATE phpcr_nodes SET ";
  1702.         $updatePathCase "path = CASE ";
  1703.         $updateParentCase "parent = CASE ";
  1704.         $updateLocalNameCase "local_name = CASE ";
  1705.         $updateSortOrderCase "sort_order = CASE ";
  1706.         $updateDepthCase "depth = CASE ";
  1707.         // TODO: Find a better way to do this
  1708.         // Calculate CAST type for CASE statement
  1709.         switch ($this->getConnection()->getDatabasePlatform()->getName()) {
  1710.             case 'pgsql':
  1711.                 $intType 'integer';
  1712.                 break;
  1713.             case 'mysql':
  1714.                 $intType 'unsigned';
  1715.                 break;
  1716.             default:
  1717.                 $intType 'integer';
  1718.         }
  1719.         $i 0;
  1720.         $values $ids = [];
  1721.         $srcAbsPathPattern '/^' preg_quote($srcAbsPath'/') . '/';
  1722.         while ($row $stmt->fetch(PDO::FETCH_ASSOC)) {
  1723.             $values[':id' $i] = $row['id'];
  1724.             $values[':path' $i] = preg_replace($srcAbsPathPattern$dstAbsPath$row['path'], 1);
  1725.             $values[':parent' $i] = PathHelper::getParentPath($values[':path' $i]);
  1726.             $values[':depth' $i] = PathHelper::getPathDepth($values[':path' $i]);
  1727.             $updatePathCase .= "WHEN id = :id$i THEN :path$i ";
  1728.             $updateParentCase .= "WHEN id = :id$i THEN :parent$i ";
  1729.             $updateDepthCase .= "WHEN id = :id$i THEN CAST(:depth$i AS $intType) ";
  1730.             if ($srcAbsPath === $row['path']) {
  1731.                 list($namespace$localName) = $this->getJcrName($values[':path' $i]);
  1732.                 $values[':localname' $i] = $localName;
  1733.                 $updateLocalNameCase .= "WHEN id = :id$i THEN :localname$i ";
  1734.                 $updateSortOrderCase .= "WHEN id = :id$i THEN (SELECT * FROM ( SELECT MAX(x.sort_order) + 1 FROM phpcr_nodes x WHERE x.parent = :parent$i) y) ";
  1735.             }
  1736.             $ids[] = $row['id'];
  1737.             $i++;
  1738.         }
  1739.         if (!$i) {
  1740.             return;
  1741.         }
  1742.         $ids implode(','$ids);
  1743.         $updateLocalNameCase .= 'ELSE local_name END, ';
  1744.         $updateSortOrderCase .= 'ELSE sort_order END ';
  1745.         $query .= $updatePathCase 'END, ' $updateParentCase 'END, ' $updateDepthCase 'END, ' $updateLocalNameCase $updateSortOrderCase;
  1746.         $query .= "WHERE id IN ($ids)";
  1747.         try {
  1748.             $this->getConnection()->executeUpdate($query$values);
  1749.         } catch (DBALException $e) {
  1750.             throw new RepositoryException(
  1751.                 "Unexpected exception while moving node from $srcAbsPath to $dstAbsPath",
  1752.                 $e->getCode(),
  1753.                 $e
  1754.             );
  1755.         }
  1756.         $this->cleanIdentifierCache($srcAbsPath);
  1757.     }
  1758.     /**
  1759.      * {@inheritDoc}
  1760.      *
  1761.      * @throws RepositoryException
  1762.      */
  1763.     public function reorderChildren(Node $node)
  1764.     {
  1765.         $this->assertLoggedIn();
  1766.         $values[':absPath'] = $node->getPath();
  1767.         $sql "UPDATE phpcr_nodes SET sort_order = CASE CONCAT(
  1768.           namespace,
  1769.           (CASE namespace WHEN '' THEN '' ELSE ':' END),
  1770.           local_name
  1771.         )";
  1772.         $i 0;
  1773.         foreach ($node->getNodeNames() as $name) {
  1774.             $values[':name' $i] = implode(':'array_filter($this->getJcrName($name)));
  1775.             $values[':order' $i] = $i// use our counter to avoid gaps
  1776.             $sql .= " WHEN :name$i THEN :order$i";
  1777.             $i++;
  1778.         }
  1779.         $sql .= ' ELSE sort_order END WHERE parent = :absPath';
  1780.         try {
  1781.             $this->getConnection()->executeUpdate($sql$values);
  1782.         } catch (DBALException $e) {
  1783.             throw new RepositoryException('Unexpected exception while reordering nodes'$e->getCode(), $e);
  1784.         }
  1785.         return true;
  1786.     }
  1787.     /**
  1788.      * Return the node processor for processing nodes
  1789.      * according to their node types.
  1790.      *
  1791.      * @return NodeProcessor
  1792.      */
  1793.     private function getNodeProcessor()
  1794.     {
  1795.         if ($this->nodeProcessor) {
  1796.             return $this->nodeProcessor;
  1797.         }
  1798.         $this->nodeProcessor = new NodeProcessor(
  1799.             $this->credentials->getUserID(), $this->getNamespacesObject(), $this->getAutoLastModified()
  1800.         );
  1801.         return $this->nodeProcessor;
  1802.     }
  1803.     /**
  1804.      * {@inheritDoc}
  1805.      */
  1806.     public function storeNodes(array $operations)
  1807.     {
  1808.         $this->assertLoggedIn();
  1809.         $additionalAddOperations = [];
  1810.         foreach ($operations as $operation) {
  1811.             if ($operation->node->isDeleted()) {
  1812.                 $properties $operation->node->getPropertiesForStoreDeletedNode();
  1813.             } else {
  1814.                 $additionalAddOperations array_merge(
  1815.                     $additionalAddOperations,
  1816.                     $this->getNodeProcessor()->process($operation->node)
  1817.                 );
  1818.                 $properties $operation->node->getProperties();
  1819.             }
  1820.             $this->storeNode($operation->srcPath$properties);
  1821.         }
  1822.         if (!empty($additionalAddOperations)) {
  1823.             $this->storeNodes($additionalAddOperations);
  1824.         }
  1825.     }
  1826.     /**
  1827.      * Make sure we have a uuid and a primaryType, then sync data into the database
  1828.      *
  1829.      * @param string $path the path to store the node at
  1830.      * @param Property[] $properties the properties of this node
  1831.      *
  1832.      * @return boolean true on success
  1833.      *
  1834.      * @throws RepositoryException if not logged in
  1835.      */
  1836.     protected function storeNode($path$properties)
  1837.     {
  1838.         $nodeIdentifier $this->getIdentifier($path$properties);
  1839.         $type = isset($properties['jcr:primaryType']) ? $properties['jcr:primaryType']->getValue() : 'nt:unstructured';
  1840.         $this->syncNode($nodeIdentifier$path$typetrue$properties);
  1841.         return true;
  1842.     }
  1843.     /**
  1844.      * Determine a UUID for the node at this path with these properties
  1845.      *
  1846.      * @param string $path
  1847.      * @param Property[] $properties
  1848.      *
  1849.      * @return string a unique id
  1850.      */
  1851.     protected function getIdentifier($path$properties)
  1852.     {
  1853.         if (isset($this->nodeIdentifiers[$path])) {
  1854.             return $this->nodeIdentifiers[$path];
  1855.         }
  1856.         if (isset($properties['jcr:uuid'])) {
  1857.             return $properties['jcr:uuid']->getValue();
  1858.         }
  1859.         // we always generate a uuid, even for non-referenceable nodes that have no automatic uuid
  1860.         return $this->generateUuid();
  1861.     }
  1862.     /**
  1863.      * {@inheritDoc}
  1864.      */
  1865.     public function getNodeTypes($nodeTypes = [])
  1866.     {
  1867.         $standardTypes StandardNodeTypes::getNodeTypeData();
  1868.         $userTypes $this->fetchUserNodeTypes();
  1869.         if ($nodeTypes) {
  1870.             $nodeTypes array_flip($nodeTypes);
  1871.             return array_values(
  1872.                 array_intersect_key($standardTypes$nodeTypes) + array_intersect_key($userTypes$nodeTypes)
  1873.             );
  1874.         }
  1875.         return array_values($standardTypes $userTypes);
  1876.     }
  1877.     /**
  1878.      * Fetch a user-defined node-type definition.
  1879.      *
  1880.      * @return array
  1881.      */
  1882.     protected function fetchUserNodeTypes()
  1883.     {
  1884.         $result = [];
  1885.         $query '
  1886. SELECT 
  1887. phpcr_type_nodes.name AS node_name, phpcr_type_nodes.is_abstract AS node_abstract, 
  1888. phpcr_type_nodes.is_mixin AS node_mixin, phpcr_type_nodes.queryable AS node_queryable, 
  1889. phpcr_type_nodes.orderable_child_nodes AS node_has_orderable_child_nodes, 
  1890. phpcr_type_nodes.primary_item AS node_primary_item_name, phpcr_type_nodes.supertypes AS declared_super_type_names, 
  1891. phpcr_type_props.name AS property_name, phpcr_type_props.auto_created AS property_auto_created, 
  1892. phpcr_type_props.mandatory AS property_mandatory, phpcr_type_props.protected AS property_protected, 
  1893. phpcr_type_props.on_parent_version AS property_on_parent_version, 
  1894. phpcr_type_props.required_type AS property_required_type, phpcr_type_props.multiple AS property_multiple, 
  1895. phpcr_type_props.fulltext_searchable AS property_fulltext_searchable, 
  1896. phpcr_type_props.query_orderable AS property_query_orderable, phpcr_type_props.default_value as property_default_value,
  1897. phpcr_type_childs.name AS child_name, phpcr_type_childs.auto_created AS child_auto_created, 
  1898. phpcr_type_childs.mandatory AS child_mandatory, phpcr_type_childs.protected AS child_protected, 
  1899. phpcr_type_childs.on_parent_version AS child_on_parent_version, phpcr_type_childs.default_type AS child_default_type, 
  1900. phpcr_type_childs.primary_types AS child_primary_types 
  1901. FROM 
  1902. phpcr_type_nodes 
  1903. LEFT JOIN 
  1904. phpcr_type_props ON phpcr_type_nodes.node_type_id = phpcr_type_props.node_type_id 
  1905. LEFT JOIN 
  1906. phpcr_type_childs ON phpcr_type_nodes.node_type_id = phpcr_type_childs.node_type_id
  1907. ';
  1908.         $statement $this->getConnection()->prepare($query);
  1909.         $statement->execute();
  1910.         while ($row $statement->fetch()) {
  1911.             $nodeName $row['node_name'];
  1912.             if (!isset($result[$nodeName])) {
  1913.                 $result[$nodeName] = [
  1914.                     'name' => $nodeName,
  1915.                     'isAbstract' => (bool)$row['node_abstract'],
  1916.                     'isMixin' => (bool)$row['node_mixin'],
  1917.                     'isQueryable' => (bool)$row['node_queryable'],
  1918.                     'hasOrderableChildNodes' => (bool)$row['node_has_orderable_child_nodes'],
  1919.                     'primaryItemName' => $row['node_primary_item_name'],
  1920.                     'declaredSuperTypeNames' => array_filter(explode(' '$row['declared_super_type_names'])),
  1921.                     'declaredPropertyDefinitions' => [],
  1922.                     'declaredNodeDefinitions' => [],
  1923.                 ];
  1924.             }
  1925.             if (($propertyName $row['property_name']) !== null) {
  1926.                 $result[$nodeName]['declaredPropertyDefinitions'][] = [
  1927.                     'declaringNodeType' => $nodeName,
  1928.                     'name' => $propertyName,
  1929.                     'isAutoCreated' => (bool)$row['property_auto_created'],
  1930.                     'isMandatory' => (bool)$row['property_mandatory'],
  1931.                     'isProtected' => (bool)$row['property_protected'],
  1932.                     'onParentVersion' => (bool)$row['property_on_parent_version'],
  1933.                     'requiredType' => (int)$row['property_required_type'],
  1934.                     'multiple' => (bool)$row['property_multiple'],
  1935.                     'isFulltextSearchable' => (bool)$row['property_fulltext_searchable'],
  1936.                     'isQueryOrderable' => (bool)$row['property_query_orderable'],
  1937.                     'queryOperators' => [
  1938.                         QOM::JCR_OPERATOR_EQUAL_TO,
  1939.                         QOM::JCR_OPERATOR_NOT_EQUAL_TO,
  1940.                         QOM::JCR_OPERATOR_GREATER_THAN,
  1941.                         QOM::JCR_OPERATOR_GREATER_THAN_OR_EQUAL_TO,
  1942.                         QOM::JCR_OPERATOR_LESS_THAN,
  1943.                         QOM::JCR_OPERATOR_LESS_THAN_OR_EQUAL_TO,
  1944.                         QOM::JCR_OPERATOR_LIKE,
  1945.                     ],
  1946.                     'defaultValues' => [$row['property_default_value']]
  1947.                 ];
  1948.             }
  1949.             if (($childName $row['child_name']) !== null) {
  1950.                 $result[$nodeName]['declaredNodeDefinitions'][] = [
  1951.                     'declaringNodeType' => $nodeName,
  1952.                     'name' => $childName,
  1953.                     'isAutoCreated' => (bool)$row['child_auto_created'],
  1954.                     'isMandatory' => (bool)$row['child_mandatory'],
  1955.                     'isProtected' => (bool)$row['child_protected'],
  1956.                     'onParentVersion' => (bool)$row['child_on_parent_version'],
  1957.                     'allowsSameNameSiblings' => false,
  1958.                     'defaultPrimaryTypeName' => $row['child_default_type'],
  1959.                     'requiredPrimaryTypeNames' => array_filter(explode(' '$row['child_primary_types']))
  1960.                 ];
  1961.             }
  1962.         }
  1963.         return $result;
  1964.     }
  1965.     /**
  1966.      * {@inheritDoc}
  1967.      */
  1968.     public function registerNodeTypes($types$allowUpdate)
  1969.     {
  1970.         $builtinTypes StandardNodeTypes::getNodeTypeData();
  1971.         /* @var $type NodeTypeDefinition */
  1972.         foreach ($types as $type) {
  1973.             if (isset($builtinTypes[$type->getName()])) {
  1974.                 throw new RepositoryException(sprintf('%s: can\'t reregister built-in node type.'$type->getName()));
  1975.             }
  1976.             $nodeTypeName $type->getName();
  1977.             $query "SELECT * FROM phpcr_type_nodes WHERE name = ?";
  1978.             $result $this->getConnection()->fetchColumn($query, [$nodeTypeName]);
  1979.             $data = [
  1980.                 'name' => $nodeTypeName,
  1981.                 'supertypes' => implode(' '$type->getDeclaredSuperTypeNames()),
  1982.                 'is_abstract' => $type->isAbstract() ? 0,
  1983.                 'is_mixin' => $type->isMixin() ? 0,
  1984.                 'queryable' => $type->isQueryable() ? 0,
  1985.                 'orderable_child_nodes' => $type->hasOrderableChildNodes() ? 0,
  1986.                 'primary_item' => $type->getPrimaryItemName(),
  1987.             ];
  1988.             if ($result) {
  1989.                 if (!$allowUpdate) {
  1990.                     throw new NodeTypeExistsException("Could not register node type with the name '$nodeTypeName'.");
  1991.                 }
  1992.                 $this->getConnection()->update('phpcr_type_nodes'$data, ['node_type_id' => $result]);
  1993.                 $this->getConnection()->delete('phpcr_type_props', ['node_type_id' => $result]);
  1994.                 $this->getConnection()->delete('phpcr_type_childs', ['node_type_id' => $result]);
  1995.                 $nodeTypeId $result;
  1996.             } else {
  1997.                 $this->getConnection()->insert('phpcr_type_nodes'$data);
  1998.                 $nodeTypeId $this->getConnection()->lastInsertId($this->sequenceTypeName);
  1999.             }
  2000.             if ($propDefs $type->getDeclaredPropertyDefinitions()) {
  2001.                 foreach ($propDefs as $propertyDef) {
  2002.                     /* @var $propertyDef PropertyDefinitionInterface */
  2003.                     $this->getConnection()->insert(
  2004.                         'phpcr_type_props',
  2005.                         [
  2006.                             'node_type_id' => $nodeTypeId,
  2007.                             'name' => $propertyDef->getName(),
  2008.                             'protected' => $propertyDef->isProtected() ? 0,
  2009.                             'mandatory' => $propertyDef->isMandatory() ? 0,
  2010.                             'auto_created' => $propertyDef->isAutoCreated() ? 0,
  2011.                             'on_parent_version' => $propertyDef->getOnParentVersion(),
  2012.                             'multiple' => $propertyDef->isMultiple() ? 0,
  2013.                             'fulltext_searchable' => $propertyDef->isFullTextSearchable() ? 0,
  2014.                             'query_orderable' => $propertyDef->isQueryOrderable() ? 0,
  2015.                             'required_type' => $propertyDef->getRequiredType(),
  2016.                             'query_operators' => 0// transform to bitmask
  2017.                             'default_value' => $propertyDef->getDefaultValues() ? current(
  2018.                                 $propertyDef->getDefaultValues()
  2019.                             ) : null,
  2020.                         ]
  2021.                     );
  2022.                 }
  2023.             }
  2024.             if ($childDefs $type->getDeclaredChildNodeDefinitions()) {
  2025.                 foreach ($childDefs as $childDef) {
  2026.                     /* @var $childDef NodeDefinitionInterface */
  2027.                     $this->getConnection()->insert(
  2028.                         'phpcr_type_childs',
  2029.                         [
  2030.                             'node_type_id' => $nodeTypeId,
  2031.                             'name' => $childDef->getName(),
  2032.                             'protected' => $childDef->isProtected() ? 0,
  2033.                             'mandatory' => $childDef->isMandatory() ? 0,
  2034.                             'auto_created' => $childDef->isAutoCreated() ? 0,
  2035.                             'on_parent_version' => $childDef->getOnParentVersion(),
  2036.                             'primary_types' => implode(' '$childDef->getRequiredPrimaryTypeNames() ?: []),
  2037.                             'default_type' => $childDef->getDefaultPrimaryTypeName(),
  2038.                         ]
  2039.                     );
  2040.                 }
  2041.             }
  2042.         }
  2043.     }
  2044.     /**
  2045.      * {@inheritDoc}
  2046.      */
  2047.     public function setNodeTypeManager($nodeTypeManager)
  2048.     {
  2049.         $this->nodeTypeManager $nodeTypeManager;
  2050.     }
  2051.     /**
  2052.      * {@inheritDoc}
  2053.      */
  2054.     public function cloneFrom($srcWorkspace$srcAbsPath$destAbsPath$removeExisting)
  2055.     {
  2056.         throw new NotImplementedException('Cloning nodes is not implemented yet');
  2057.     }
  2058.     /**
  2059.      * {@inheritDoc}
  2060.      */
  2061.     public function updateNode(Node $node$srcWorkspace)
  2062.     {
  2063.         throw new NotImplementedException('Updating nodes is not implemented yet');
  2064.     }
  2065.     /**
  2066.      * {@inheritDoc}
  2067.      * @throws RepositoryException when no binary data found
  2068.      */
  2069.     public function getBinaryStream($path)
  2070.     {
  2071.         $this->assertLoggedIn();
  2072.         $nodePath PathHelper::getParentPath($path);
  2073.         $nodeId $this->getSystemIdForNode($nodePath);
  2074.         $propertyName PathHelper::getNodeName($path);
  2075.         $data $this->getConnection()->fetchAll(
  2076.             'SELECT data, idx FROM phpcr_binarydata WHERE node_id = ? AND property_name = ? AND workspace_name = ?',
  2077.             [$nodeId$propertyName$this->workspaceName]
  2078.         );
  2079.         if (count($data) === 0) {
  2080.             throw new RepositoryException('No binary data found in stream');
  2081.         }
  2082.         $streams = [];
  2083.         foreach ($data as $row) {
  2084.             if (is_resource($row['data'])) {
  2085.                 $stream $row['data'];
  2086.             } else {
  2087.                 $stream fopen('php://memory''rwb+');
  2088.                 fwrite($stream$row['data']);
  2089.                 rewind($stream);
  2090.             }
  2091.             $streams[] = $stream;
  2092.         }
  2093.         if (count($data) === 1) {
  2094.             // we don't know if this is a multivalue property or not.
  2095.             // TODO we should have something more efficient to know this. a flag in the database?
  2096.             // TODO use self::getProperty()->isMultiple() once implemented
  2097.             $node $this->getNode($nodePath);
  2098.             if (!is_array($node->{':' $propertyName})) {
  2099.                 return reset($streams);
  2100.             }
  2101.         }
  2102.         return $streams;
  2103.     }
  2104.     /**
  2105.      * {@inheritDoc}
  2106.      */
  2107.     public function getProperty($path)
  2108.     {
  2109.         throw new NotImplementedException('Getting properties by path is not implemented yet');
  2110.     }
  2111.     /**
  2112.      * {@inheritDoc}
  2113.      *
  2114.      * @throws InvalidQueryException
  2115.      */
  2116.     public function query(Query $query)
  2117.     {
  2118.         $this->assertLoggedIn();
  2119.         if (!$query instanceof QueryObjectModelInterface) {
  2120.             $parser = new Sql2ToQomQueryConverter($this->factory->get(QueryObjectModelFactory::class));
  2121.             try {
  2122.                 $qom $parser->parse($query->getStatement());
  2123.                 $qom->setLimit($query->getLimit());
  2124.                 $qom->setOffset($query->getOffset());
  2125.             } catch (Exception $e) {
  2126.                 throw new InvalidQueryException('Invalid query: ' $query->getStatement(), null$e);
  2127.             }
  2128.         } else {
  2129.             $qom $query;
  2130.         }
  2131.         $qomWalker = new QOMWalker($this->nodeTypeManager$this->getConnection(), $this->getNamespaces());
  2132.         list($selectors$selectorAliases$sql) = $qomWalker->walkQOMQuery($qom);
  2133.         $primarySource reset($selectors);
  2134.         $primaryType $primarySource->getSelectorName() ?: $primarySource->getNodeTypeName();
  2135.         $data $this->getConnection()->fetchAll($sql, [$this->workspaceName]);
  2136.         $results $properties $standardColumns = [];
  2137.         foreach ($data as $row) {
  2138.             $result = [];
  2139.             /** @var SelectorInterface $selector */
  2140.             foreach ($selectors as $selector) {
  2141.                 $selectorName $selector->getSelectorName() ?: $selector->getNodeTypeName();
  2142.                 $columnPrefix = isset($selectorAliases[$selectorName]) ? $selectorAliases[$selectorName] . '_' $selectorAliases[''] . '_';
  2143.                 if ($primaryType === $selector->getNodeTypeName()) {
  2144.                     $result[] = [
  2145.                         'dcr:name' => 'jcr:path',
  2146.                         'dcr:value' => $row[$columnPrefix 'path'],
  2147.                         'dcr:selectorName' => $selectorName
  2148.                     ];
  2149.                 }
  2150.                 $result[] = [
  2151.                     'dcr:name' => 'jcr:path',
  2152.                     'dcr:value' => $row[$columnPrefix 'path'],
  2153.                     'dcr:selectorName' => $selectorName
  2154.                 ];
  2155.                 $result[] = ['dcr:name' => 'jcr:score''dcr:value' => 0'dcr:selectorName' => $selectorName];
  2156.                 if (=== count($qom->getColumns())) {
  2157.                     $selectorPrefix null !== $selector->getSelectorName() ? $selectorName '.' '';
  2158.                     $result[] = [
  2159.                         'dcr:name' => $selectorPrefix 'jcr:primaryType',
  2160.                         'dcr:value' => $primaryType,
  2161.                         'dcr:selectorName' => $selectorName
  2162.                     ];
  2163.                 }
  2164.                 if (isset($row[$columnPrefix 'props'])) {
  2165.                     $propertyNames = [];
  2166.                     $columns $qom->getColumns();
  2167.                     // Always populate jcr:created and jcr:createdBy if a wildcard selector is used.
  2168.                     // This emulates the behavior of Jackrabbit
  2169.                     if (=== count($columns)) {
  2170.                         $propertyNames = ['jcr:created''jcr:createdBy'];
  2171.                     }
  2172.                     foreach ($columns as $column) {
  2173.                         if (!$column->getSelectorName() || $column->getSelectorName() == $selectorName) {
  2174.                             $propertyNames[] = $column->getPropertyName();
  2175.                         }
  2176.                     }
  2177.                     $properties[$selectorName] = (array)$this->xmlToColumns(
  2178.                         $row[$columnPrefix 'props'],
  2179.                         $propertyNames
  2180.                     );
  2181.                 } else {
  2182.                     $properties[$selectorName] = [];
  2183.                 }
  2184.                 // TODO: add other default columns that Jackrabbit provides to provide a more consistent behavior
  2185.                 if (isset($properties[$selectorName]['jcr:createdBy'])) {
  2186.                     $standardColumns[$selectorName]['jcr:createdBy'] = $properties[$selectorName]['jcr:createdBy'];
  2187.                 }
  2188.                 if (isset($properties[$selectorName]['jcr:created'])) {
  2189.                     $standardColumns[$selectorName]['jcr:created'] = $properties[$selectorName]['jcr:created'];
  2190.                 }
  2191.             }
  2192.             $reservedNames = ['jcr:path''jcr:score'];
  2193.             foreach ($qom->getColumns() as $column) {
  2194.                 $selectorName $column->getSelectorName();
  2195.                 $columnName $column->getPropertyName();
  2196.                 $columnPrefix = isset($selectorAliases[$selectorName]) ? $selectorAliases[$selectorName] . '_' $selectorAliases[''] . '_';
  2197.                 if (in_array($column->getColumnName(), $reservedNames)) {
  2198.                     throw new InvalidQueryException(
  2199.                         sprintf(
  2200.                             'Cannot reserved name "%s". Reserved names are "%s"',
  2201.                             $column->getColumnName(),
  2202.                             implode('", "'$reservedNames)
  2203.                         )
  2204.                     );
  2205.                 }
  2206.                 $dcrValue 'jcr:uuid' === $columnName $row[$columnPrefix 'identifier'] : (isset($properties[$selectorName][$columnName]) ? $properties[$selectorName][$columnName] : '');
  2207.                 if (isset($standardColumns[$selectorName][$columnName])) {
  2208.                     unset($standardColumns[$selectorName][$columnName]);
  2209.                 }
  2210.                 $result[] = [
  2211.                     'dcr:name' => $column->getColumnName(
  2212.                     ) === $columnName && isset($properties[$selectorName][$columnName]) ? $selectorName '.' $columnName $column->getColumnName(
  2213.                     ),
  2214.                     'dcr:value' => $dcrValue,
  2215.                     'dcr:selectorName' => $selectorName ?: $primaryType,
  2216.                 ];
  2217.             }
  2218.             foreach ($standardColumns as $selectorName => $columns) {
  2219.                 foreach ($columns as $columnName => $value) {
  2220.                     $result[] = [
  2221.                         'dcr:name' => $primaryType '.' $columnName,
  2222.                         'dcr:value' => $value,
  2223.                         'dcr:selectorName' => $selectorName,
  2224.                     ];
  2225.                 }
  2226.             }
  2227.             $results[] = $result;
  2228.         }
  2229.         return $results;
  2230.     }
  2231.     /**
  2232.      * {@inheritDoc}
  2233.      */
  2234.     public function getSupportedQueryLanguages()
  2235.     {
  2236.         return [
  2237.             QueryInterface::JCR_SQL2,
  2238.             QueryInterface::JCR_JQOM,
  2239.             QueryInterface::SQL,
  2240.         ];
  2241.     }
  2242.     /**
  2243.      * We need to create an in memory backup when we are inside a transaction
  2244.      * so that we can efficiently restore the original state in the namespaces
  2245.      * property in case of a rollback.
  2246.      *
  2247.      * This method also ensures that namespaces are loaded to begin with.
  2248.      */
  2249.     private function ensureNamespacesBackup()
  2250.     {
  2251.         if (!$this->namespaces instanceof \ArrayObject) {
  2252.             $this->getNamespacesObject();
  2253.         }
  2254.         if (!$this->inTransaction) {
  2255.             return;
  2256.         }
  2257.         if (null === $this->originalNamespaces) {
  2258.             $this->originalNamespaces $this->namespaces->getArrayCopy();
  2259.         }
  2260.     }
  2261.     /**
  2262.      * {@inheritDoc}
  2263.      *
  2264.      * @throws NamespaceException
  2265.      */
  2266.     public function registerNamespace($prefix$uri)
  2267.     {
  2268.         if (isset($this->namespaces[$prefix])) {
  2269.             if ($this->namespaces[$prefix] === $uri) {
  2270.                 return;
  2271.             }
  2272.             if (isset($this->coreNamespaces[$prefix])) {
  2273.                 throw new NamespaceException(
  2274.                     "Cannot overwrite JCR core namespace prefix '$prefix' to a new uri '$uri'."
  2275.                 );
  2276.             }
  2277.         }
  2278.         $this->ensureNamespacesBackup();
  2279.         $this->getConnection()->delete('phpcr_namespaces', ['prefix' => $prefix]);
  2280.         $this->getConnection()->delete('phpcr_namespaces', ['uri' => $uri]);
  2281.         $this->getConnection()->insert(
  2282.             'phpcr_namespaces',
  2283.             [
  2284.                 'prefix' => $prefix,
  2285.                 'uri' => $uri,
  2286.             ]
  2287.         );
  2288.         if (!empty($this->namespaces)) {
  2289.             $this->namespaces[$prefix] = $uri;
  2290.         }
  2291.     }
  2292.     /**
  2293.      * {@inheritDoc}
  2294.      */
  2295.     public function unregisterNamespace($prefix)
  2296.     {
  2297.         if (isset($this->coreNamespaces[$prefix])) {
  2298.             throw new NamespaceException("Cannot unregister JCR core namespace prefix '$prefix'.");
  2299.         }
  2300.         $this->ensureNamespacesBackup();
  2301.         $this->getConnection()->delete('phpcr_namespaces', ['prefix' => $prefix]);
  2302.         if (!empty($this->namespaces)) {
  2303.             unset($this->namespaces[$prefix]);
  2304.         }
  2305.     }
  2306.     /**
  2307.      * {@inheritDoc}
  2308.      */
  2309.     public function getReferences($path$name null)
  2310.     {
  2311.         return $this->getNodeReferences($path$namefalse);
  2312.     }
  2313.     /**
  2314.      * {@inheritDoc}
  2315.      */
  2316.     public function getWeakReferences($path$name null)
  2317.     {
  2318.         return $this->getNodeReferences($path$nametrue);
  2319.     }
  2320.     /**
  2321.      * @param string $path the path for which we need the references
  2322.      * @param string $name the name of the referencing properties or null for all
  2323.      * @param boolean $weakReference whether to get weak or strong references
  2324.      *
  2325.      * @return array list of paths to nodes that reference $path
  2326.      */
  2327.     private function getNodeReferences($path$name null$weakReference false)
  2328.     {
  2329.         $targetId $this->getSystemIdForNode($path);
  2330.         $params = [$targetId];
  2331.         $table $weakReference $this->referenceTables[PropertyType::WEAKREFERENCE] : $this->referenceTables[PropertyType::REFERENCE];
  2332.         $query "SELECT CONCAT(n.path, '/', r.source_property_name) FROM phpcr_nodes n
  2333.                INNER JOIN $table r ON n.id = r.source_id
  2334.                WHERE r.target_id = ?";
  2335.         if (null !== $name) {
  2336.             $query .= " AND source_property_name = ?";
  2337.             $params[] = $name;
  2338.         }
  2339.         $stmt $this->getConnection()->executeQuery($query$params);
  2340.         return $stmt->fetchAll(PDO::FETCH_COLUMN);
  2341.     }
  2342.     /**
  2343.      * {@inheritDoc}
  2344.      */
  2345.     public function beginTransaction()
  2346.     {
  2347.         if ($this->inTransaction) {
  2348.             throw new RepositoryException('Begin transaction failed: transaction already open');
  2349.         }
  2350.         $this->assertLoggedIn();
  2351.         try {
  2352.             $this->getConnection()->beginTransaction();
  2353.             $this->inTransaction true;
  2354.         } catch (Exception $e) {
  2355.             throw new RepositoryException('Begin transaction failed: ' $e->getMessage());
  2356.         }
  2357.     }
  2358.     /**
  2359.      * {@inheritDoc}
  2360.      *
  2361.      * @throws RepositoryException
  2362.      */
  2363.     public function commitTransaction()
  2364.     {
  2365.         if (!$this->inTransaction) {
  2366.             throw new RepositoryException('Commit transaction failed: no transaction open');
  2367.         }
  2368.         $this->assertLoggedIn();
  2369.         try {
  2370.             $this->inTransaction false;
  2371.             $this->getConnection()->commit();
  2372.             if ($this->originalNamespaces) {
  2373.                 // now that the transaction is committed, reset the cache of the stored namespaces.
  2374.                 $this->originalNamespaces null;
  2375.             }
  2376.         } catch (Exception $e) {
  2377.             throw new RepositoryException('Commit transaction failed: ' $e->getMessage());
  2378.         }
  2379.     }
  2380.     /**
  2381.      * {@inheritDoc}
  2382.      *
  2383.      * @throws RepositoryException
  2384.      */
  2385.     public function rollbackTransaction()
  2386.     {
  2387.         if (!$this->inTransaction) {
  2388.             throw new RepositoryException('Rollback transaction failed: no transaction open');
  2389.         }
  2390.         $this->assertLoggedIn();
  2391.         try {
  2392.             $this->inTransaction false;
  2393.             $this->getConnection()->rollBack();
  2394.             if ($this->originalNamespaces) {
  2395.                 // reset namespaces
  2396.                 $this->setNamespaces($this->originalNamespaces);
  2397.                 $this->originalNamespaces null;
  2398.             }
  2399.         } catch (Exception $e) {
  2400.             throw new RepositoryException('Rollback transaction failed: ' $e->getMessage(), 0$e);
  2401.         }
  2402.     }
  2403.     /**
  2404.      * Sets the default transaction timeout
  2405.      *
  2406.      * @param int $seconds The value of the timeout in seconds
  2407.      */
  2408.     public function setTransactionTimeout($seconds)
  2409.     {
  2410.         $this->assertLoggedIn();
  2411.         throw new NotImplementedException("Setting a transaction timeout is not yet implemented");
  2412.     }
  2413.     /**
  2414.      * {@inheritDoc}
  2415.      */
  2416.     public function prepareSave()
  2417.     {
  2418.         $this->getConnection()->beginTransaction();
  2419.     }
  2420.     /**
  2421.      * {@inheritDoc}
  2422.      */
  2423.     public function finishSave()
  2424.     {
  2425.         $this->syncReferences($this->referencesToUpdate);
  2426.         $this->referencesToUpdate $this->referencesToDelete = [];
  2427.         $this->getConnection()->commit();
  2428.     }
  2429.     /**
  2430.      * {@inheritDoc}
  2431.      */
  2432.     public function rollbackSave()
  2433.     {
  2434.         $this->referencesToUpdate $this->referencesToDelete = [];
  2435.         $this->getConnection()->rollBack();
  2436.     }
  2437.     /**
  2438.      *
  2439.      * @param Node $node the node to update
  2440.      */
  2441.     public function updateProperties(Node $node)
  2442.     {
  2443.         $this->assertLoggedIn();
  2444.         // we can ignore the operations returned, there will be no additions because of property updates
  2445.         $this->getNodeProcessor()->process($node);
  2446.         $this->syncNode(
  2447.             $node->getIdentifier(),
  2448.             $node->getPath(),
  2449.             $node->getPrimaryNodeType(),
  2450.             false,
  2451.             $node->getProperties()
  2452.         );
  2453.         return true;
  2454.     }
  2455.     /**
  2456.      * Initialize the dbal connection lazily
  2457.      */
  2458.     private function initConnection()
  2459.     {
  2460.         if (true === $this->conn->isConnected() && true === $this->connectionInitialized) {
  2461.             return;
  2462.         }
  2463.         if ($this->conn->getDatabasePlatform() instanceof PostgreSqlPlatform) {
  2464.             $this->sequenceNodeName 'phpcr_nodes_id_seq';
  2465.             $this->sequenceTypeName 'phpcr_type_nodes_node_type_id_seq';
  2466.         }
  2467.         // @TODO: move to "SqlitePlatform" and rename to "registerExtraFunctions"?
  2468.         if ($this->conn->getDatabasePlatform() instanceof SqlitePlatform) {
  2469.             $this->registerSqliteFunctions($this->conn->getWrappedConnection());
  2470.         }
  2471.         $this->connectionInitialized true;
  2472.     }
  2473. }