PageRenderTime 60ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Jackalope/Transport/DoctrineDBAL/Client.php

https://github.com/Nemrtvej/jackalope-doctrine-dbal
PHP | 2281 lines | 1616 code | 296 blank | 369 comment | 146 complexity | 87fc767610c205a536a6ca03803fe2d2 MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. namespace Jackalope\Transport\DoctrineDBAL;
  3. use PHPCR\LoginException;
  4. use PHPCR\NodeType\NodeDefinitionInterface;
  5. use PHPCR\NodeType\NodeTypeDefinitionInterface;
  6. use PHPCR\NodeType\NodeTypeExistsException;
  7. use PHPCR\NodeType\PropertyDefinitionInterface;
  8. use PHPCR\Query\QOM\ColumnInterface;
  9. use PHPCR\Query\QOM\JoinInterface;
  10. use PHPCR\Query\QOM\SourceInterface;
  11. use PHPCR\RepositoryInterface;
  12. use PHPCR\NamespaceRegistryInterface;
  13. use PHPCR\CredentialsInterface;
  14. use PHPCR\PropertyType;
  15. use PHPCR\Query\QOM\QueryObjectModelInterface;
  16. use PHPCR\Query\QOM\SelectorInterface;
  17. use PHPCR\Query\QueryInterface;
  18. use PHPCR\RepositoryException;
  19. use PHPCR\NamespaceException;
  20. use PHPCR\NoSuchWorkspaceException;
  21. use PHPCR\ItemExistsException;
  22. use PHPCR\ItemNotFoundException;
  23. use PHPCR\ReferentialIntegrityException;
  24. use PHPCR\SimpleCredentials;
  25. use PHPCR\Util\ValueConverter;
  26. use PHPCR\ValueFormatException;
  27. use PHPCR\PathNotFoundException;
  28. use PHPCR\Query\InvalidQueryException;
  29. use PHPCR\NodeType\ConstraintViolationException;
  30. use PHPCR\Util\UUIDHelper;
  31. use PHPCR\Util\QOM\Sql2ToQomQueryConverter;
  32. use PHPCR\Util\PathHelper;
  33. use Doctrine\DBAL\Connection;
  34. use Doctrine\DBAL\Driver\PDOConnection;
  35. use Doctrine\DBAL\Platforms\MySqlPlatform;
  36. use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
  37. use Doctrine\DBAL\Platforms\SqlitePlatform;
  38. use Doctrine\DBAL\DBALException;
  39. use Jackalope\Node;
  40. use Jackalope\Property;
  41. use Jackalope\Query\Query;
  42. use Jackalope\Transport\AddNodeOperation;
  43. use Jackalope\Transport\MoveNodeOperation;
  44. use Jackalope\Transport\BaseTransport;
  45. use Jackalope\Transport\QueryInterface as QueryTransport;
  46. use Jackalope\Transport\WritingInterface;
  47. use Jackalope\Transport\WorkspaceManagementInterface;
  48. use Jackalope\Transport\NodeTypeManagementInterface;
  49. use Jackalope\Transport\TransactionInterface;
  50. use Jackalope\Transport\StandardNodeTypes;
  51. use Jackalope\Transport\DoctrineDBAL\Query\QOMWalker;
  52. use Jackalope\NodeType\NodeTypeManager;
  53. use Jackalope\NodeType\NodeType;
  54. use Jackalope\NodeType\NodeTypeDefinition;
  55. use Jackalope\FactoryInterface;
  56. use Jackalope\NotImplementedException;
  57. /**
  58. * Class to handle the communication between Jackalope and RDBMS via Doctrine DBAL.
  59. *
  60. * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004
  61. * @license http://opensource.org/licenses/MIT MIT License
  62. *
  63. * @author Benjamin Eberlei <kontakt@beberlei.de>
  64. * @author Lukas Kahwe Smith <smith@pooteeweet.org>
  65. */
  66. class Client extends BaseTransport implements QueryTransport, WritingInterface, WorkspaceManagementInterface, NodeTypeManagementInterface, TransactionInterface
  67. {
  68. /**
  69. * The factory to instantiate objects
  70. * @var FactoryInterface
  71. */
  72. protected $factory;
  73. /**
  74. * @var ValueConverter
  75. */
  76. protected $valueConverter;
  77. /**
  78. * @var Connection
  79. */
  80. private $conn;
  81. /**
  82. * @var bool
  83. */
  84. private $loggedIn = false;
  85. /**
  86. * @var SimpleCredentials
  87. */
  88. private $credentials;
  89. /**
  90. * @var string
  91. */
  92. protected $workspaceName;
  93. /**
  94. * @var array
  95. */
  96. private $nodeIdentifiers = array();
  97. /**
  98. * @var NodeTypeManager
  99. */
  100. private $nodeTypeManager;
  101. /**
  102. * @var bool
  103. */
  104. protected $inTransaction = false;
  105. /**
  106. * Check if an initial request on login should be send to check if repository exists
  107. * This is according to the JCR specifications and set to true by default
  108. * @see setCheckLoginOnServer
  109. * @var bool
  110. */
  111. private $checkLoginOnServer = true;
  112. /**
  113. * @var array
  114. */
  115. protected $namespaces = array();
  116. /**
  117. * @var string|null
  118. */
  119. private $sequenceWorkspaceName;
  120. /**
  121. * @var string|null
  122. */
  123. private $sequenceNodeName;
  124. /**
  125. * @var string|null
  126. */
  127. private $sequenceTypeName;
  128. /**
  129. * @var array
  130. */
  131. private $referencesToUpdate = array();
  132. /**
  133. * @var array
  134. */
  135. private $referenceTables = array(
  136. PropertyType::REFERENCE => 'phpcr_nodes_references',
  137. PropertyType::WEAKREFERENCE => 'phpcr_nodes_weakreferences',
  138. );
  139. /**
  140. * @var array
  141. */
  142. private $referencesToDelete = array();
  143. /**
  144. * @param FactoryInterface $factory
  145. * @param Connection $conn
  146. */
  147. public function __construct(FactoryInterface $factory, Connection $conn)
  148. {
  149. $this->factory = $factory;
  150. $this->valueConverter = $this->factory->get('PHPCR\Util\ValueConverter');
  151. $this->conn = $conn;
  152. if ($conn->getDatabasePlatform() instanceof PostgreSqlPlatform) {
  153. $this->sequenceWorkspaceName = 'phpcr_workspaces_id_seq';
  154. $this->sequenceNodeName = 'phpcr_nodes_id_seq';
  155. $this->sequenceTypeName = 'phpcr_type_nodes_node_type_id_seq';
  156. }
  157. // @TODO: move to "SqlitePlatform" and rename to "registerExtraFunctions"?
  158. if ($this->conn->getDatabasePlatform() instanceof SqlitePlatform) {
  159. $this->registerSqliteFunctions($this->conn->getWrappedConnection());
  160. }
  161. }
  162. /**
  163. * @TODO: move to "SqlitePlatform" and rename to "registerExtraFunctions"?
  164. *
  165. * @param PDOConnection $sqliteConnection
  166. *
  167. * @return Client
  168. */
  169. private function registerSqliteFunctions(PDOConnection $sqliteConnection)
  170. {
  171. $sqliteConnection->sqliteCreateFunction('EXTRACTVALUE', function ($string, $expression) {
  172. $dom = new \DOMDocument('1.0', 'UTF-8');
  173. $dom->loadXML($string);
  174. $xpath = new \DOMXPath($dom);
  175. $list = $xpath->evaluate($expression);
  176. if (!is_object($list)) {
  177. return $list;
  178. }
  179. // @TODO: don't know if there are expressions returning more then one row
  180. if ($list->length > 0) {
  181. return $list->item(0)->textContent;
  182. }
  183. // @TODO: don't know if return value is right
  184. return null;
  185. }, 2);
  186. $sqliteConnection->sqliteCreateFunction('CONCAT', function () {
  187. return implode('', func_get_args());
  188. });
  189. return $this;
  190. }
  191. /**
  192. * @return Connection
  193. */
  194. public function getConnection()
  195. {
  196. return $this->conn;
  197. }
  198. /**
  199. * {@inheritDoc}
  200. *
  201. */
  202. public function createWorkspace($name, $srcWorkspace = null)
  203. {
  204. if (null !== $srcWorkspace) {
  205. throw new NotImplementedException('Creating workspace as clone of existing workspace not supported');
  206. }
  207. if ($this->workspaceExists($name)) {
  208. throw new RepositoryException("Workspace '$name' already exists");
  209. }
  210. try {
  211. $this->conn->insert('phpcr_workspaces', array('name' => $name));
  212. } catch (\Exception $e) {
  213. throw new RepositoryException("Couldn't create Workspace '$name': ".$e->getMessage(), 0, $e);
  214. }
  215. $this->conn->insert('phpcr_nodes', array(
  216. 'path' => '/',
  217. 'parent' => '',
  218. 'workspace_name'=> $name,
  219. 'identifier' => UUIDHelper::generateUUID(),
  220. 'type' => 'nt:unstructured',
  221. 'local_name' => '',
  222. 'namespace' => '',
  223. 'props' => '<?xml version="1.0" encoding="UTF-8"?>
  224. <sv:node xmlns:mix="http://www.jcp.org/jcr/mix/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:sv="http://www.jcp.org/jcr/sv/1.0" xmlns:rep="internal" />',
  225. // TODO compute proper value
  226. 'depth' => 0,
  227. ));
  228. }
  229. /**
  230. * {@inheritDoc}
  231. */
  232. public function deleteWorkspace($name)
  233. {
  234. if (!$this->workspaceExists($name)) {
  235. throw new RepositoryException("Workspace '$name' cannot be
  236. deleted as it does not exist");
  237. }
  238. try {
  239. $this->conn->delete('phpcr_workspaces', array('name' => $name));
  240. } catch (\Exception $e) {
  241. throw new RepositoryException("Couldn't delete workspace '$name': "
  242. .$e->getMessage(), 0, $e);
  243. }
  244. try {
  245. $this->conn->delete('phpcr_nodes', array('workspace_name'=>
  246. $name));
  247. } catch (\Exception $e) {
  248. throw new RepositoryException("Couldn't delete nodes in workspace
  249. '$name': ".$e->getMessage(), 0, $e);
  250. }
  251. try {
  252. $this->conn->delete('phpcr_binarydata', array('workspace_name'=>
  253. $name));
  254. } catch (\Exception $e) {
  255. throw new RepositoryException("Couldn't delete binary data in
  256. workspace '$name': ".$e->getMessage(), 0, $e);
  257. }
  258. }
  259. /**
  260. * {@inheritDoc}
  261. */
  262. public function login(CredentialsInterface $credentials = null, $workspaceName = null)
  263. {
  264. $this->credentials = $credentials;
  265. if (empty($workspaceName)) {
  266. $workspaceName = 'default';
  267. }
  268. $this->workspaceName = $workspaceName;
  269. if (!$this->checkLoginOnServer) {
  270. return $workspaceName;
  271. }
  272. if (!$this->workspaceExists($workspaceName)) {
  273. if ('default' !== $workspaceName) {
  274. throw new NoSuchWorkspaceException("Requested workspace: '$workspaceName'");
  275. }
  276. // create default workspace if it not exists
  277. $this->createWorkspace($workspaceName);
  278. }
  279. $this->loggedIn = true;
  280. return $workspaceName;
  281. }
  282. /**
  283. * {@inheritDoc}
  284. */
  285. public function logout()
  286. {
  287. if ($this->loggedIn) {
  288. $this->loggedIn = false;
  289. $this->conn->close();
  290. $this->conn = null;
  291. }
  292. }
  293. /**
  294. * Configure whether to check if we are logged in before doing a request.
  295. *
  296. * Will improve error reporting at the cost of some round trips.
  297. */
  298. public function setCheckLoginOnServer($bool)
  299. {
  300. $this->checkLoginOnServer = $bool;
  301. }
  302. protected function workspaceExists($workspaceName)
  303. {
  304. try {
  305. $query = 'SELECT 1 FROM phpcr_workspaces WHERE name = ?';
  306. $result = $this->conn->fetchColumn($query, array($workspaceName));
  307. } catch (\Exception $e) {
  308. if ($e instanceof DBALException || $e instanceof \PDOException) {
  309. if (1045 == $e->getCode()) {
  310. throw new LoginException('Access denied with your credentials: '.$e->getMessage());
  311. }
  312. if ('42S02' == $e->getCode()) {
  313. throw new RepositoryException('You did not properly set up the database for the repository. See README.md for more information. Message from backend: '.$e->getMessage());
  314. }
  315. throw new RepositoryException('Unexpected error talking to the backend: '.$e->getMessage());
  316. }
  317. throw $e;
  318. }
  319. return $result;
  320. }
  321. /**
  322. * Ensure that we are currently logged in, executing the login in case we
  323. * did lazy login.
  324. *
  325. * @throws RepositoryException if this transport is not logged in.
  326. */
  327. protected function assertLoggedIn()
  328. {
  329. if (!$this->loggedIn) {
  330. if (!$this->checkLoginOnServer && $this->workspaceName) {
  331. $this->checkLoginOnServer = true;
  332. if ($this->login($this->credentials, $this->workspaceName)) {
  333. return;
  334. }
  335. }
  336. throw new RepositoryException('You need to be logged in for this operation');
  337. }
  338. }
  339. /**
  340. * {@inheritDoc}
  341. */
  342. public function getRepositoryDescriptors()
  343. {
  344. return array(
  345. RepositoryInterface::IDENTIFIER_STABILITY => RepositoryInterface::IDENTIFIER_STABILITY_INDEFINITE_DURATION,
  346. RepositoryInterface::REP_NAME_DESC => 'jackalope_doctrine_dbal',
  347. RepositoryInterface::REP_VENDOR_DESC => 'Jackalope Community',
  348. RepositoryInterface::REP_VENDOR_URL_DESC => 'http://github.com/jackalope',
  349. RepositoryInterface::REP_VERSION_DESC => '1.0.0-DEV',
  350. RepositoryInterface::SPEC_NAME_DESC => 'Content Repository for PHP',
  351. RepositoryInterface::SPEC_VERSION_DESC => '2.1',
  352. RepositoryInterface::NODE_TYPE_MANAGEMENT_AUTOCREATED_DEFINITIONS_SUPPORTED => true,
  353. RepositoryInterface::NODE_TYPE_MANAGEMENT_INHERITANCE => RepositoryInterface::NODE_TYPE_MANAGEMENT_INHERITANCE_SINGLE,
  354. RepositoryInterface::NODE_TYPE_MANAGEMENT_MULTIPLE_BINARY_PROPERTIES_SUPPORTED => true,
  355. RepositoryInterface::NODE_TYPE_MANAGEMENT_MULTIVALUED_PROPERTIES_SUPPORTED => true,
  356. RepositoryInterface::NODE_TYPE_MANAGEMENT_ORDERABLE_CHILD_NODES_SUPPORTED => true,
  357. RepositoryInterface::NODE_TYPE_MANAGEMENT_OVERRIDES_SUPPORTED => false,
  358. RepositoryInterface::NODE_TYPE_MANAGEMENT_PRIMARY_ITEM_NAME_SUPPORTED => true,
  359. RepositoryInterface::NODE_TYPE_MANAGEMENT_PROPERTY_TYPES => true,
  360. RepositoryInterface::NODE_TYPE_MANAGEMENT_RESIDUAL_DEFINITIONS_SUPPORTED => false,
  361. RepositoryInterface::NODE_TYPE_MANAGEMENT_SAME_NAME_SIBLINGS_SUPPORTED => false,
  362. RepositoryInterface::NODE_TYPE_MANAGEMENT_UPDATE_IN_USE_SUPPORTED => false,
  363. RepositoryInterface::NODE_TYPE_MANAGEMENT_VALUE_CONSTRAINTS_SUPPORTED => false,
  364. RepositoryInterface::OPTION_ACCESS_CONTROL_SUPPORTED => false,
  365. RepositoryInterface::OPTION_ACTIVITIES_SUPPORTED => false,
  366. RepositoryInterface::OPTION_BASELINES_SUPPORTED => false,
  367. RepositoryInterface::OPTION_JOURNALED_OBSERVATION_SUPPORTED => false,
  368. RepositoryInterface::OPTION_LIFECYCLE_SUPPORTED => false,
  369. RepositoryInterface::OPTION_LOCKING_SUPPORTED => false,
  370. RepositoryInterface::OPTION_NODE_AND_PROPERTY_WITH_SAME_NAME_SUPPORTED => true,
  371. RepositoryInterface::OPTION_NODE_TYPE_MANAGEMENT_SUPPORTED => true,
  372. RepositoryInterface::OPTION_OBSERVATION_SUPPORTED => false,
  373. RepositoryInterface::OPTION_RETENTION_SUPPORTED => false,
  374. RepositoryInterface::OPTION_SHAREABLE_NODES_SUPPORTED => false,
  375. RepositoryInterface::OPTION_SIMPLE_VERSIONING_SUPPORTED => false,
  376. RepositoryInterface::OPTION_TRANSACTIONS_SUPPORTED => true,
  377. RepositoryInterface::OPTION_UNFILED_CONTENT_SUPPORTED => true,
  378. RepositoryInterface::OPTION_UPDATE_MIXIN_NODETYPES_SUPPORTED => true,
  379. RepositoryInterface::OPTION_UPDATE_PRIMARY_NODETYPE_SUPPORTED => true,
  380. RepositoryInterface::OPTION_VERSIONING_SUPPORTED => false,
  381. RepositoryInterface::OPTION_WORKSPACE_MANAGEMENT_SUPPORTED => true,
  382. RepositoryInterface::OPTION_XML_EXPORT_SUPPORTED => true,
  383. RepositoryInterface::OPTION_XML_IMPORT_SUPPORTED => true,
  384. RepositoryInterface::QUERY_FULL_TEXT_SEARCH_SUPPORTED => true,
  385. RepositoryInterface::QUERY_CANCEL_SUPPORTED => false,
  386. RepositoryInterface::QUERY_JOINS => RepositoryInterface::QUERY_JOINS_NONE,
  387. RepositoryInterface::QUERY_LANGUAGES => array(QueryInterface::JCR_SQL2, QueryInterface::JCR_JQOM),
  388. RepositoryInterface::QUERY_STORED_QUERIES_SUPPORTED => false,
  389. RepositoryInterface::WRITE_SUPPORTED => true,
  390. );
  391. }
  392. /**
  393. * {@inheritDoc}
  394. */
  395. public function getNamespaces()
  396. {
  397. if (empty($this->namespaces)) {
  398. $query = 'SELECT * FROM phpcr_namespaces';
  399. $data = $this->conn->fetchAll($query);
  400. $this->namespaces = array(
  401. NamespaceRegistryInterface::PREFIX_EMPTY => NamespaceRegistryInterface::NAMESPACE_EMPTY,
  402. NamespaceRegistryInterface::PREFIX_JCR => NamespaceRegistryInterface::NAMESPACE_JCR,
  403. NamespaceRegistryInterface::PREFIX_NT => NamespaceRegistryInterface::NAMESPACE_NT,
  404. NamespaceRegistryInterface::PREFIX_MIX => NamespaceRegistryInterface::NAMESPACE_MIX,
  405. NamespaceRegistryInterface::PREFIX_XML => NamespaceRegistryInterface::NAMESPACE_XML,
  406. NamespaceRegistryInterface::PREFIX_SV => NamespaceRegistryInterface::NAMESPACE_SV,
  407. );
  408. foreach ($data as $row) {
  409. $this->namespaces[$row['prefix']] = $row['uri'];
  410. }
  411. }
  412. return $this->namespaces;
  413. }
  414. /**
  415. * {@inheritDoc}
  416. */
  417. public function copyNode($srcAbsPath, $dstAbsPath, $srcWorkspace = null)
  418. {
  419. $this->assertLoggedIn();
  420. $workspaceName = $this->workspaceName;
  421. if (null !== $srcWorkspace) {
  422. if (!$this->workspaceExists($srcWorkspace)) {
  423. throw new NoSuchWorkspaceException("Source workspace '$srcWorkspace' does not exist.");
  424. }
  425. }
  426. PathHelper::assertValidAbsolutePath($dstAbsPath, true);
  427. $srcNodeId = $this->pathExists($srcAbsPath);
  428. if (!$srcNodeId) {
  429. throw new PathNotFoundException("Source path '$srcAbsPath' not found");
  430. }
  431. if ($this->pathExists($dstAbsPath)) {
  432. throw new ItemExistsException("Cannot copy to destination path '$dstAbsPath' that already exists.");
  433. }
  434. if (!$this->pathExists(PathHelper::getParentPath($dstAbsPath))) {
  435. throw new PathNotFoundException("Parent of the destination path '" . $dstAbsPath . "' has to exist.");
  436. }
  437. // Algorithm:
  438. // 1. Select all nodes with path $srcAbsPath."%" and iterate them
  439. // 2. create a new node with path $dstAbsPath + leftovers, with a new uuid. Save old => new uuid
  440. // 3. copy all properties from old node to new node
  441. // 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.
  442. // 5. "May drop mixin types"
  443. $query = 'SELECT * FROM phpcr_nodes WHERE path LIKE ? AND workspace_name = ?';
  444. $stmt = $this->conn->executeQuery($query, array($srcAbsPath . '%', $workspaceName));
  445. foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) {
  446. $newPath = str_replace($srcAbsPath, $dstAbsPath, $row['path']);
  447. $dom = new \DOMDocument('1.0', 'UTF-8');
  448. $dom->loadXML($row['props']);
  449. $propsData = array('dom' => $dom);
  450. // when copying a node, the copy is always a new node. set $isNewNode to true
  451. $newNodeId = $this->syncNode(null, $newPath, $row['type'], true, array(), $propsData);
  452. $query = 'INSERT INTO phpcr_binarydata (node_id, property_name, workspace_name, idx, data)'.
  453. ' SELECT ?, b.property_name, ?, b.idx, b.data FROM phpcr_binarydata b WHERE b.node_id = ?';
  454. try {
  455. $this->conn->executeUpdate($query, array($newNodeId, $this->workspaceName, $srcNodeId));
  456. } catch (DBALException $e) {
  457. throw new RepositoryException("Unexpected exception while copying node from $srcAbsPath to $dstAbsPath", $e->getCode(), $e);
  458. }
  459. }
  460. }
  461. /**
  462. * @param string $path
  463. *
  464. * @return array
  465. */
  466. private function getJcrName($path)
  467. {
  468. $name = implode('', array_slice(explode('/', $path), -1, 1));
  469. if (($aliasLength = strpos($name, ':')) !== false) {
  470. $alias = substr($name, 0, $aliasLength);
  471. $name = substr($name, $aliasLength + 1);
  472. } else {
  473. $alias = '';
  474. }
  475. $namespaces = $this->getNamespaces();
  476. if (!isset($namespaces[$alias])) {
  477. throw new NamespaceException('the namespace ' . $alias . ' was not registered.');
  478. }
  479. return array($namespaces[$alias], $name);
  480. }
  481. /**
  482. * Actually write the node into the database
  483. *
  484. * @param string $uuid node uuid
  485. * @param string $path absolute path of the node
  486. * @param string $type node type
  487. * @param boolean $isNewNode new nodes to insert (true) or existing node to update (false)
  488. * @param array $props
  489. * @param array $propsData
  490. *
  491. * @return boolean|string
  492. *
  493. * @throws ItemExistsException
  494. * @throws RepositoryException
  495. */
  496. private function syncNode($uuid, $path, $type, $isNewNode, $props = array(), $propsData = array())
  497. {
  498. // TODO: Not sure if there are always ALL props in $props, should we grab the online data here?
  499. // TODO: Binary data is handled very inefficiently here, UPSERT will really be necessary here as well as lazy handling
  500. if (!$propsData) {
  501. $propsData = $this->propsToXML($props);
  502. }
  503. if (null === $uuid) {
  504. $uuid = UUIDHelper::generateUUID();
  505. }
  506. if ($isNewNode) {
  507. list($namespace, $localName) = $this->getJcrName($path);
  508. $qb = $this->conn->createQueryBuilder();
  509. $qb->select(':identifier, :type, :path, :local_name, :namespace, :parent, :workspace_name, :props, :depth, COALESCE(MAX(n.sort_order), 0) + 1')
  510. ->from('phpcr_nodes', 'n')
  511. ->where('n.parent = :parent_a');
  512. $sql = $qb->getSql();
  513. try {
  514. $insert = "INSERT INTO phpcr_nodes (identifier, type, path, local_name, namespace, parent, workspace_name, props, depth, sort_order) " . $sql;
  515. $this->conn->executeUpdate($insert, array(
  516. 'identifier' => $uuid,
  517. 'type' => $type,
  518. 'path' => $path,
  519. 'local_name' => $localName,
  520. 'namespace' => $namespace,
  521. 'parent' => PathHelper::getParentPath($path),
  522. 'workspace_name' => $this->workspaceName,
  523. 'props' => $propsData['dom']->saveXML(),
  524. 'depth' => PathHelper::getPathDepth($path),
  525. 'parent_a' => PathHelper::getParentPath($path),
  526. ));
  527. } catch (\PDOException $e) {
  528. throw new ItemExistsException('Item ' . $path . ' already exists in the database');
  529. } catch (DBALException $e) {
  530. throw new ItemExistsException('Item ' . $path . ' already exists in the database');
  531. }
  532. $nodeId = $this->conn->lastInsertId($this->sequenceNodeName);
  533. } else {
  534. $nodeId = $this->pathExists($path);
  535. if (!$nodeId) {
  536. throw new RepositoryException("nodeId for $path not found");
  537. }
  538. $this->conn->update('phpcr_nodes', array('props' => $propsData['dom']->saveXML()), array('id' => $nodeId));
  539. }
  540. $this->nodeIdentifiers[$path] = $uuid;
  541. if (!empty($propsData['binaryData'])) {
  542. $this->syncBinaryData($nodeId, $propsData['binaryData']);
  543. }
  544. if (!empty($propsData['references'])) {
  545. $this->referencesToUpdate[$nodeId] = array('path' => $path, 'properties' => $propsData['references']);
  546. }
  547. return $nodeId;
  548. }
  549. private function syncBinaryData($nodeId, $binaryData)
  550. {
  551. foreach ($binaryData as $propertyName => $binaryValues) {
  552. foreach ($binaryValues as $idx => $data) {
  553. // TODO verify in which cases we can just update
  554. $params = array(
  555. 'node_id' => $nodeId,
  556. 'property_name' => $propertyName,
  557. 'workspace_name' => $this->workspaceName,
  558. );
  559. $this->conn->delete('phpcr_binarydata', $params);
  560. $params['idx'] = $idx;
  561. $params['data'] = $data;
  562. $types = array(
  563. \PDO::PARAM_INT,
  564. \PDO::PARAM_STR,
  565. \PDO::PARAM_STR,
  566. \PDO::PARAM_INT,
  567. \PDO::PARAM_LOB
  568. );
  569. $this->conn->insert('phpcr_binarydata', $params, $types);
  570. }
  571. }
  572. }
  573. private function syncReferences()
  574. {
  575. if ($this->referencesToUpdate) {
  576. // do not update references that are going to be deleted anyways
  577. $toUpdate = array_diff_assoc($this->referencesToUpdate, $this->referencesToDelete);
  578. try {
  579. foreach ($this->referenceTables as $table) {
  580. $query = "DELETE FROM $table WHERE source_id IN (?)";
  581. $this->conn->executeUpdate($query, array(array_keys($toUpdate)), array(Connection::PARAM_INT_ARRAY));
  582. }
  583. } catch (DBALException $e) {
  584. throw new RepositoryException('Unexpected exception while cleaning up after saving', $e->getCode(), $e);
  585. }
  586. foreach ($toUpdate as $nodeId => $references) {
  587. foreach ($references['properties'] as $name => $data) {
  588. foreach ($data['values'] as $value) {
  589. try {
  590. $params = array(
  591. 'source_id' => $nodeId,
  592. 'source_property_name' => $name,
  593. 'target_id' => $this->pathExists(self::getNodePathForIdentifier($value)),
  594. );
  595. $this->conn->insert($this->referenceTables[$data['type']], $params);
  596. } catch (ItemNotFoundException $e) {
  597. if (PropertyType::REFERENCE === $data['type']) {
  598. throw new ReferentialIntegrityException(
  599. "Trying to store reference to non-existant node with path '$value' in node '{$references['path']}' property '$name'."
  600. );
  601. }
  602. }
  603. }
  604. }
  605. }
  606. }
  607. // TODO on RDBMS that support deferred FKs we could skip this step
  608. if ($this->referencesToDelete) {
  609. $params = array(array_keys($this->referencesToDelete));
  610. // remove all PropertyType::REFERENCE with a source_id on a deleted node
  611. try {
  612. $query = "DELETE FROM phpcr_nodes_references WHERE source_id IN (?)";
  613. $this->conn->executeUpdate($query, $params, array(Connection::PARAM_INT_ARRAY));
  614. } catch (DBALException $e) {
  615. throw new RepositoryException('Unexpected exception while cleaning up deleted nodes', $e->getCode(), $e);
  616. }
  617. // ensure that there are no PropertyType::REFERENCE pointing to nodes that will be deleted
  618. // Note: due to the outer join we cannot filter on workspace_name, but this is ok
  619. // since within a transaction there can never be missing referenced nodes within the current workspace
  620. // make sure the target node is not in the list of nodes being deleted, to allow deletion in same request
  621. $query = 'SELECT DISTINCT r.target_id
  622. FROM phpcr_nodes_references r
  623. LEFT OUTER JOIN phpcr_nodes n ON r.target_id = n.id
  624. WHERE r.target_id IN (?)';
  625. $stmt = $this->conn->executeQuery($query, $params, array(Connection::PARAM_INT_ARRAY));
  626. $missingTargets = $stmt->fetchAll(\PDO::FETCH_COLUMN);
  627. if ($missingTargets) {
  628. $paths = array();
  629. foreach ($missingTargets as $id) {
  630. if (isset($this->referencesToDelete[$id])) {
  631. $paths[] = $this->referencesToDelete[$id];
  632. }
  633. }
  634. throw new ReferentialIntegrityException("Cannot delete '".implode("', '", $paths)."': A reference points to this node or a subnode");
  635. }
  636. // clean up all references
  637. try {
  638. foreach ($this->referenceTables as $table) {
  639. $query = "DELETE FROM $table WHERE target_id IN (?)";
  640. $this->conn->executeUpdate($query, $params, array(Connection::PARAM_INT_ARRAY));
  641. }
  642. } catch (DBALException $e) {
  643. throw new RepositoryException('Unexpected exception while cleaning up deleted nodes', $e->getCode(), $e);
  644. }
  645. }
  646. }
  647. public static function xmlToProps($xml, ValueConverter $valueConverter, $filter = null)
  648. {
  649. $data = new \stdClass();
  650. $dom = new \DOMDocument('1.0', 'UTF-8');
  651. $dom->loadXML($xml);
  652. foreach ($dom->getElementsByTagNameNS('http://www.jcp.org/jcr/sv/1.0', 'property') as $propertyNode) {
  653. $name = $propertyNode->getAttribute('sv:name');
  654. // only return the properties that pass through the filter callback
  655. if (null !== $filter && is_callable($filter) && false === $filter($name)) {
  656. continue;
  657. }
  658. $values = array();
  659. $type = PropertyType::valueFromName($propertyNode->getAttribute('sv:type'));
  660. foreach ($propertyNode->childNodes as $valueNode) {
  661. switch ($type) {
  662. case PropertyType::NAME:
  663. case PropertyType::URI:
  664. case PropertyType::WEAKREFERENCE:
  665. case PropertyType::REFERENCE:
  666. case PropertyType::PATH:
  667. case PropertyType::DECIMAL:
  668. case PropertyType::STRING:
  669. $values[] = $valueNode->nodeValue;
  670. break;
  671. case PropertyType::BOOLEAN:
  672. $values[] = (bool) $valueNode->nodeValue;
  673. break;
  674. case PropertyType::LONG:
  675. $values[] = (int) $valueNode->nodeValue;
  676. break;
  677. case PropertyType::BINARY:
  678. $values[] = (int) $valueNode->nodeValue;
  679. break;
  680. case PropertyType::DATE:
  681. $date = $valueNode->nodeValue;
  682. if ($date) {
  683. $date = new \DateTime($date);
  684. $date->setTimezone(new \DateTimeZone(date_default_timezone_get()));
  685. // Jackalope expects a string, might make sense to refactor to allow \DateTime instances too
  686. $date = $valueConverter->convertType($date, PropertyType::STRING);
  687. }
  688. $values[] = $date;
  689. break;
  690. case PropertyType::DOUBLE:
  691. $values[] = (double) $valueNode->nodeValue;
  692. break;
  693. default:
  694. throw new \InvalidArgumentException("Type with constant $type not found.");
  695. }
  696. }
  697. switch ($type) {
  698. case PropertyType::BINARY:
  699. $data->{':' . $name} = $propertyNode->getAttribute('sv:multi-valued') ? $values : $values[0];
  700. break;
  701. default:
  702. $data->{$name} = $propertyNode->getAttribute('sv:multi-valued') ? $values : $values[0];
  703. $data->{':' . $name} = $type;
  704. break;
  705. }
  706. }
  707. return $data;
  708. }
  709. /**
  710. * Seperate properties array into an xml and binary data.
  711. *
  712. * @param array $properties
  713. * @param boolean $inlineBinaries
  714. *
  715. * @return array ('dom' => $dom, 'binaryData' => streams, 'references' => array('type' => INT, 'values' => array(UUIDs)))
  716. */
  717. private function propsToXML($properties, $inlineBinaries = false)
  718. {
  719. $namespaces = array(
  720. 'mix' => "http://www.jcp.org/jcr/mix/1.0",
  721. 'nt' => "http://www.jcp.org/jcr/nt/1.0",
  722. 'xs' => "http://www.w3.org/2001/XMLSchema",
  723. 'jcr' => "http://www.jcp.org/jcr/1.0",
  724. 'sv' => "http://www.jcp.org/jcr/sv/1.0",
  725. 'rep' => "internal"
  726. );
  727. $dom = new \DOMDocument('1.0', 'UTF-8');
  728. $rootNode = $dom->createElement('sv:node');
  729. foreach ($namespaces as $namespace => $uri) {
  730. $rootNode->setAttribute('xmlns:' . $namespace, $uri);
  731. }
  732. $dom->appendChild($rootNode);
  733. $binaryData = $references = array();
  734. foreach ($properties as $property) {
  735. /* @var $property Property */
  736. $propertyNode = $dom->createElement('sv:property');
  737. $propertyNode->setAttribute('sv:name', $property->getName());
  738. $propertyNode->setAttribute('sv:type', PropertyType::nameFromValue($property->getType()));
  739. $propertyNode->setAttribute('sv:multi-valued', $property->isMultiple() ? '1' : '0');
  740. switch ($property->getType()) {
  741. case PropertyType::WEAKREFERENCE:
  742. case PropertyType::REFERENCE:
  743. $references[$property->getName()] = array(
  744. 'type' => $property->getType(),
  745. 'values' => $property->isMultiple() ? array_unique($property->getString()) : array($property->getString()),
  746. );
  747. case PropertyType::NAME:
  748. case PropertyType::URI:
  749. case PropertyType::PATH:
  750. case PropertyType::STRING:
  751. $values = $property->getString();
  752. break;
  753. case PropertyType::DECIMAL:
  754. $values = $property->getDecimal();
  755. break;
  756. case PropertyType::BOOLEAN:
  757. $values = array_map('intval', (array) $property->getBoolean());
  758. break;
  759. case PropertyType::LONG:
  760. $values = $property->getLong();
  761. break;
  762. case PropertyType::BINARY:
  763. if ($property->isNew() || $property->isModified()) {
  764. $values = array();
  765. foreach ((array) $property->getValueForStorage() as $stream) {
  766. if (null === $stream) {
  767. $binary = '';
  768. } else {
  769. $binary = stream_get_contents($stream);
  770. fclose($stream);
  771. }
  772. $binaryData[$property->getName()][] = $binary;
  773. $values[] = strlen($binary);
  774. }
  775. } else {
  776. $values = $property->getLength();
  777. if (!$property->isMultiple() && empty($values)) {
  778. $values = array(0);
  779. }
  780. }
  781. break;
  782. case PropertyType::DATE:
  783. $values = $property->getDate();
  784. if ($values instanceof \DateTime) {
  785. $values = array($values);
  786. }
  787. foreach ((array) $values as $key => $date) {
  788. if ($date instanceof \DateTime) {
  789. $date->setTimezone(new \DateTimeZone('UTC'));
  790. }
  791. $values[$key] = $date;
  792. }
  793. $values = $this->valueConverter->convertType($values, PropertyType::STRING);
  794. break;
  795. case PropertyType::DOUBLE:
  796. $values = $property->getDouble();
  797. break;
  798. default:
  799. throw new RepositoryException('unknown type '.$property->getType());
  800. }
  801. foreach ((array) $values as $value) {
  802. $element = $propertyNode->appendChild($dom->createElement('sv:value'));
  803. $element->appendChild($dom->createTextNode($value));
  804. }
  805. $rootNode->appendChild($propertyNode);
  806. }
  807. return array('dom' => $dom, 'binaryData' => $binaryData, 'references' => $references);
  808. }
  809. /**
  810. * {@inheritDoc}
  811. */
  812. public function getAccessibleWorkspaceNames()
  813. {
  814. $query = "SELECT DISTINCT name FROM phpcr_workspaces";
  815. $stmt = $this->conn->executeQuery($query);
  816. return $stmt->fetchAll(\PDO::FETCH_COLUMN);
  817. }
  818. /**
  819. * {@inheritDoc}
  820. */
  821. public function getNode($path)
  822. {
  823. $this->assertLoggedIn();
  824. PathHelper::assertValidAbsolutePath($path);
  825. $values[':path'] = $path;
  826. $values[':pathd'] = rtrim($path,'/') . '/%';
  827. $values[':workspace'] = $this->workspaceName;
  828. $values[':fetchDepth'] = $this->fetchDepth;
  829. $query = 'SELECT * FROM phpcr_nodes
  830. WHERE (path LIKE :pathd OR path = :path)
  831. AND workspace_name = :workspace
  832. AND depth <= ((SELECT depth FROM phpcr_nodes WHERE path = :path AND workspace_name = :workspace) + :fetchDepth)
  833. ORDER BY sort_order ASC';
  834. $stmt = $this->conn->executeQuery($query, $values);
  835. $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
  836. $nodeData = array();
  837. foreach ($rows as $row) {
  838. if ($row['path'] === $path) {
  839. $node = $this->getNodeData($path, $row);
  840. } else {
  841. $pathDiff = ltrim(substr($row['path'], strlen($path)),'/');
  842. $nodeData[$pathDiff] = $this->getNodeData($row['path'], $row);
  843. }
  844. }
  845. if (empty($node)) {
  846. throw new ItemNotFoundException("Item $path not found in workspace ".$this->workspaceName);
  847. }
  848. foreach ($nodeData as $key => $value) {
  849. $node->{$key} = $value;
  850. }
  851. return $node;
  852. }
  853. private function getNodeData($path, $row)
  854. {
  855. $this->nodeIdentifiers[$path] = $row['identifier'];
  856. $data = self::xmlToProps($row['props'], $this->valueConverter);
  857. $data->{'jcr:primaryType'} = $row['type'];
  858. $query = 'SELECT path FROM phpcr_nodes WHERE parent = ? AND workspace_name = ? ORDER BY sort_order ASC';
  859. $children = $this->conn->fetchAll($query, array($path, $this->workspaceName));
  860. foreach ($children as $child) {
  861. $childName = explode('/', $child['path']);
  862. $childName = end($childName);
  863. if (!isset($data->{$childName})) {
  864. $data->{$childName} = new \stdClass();
  865. }
  866. }
  867. // If the node is referenceable, return jcr:uuid.
  868. if (isset($data->{"jcr:mixinTypes"})) {
  869. foreach ((array) $data->{"jcr:mixinTypes"} as $mixin) {
  870. if ($this->nodeTypeManager->getNodeType($mixin)->isNodeType('mix:referenceable')) {
  871. $data->{'jcr:uuid'} = $row['identifier'];
  872. break;
  873. }
  874. }
  875. }
  876. return $data;
  877. }
  878. /**
  879. * {@inheritDoc}
  880. */
  881. public function getNodes($paths)
  882. {
  883. $this->assertLoggedIn();
  884. if (empty($paths)) {
  885. return array();
  886. }
  887. foreach ($paths as $path) {
  888. PathHelper::assertValidAbsolutePath($path);
  889. }
  890. $params[':workspace'] = $this->workspaceName;
  891. $params[':fetchDepth'] = $this->fetchDepth;
  892. $query = 'SELECT path AS arraykey, id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  893. FROM phpcr_nodes WHERE workspace_name = :workspace AND (';
  894. $i = 0;
  895. foreach ($paths as $path) {
  896. $params[':path'.$i] = $path;
  897. $params[':pathd'.$i] = rtrim($path,'/') . '/%';
  898. $subquery = 'SELECT depth FROM phpcr_nodes WHERE path = :path'.$i.' AND workspace_name = :workspace';
  899. $query .= '(path LIKE :pathd'.$i.' OR path = :path'.$i.') AND depth <= ((' . $subquery . ') + :fetchDepth) OR ';
  900. $i++;
  901. }
  902. $query = rtrim($query, 'OR ');
  903. $query .= ') ORDER BY sort_order ASC';
  904. $stmt = $this->conn->executeQuery($query, $params);
  905. $all = $stmt->fetchAll(\PDO::FETCH_UNIQUE | \PDO::FETCH_GROUP);
  906. $nodes = array();
  907. foreach ($paths as $path) {
  908. if (isset($all[$path])) {
  909. $nodes[$path] = $this->getNodeData($path, $all[$path]);
  910. }
  911. }
  912. return $nodes;
  913. }
  914. private function pathExists($path)
  915. {
  916. $query = 'SELECT id FROM phpcr_nodes WHERE path = ? AND workspace_name = ?';
  917. if ($nodeId = $this->conn->fetchColumn($query, array($path, $this->workspaceName))) {
  918. return $nodeId;
  919. }
  920. return false;
  921. }
  922. /**
  923. * {@inheritDoc}
  924. */
  925. public function getNodeByIdentifier($uuid)
  926. {
  927. $this->assertLoggedIn();
  928. $query = 'SELECT * FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?';
  929. $row = $this->conn->fetchAssoc($query, array($uuid, $this->workspaceName));
  930. if (!$row) {
  931. throw new ItemNotFoundException("Item $uuid not found in workspace ".$this->workspaceName);
  932. }
  933. $path = $row['path'];
  934. $data = $this->getNodeData($path, $row);
  935. $data->{':jcr:path'} = $path;
  936. return $data;
  937. }
  938. /**
  939. * {@inheritDoc}
  940. */
  941. public function getNodesByIdentifier($identifiers)
  942. {
  943. $this->assertLoggedIn();
  944. if (empty($identifiers)) {
  945. return array();
  946. }
  947. $query = 'SELECT identifier AS arraykey, id, path, parent, local_name, namespace, workspace_name, identifier, type, props, depth, sort_order
  948. FROM phpcr_nodes WHERE workspace_name = ? AND identifier IN (?)';
  949. $params = array($this->workspaceName, $identifiers);
  950. $stmt = $this->conn->executeQuery($query, $params, array(\PDO::PARAM_STR, Connection::PARAM_STR_ARRAY));
  951. $all = $stmt->fetchAll(\PDO::FETCH_UNIQUE | \PDO::FETCH_GROUP);
  952. $nodes = array();
  953. foreach ($identifiers as $id) {
  954. if (isset($all[$id])) {
  955. $path = $all[$id]['path'];
  956. $nodes[$path] = $this->getNodeData($path, $all[$id]);
  957. }
  958. }
  959. return $nodes;
  960. }
  961. /**
  962. * {@inheritDoc}
  963. */
  964. public function getNodePathForIdentifier($uuid, $workspace = null)
  965. {
  966. if (null !== $workspace) {
  967. throw new NotImplementedException('Specifying the workspace is not yet supported.');
  968. }
  969. $this->assertLoggedIn();
  970. $query = "SELECT path FROM phpcr_nodes WHERE identifier = ? AND workspace_name = ?";
  971. $path = $this->conn->fetchColumn($query, array($uuid, $this->workspaceName));
  972. if (!$path) {
  973. throw new ItemNotFoundException("no item found with uuid ".$uuid);
  974. }
  975. return $path;
  976. }
  977. /**
  978. * {@inheritDoc}
  979. */
  980. public function deleteNodes(array $operations)
  981. {
  982. $this->assertLoggedIn();
  983. foreach ($operations as $op) {
  984. $this->deleteNode($op->srcPath);
  985. }
  986. return true;
  987. }
  988. /**
  989. * {@inheritDoc}
  990. */
  991. public function deleteNodeImmediately($path)
  992. {
  993. $this->prepareSave();
  994. $this->deleteNode($path);
  995. $this->finishSave();
  996. return true;
  997. }
  998. /**
  999. * TODO instead of calling the deletes separately, we should batch the delete query
  1000. * but careful with the caching!
  1001. *
  1002. * @param string $path node path to delete
  1003. *
  1004. */
  1005. protected function deleteNode($path)
  1006. {
  1007. if ('/' == $path) {
  1008. throw new ConstraintViolationException('You can not delete the root node of a repository');
  1009. }
  1010. $nodeId = $this->pathExists($path);
  1011. if (!$nodeId) {
  1012. throw new ItemNotFoundException("No node found at ".$path);
  1013. }
  1014. $params = array($path, $path."/%", $this->workspaceName);
  1015. // TODO on RDBMS that support deferred FKs we could skip this step
  1016. $query = 'SELECT id, path FROM phpcr_nodes WHERE (path = ? OR path LIKE ?) AND workspace_name = ?';
  1017. $stmt = $this->conn->executeQuery($query, $params);
  1018. $this->referencesToDelete += $stmt->fetchAll(\PDO::FETCH_UNIQUE | \PDO::FETCH_COLUMN);
  1019. try {
  1020. $query = 'DELETE FROM phpcr_nodes WHERE (path = ? OR path LIKE ?) AND workspace_name = ?';
  1021. $this->conn->executeUpdate($query, $params);
  1022. } catch (DBALException $e) {
  1023. throw new RepositoryException('Unexpected exception while deleting node ' . $path, $e->getCode(), $e);
  1024. }
  1025. }
  1026. /**
  1027. * {@inheritDoc}
  1028. */
  1029. public function deleteProperties(array $operations)
  1030. {
  1031. $this->assertLoggedIn();
  1032. foreach ($operations as $op) {
  1033. $this->deleteProperty($op->srcPath);
  1034. }
  1035. return true;
  1036. }
  1037. /**
  1038. * {@inheritDoc}
  1039. */
  1040. public function deletePropertyImmediately($path)
  1041. {
  1042. $this->prepareSave();
  1043. $this->deleteProperty($path);
  1044. $this->finishSave();
  1045. return true;
  1046. }
  1047. /**
  1048. * {@inheritDoc}
  1049. */
  1050. protected function deleteProperty($path)
  1051. {
  1052. $this->assertLoggedIn();
  1053. $nodePath = PathHelper::getParentPath($path);
  1054. $nodeId = $this->pathExists($nodePath);
  1055. if (!$nodeId) {
  1056. // no we really don't know that path
  1057. throw new ItemNotFoundException("No item found at ".$path);
  1058. }
  1059. $query = 'SELECT props FROM phpcr_nodes WHERE id = ?';
  1060. $xml = $this->conn->fetchColumn($query, array($nodeId));
  1061. $dom = new \DOMDocument('1.0', 'UTF-8');
  1062. $dom->loadXml($xml);
  1063. $found = false;
  1064. $propertyName = PathHelper::getNodeName($path);
  1065. foreach ($dom->getElementsByTagNameNS('http://www.jcp.org/jcr/sv/1.0', 'property') as $propertyNode) {
  1066. if ($propertyName == $propertyNode->getAttribute('sv:name')) {
  1067. $found = true;
  1068. // would be nice to have the property object to ask for type
  1069. // but its in state deleted, would mean lots of refactoring
  1070. if ($propertyNode->hasAttribute('sv:type')) {
  1071. $type = strtolower($propertyNode->getAttribute('sv:type'));
  1072. if (in_array($type, array('reference', 'weakreference'))) {
  1073. $table = $this->referenceTables['reference' === $type ? PropertyType::REFERENCE : PropertyType::WEAKREFERENCE];
  1074. try {
  1075. $query = "DELETE FROM $table WHERE source_id = ? AND source_property_name = ?";
  1076. $this->conn->executeUpdate($query, array($nodeId, $propertyName));
  1077. } catch (DBALException $e) {
  1078. throw new RepositoryException('Unexpected exception while cleaning up deleted nodes', $e->getCode(), $e);
  1079. }
  1080. }
  1081. }
  1082. $propertyNode->parentNode->removeChild($propertyNode);
  1083. break;
  1084. }
  1085. }
  1086. if (! $found) {
  1087. throw new ItemNotFoundException("Node $nodePath has no property $propertyName");
  1088. }
  1089. $xml = $dom->saveXML();
  1090. $query = 'UPDATE phpcr_nodes SET props = ? WHERE id = ?';
  1091. $params = array($xml, $nodeId);
  1092. try {
  1093. $this->conn->executeUpdate($query, $params);
  1094. } catch (DBALException $e) {
  1095. throw new RepositoryException("Unexpected exception while updating properties of $path", $e->getCode(), $e);
  1096. }
  1097. }
  1098. /**
  1099. * {@inheritDoc}
  1100. */
  1101. public function moveNodes(array $operations)
  1102. {
  1103. /** @var $op MoveNodeOperation */
  1104. foreach ($operations as $op) {
  1105. $this->moveNode($op->srcPath, $op->dstPath);
  1106. }
  1107. return true;
  1108. }
  1109. /**
  1110. * {@inheritDoc}
  1111. */
  1112. public function moveNodeImmediately($srcAbsPath, $dstAbspath)
  1113. {
  1114. $this->prepareSave();
  1115. $this->moveNode($srcAbsPath, $dstAbspath);
  1116. $this->finishSave();
  1117. return true;
  1118. }
  1119. /**
  1120. * Execute moving a single node
  1121. */
  1122. protected function moveNode($srcAbsPath, $dstAbsPath)
  1123. {
  1124. $this->assertLoggedIn();
  1125. PathHelper::assertValidAbsolutePath($dstAbsPath, true);
  1126. $srcNodeId = $this->pathExists($srcAbsPath);
  1127. if (!$srcNodeId) {
  1128. throw new PathNotFoundException("Source path '$srcAbsPath' not found");
  1129. }
  1130. if ($this->pathExists($dstAbsPath)) {
  1131. throw new ItemExistsException("Cannot move '$srcAbsPath' to '$dstAbsPath' because destination node already exists.");
  1132. }
  1133. if (!$this->pathExists(PathHelper::getParentPath($dstAbsPath))) {
  1134. throw new PathNotFoundException("Parent of the destination path '" . $dstAbsPath . "' has to exist.");
  1135. }
  1136. $query = 'SELECT path, id FROM phpcr_nodes WHERE path LIKE ? OR path = ? AND workspace_name = ? '
  1137. . $this->conn->getDatabasePlatform()->getForUpdateSQL();
  1138. $stmt = $this->conn->executeQuery($query, array($srcAbsPath . '/%', $srcAbsPath, $this->workspaceName));
  1139. /*
  1140. * TODO: https://github.com/jackalope/jackalope-doctrine-dbal/pull/26/files#L0R1057
  1141. * the other thing i wonder: can't you do the replacement inside sql instead of loading and then storing
  1142. * the node? this will be extremely slow for a large set of nodes. i think you should use query builder here
  1143. * rather than raw sql, to make it work on a maximum of platforms.
  1144. *
  1145. * can you try to do this please? if we don't figure out how to do it, at least fix the where criteria, and
  1146. * we can ask the doctrine community how to do the substring operation.
  1147. * http://stackoverflow.com/questions/8619421/correct-syntax-for-doctrine2s-query-builder-substring-helper-method
  1148. */
  1149. $query = "UPDATE phpcr_nodes SET ";
  1150. $updatePathCase = "path = CASE ";
  1151. $updateParentCase = "parent = CASE ";
  1152. $updateLocalNameCase = "local_name = CASE ";
  1153. $updateSortOrderCase = "sort_order = CASE ";
  1154. $updateDepthCase = "depth = CASE ";
  1155. // TODO: Find a better way to do this
  1156. // Calculate CAST type for CASE statement
  1157. switch ($this->conn->getDatabasePlatform()->getName()) {
  1158. case 'pgsql':
  1159. $intType = 'integer';
  1160. break;
  1161. case 'mysql':
  1162. $intType = 'unsigned';
  1163. break;
  1164. default:
  1165. $intType = 'integer';
  1166. }
  1167. $i = 0;
  1168. $values = $ids = array();
  1169. while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
  1170. $values[':id' . $i] = $row['id'];
  1171. $values[':path' . $i] = str_replace($srcAbsPath, $dstAbsPath, $row['path']);
  1172. $values[':parent' . $i] = PathHelper::getParentPath($values[':path' . $i]);
  1173. $values[':depth' . $i] = PathHelper::getPathDepth($values[':path' . $i]);
  1174. $updatePathCase .= "WHEN id = :id" . $i . " THEN :path" . $i . " ";
  1175. $updateParentCase .= "WHEN id = :id" . $i . " THEN :parent" . $i . " ";
  1176. $updateDepthCase .= "WHEN id = :id" . $i . " THEN CAST(:depth" . $i . " AS " . $intType . ") ";
  1177. if ($srcAbsPath === $row['path']) {
  1178. $values[':localname' . $i] = PathHelper::getNodeName($values[':path' . $i]);
  1179. $updateLocalNameCase .= "WHEN id = :id" . $i . " THEN :localname" . $i . " ";
  1180. $updateSortOrderCase .= "WHEN id = :id" . $i . " THEN (SELECT * FROM ( SELECT MAX(x.sort_order) + 1 FROM phpcr_nodes x WHERE x.parent = :parent" . $i . ") y) ";
  1181. }
  1182. $ids[] = $row['id'];
  1183. $i++;
  1184. }
  1185. if (!$i) {
  1186. return;
  1187. }
  1188. $ids = implode($ids, ',');
  1189. $updateLocalNameCase .= "ELSE local_name END, ";
  1190. $updateSortOrderCase .= "ELSE sort_order END ";
  1191. $query .= $updatePathCase . "END, " . $updateParentCase . "END, " . $updateDepthCase . "END, " . $updateLocalNameCase . $updateSortOrderCase;
  1192. $query .= "WHERE id IN (" . $ids . ")";
  1193. try {
  1194. $this->conn->executeUpdate($query, $values);
  1195. } catch (DBALException $e) {
  1196. throw new RepositoryException("Unexpected exception while moving node from $srcAbsPath to $dstAbsPath", $e->getCode(), $e);
  1197. }
  1198. }
  1199. /**
  1200. * {@inheritDoc}
  1201. */
  1202. public function reorderChildren(Node $node)
  1203. {
  1204. $this->assertLoggedIn();
  1205. $values[':absPath'] = $node->getPath();
  1206. $sql = "UPDATE phpcr_nodes SET sort_order = CASE CONCAT(
  1207. namespace,
  1208. (CASE namespace WHEN '' THEN '' ELSE ':' END),
  1209. local_name
  1210. )";
  1211. $i = 0;
  1212. foreach ($node->getNodeNames() as $name) {
  1213. $values[':name' . $i] = $name;
  1214. $values[':order' . $i] = $i; // use our counter to avoid gaps
  1215. $sql .= " WHEN :name" . $i . " THEN :order" . $i;
  1216. $i++;
  1217. }
  1218. $sql .= " ELSE sort_order END WHERE parent = :absPath";
  1219. try {
  1220. $this->conn->executeUpdate($sql, $values);
  1221. } catch (DBALException $e) {
  1222. throw new RepositoryException('Unexpected exception while reordering nodes', $e->getCode(), $e);
  1223. }
  1224. return true;
  1225. }
  1226. private function validateNode(Node $node)
  1227. {
  1228. // This is very slow i believe :-(
  1229. $nodeDef = $node->getPrimaryNodeType();
  1230. $nodeTypes = $node->getMixinNodeTypes();
  1231. array_unshift($nodeTypes, $nodeDef);
  1232. foreach ($nodeTypes as $nodeType) {
  1233. /* @var $nodeType NodeTypeDefinitionInterface */
  1234. $this->validateNodeWithType($node, $nodeType);
  1235. }
  1236. }
  1237. /**
  1238. * TODO: we should move that into the common Jackalope BaseTransport or as new method of NodeType
  1239. * it will be helpful for other implementations.
  1240. *
  1241. * Validate this node with the nodetype and generate not yet existing
  1242. * autogenerated properties as necessary.
  1243. *
  1244. * @param Node $node
  1245. * @param NodeType $def
  1246. */
  1247. private function validateNodeWithType(Node $node, NodeType $def)
  1248. {
  1249. foreach ($def->getDeclaredChildNodeDefinitions() as $childDef) {
  1250. /* @var $childDef NodeDefinitionInterface */
  1251. if (!$node->hasNode($childDef->getName())) {
  1252. if ('*' === $childDef->getName()) {
  1253. continue;
  1254. }
  1255. if ($childDef->isMandatory() && !$childDef->isAutoCreated()) {
  1256. throw new RepositoryException(
  1257. "Child " . $childDef->getName() . " is mandatory, but is not present while ".
  1258. "saving " . $def->getName() . " at " . $node->getPath()
  1259. );
  1260. }
  1261. if ($childDef->isAutoCreated()) {
  1262. throw new NotImplementedException("Auto-creation of child node '".$def->getName()."#".$childDef->getName()."' is not yet supported in DoctrineDBAL transport.");
  1263. }
  1264. }
  1265. }
  1266. foreach ($def->getDeclaredPropertyDefinitions() as $propertyDef) {
  1267. /* @var $propertyDef PropertyDefinitionInterface */
  1268. if ('*' == $propertyDef->getName()) {
  1269. continue;
  1270. }
  1271. if (!$node->hasProperty($propertyDef->getName())) {
  1272. if ($propertyDef->isMandatory() && !$propertyDef->isAutoCreated()) {
  1273. throw new RepositoryException(
  1274. "Property " . $propertyDef->getName() . " is mandatory, but is not present while ".
  1275. "saving " . $def->getName() . " at " . $node->getPath()
  1276. );
  1277. }
  1278. if ($propertyDef->isAutoCreated()) {
  1279. switch ($propertyDef->getName()) {
  1280. case 'jcr:uuid':
  1281. $value = UUIDHelper::generateUUID();
  1282. break;
  1283. case 'jcr:createdBy':
  1284. case 'jcr:lastModifiedBy':
  1285. $value = $this->credentials->getUserID();
  1286. break;
  1287. case 'jcr:created':
  1288. case 'jcr:lastModified':
  1289. $value = new \DateTime();
  1290. break;
  1291. case 'jcr:etag':
  1292. // TODO: http://www.day.com/specs/jcr/2.0/3_Repository_Model.html#3.7.12.1%20mix:etag
  1293. $value = 'TODO: generate from binary properties of this node';
  1294. break;
  1295. default:
  1296. $defaultValues = $propertyDef->getDefaultValues();
  1297. if ($propertyDef->isMultiple()) {
  1298. $value = $defaultValues;
  1299. } elseif (isset($defaultValues[0])) {
  1300. $value = $defaultValues[0];
  1301. } else {
  1302. // When implementing versionable or activity, we need to handle more properties explicitly
  1303. throw new RepositoryException('No default value for autocreated property '.
  1304. $propertyDef->getName(). ' at '.$node->getPath());
  1305. }
  1306. }
  1307. $node->setProperty(
  1308. $propertyDef->getName(),
  1309. $value,
  1310. $propertyDef->getRequiredType()
  1311. );
  1312. }
  1313. }
  1314. }
  1315. foreach ($def->getDeclaredSupertypes() as $superType) {
  1316. $this->validateNodeWithType($node, $superType);
  1317. }
  1318. foreach ($node->getProperties() as $property) {
  1319. $this->assertValidProperty($property);
  1320. }
  1321. }
  1322. /**
  1323. * {@inheritDoc}
  1324. */
  1325. public function storeNodes(array $operations)
  1326. {
  1327. $this->assertLoggedIn();
  1328. /** @var $operation AddNodeOperation */
  1329. foreach ($operations as $operation) {
  1330. if ($operation->node->isDeleted()) {
  1331. $properties = $operation->node->getPropertiesForStoreDeletedNode();
  1332. } else {
  1333. $this->validateNode($operation->node);
  1334. $properties = $operation->node->getProperties();
  1335. }
  1336. $this->storeNode($operation->srcPath, $properties);
  1337. }
  1338. }
  1339. /**
  1340. * Make sure we have a uuid and a primaryType, then sync data into the database
  1341. *
  1342. * @param string $path the path to store the node at
  1343. * @param Property[] $properties the properties of this node
  1344. *
  1345. * @return boolean true on success
  1346. *
  1347. * @throws RepositoryException if not logged in
  1348. */
  1349. protected function storeNode($path, $properties)
  1350. {
  1351. $nodeIdentifier = $this->getIdentifier($path, $properties);
  1352. $type = isset($properties['jcr:primaryType']) ? $properties['jcr:primaryType']->getValue() : "nt:unstructured";
  1353. $this->syncNode($nodeIdentifier, $path, $type, true, $properties);
  1354. return true;
  1355. }
  1356. /**
  1357. * Determine a UUID for the node at this path with these properties
  1358. *
  1359. * @param string $path
  1360. * @param Property[] $properties
  1361. *
  1362. * @return string a unique id
  1363. */
  1364. protected function getIdentifier($path, $properties)
  1365. {
  1366. if (isset($this->nodeIdentifiers[$path])) {
  1367. return $this->nodeIdentifiers[$path];
  1368. }
  1369. if (isset($properties['jcr:uuid'])) {
  1370. return $properties['jcr:uuid']->getValue();
  1371. }
  1372. // we always generate a uuid, even for non-referenceable nodes that have no automatic uuid
  1373. return UUIDHelper::generateUUID();
  1374. }
  1375. /**
  1376. * {@inheritDoc}
  1377. */
  1378. public function storeProperty(Property $property)
  1379. {
  1380. $this->assertLoggedIn();
  1381. // just store the node with this property
  1382. // TODO: we should really delegate more of this from ObjectManager to transport.
  1383. // this is called for each property of a node - for jackrabbit it makes sense but not here
  1384. $node = $property->getParent();
  1385. $this->validateNode($node);
  1386. $path = $node->getPath();
  1387. $properties = $node->getProperties();
  1388. $this->syncNode(
  1389. $this->getIdentifier($path, $properties),
  1390. $path,
  1391. $node->getPropertyValue('jcr:primaryType'),
  1392. false,
  1393. $properties
  1394. );
  1395. return true;
  1396. }
  1397. /**
  1398. * Validation if all the data is correct before writing it into the database.
  1399. *
  1400. * @param PropertyInterface $property
  1401. *
  1402. * @throws ValueFormatException
  1403. */
  1404. private function assertValidProperty($property)
  1405. {
  1406. $type = $property->getType();
  1407. switch ($type) {
  1408. case PropertyType::NAME:
  1409. $values = $property->getValue();
  1410. if (!$property->isMultiple()) {
  1411. $values = array($values);
  1412. }
  1413. foreach ($values as $value) {
  1414. $pos = strpos($value, ':');
  1415. if (false !== $pos) {
  1416. $prefix = substr($value, 0, $pos);
  1417. $this->getNamespaces();
  1418. if (!isset($this->namespaces[$prefix])) {
  1419. throw new ValueFormatException("Invalid PHPCR NAME at '" . $property->getPath() . "': The namespace prefix " . $prefix . " does not exist.");
  1420. }
  1421. }
  1422. }
  1423. break;
  1424. case PropertyType::PATH:
  1425. $values = $property->getValue();
  1426. if (!$property->isMultiple()) {
  1427. $values = array($values);
  1428. }
  1429. foreach ($values as $value) {
  1430. if (!preg_match('(((/|..)?[-a-zA-Z0-9:_]+)+)', $value)) {
  1431. throw new ValueFormatException("Invalid PATH '$value' at '" . $property->getPath() ."': Segments are separated by / and allowed chars are -a-zA-Z0-9:_");
  1432. }
  1433. }
  1434. break;
  1435. case PropertyType::URI:
  1436. $values = $property->getValue();
  1437. if (!$property->isMultiple()) {
  1438. $values = array($values);
  1439. }
  1440. foreach ($values as $value) {
  1441. if (!preg_match(self::VALIDATE_URI_RFC3986, $value)) {
  1442. throw new ValueFormatException("Invalid URI '$value' at '" . $property->getPath() ."': Has to follow RFC 3986.");
  1443. }
  1444. }
  1445. break;
  1446. }
  1447. }
  1448. /**
  1449. * {@inheritDoc}
  1450. */
  1451. public function getNodeTypes($nodeTypes = array())
  1452. {
  1453. $standardTypes = array();
  1454. foreach (StandardNodeTypes::getNodeTypeData() as $nodeTypeData) {
  1455. $standardTypes[$nodeTypeData['name']] = $nodeTypeData;
  1456. }
  1457. $userTypes = $this->fetchUserNodeTypes();
  1458. if ($nodeTypes) {
  1459. $nodeTypes = array_flip($nodeTypes);
  1460. // TODO: check if user types can override standard types.
  1461. return array_values(array_intersect_key($standardTypes, $nodeTypes) + array_intersect_key($userTypes, $nodeTypes));
  1462. }
  1463. return array_values($standardTypes + $userTypes);
  1464. }
  1465. /**
  1466. * Fetch a user-defined node-type definition.
  1467. *
  1468. * @param string $name
  1469. *
  1470. * @return array
  1471. */
  1472. protected function fetchUserNodeTypes()
  1473. {
  1474. $result = array();
  1475. $query = "SELECT * FROM phpcr_type_nodes";
  1476. foreach ($this->conn->fetchAll($query) as $data) {
  1477. $name = $data['name'];
  1478. $result[$name] = array(
  1479. 'name' => $name,
  1480. 'isAbstract' => (bool) $data['is_abstract'],
  1481. 'isMixin' => (bool) ($data['is_mixin']),
  1482. 'isQueryable' => (bool) $data['queryable'],
  1483. 'hasOrderableChildNodes' => (bool) $data['orderable_child_nodes'],
  1484. 'primaryItemName' => $data['primary_item'],
  1485. 'declaredSuperTypeNames' => array_filter(explode(' ', $data['supertypes'])),
  1486. 'declaredPropertyDefinitions' => array(),
  1487. 'declaredNodeDefinitions' => array(),
  1488. );
  1489. $query = 'SELECT * FROM phpcr_type_props WHERE node_type_id = ?';
  1490. $props = $this->conn->fetchAll($query, array($data['node_type_id']));
  1491. foreach ($props as $propertyData) {
  1492. $result[$name]['declaredPropertyDefinitions'][] = array(
  1493. 'declaringNodeType' => $data['name'],
  1494. 'name' => $propertyData['name'],
  1495. 'isAutoCreated' => (bool) $propertyData['auto_created'],
  1496. 'isMandatory' => (bool) $propertyData['mandatory'],
  1497. 'isProtected' => (bool) $propertyData['protected'],
  1498. 'onParentVersion' => $propertyData['on_parent_version'],
  1499. 'requiredType' => $propertyData['required_type'],
  1500. 'multiple' => (bool) $propertyData['multiple'],
  1501. 'isFulltextSearchable' => (bool) $propertyData['fulltext_searchable'],
  1502. 'isQueryOrderable' => (bool) $propertyData['query_orderable'],
  1503. 'queryOperators' => array (
  1504. 0 => 'jcr.operator.equal.to',
  1505. 1 => 'jcr.operator.not.equal.to',
  1506. 2 => 'jcr.operator.greater.than',
  1507. 3 => 'jcr.operator.greater.than.or.equal.to',
  1508. 4 => 'jcr.operator.less.than',
  1509. 5 => 'jcr.operator.less.than.or.equal.to',
  1510. 6 => 'jcr.operator.like',
  1511. ),
  1512. 'defaultValue' => array($propertyData['default_value']),
  1513. );
  1514. }
  1515. $query = 'SELECT * FROM phpcr_type_childs WHERE node_type_id = ?';
  1516. $childs = $this->conn->fetchAll($query, array($data['node_type_id']));
  1517. foreach ($childs as $childData) {
  1518. $result[$name]['declaredNodeDefinitions'][] = array(
  1519. 'declaringNodeType' => $data['name'],
  1520. 'name' => $childData['name'],
  1521. 'isAutoCreated' => (bool) $childData['auto_created'],
  1522. 'isMandatory' => (bool) $childData['mandatory'],
  1523. 'isProtected' => (bool) $childData['protected'],
  1524. 'onParentVersion' => $childData['on_parent_version'],
  1525. 'allowsSameNameSiblings' => false,
  1526. 'defaultPrimaryTypeName' => $childData['default_type'],
  1527. 'requiredPrimaryTypeNames' => array_filter(explode(" ", $childData['primary_types'])),
  1528. );
  1529. }
  1530. }
  1531. return $result;
  1532. }
  1533. /**
  1534. * {@inheritDoc}
  1535. */
  1536. public function registerNodeTypes($types, $allowUpdate)
  1537. {
  1538. foreach ($types as $type) {
  1539. /* @var $type NodeTypeDefinition */
  1540. if ($allowUpdate) {
  1541. $query = "SELECT * FROM phpcr_type_nodes WHERE name = ?";
  1542. $result = $this->conn->fetchColumn($query, array($type->getName()));
  1543. if ($result) {
  1544. $this->conn->delete('phpcr_type_nodes', array('node_type_id' => $result));
  1545. $this->conn->delete('phpcr_type_props', array('node_type_id' => $result));
  1546. $this->conn->delete('phpcr_type_childs', array('node_type_id' => $result));
  1547. }
  1548. }
  1549. try {
  1550. $this->conn->insert('phpcr_type_nodes', array(
  1551. 'name' => $type->getName(),
  1552. 'supertypes' => implode(' ', $type->getDeclaredSuperTypeNames()),
  1553. 'is_abstract' => $type->isAbstract() ? 1 : 0,
  1554. 'is_mixin' => $type->isMixin() ? 1 : 0,
  1555. 'queryable' => $type->isQueryable() ? 1 : 0,
  1556. 'orderable_child_nodes' => $type->hasOrderableChildNodes() ? 1 : 0,
  1557. 'primary_item' => $type->getPrimaryItemName(),
  1558. ));
  1559. } catch (DBALException $e) {
  1560. throw new NodeTypeExistsException("Could not register node type with the name '".$type->getName()."'");
  1561. }
  1562. $nodeTypeId = $this->conn->lastInsertId($this->sequenceTypeName);
  1563. if ($propDefs = $type->getDeclaredPropertyDefinitions()) {
  1564. foreach ($propDefs as $propertyDef) {
  1565. /* @var $propertyDef PropertyDefinitionInterface */
  1566. $this->conn->insert('phpcr_type_props', array(
  1567. 'node_type_id' => $nodeTypeId,
  1568. 'name' => $propertyDef->getName(),
  1569. 'protected' => $propertyDef->isProtected() ? 1 : 0,
  1570. 'mandatory' => $propertyDef->isMandatory() ? 1 : 0,
  1571. 'auto_created' => $propertyDef->isAutoCreated() ? 1 : 0,
  1572. 'on_parent_version' => $propertyDef->getOnParentVersion(),
  1573. 'multiple' => $propertyDef->isMultiple() ? 1 : 0,
  1574. 'fulltext_searchable' => $propertyDef->isFullTextSearchable() ? 1 : 0,
  1575. 'query_orderable' => $propertyDef->isQueryOrderable() ? 1 : 0,
  1576. 'required_type' => $propertyDef->getRequiredType(),
  1577. 'query_operators' => 0, // transform to bitmask
  1578. 'default_value' => $propertyDef->getDefaultValues() ? current($propertyDef->getDefaultValues()) : null,
  1579. ));
  1580. }
  1581. }
  1582. if ($childDefs = $type->getDeclaredChildNodeDefinitions()) {
  1583. foreach ($childDefs as $childDef) {
  1584. /* @var $childDef NodeDefinitionInterface */
  1585. $this->conn->insert('phpcr_type_childs', array(
  1586. 'node_type_id' => $nodeTypeId,
  1587. 'name' => $childDef->getName(),
  1588. 'protected' => $childDef->isProtected() ? 1 : 0,
  1589. 'mandatory' => $childDef->isMandatory() ? 1 : 0,
  1590. 'auto_created' => $childDef->isAutoCreated() ? 1 : 0,
  1591. 'on_parent_version' => $childDef->getOnParentVersion(),
  1592. 'primary_types' => implode(' ', $childDef->getRequiredPrimaryTypeNames() ?: array()),
  1593. 'default_type' => $childDef->getDefaultPrimaryTypeName(),
  1594. ));
  1595. }
  1596. }
  1597. }
  1598. }
  1599. /**
  1600. * {@inheritDoc}
  1601. */
  1602. public function setNodeTypeManager($nodeTypeManager)
  1603. {
  1604. $this->nodeTypeManager = $nodeTypeManager;
  1605. }
  1606. /**
  1607. * {@inheritDoc}
  1608. */
  1609. public function cloneFrom($srcWorkspace, $srcAbsPath, $destAbsPath, $removeExisting)
  1610. {
  1611. throw new NotImplementedException('Cloning nodes is not implemented yet');
  1612. }
  1613. /**
  1614. * {@inheritDoc}
  1615. */
  1616. public function updateNode(Node $node, $srcWorkspace)
  1617. {
  1618. throw new NotImplementedException('Updating nodes is not implemented yet');
  1619. }
  1620. /**
  1621. * {@inheritDoc}
  1622. */
  1623. public function getBinaryStream($path)
  1624. {
  1625. $this->assertLoggedIn();
  1626. $nodePath = PathHelper::getParentPath($path);
  1627. $nodeId = $this->pathExists($nodePath);
  1628. $propertyName = PathHelper::getNodeName($path);
  1629. $data = $this->conn->fetchAll(
  1630. 'SELECT data, idx FROM phpcr_binarydata WHERE node_id = ? AND property_name = ? AND workspace_name = ?',
  1631. array($nodeId, $propertyName, $this->workspaceName)
  1632. );
  1633. $streams = array();
  1634. foreach ($data as $row) {
  1635. if (is_resource($row['data'])) {
  1636. $stream = $row['data'];
  1637. } else {
  1638. $stream = fopen('php://memory', 'rwb+');
  1639. fwrite($stream, $row['data']);
  1640. rewind($stream);
  1641. }
  1642. $streams[] = $stream;
  1643. }
  1644. // TODO even a multi value field could have only one value stored
  1645. // we need to also fetch if the property is multi valued instead of this count() check
  1646. if (count($data) > 1) {
  1647. return $streams;
  1648. }
  1649. return reset($streams);
  1650. }
  1651. /**
  1652. * {@inheritDoc}
  1653. */
  1654. public function getProperty($path)
  1655. {
  1656. throw new NotImplementedException('Getting properties by path is implemented yet');
  1657. }
  1658. /**
  1659. * {@inheritDoc}
  1660. */
  1661. public function query(Query $query)
  1662. {
  1663. $this->assertLoggedIn();
  1664. $limit = $query->getLimit();
  1665. $offset = $query->getOffset();
  1666. if (null !== $offset && null == $limit &&
  1667. ($this->conn->getDatabasePlatform() instanceof MySqlPlatform
  1668. || $this->conn->getDatabasePlatform() instanceof SqlitePlatform
  1669. )
  1670. ) {
  1671. $limit = PHP_INT_MAX;
  1672. }
  1673. if (!$query instanceof QueryObjectModelInterface) {
  1674. $parser = new Sql2ToQomQueryConverter($this->factory->get('Query\QOM\QueryObjectModelFactory'));
  1675. try {
  1676. $query = $parser->parse($query->getStatement());
  1677. } catch (\Exception $e) {
  1678. throw new InvalidQueryException('Invalid query: '.$query->getStatement());
  1679. }
  1680. }
  1681. $source = $query->getSource();
  1682. $this->validateSource($source);
  1683. $qomWalker = new QOMWalker($this->nodeTypeManager, $this->conn, $this->getNamespaces());
  1684. $sql = $qomWalker->walkQOMQuery($query);
  1685. $sql = $this->conn->getDatabasePlatform()->modifyLimitQuery($sql, $limit, $offset);
  1686. $data = $this->conn->fetchAll($sql, array($this->workspaceName));
  1687. // The list of columns is required to filter each records props
  1688. $columns = array();
  1689. /** @var $column ColumnInterface */
  1690. foreach ($query->getColumns() as $column) {
  1691. $columns[$column->getPropertyName()] = $column->getSelectorName();
  1692. }
  1693. $selectors = array();
  1694. if ($source instanceOf SelectorInterface) {
  1695. $selectors[] = $source;
  1696. $mainSelector = $source;
  1697. } elseif ($source instanceof JoinInterface) {
  1698. if ($source->getLeft() instanceOf SelectorInterface) {
  1699. $selectors[] = $source->getLeft();
  1700. $mainSelector = $source->getLeft();
  1701. }
  1702. if ($source->getRight() instanceOf SelectorInterface) {
  1703. $selectors[] = $source->getRight();
  1704. }
  1705. }
  1706. if (!isset($mainSelector)) {
  1707. throw new InvalidQueryException('The source needs to be a Selector or a Join with left as Selector');
  1708. }
  1709. $mainSelectorName = $mainSelector->getSelectorName();
  1710. if (null === $mainSelectorName) {
  1711. $mainSelectorName = $mainSelector->getNodeTypeName();
  1712. }
  1713. if (empty($columns)) {
  1714. $columns = array(
  1715. 'jcr:createdBy' => $mainSelectorName,
  1716. 'jcr:created' => $mainSelectorName,
  1717. );
  1718. }
  1719. $columns['jcr:primaryType'] = $mainSelectorName;
  1720. $results = array();
  1721. foreach ($data as $row) {
  1722. $result = array();
  1723. /** @var SelectorInterface $selector */
  1724. foreach ($selectors as $selector) {
  1725. $columnPrefix = null !== $selector->getSelectorName() ? $selector->getSelectorName() . '_' : '';
  1726. $selectorPrefix = null !== $selector->getSelectorName() ? $selector->getSelectorName() . '.' : '';
  1727. $selectorName = $selector->getSelectorName() ?: $selector->getNodeTypeName();
  1728. $result[] = array('dcr:name' => $selectorPrefix . 'jcr:path', 'dcr:value' => $row[$columnPrefix . 'path'], 'dcr:selectorName' => $selectorName);
  1729. $result[] = array('dcr:name' => $selectorPrefix . 'jcr:score', 'dcr:value' => 0, 'dcr:selectorName' => $selectorName);
  1730. }
  1731. $properties = array();
  1732. foreach ($columns as $columnName => $columnAlias) {
  1733. $columnPrefix = null !== $mainSelector->getSelectorName() ? $columnAlias . '_' : '';
  1734. if (!isset($properties[$columnAlias])) {
  1735. if (isset($row[$columnPrefix . 'props'])) {
  1736. // extract only the properties that have been requested in the query
  1737. $properties[$columnAlias] = static::xmlToProps(
  1738. $row[$columnPrefix . 'props'],
  1739. $this->valueConverter,
  1740. function ($name) use ($columns, $columnAlias, $columnName) {
  1741. return array_key_exists($name, $columns) && $columns[$name] === $columnAlias;
  1742. }
  1743. );
  1744. } else { // props field is empty, can happen with OUTER joins
  1745. $properties[$columnAlias] = array();
  1746. }
  1747. }
  1748. $props = (array) $properties[$columnAlias];
  1749. $dcrValue = null;
  1750. if ('jcr:uuid' === $columnName) {
  1751. $dcrValue = $row[$columnPrefix . 'identifier'];
  1752. }
  1753. $result[] = array(
  1754. 'dcr:name' => null === $columnAlias ? $columnName : "{$columnAlias}.{$columnName}",
  1755. 'dcr:value' => null !== $dcrValue ? $dcrValue : (array_key_exists($columnName, $props) ? $props[$columnName] : null),
  1756. 'dcr:selectorName' => $columnAlias ?: $mainSelectorName,
  1757. );
  1758. }
  1759. $results[] = $result;
  1760. }
  1761. return $results;
  1762. }
  1763. /**
  1764. * {@inheritDoc}
  1765. */
  1766. public function getSupportedQueryLanguages()
  1767. {
  1768. return array(
  1769. QueryInterface::JCR_SQL2,
  1770. QueryInterface::JCR_JQOM,
  1771. );
  1772. }
  1773. /**
  1774. * {@inheritDoc}
  1775. */
  1776. public function registerNamespace($prefix, $uri)
  1777. {
  1778. if (isset($this->namespaces[$prefix]) && $this->namespaces[$prefix] === $uri) {
  1779. return;
  1780. }
  1781. $this->conn->delete('phpcr_namespaces', array('prefix' => $prefix));
  1782. $this->conn->delete('phpcr_namespaces', array('uri' => $uri));
  1783. $this->conn->insert('phpcr_namespaces', array(
  1784. 'prefix' => $prefix,
  1785. 'uri' => $uri,
  1786. ));
  1787. if (!empty($this->namespaces)) {
  1788. $this->namespaces[$prefix] = $uri;
  1789. }
  1790. }
  1791. /**
  1792. * {@inheritDoc}
  1793. */
  1794. public function unregisterNamespace($prefix)
  1795. {
  1796. $this->conn->delete('phpcr_namespaces', array('prefix' => $prefix));
  1797. if (!empty($this->namespaces)) {
  1798. unset($this->namespaces[$prefix]);
  1799. }
  1800. }
  1801. /**
  1802. * {@inheritDoc}
  1803. */
  1804. public function getReferences($path, $name = null)
  1805. {
  1806. return $this->getNodeReferences($path, $name, false);
  1807. }
  1808. /**
  1809. * {@inheritDoc}
  1810. */
  1811. public function getWeakReferences($path, $name = null)
  1812. {
  1813. return $this->getNodeReferences($path, $name, true);
  1814. }
  1815. /**
  1816. * @param string $path the path for which we need the references
  1817. * @param string $name the name of the referencing properties or null for all
  1818. * @param boolean $weak_reference whether to get weak or strong references
  1819. *
  1820. * @return array list of paths to nodes that reference $path
  1821. */
  1822. private function getNodeReferences($path, $name = null, $weakReference = false)
  1823. {
  1824. $targetId = $this->pathExists($path);
  1825. $params = array($targetId);
  1826. $table = $weakReference ? $this->referenceTables[PropertyType::WEAKREFERENCE] : $this->referenceTables[PropertyType::REFERENCE];
  1827. $query = "SELECT CONCAT(n.path, '/', r.source_property_name) FROM phpcr_nodes n
  1828. INNER JOIN $table r ON n.id = r.source_id
  1829. WHERE r.target_id = ?";
  1830. if (null !== $name) {
  1831. $query.= " AND source_property_name = ?";
  1832. $params[] = $name;
  1833. }
  1834. $stmt = $this->conn->executeQuery($query, $params);
  1835. return $stmt->fetchAll(\PDO::FETCH_COLUMN);
  1836. }
  1837. /**
  1838. * {@inheritDoc}
  1839. */
  1840. public function beginTransaction()
  1841. {
  1842. if ($this->inTransaction) {
  1843. throw new RepositoryException('Begin transaction failed: transaction already open');
  1844. }
  1845. $this->assertLoggedIn();
  1846. try {
  1847. $this->conn->beginTransaction();
  1848. $this->inTransaction = true;
  1849. } catch (\Exception $e) {
  1850. throw new RepositoryException('Begin transaction failed: '.$e->getMessage());
  1851. }
  1852. }
  1853. /**
  1854. * {@inheritDoc}
  1855. */
  1856. public function commitTransaction()
  1857. {
  1858. if (!$this->inTransaction) {
  1859. throw new RepositoryException('Commit transaction failed: no transaction open');
  1860. }
  1861. $this->assertLoggedIn();
  1862. try {
  1863. $this->inTransaction = false;
  1864. $this->conn->commit();
  1865. } catch (\Exception $e) {
  1866. throw new RepositoryException('Commit transaction failed: ' . $e->getMessage());
  1867. }
  1868. }
  1869. /**
  1870. * {@inheritDoc}
  1871. */
  1872. public function rollbackTransaction()
  1873. {
  1874. if (!$this->inTransaction) {
  1875. throw new RepositoryException('Rollback transaction failed: no transaction open');
  1876. }
  1877. $this->assertLoggedIn();
  1878. try {
  1879. $this->inTransaction = false;
  1880. $this->namespaces = array();
  1881. $this->conn->rollback();
  1882. } catch (\Exception $e) {
  1883. throw new RepositoryException('Rollback transaction failed: ' . $e->getMessage());
  1884. }
  1885. }
  1886. /**
  1887. * Sets the default transaction timeout
  1888. *
  1889. * @param int $seconds The value of the timeout in seconds
  1890. */
  1891. public function setTransactionTimeout($seconds)
  1892. {
  1893. $this->assertLoggedIn();
  1894. throw new NotImplementedException("Setting a transaction timeout is not yet implemented");
  1895. }
  1896. /**
  1897. * {@inheritDoc}
  1898. */
  1899. public function prepareSave()
  1900. {
  1901. $this->conn->beginTransaction();
  1902. }
  1903. /**
  1904. * {@inheritDoc}
  1905. */
  1906. public function finishSave()
  1907. {
  1908. $this->syncReferences();
  1909. $this->referencesToUpdate = $this->referencesToDelete = array();
  1910. $this->conn->commit();
  1911. }
  1912. /**
  1913. * {@inheritDoc}
  1914. */
  1915. public function rollbackSave()
  1916. {
  1917. $this->referencesToUpdate = $this->referencesToDelete = array();
  1918. $this->conn->rollback();
  1919. }
  1920. /**
  1921. * Validates the nodeTypes in given source
  1922. *
  1923. * @param SourceInterface $source
  1924. *
  1925. * @throws InvalidQueryException
  1926. */
  1927. protected function validateSource(SourceInterface $source)
  1928. {
  1929. if ($source instanceOf SelectorInterface) {
  1930. $this->validateSelectorSource($source);
  1931. } elseif ($source instanceOf JoinInterface) {
  1932. $this->validateJoinSource($source);
  1933. }
  1934. }
  1935. /**
  1936. * @param SelectorInterface $source
  1937. *
  1938. * @throws InvalidQueryException
  1939. */
  1940. protected function validateSelectorSource(SelectorInterface $source)
  1941. {
  1942. $nodeType = $source->getNodeTypeName();
  1943. if (!$this->nodeTypeManager->hasNodeType($nodeType)) {
  1944. $msg = 'Selected node type does not exist: ' . $nodeType;
  1945. if ($alias = $source->getSelectorName()) {
  1946. $msg .= ' AS ' . $alias;
  1947. }
  1948. throw new InvalidQueryException($msg);
  1949. }
  1950. }
  1951. /**
  1952. * @param JoinInterface $source
  1953. *
  1954. * @throws InvalidQueryException
  1955. */
  1956. protected function validateJoinSource(JoinInterface $source)
  1957. {
  1958. $left = $source->getLeft();
  1959. $right = $source->getRight();
  1960. if ($left) {
  1961. $this->validateSource($left);
  1962. }
  1963. if ($right) {
  1964. $this->validateSource($right);
  1965. }
  1966. }
  1967. }