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

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