/libraries/joomla/table/nested.php

https://bitbucket.org/eternaware/joomus · PHP · 1606 lines · 889 code · 201 blank · 516 comment · 103 complexity · 05d12d6e620248760a9bc5ad4480ea11 MD5 · raw file

  1. <?php
  2. /**
  3. * @package Joomla.Platform
  4. * @subpackage Table
  5. *
  6. * @copyright Copyright (C) 2005 - 2012 Open Source Matters, Inc. All rights reserved.
  7. * @license GNU General Public License version 2 or later; see LICENSE
  8. */
  9. defined('JPATH_PLATFORM') or die;
  10. /**
  11. * Table class supporting modified pre-order tree traversal behavior.
  12. *
  13. * @package Joomla.Platform
  14. * @subpackage Table
  15. * @link http://docs.joomla.org/JTableNested
  16. * @since 11.1
  17. */
  18. class JTableNested extends JTable
  19. {
  20. /**
  21. * Object property holding the primary key of the parent node. Provides
  22. * adjacency list data for nodes.
  23. *
  24. * @var integer
  25. * @since 11.1
  26. */
  27. public $parent_id;
  28. /**
  29. * Object property holding the depth level of the node in the tree.
  30. *
  31. * @var integer
  32. * @since 11.1
  33. */
  34. public $level;
  35. /**
  36. * Object property holding the left value of the node for managing its
  37. * placement in the nested sets tree.
  38. *
  39. * @var integer
  40. * @since 11.1
  41. */
  42. public $lft;
  43. /**
  44. * Object property holding the right value of the node for managing its
  45. * placement in the nested sets tree.
  46. *
  47. * @var integer
  48. * @since 11.1
  49. */
  50. public $rgt;
  51. /**
  52. * Object property holding the alias of this node used to constuct the
  53. * full text path, forward-slash delimited.
  54. *
  55. * @var string
  56. * @since 11.1
  57. */
  58. public $alias;
  59. /**
  60. * Object property to hold the location type to use when storing the row.
  61. * Possible values are: ['before', 'after', 'first-child', 'last-child'].
  62. *
  63. * @var string
  64. * @since 11.1
  65. */
  66. protected $_location;
  67. /**
  68. * Object property to hold the primary key of the location reference node to
  69. * use when storing the row. A combination of location type and reference
  70. * node describes where to store the current node in the tree.
  71. *
  72. * @var integer
  73. * @since 11.1
  74. */
  75. protected $_location_id;
  76. /**
  77. * An array to cache values in recursive processes.
  78. *
  79. * @var array
  80. * @since 11.1
  81. */
  82. protected $_cache = array();
  83. /**
  84. * Debug level
  85. *
  86. * @var integer
  87. * @since 11.1
  88. */
  89. protected $_debug = 0;
  90. /**
  91. * Sets the debug level on or off
  92. *
  93. * @param integer $level 0 = off, 1 = on
  94. *
  95. * @return void
  96. *
  97. * @since 11.1
  98. */
  99. public function debug($level)
  100. {
  101. $this->_debug = (int) $level;
  102. }
  103. /**
  104. * Method to get an array of nodes from a given node to its root.
  105. *
  106. * @param integer $pk Primary key of the node for which to get the path.
  107. * @param boolean $diagnostic Only select diagnostic data for the nested sets.
  108. *
  109. * @return mixed An array of node objects including the start node.
  110. *
  111. * @since 11.1
  112. * @throws RuntimeException on database error
  113. */
  114. public function getPath($pk = null, $diagnostic = false)
  115. {
  116. $k = $this->_tbl_key;
  117. $pk = (is_null($pk)) ? $this->$k : $pk;
  118. // Get the path from the node to the root.
  119. $query = $this->_db->getQuery(true);
  120. $select = ($diagnostic) ? 'p.' . $k . ', p.parent_id, p.level, p.lft, p.rgt' : 'p.*';
  121. $query->select($select)
  122. ->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p')
  123. ->where('n.lft BETWEEN p.lft AND p.rgt')
  124. ->where('n.' . $k . ' = ' . (int) $pk)
  125. ->order('p.lft');
  126. $this->_db->setQuery($query);
  127. return $this->_db->loadObjectList();
  128. }
  129. /**
  130. * Method to get a node and all its child nodes.
  131. *
  132. * @param integer $pk Primary key of the node for which to get the tree.
  133. * @param boolean $diagnostic Only select diagnostic data for the nested sets.
  134. *
  135. * @return mixed Boolean false on failure or array of node objects on success.
  136. *
  137. * @since 11.1
  138. * @throws RuntimeException on database error.
  139. */
  140. public function getTree($pk = null, $diagnostic = false)
  141. {
  142. $k = $this->_tbl_key;
  143. $pk = (is_null($pk)) ? $this->$k : $pk;
  144. // Get the node and children as a tree.
  145. $query = $this->_db->getQuery(true);
  146. $select = ($diagnostic) ? 'n.' . $k . ', n.parent_id, n.level, n.lft, n.rgt' : 'n.*';
  147. $query->select($select)
  148. ->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p')
  149. ->where('n.lft BETWEEN p.lft AND p.rgt')
  150. ->where('p.' . $k . ' = ' . (int) $pk)
  151. ->order('n.lft');
  152. return $this->_db->setQuery($query)->loadObjectList();
  153. }
  154. /**
  155. * Method to determine if a node is a leaf node in the tree (has no children).
  156. *
  157. * @param integer $pk Primary key of the node to check.
  158. *
  159. * @return boolean True if a leaf node, false if not or null if the node does not exist.
  160. *
  161. * @note Since 12.1 this method returns null if the node does not exist.
  162. * @since 11.1
  163. * @throws RuntimeException on database error.
  164. */
  165. public function isLeaf($pk = null)
  166. {
  167. $k = $this->_tbl_key;
  168. $pk = (is_null($pk)) ? $this->$k : $pk;
  169. $node = $this->_getNode($pk);
  170. // Get the node by primary key.
  171. if (empty($node))
  172. {
  173. // Error message set in getNode method.
  174. return null;
  175. }
  176. // The node is a leaf node.
  177. return (($node->rgt - $node->lft) == 1);
  178. }
  179. /**
  180. * Method to set the location of a node in the tree object. This method does not
  181. * save the new location to the database, but will set it in the object so
  182. * that when the node is stored it will be stored in the new location.
  183. *
  184. * @param integer $referenceId The primary key of the node to reference new location by.
  185. * @param string $position Location type string. ['before', 'after', 'first-child', 'last-child']
  186. *
  187. * @return void
  188. *
  189. * @note Since 12.1 this method returns void and throws an InvalidArgumentException when an invalid position is passed.
  190. * @since 11.1
  191. * @throws InvalidArgumentException
  192. */
  193. public function setLocation($referenceId, $position = 'after')
  194. {
  195. // Make sure the location is valid.
  196. if (($position != 'before') && ($position != 'after') && ($position != 'first-child') && ($position != 'last-child'))
  197. {
  198. throw new InvalidArgumentException(sprintf('%s::setLocation(%d, *%s*)', get_class($this), $referenceId, $position));
  199. }
  200. // Set the location properties.
  201. $this->_location = $position;
  202. $this->_location_id = $referenceId;
  203. }
  204. /**
  205. * Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
  206. * Negative numbers move the row up in the sequence and positive numbers move it down.
  207. *
  208. * @param integer $delta The direction and magnitude to move the row in the ordering sequence.
  209. * @param string $where WHERE clause to use for limiting the selection of rows to compact the
  210. * ordering values.
  211. *
  212. * @return mixed Boolean true on success.
  213. *
  214. * @link http://docs.joomla.org/JTable/move
  215. * @since 11.1
  216. */
  217. public function move($delta, $where = '')
  218. {
  219. $k = $this->_tbl_key;
  220. $pk = $this->$k;
  221. $query = $this->_db->getQuery(true);
  222. $query->select($k);
  223. $query->from($this->_tbl);
  224. $query->where('parent_id = ' . $this->parent_id);
  225. if ($where)
  226. {
  227. $query->where($where);
  228. }
  229. $position = 'after';
  230. if ($delta > 0)
  231. {
  232. $query->where('rgt > ' . $this->rgt);
  233. $query->order('rgt ASC');
  234. $position = 'after';
  235. }
  236. else
  237. {
  238. $query->where('lft < ' . $this->lft);
  239. $query->order('lft DESC');
  240. $position = 'before';
  241. }
  242. $this->_db->setQuery($query);
  243. $referenceId = $this->_db->loadResult();
  244. if ($referenceId)
  245. {
  246. return $this->moveByReference($referenceId, $position, $pk);
  247. }
  248. else
  249. {
  250. return false;
  251. }
  252. }
  253. /**
  254. * Method to move a node and its children to a new location in the tree.
  255. *
  256. * @param integer $referenceId The primary key of the node to reference new location by.
  257. * @param string $position Location type string. ['before', 'after', 'first-child', 'last-child']
  258. * @param integer $pk The primary key of the node to move.
  259. *
  260. * @return boolean True on success.
  261. *
  262. * @link http://docs.joomla.org/JTableNested/moveByReference
  263. * @since 11.1
  264. * @throws RuntimeException on database error.
  265. */
  266. public function moveByReference($referenceId, $position = 'after', $pk = null)
  267. {
  268. // @codeCoverageIgnoreStart
  269. if ($this->_debug)
  270. {
  271. echo "\nMoving ReferenceId:$referenceId, Position:$position, PK:$pk";
  272. }
  273. // @codeCoverageIgnoreEnd
  274. $k = $this->_tbl_key;
  275. $pk = (is_null($pk)) ? $this->$k : $pk;
  276. // Get the node by id.
  277. if (!$node = $this->_getNode($pk))
  278. {
  279. // Error message set in getNode method.
  280. return false;
  281. }
  282. // Get the ids of child nodes.
  283. $query = $this->_db->getQuery(true);
  284. $query->select($k)
  285. ->from($this->_tbl)
  286. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  287. $children = $this->_db->setQuery($query)->loadColumn();
  288. // @codeCoverageIgnoreStart
  289. if ($this->_debug)
  290. {
  291. $this->_logtable(false);
  292. }
  293. // @codeCoverageIgnoreEnd
  294. // Cannot move the node to be a child of itself.
  295. if (in_array($referenceId, $children))
  296. {
  297. $e = new UnexpectedValueException(
  298. sprintf('%s::moveByReference(%d, %s, %d) parenting to child.', get_class($this), $referenceId, $position, $pk)
  299. );
  300. $this->setError($e);
  301. return false;
  302. }
  303. // Lock the table for writing.
  304. if (!$this->_lock())
  305. {
  306. return false;
  307. }
  308. /*
  309. * Move the sub-tree out of the nested sets by negating its left and right values.
  310. */
  311. $query = $this->_db->getQuery(true);
  312. $query->update($this->_tbl)
  313. ->set('lft = lft * (-1), rgt = rgt * (-1)')
  314. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  315. $this->_db->setQuery($query);
  316. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  317. /*
  318. * Close the hole in the tree that was opened by removing the sub-tree from the nested sets.
  319. */
  320. // Compress the left values.
  321. $query = $this->_db->getQuery(true);
  322. $query->update($this->_tbl)
  323. ->set('lft = lft - ' . (int) $node->width)
  324. ->where('lft > ' . (int) $node->rgt);
  325. $this->_db->setQuery($query);
  326. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  327. // Compress the right values.
  328. $query = $this->_db->getQuery(true);
  329. $query->update($this->_tbl)
  330. ->set('rgt = rgt - ' . (int) $node->width)
  331. ->where('rgt > ' . (int) $node->rgt);
  332. $this->_db->setQuery($query);
  333. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  334. // We are moving the tree relative to a reference node.
  335. if ($referenceId)
  336. {
  337. // Get the reference node by primary key.
  338. if (!$reference = $this->_getNode($referenceId))
  339. {
  340. // Error message set in getNode method.
  341. $this->_unlock();
  342. return false;
  343. }
  344. // Get the reposition data for shifting the tree and re-inserting the node.
  345. if (!$repositionData = $this->_getTreeRepositionData($reference, $node->width, $position))
  346. {
  347. // Error message set in getNode method.
  348. $this->_unlock();
  349. return false;
  350. }
  351. }
  352. // We are moving the tree to be the last child of the root node
  353. else
  354. {
  355. // Get the last root node as the reference node.
  356. $query = $this->_db->getQuery(true);
  357. $query->select($this->_tbl_key . ', parent_id, level, lft, rgt')
  358. ->from($this->_tbl)
  359. ->where('parent_id = 0')
  360. ->order('lft DESC');
  361. $this->_db->setQuery($query, 0, 1);
  362. $reference = $this->_db->loadObject();
  363. // @codeCoverageIgnoreStart
  364. if ($this->_debug)
  365. {
  366. $this->_logtable(false);
  367. }
  368. // @codeCoverageIgnoreEnd
  369. // Get the reposition data for re-inserting the node after the found root.
  370. if (!$repositionData = $this->_getTreeRepositionData($reference, $node->width, 'last-child'))
  371. {
  372. // Error message set in getNode method.
  373. $this->_unlock();
  374. return false;
  375. }
  376. }
  377. /*
  378. * Create space in the nested sets at the new location for the moved sub-tree.
  379. */
  380. // Shift left values.
  381. $query = $this->_db->getQuery(true);
  382. $query->update($this->_tbl)
  383. ->set('lft = lft + ' . (int) $node->width)
  384. ->where($repositionData->left_where);
  385. $this->_db->setQuery($query);
  386. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  387. // Shift right values.
  388. $query = $this->_db->getQuery(true);
  389. $query->update($this->_tbl)
  390. ->set('rgt = rgt + ' . (int) $node->width)
  391. ->where($repositionData->right_where);
  392. $this->_db->setQuery($query);
  393. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  394. /*
  395. * Calculate the offset between where the node used to be in the tree and
  396. * where it needs to be in the tree for left ids (also works for right ids).
  397. */
  398. $offset = $repositionData->new_lft - $node->lft;
  399. $levelOffset = $repositionData->new_level - $node->level;
  400. // Move the nodes back into position in the tree using the calculated offsets.
  401. $query = $this->_db->getQuery(true);
  402. $query->update($this->_tbl)
  403. ->set('rgt = ' . (int) $offset . ' - rgt')
  404. ->set('lft = ' . (int) $offset . ' - lft')
  405. ->set('level = level + ' . (int) $levelOffset)
  406. ->where('lft < 0');
  407. $this->_db->setQuery($query);
  408. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  409. // Set the correct parent id for the moved node if required.
  410. if ($node->parent_id != $repositionData->new_parent_id)
  411. {
  412. $query = $this->_db->getQuery(true);
  413. $query->update($this->_tbl);
  414. // Update the title and alias fields if they exist for the table.
  415. if (property_exists($this, 'title') && $this->title !== null)
  416. {
  417. $query->set('title = ' . $this->_db->Quote($this->title));
  418. }
  419. if (property_exists($this, 'alias') && $this->alias !== null)
  420. {
  421. $query->set('alias = ' . $this->_db->Quote($this->alias));
  422. }
  423. $query->set('parent_id = ' . (int) $repositionData->new_parent_id)
  424. ->where($this->_tbl_key . ' = ' . (int) $node->$k);
  425. $this->_db->setQuery($query);
  426. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  427. }
  428. // Unlock the table for writing.
  429. $this->_unlock();
  430. // Set the object values.
  431. $this->parent_id = $repositionData->new_parent_id;
  432. $this->level = $repositionData->new_level;
  433. $this->lft = $repositionData->new_lft;
  434. $this->rgt = $repositionData->new_rgt;
  435. return true;
  436. }
  437. /**
  438. * Method to delete a node and, optionally, its child nodes from the table.
  439. *
  440. * @param integer $pk The primary key of the node to delete.
  441. * @param boolean $children True to delete child nodes, false to move them up a level.
  442. *
  443. * @return boolean True on success.
  444. *
  445. * @since 11.1
  446. */
  447. public function delete($pk = null, $children = true)
  448. {
  449. $k = $this->_tbl_key;
  450. $pk = (is_null($pk)) ? $this->$k : $pk;
  451. // Lock the table for writing.
  452. if (!$this->_lock())
  453. {
  454. // Error message set in lock method.
  455. return false;
  456. }
  457. // If tracking assets, remove the asset first.
  458. if ($this->_trackAssets)
  459. {
  460. $name = $this->_getAssetName();
  461. $asset = JTable::getInstance('Asset');
  462. // Lock the table for writing.
  463. if (!$asset->_lock())
  464. {
  465. // Error message set in lock method.
  466. return false;
  467. }
  468. if ($asset->loadByName($name))
  469. {
  470. // Delete the node in assets table.
  471. if (!$asset->delete(null, $children))
  472. {
  473. $this->setError($asset->getError());
  474. $asset->_unlock();
  475. return false;
  476. }
  477. $asset->_unlock();
  478. }
  479. else
  480. {
  481. $this->setError($asset->getError());
  482. $asset->_unlock();
  483. return false;
  484. }
  485. }
  486. // Get the node by id.
  487. $node = $this->_getNode($pk);
  488. if (empty($node))
  489. {
  490. // Error message set in getNode method.
  491. $this->_unlock();
  492. return false;
  493. }
  494. // Should we delete all children along with the node?
  495. if ($children)
  496. {
  497. // Delete the node and all of its children.
  498. $query = $this->_db->getQuery(true);
  499. $query->delete()
  500. ->from($this->_tbl)
  501. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  502. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  503. // Compress the left values.
  504. $query = $this->_db->getQuery(true);
  505. $query->update($this->_tbl)
  506. ->set('lft = lft - ' . (int) $node->width)
  507. ->where('lft > ' . (int) $node->rgt);
  508. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  509. // Compress the right values.
  510. $query = $this->_db->getQuery(true);
  511. $query->update($this->_tbl)
  512. ->set('rgt = rgt - ' . (int) $node->width)
  513. ->where('rgt > ' . (int) $node->rgt);
  514. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  515. }
  516. // Leave the children and move them up a level.
  517. else
  518. {
  519. // Delete the node.
  520. $query = $this->_db->getQuery(true);
  521. $query->delete()
  522. ->from($this->_tbl)
  523. ->where('lft = ' . (int) $node->lft);
  524. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  525. // Shift all node's children up a level.
  526. $query->clear()
  527. ->update($this->_tbl)
  528. ->set('lft = lft - 1')
  529. ->set('rgt = rgt - 1')
  530. ->set('level = level - 1')
  531. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  532. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  533. // Adjust all the parent values for direct children of the deleted node.
  534. $query->clear()
  535. ->update($this->_tbl)
  536. ->set('parent_id = ' . (int) $node->parent_id)
  537. ->where('parent_id = ' . (int) $node->$k);
  538. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  539. // Shift all of the left values that are right of the node.
  540. $query->clear()
  541. ->update($this->_tbl)
  542. ->set('lft = lft - 2')
  543. ->where('lft > ' . (int) $node->rgt);
  544. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  545. // Shift all of the right values that are right of the node.
  546. $query->clear()
  547. ->update($this->_tbl)
  548. ->set('rgt = rgt - 2')
  549. ->where('rgt > ' . (int) $node->rgt);
  550. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  551. }
  552. // Unlock the table for writing.
  553. $this->_unlock();
  554. return true;
  555. }
  556. /**
  557. * Checks that the object is valid and able to be stored.
  558. *
  559. * This method checks that the parent_id is non-zero and exists in the database.
  560. * Note that the root node (parent_id = 0) cannot be manipulated with this class.
  561. *
  562. * @return boolean True if all checks pass.
  563. *
  564. * @since 11.1
  565. * @throws RuntimeException on database error.
  566. */
  567. public function check()
  568. {
  569. $this->parent_id = (int) $this->parent_id;
  570. // Set up a mini exception handler.
  571. try
  572. {
  573. // Check that the parent_id field is valid.
  574. if ($this->parent_id == 0)
  575. {
  576. throw new UnexpectedValueException(sprintf('Invalid `parent_id` [%d] in %s', $this->parent_id, get_class($this)));
  577. }
  578. $query = $this->_db->getQuery(true);
  579. $query->select('COUNT(' . $this->_tbl_key . ')')
  580. ->from($this->_tbl)
  581. ->where($this->_tbl_key . ' = ' . $this->parent_id);
  582. if (!$this->_db->setQuery($query)->loadResult())
  583. {
  584. throw new UnexpectedValueException(sprintf('Invalid `parent_id` [%d] in %s', $this->parent_id, get_class($this)));
  585. }
  586. }
  587. catch (UnexpectedValueException $e)
  588. {
  589. // Validation error - record it and return false.
  590. $this->setError($e);
  591. return false;
  592. }
  593. // @codeCoverageIgnoreStart
  594. catch (Exception $e)
  595. {
  596. // Database error - rethrow.
  597. throw $e;
  598. }
  599. // @codeCoverageIgnoreEnd
  600. return true;
  601. }
  602. /**
  603. * Method to store a node in the database table.
  604. *
  605. * @param boolean $updateNulls True to update null values as well.
  606. *
  607. * @return boolean True on success.
  608. *
  609. * @link http://docs.joomla.org/JTableNested/store
  610. * @since 11.1
  611. */
  612. public function store($updateNulls = false)
  613. {
  614. $k = $this->_tbl_key;
  615. // @codeCoverageIgnoreStart
  616. if ($this->_debug)
  617. {
  618. echo "\n" . get_class($this) . "::store\n";
  619. $this->_logtable(true, false);
  620. }
  621. // @codeCoverageIgnoreEnd
  622. /*
  623. * If the primary key is empty, then we assume we are inserting a new node into the
  624. * tree. From this point we would need to determine where in the tree to insert it.
  625. */
  626. if (empty($this->$k))
  627. {
  628. /*
  629. * We are inserting a node somewhere in the tree with a known reference
  630. * node. We have to make room for the new node and set the left and right
  631. * values before we insert the row.
  632. */
  633. if ($this->_location_id >= 0)
  634. {
  635. // Lock the table for writing.
  636. if (!$this->_lock())
  637. {
  638. // Error message set in lock method.
  639. return false;
  640. }
  641. // We are inserting a node relative to the last root node.
  642. if ($this->_location_id == 0)
  643. {
  644. // Get the last root node as the reference node.
  645. $query = $this->_db->getQuery(true);
  646. $query->select($this->_tbl_key . ', parent_id, level, lft, rgt')
  647. ->from($this->_tbl)
  648. ->where('parent_id = 0')
  649. ->order('lft DESC');
  650. $this->_db->setQuery($query, 0, 1);
  651. $reference = $this->_db->loadObject();
  652. // @codeCoverageIgnoreStart
  653. if ($this->_debug)
  654. {
  655. $this->_logtable(false);
  656. }
  657. // @codeCoverageIgnoreEnd
  658. }
  659. // We have a real node set as a location reference.
  660. else
  661. {
  662. // Get the reference node by primary key.
  663. if (!$reference = $this->_getNode($this->_location_id))
  664. {
  665. // Error message set in getNode method.
  666. $this->_unlock();
  667. return false;
  668. }
  669. }
  670. // Get the reposition data for shifting the tree and re-inserting the node.
  671. if (!($repositionData = $this->_getTreeRepositionData($reference, 2, $this->_location)))
  672. {
  673. // Error message set in getNode method.
  674. $this->_unlock();
  675. return false;
  676. }
  677. // Create space in the tree at the new location for the new node in left ids.
  678. $query = $this->_db->getQuery(true);
  679. $query->update($this->_tbl)
  680. ->set('lft = lft + 2')
  681. ->where($repositionData->left_where);
  682. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED');
  683. // Create space in the tree at the new location for the new node in right ids.
  684. $query = $this->_db->getQuery(true);
  685. $query->update($this->_tbl)
  686. ->set('rgt = rgt + 2')
  687. ->where($repositionData->right_where);
  688. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED');
  689. // Set the object values.
  690. $this->parent_id = $repositionData->new_parent_id;
  691. $this->level = $repositionData->new_level;
  692. $this->lft = $repositionData->new_lft;
  693. $this->rgt = $repositionData->new_rgt;
  694. }
  695. else
  696. {
  697. // Negative parent ids are invalid
  698. $e = new UnexpectedValueException(sprintf('%s::store() used a negative _location_id', get_class($this)));
  699. $this->setError($e);
  700. return false;
  701. }
  702. }
  703. /*
  704. * If we have a given primary key then we assume we are simply updating this
  705. * node in the tree. We should assess whether or not we are moving the node
  706. * or just updating its data fields.
  707. */
  708. else
  709. {
  710. // If the location has been set, move the node to its new location.
  711. if ($this->_location_id > 0)
  712. {
  713. if (!$this->moveByReference($this->_location_id, $this->_location, $this->$k))
  714. {
  715. // Error message set in move method.
  716. return false;
  717. }
  718. }
  719. // Lock the table for writing.
  720. if (!$this->_lock())
  721. {
  722. // Error message set in lock method.
  723. return false;
  724. }
  725. }
  726. // Store the row to the database.
  727. if (!parent::store($updateNulls))
  728. {
  729. $this->_unlock();
  730. return false;
  731. }
  732. // @codeCoverageIgnoreStart
  733. if ($this->_debug)
  734. {
  735. $this->_logtable();
  736. }
  737. // @codeCoverageIgnoreEnd
  738. // Unlock the table for writing.
  739. $this->_unlock();
  740. return true;
  741. }
  742. /**
  743. * Method to set the publishing state for a node or list of nodes in the database
  744. * table. The method respects rows checked out by other users and will attempt
  745. * to checkin rows that it can after adjustments are made. The method will not
  746. * allow you to set a publishing state higher than any ancestor node and will
  747. * not allow you to set a publishing state on a node with a checked out child.
  748. *
  749. * @param mixed $pks An optional array of primary key values to update. If not
  750. * set the instance property value is used.
  751. * @param integer $state The publishing state. eg. [0 = unpublished, 1 = published]
  752. * @param integer $userId The user id of the user performing the operation.
  753. *
  754. * @return boolean True on success.
  755. *
  756. * @link http://docs.joomla.org/JTableNested/publish
  757. * @since 11.1
  758. */
  759. public function publish($pks = null, $state = 1, $userId = 0)
  760. {
  761. $k = $this->_tbl_key;
  762. // Sanitize input.
  763. JArrayHelper::toInteger($pks);
  764. $userId = (int) $userId;
  765. $state = (int) $state;
  766. // If $state > 1, then we allow state changes even if an ancestor has lower state
  767. // (for example, can change a child state to Archived (2) if an ancestor is Published (1)
  768. $compareState = ($state > 1) ? 1 : $state;
  769. // If there are no primary keys set check to see if the instance key is set.
  770. if (empty($pks))
  771. {
  772. if ($this->$k)
  773. {
  774. $pks = explode(',', $this->$k);
  775. }
  776. // Nothing to set publishing state on, return false.
  777. else
  778. {
  779. $e = new UnexpectedValueException(sprintf('%s::publish(%s, %d, %d) empty.', get_class($this), $pks, $state, $userId));
  780. $this->setError($e);
  781. return false;
  782. }
  783. }
  784. // Determine if there is checkout support for the table.
  785. $checkoutSupport = (property_exists($this, 'checked_out') || property_exists($this, 'checked_out_time'));
  786. // Iterate over the primary keys to execute the publish action if possible.
  787. foreach ($pks as $pk)
  788. {
  789. // Get the node by primary key.
  790. if (!$node = $this->_getNode($pk))
  791. {
  792. // Error message set in getNode method.
  793. return false;
  794. }
  795. // If the table has checkout support, verify no children are checked out.
  796. if ($checkoutSupport)
  797. {
  798. // Ensure that children are not checked out.
  799. $query = $this->_db->getQuery(true);
  800. $query->select('COUNT(' . $k . ')');
  801. $query->from($this->_tbl);
  802. $query->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  803. $query->where('(checked_out <> 0 AND checked_out <> ' . (int) $userId . ')');
  804. $this->_db->setQuery($query);
  805. // Check for checked out children.
  806. if ($this->_db->loadResult())
  807. {
  808. // TODO Convert to a conflict exception when available.
  809. $e = new RuntimeException(sprintf('%s::publish(%s, %d, %d) checked-out conflict.', get_class($this), $pks, $state, $userId));
  810. $this->setError($e);
  811. return false;
  812. }
  813. }
  814. // If any parent nodes have lower published state values, we cannot continue.
  815. if ($node->parent_id)
  816. {
  817. // Get any ancestor nodes that have a lower publishing state.
  818. $query = $this->_db->getQuery(true)->select('n.' . $k)->from($this->_db->quoteName($this->_tbl) . ' AS n')
  819. ->where('n.lft < ' . (int) $node->lft)->where('n.rgt > ' . (int) $node->rgt)->where('n.parent_id > 0')
  820. ->where('n.published < ' . (int) $compareState);
  821. // Just fetch one row (one is one too many).
  822. $this->_db->setQuery($query, 0, 1);
  823. $rows = $this->_db->loadColumn();
  824. if (!empty($rows))
  825. {
  826. $e = new UnexpectedValueException(
  827. sprintf('%s::publish(%s, %d, %d) ancestors have lower state.', get_class($this), $pks, $state, $userId)
  828. );
  829. $this->setError($e);
  830. return false;
  831. }
  832. }
  833. // Update and cascade the publishing state.
  834. $query = $this->_db->getQuery(true)->update($this->_db->quoteName($this->_tbl))->set('published = ' . (int) $state)
  835. ->where('(lft > ' . (int) $node->lft . ' AND rgt < ' . (int) $node->rgt . ')' . ' OR ' . $k . ' = ' . (int) $pk);
  836. $this->_db->setQuery($query)->execute();
  837. // If checkout support exists for the object, check the row in.
  838. if ($checkoutSupport)
  839. {
  840. $this->checkin($pk);
  841. }
  842. }
  843. // If the JTable instance value is in the list of primary keys that were set, set the instance.
  844. if (in_array($this->$k, $pks))
  845. {
  846. $this->published = $state;
  847. }
  848. $this->setError('');
  849. return true;
  850. }
  851. /**
  852. * Method to move a node one position to the left in the same level.
  853. *
  854. * @param integer $pk Primary key of the node to move.
  855. *
  856. * @return boolean True on success.
  857. *
  858. * @since 11.1
  859. * @throws RuntimeException on database error.
  860. */
  861. public function orderUp($pk)
  862. {
  863. $k = $this->_tbl_key;
  864. $pk = (is_null($pk)) ? $this->$k : $pk;
  865. // Lock the table for writing.
  866. if (!$this->_lock())
  867. {
  868. // Error message set in lock method.
  869. return false;
  870. }
  871. // Get the node by primary key.
  872. $node = $this->_getNode($pk);
  873. if (empty($node))
  874. {
  875. // Error message set in getNode method.
  876. $this->_unlock();
  877. return false;
  878. }
  879. // Get the left sibling node.
  880. $sibling = $this->_getNode($node->lft - 1, 'right');
  881. if (empty($sibling))
  882. {
  883. // Error message set in getNode method.
  884. $this->_unlock();
  885. return false;
  886. }
  887. try
  888. {
  889. // Get the primary keys of child nodes.
  890. $query = $this->_db->getQuery(true);
  891. $query->select($this->_tbl_key)
  892. ->from($this->_tbl)
  893. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  894. $children = $this->_db->setQuery($query)->loadColumn();
  895. // Shift left and right values for the node and it's children.
  896. $query->clear()
  897. ->update($this->_tbl)
  898. ->set('lft = lft - ' . (int) $sibling->width)
  899. ->set('rgt = rgt - ' . (int) $sibling->width)
  900. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  901. $this->_db->setQuery($query)->execute();
  902. // Shift left and right values for the sibling and it's children.
  903. $query->clear()
  904. ->update($this->_tbl)
  905. ->set('lft = lft + ' . (int) $node->width)
  906. ->set('rgt = rgt + ' . (int) $node->width)
  907. ->where('lft BETWEEN ' . (int) $sibling->lft . ' AND ' . (int) $sibling->rgt)
  908. ->where($this->_tbl_key . ' NOT IN (' . implode(',', $children) . ')');
  909. $this->_db->setQuery($query)->execute();
  910. }
  911. catch (RuntimeException $e)
  912. {
  913. $this->_unlock();
  914. throw $e;
  915. }
  916. // Unlock the table for writing.
  917. $this->_unlock();
  918. return true;
  919. }
  920. /**
  921. * Method to move a node one position to the right in the same level.
  922. *
  923. * @param integer $pk Primary key of the node to move.
  924. *
  925. * @return boolean True on success.
  926. *
  927. * @since 11.1
  928. * @throws RuntimeException on database error.
  929. */
  930. public function orderDown($pk)
  931. {
  932. $k = $this->_tbl_key;
  933. $pk = (is_null($pk)) ? $this->$k : $pk;
  934. // Lock the table for writing.
  935. if (!$this->_lock())
  936. {
  937. // Error message set in lock method.
  938. return false;
  939. }
  940. // Get the node by primary key.
  941. $node = $this->_getNode($pk);
  942. if (empty($node))
  943. {
  944. // Error message set in getNode method.
  945. $this->_unlock();
  946. return false;
  947. }
  948. $query = $this->_db->getQuery(true);
  949. // Get the right sibling node.
  950. $sibling = $this->_getNode($node->rgt + 1, 'left');
  951. if (empty($sibling))
  952. {
  953. // Error message set in getNode method.
  954. $query->_unlock($this->_db);
  955. $this->_locked = false;
  956. return false;
  957. }
  958. try
  959. {
  960. // Get the primary keys of child nodes.
  961. $query->clear()
  962. ->select($this->_tbl_key)
  963. ->from($this->_tbl)
  964. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  965. $this->_db->setQuery($query);
  966. $children = $this->_db->loadColumn();
  967. // Shift left and right values for the node and it's children.
  968. $query->clear()
  969. ->update($this->_tbl)
  970. ->set('lft = lft + ' . (int) $sibling->width)
  971. ->set('rgt = rgt + ' . (int) $sibling->width)
  972. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  973. $this->_db->setQuery($query)->execute();
  974. // Shift left and right values for the sibling and it's children.
  975. $query->clear()
  976. ->update($this->_tbl)
  977. ->set('lft = lft - ' . (int) $node->width)
  978. ->set('rgt = rgt - ' . (int) $node->width)
  979. ->where('lft BETWEEN ' . (int) $sibling->lft . ' AND ' . (int) $sibling->rgt)
  980. ->where($this->_tbl_key . ' NOT IN (' . implode(',', $children) . ')');
  981. $this->_db->setQuery($query)->execute();
  982. }
  983. catch (RuntimeException $e)
  984. {
  985. $this->_unlock();
  986. throw $e;
  987. }
  988. // Unlock the table for writing.
  989. $this->_unlock();
  990. return true;
  991. }
  992. /**
  993. * Gets the ID of the root item in the tree
  994. *
  995. * @return mixed The primary id of the root row, or false if not found and the internal error is set.
  996. *
  997. * @since 11.1
  998. */
  999. public function getRootId()
  1000. {
  1001. // Get the root item.
  1002. $k = $this->_tbl_key;
  1003. // Test for a unique record with parent_id = 0
  1004. $query = $this->_db->getQuery(true);
  1005. $query->select($k)
  1006. ->from($this->_tbl)
  1007. ->where('parent_id = 0');
  1008. $result = $this->_db->setQuery($query)->loadColumn();
  1009. if (count($result) == 1)
  1010. {
  1011. return $result[0];
  1012. }
  1013. // Test for a unique record with lft = 0
  1014. $query = $this->_db->getQuery(true);
  1015. $query->select($k)
  1016. ->from($this->_tbl)
  1017. ->where('lft = 0');
  1018. $result = $this->_db->setQuery($query)->loadColumn();
  1019. if (count($result) == 1)
  1020. {
  1021. return $result[0];
  1022. }
  1023. if (property_exists($this, 'alias'))
  1024. {
  1025. // Test for a unique record alias = root
  1026. $query = $this->_db->getQuery(true);
  1027. $query->select($k)
  1028. ->from($this->_tbl)
  1029. ->where('alias = ' . $this->_db->quote('root'));
  1030. $result = $this->_db->setQuery($query)->loadColumn();
  1031. if (count($result) == 1)
  1032. {
  1033. return $result[0];
  1034. }
  1035. }
  1036. $e = new UnexpectedValueException(sprintf('%s::getRootId', get_class($this)));
  1037. $this->setError($e);
  1038. return false;
  1039. }
  1040. /**
  1041. * Method to recursively rebuild the whole nested set tree.
  1042. *
  1043. * @param integer $parentId The root of the tree to rebuild.
  1044. * @param integer $leftId The left id to start with in building the tree.
  1045. * @param integer $level The level to assign to the current nodes.
  1046. * @param string $path The path to the current nodes.
  1047. *
  1048. * @return integer 1 + value of root rgt on success, false on failure
  1049. *
  1050. * @link http://docs.joomla.org/JTableNested/rebuild
  1051. * @since 11.1
  1052. * @throws RuntimeException on database error.
  1053. */
  1054. public function rebuild($parentId = null, $leftId = 0, $level = 0, $path = '')
  1055. {
  1056. // If no parent is provided, try to find it.
  1057. if ($parentId === null)
  1058. {
  1059. // Get the root item.
  1060. $parentId = $this->getRootId();
  1061. if ($parentId === false)
  1062. {
  1063. return false;
  1064. }
  1065. }
  1066. // Build the structure of the recursive query.
  1067. if (!isset($this->_cache['rebuild.sql']))
  1068. {
  1069. $query = $this->_db->getQuery(true);
  1070. $query->select($this->_tbl_key . ', alias')
  1071. ->from($this->_tbl)
  1072. ->where('parent_id = %d');
  1073. // If the table has an ordering field, use that for ordering.
  1074. if (property_exists($this, 'ordering'))
  1075. {
  1076. $query->order('parent_id, ordering, lft');
  1077. }
  1078. else
  1079. {
  1080. $query->order('parent_id, lft');
  1081. }
  1082. $this->_cache['rebuild.sql'] = (string) $query;
  1083. }
  1084. // Make a shortcut to database object.
  1085. // Assemble the query to find all children of this node.
  1086. $this->_db->setQuery(sprintf($this->_cache['rebuild.sql'], (int) $parentId));
  1087. $children = $this->_db->loadObjectList();
  1088. // The right value of this node is the left value + 1
  1089. $rightId = $leftId + 1;
  1090. // Execute this function recursively over all children
  1091. foreach ($children as $node)
  1092. {
  1093. /*
  1094. * $rightId is the current right value, which is incremented on recursion return.
  1095. * Increment the level for the children.
  1096. * Add this item's alias to the path (but avoid a leading /)
  1097. */
  1098. $rightId = $this->rebuild($node->{$this->_tbl_key}, $rightId, $level + 1, $path . (empty($path) ? '' : '/') . $node->alias);
  1099. // If there is an update failure, return false to break out of the recursion.
  1100. if ($rightId === false)
  1101. {
  1102. return false;
  1103. }
  1104. }
  1105. // We've got the left value, and now that we've processed
  1106. // the children of this node we also know the right value.
  1107. $query = $this->_db->getQuery(true);
  1108. $query->update($this->_tbl)
  1109. ->set('lft = ' . (int) $leftId)
  1110. ->set('rgt = ' . (int) $rightId)
  1111. ->set('level = ' . (int) $level)
  1112. ->set('path = ' . $this->_db->quote($path))
  1113. ->where($this->_tbl_key . ' = ' . (int) $parentId);
  1114. $this->_db->setQuery($query)->execute();
  1115. // Return the right value of this node + 1.
  1116. return $rightId + 1;
  1117. }
  1118. /**
  1119. * Method to rebuild the node's path field from the alias values of the
  1120. * nodes from the current node to the root node of the tree.
  1121. *
  1122. * @param integer $pk Primary key of the node for which to get the path.
  1123. *
  1124. * @return boolean True on success.
  1125. *
  1126. * @link http://docs.joomla.org/JTableNested/rebuildPath
  1127. * @since 11.1
  1128. */
  1129. public function rebuildPath($pk = null)
  1130. {
  1131. // If there is no alias or path field, just return true.
  1132. if (!property_exists($this, 'alias') || !property_exists($this, 'path'))
  1133. {
  1134. return true;
  1135. }
  1136. $k = $this->_tbl_key;
  1137. $pk = (is_null($pk)) ? $this->$k : $pk;
  1138. // Get the aliases for the path from the node to the root node.
  1139. $query = $this->_db->getQuery(true);
  1140. $query->select('p.alias');
  1141. $query->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p');
  1142. $query->where('n.lft BETWEEN p.lft AND p.rgt');
  1143. $query->where('n.' . $this->_tbl_key . ' = ' . (int) $pk);
  1144. $query->order('p.lft');
  1145. $this->_db->setQuery($query);
  1146. $segments = $this->_db->loadColumn();
  1147. // Make sure to remove the root path if it exists in the list.
  1148. if ($segments[0] == 'root')
  1149. {
  1150. array_shift($segments);
  1151. }
  1152. // Build the path.
  1153. $path = trim(implode('/', $segments), ' /\\');
  1154. // Update the path field for the node.
  1155. $query = $this->_db->getQuery(true);
  1156. $query->update($this->_tbl);
  1157. $query->set('path = ' . $this->_db->quote($path));
  1158. $query->where($this->_tbl_key . ' = ' . (int) $pk);
  1159. $this->_db->setQuery($query)->execute();
  1160. // Update the current record's path to the new one:
  1161. $this->path = $path;
  1162. return true;
  1163. }
  1164. /**
  1165. * Method to update order of table rows
  1166. *
  1167. * @param array $idArray id numbers of rows to be reordered.
  1168. * @param array $lft_array lft values of rows to be reordered.
  1169. *
  1170. * @return integer 1 + value of root rgt on success, false on failure.
  1171. *
  1172. * @since 11.1
  1173. * @throws RuntimeException on database error.
  1174. */
  1175. public function saveorder($idArray = null, $lft_array = null)
  1176. {
  1177. try
  1178. {
  1179. $query = $this->_db->getQuery(true);
  1180. // Validate arguments
  1181. if (is_array($idArray) && is_array($lft_array) && count($idArray) == count($lft_array))
  1182. {
  1183. for ($i = 0, $count = count($idArray); $i < $count; $i++)
  1184. {
  1185. // Do an update to change the lft values in the table for each id
  1186. $query->clear()
  1187. ->update($this->_tbl)
  1188. ->where($this->_tbl_key . ' = ' . (int) $idArray[$i])
  1189. ->set('lft = ' . (int) $lft_array[$i]);
  1190. $this->_db->setQuery($query)->execute();
  1191. // @codeCoverageIgnoreStart
  1192. if ($this->_debug)
  1193. {
  1194. $this->_logtable();
  1195. }
  1196. // @codeCoverageIgnoreEnd
  1197. }
  1198. return $this->rebuild();
  1199. }
  1200. else
  1201. {
  1202. return false;
  1203. }
  1204. }
  1205. catch (Exception $e)
  1206. {
  1207. $this->_unlock();
  1208. throw $e;
  1209. }
  1210. }
  1211. /**
  1212. * Method to get nested set properties for a node in the tree.
  1213. *
  1214. * @param integer $id Value to look up the node by.
  1215. * @param string $key An optional key to look up the node by (parent | left | right).
  1216. * If omitted, the primary key of the table is used.
  1217. *
  1218. * @return mixed Boolean false on failure or node object on success.
  1219. *
  1220. * @since 11.1
  1221. * @throws RuntimeException on database error.
  1222. */
  1223. protected function _getNode($id, $key = null)
  1224. {
  1225. // Determine which key to get the node base on.
  1226. switch ($key)
  1227. {
  1228. case 'parent':
  1229. $k = 'parent_id';
  1230. break;
  1231. case 'left':
  1232. $k = 'lft';
  1233. break;
  1234. case 'right':
  1235. $k = 'rgt';
  1236. break;
  1237. default:
  1238. $k = $this->_tbl_key;
  1239. break;
  1240. }
  1241. // Get the node data.
  1242. $query = $this->_db->getQuery(true);
  1243. $query->select($this->_tbl_key . ', parent_id, level, lft, rgt')
  1244. ->from($this->_tbl)
  1245. ->where($k . ' = ' . (int) $id);
  1246. $row = $this->_db->setQuery($query, 0, 1)->loadObject();
  1247. // Check for no $row returned
  1248. if (empty($row))
  1249. {
  1250. $e = new UnexpectedValueException(sprintf('%s::_getNode(%d, %s) failed.', get_class($this), $id, $key));
  1251. $this->setError($e);
  1252. return false;
  1253. }
  1254. // Do some simple calculations.
  1255. $row->numChildren = (int) ($row->rgt - $row->lft - 1) / 2;
  1256. $row->width = (int) $row->rgt - $row->lft + 1;
  1257. return $row;
  1258. }
  1259. /**
  1260. * Method to get various data necessary to make room in the tree at a location
  1261. * for a node and its children. The returned data object includes conditions
  1262. * for SQL WHERE clauses for updating left and right id values to make room for
  1263. * the node as well as the new left and right ids for the node.
  1264. *
  1265. * @param object $referenceNode A node object with at least a 'lft' and 'rgt' with
  1266. * which to make room in the tree around for a new node.
  1267. * @param integer $nodeWidth The width of the node for which to make room in the tree.
  1268. * @param string $position The position relative to the reference node where the room
  1269. * should be made.
  1270. *
  1271. * @return mixed Boolean false on failure or data object on success.
  1272. *
  1273. * @since 11.1
  1274. */
  1275. protected function _getTreeRepositionData($referenceNode, $nodeWidth, $position = 'before')
  1276. {
  1277. // Make sure the reference an object with a left and right id.
  1278. if (!is_object($referenceNode) || !(isset($referenceNode->lft) && isset($referenceNode->rgt)))
  1279. {
  1280. return false;
  1281. }
  1282. // A valid node cannot have a width less than 2.
  1283. if ($nodeWidth < 2)
  1284. {
  1285. return false;
  1286. }
  1287. $k = $this->_tbl_key;
  1288. $data = new stdClass;
  1289. // Run the calculations and build the data object by reference position.
  1290. switch ($position)
  1291. {
  1292. case 'first-child':
  1293. $data->left_where = 'lft > ' . $referenceNode->lft;
  1294. $data->right_where = 'rgt >= ' . $referenceNode->lft;
  1295. $data->new_lft = $referenceNode->lft + 1;
  1296. $data->new_rgt = $referenceNode->lft + $nodeWidth;
  1297. $data->new_parent_id = $referenceNode->$k;
  1298. $data->new_level = $referenceNode->level + 1;
  1299. break;
  1300. case 'last-child':
  1301. $data->left_where = 'lft > ' . ($referenceNode->rgt);
  1302. $data->right_where = 'rgt >= ' . ($referenceNode->rgt);
  1303. $data->new_lft = $referenceNode->rgt;
  1304. $data->new_rgt = $referenceNode->rgt + $nodeWidth - 1;
  1305. $data->new_parent_id = $referenceNode->$k;
  1306. $data->new_level = $referenceNode->level + 1;
  1307. break;
  1308. case 'before':
  1309. $data->left_where = 'lft >= ' . $referenceNode->lft;
  1310. $data->right_where = 'rgt >= ' . $referenceNode->lft;
  1311. $data->new_lft = $referenceNode->lft;
  1312. $data->new_rgt = $referenceNode->lft + $nodeWidth - 1;
  1313. $data->new_parent_id = $referenceNode->parent_id;
  1314. $data->new_level = $referenceNode->level;
  1315. break;
  1316. default:
  1317. case 'after':
  1318. $data->left_where = 'lft > ' . $referenceNode->rgt;
  1319. $data->right_where = 'rgt > ' . $referenceNode->rgt;
  1320. $data->new_lft = $referenceNode->rgt + 1;
  1321. $data->new_rgt = $referenceNode->rgt + $nodeWidth;
  1322. $data->new_parent_id = $referenceNode->parent_id;
  1323. $data->new_level = $referenceNode->level;
  1324. break;
  1325. }
  1326. // @codeCoverageIgnoreStart
  1327. if ($this->_debug)
  1328. {
  1329. echo "\nRepositioning Data for $position" . "\n-----------------------------------" . "\nLeft Where: $data->left_where"
  1330. . "\nRight Where: $data->right_where" . "\nNew Lft: $data->new_lft" . "\nNew Rgt: $data->new_rgt"
  1331. . "\nNew Parent ID: $data->new_parent_id" . "\nNew Level: $data->new_level" . "\n";
  1332. }
  1333. // @codeCoverageIgnoreEnd
  1334. return $data;
  1335. }
  1336. /**
  1337. * Method to create a log table in the buffer optionally showing the query and/or data.
  1338. *
  1339. * @param boolean $showData True to show data
  1340. * @param boolean $showQuery True to show query
  1341. *
  1342. * @return void
  1343. *
  1344. * @codeCoverageIgnore
  1345. * @since 11.1
  1346. */
  1347. protected function _logtable($showData = true, $showQuery = true)
  1348. {
  1349. $sep = "\n" . str_pad('', 40, '-');
  1350. $buffer = '';
  1351. if ($showQuery)
  1352. {
  1353. $buffer .= "\n" . $this->_db->getQuery() . $sep;
  1354. }
  1355. if ($showData)
  1356. {
  1357. $query = $this->_db->getQuery(true);
  1358. $query->select($this->_tbl_key . ', parent_id, lft, rgt, level');
  1359. $query->from($this->_tbl);
  1360. $query->order($this->_tbl_key);
  1361. $this->_db->setQuery($query);
  1362. $rows = $this->_db->loadRowList();
  1363. $buffer .= sprintf("\n| %4s | %4s | %4s | %4s |", $this->_tbl_key, 'par', 'lft', 'rgt');
  1364. $buffer .= $sep;
  1365. foreach ($rows as $row)
  1366. {
  1367. $buffer .= sprintf("\n| %4s | %4s | %4s | %4s |", $row[0], $row[1], $row[2], $row[3]);
  1368. }
  1369. $buffer .= $sep;
  1370. }
  1371. echo $buffer;
  1372. }
  1373. /**
  1374. * Runs a query and unlocks the database on an error.
  1375. *
  1376. * @param mixed $query A string or JDatabaseQuery object.
  1377. * @param string $errorMessage Unused.
  1378. *
  1379. * @return boolean void
  1380. *
  1381. * @note Since 12.1 this method returns void and will rethrow the database exception.
  1382. * @since 11.1
  1383. * @throws RuntimeException on database error.
  1384. */
  1385. protected function _runQuery($query, $errorMessage)
  1386. {
  1387. // Prepare to catch an exception.
  1388. try
  1389. {
  1390. $this->_db->setQuery($query)->execute();
  1391. // @codeCoverageIgnoreStart
  1392. if ($this->_debug)
  1393. {
  1394. $this->_logtable();
  1395. }
  1396. // @codeCoverageIgnoreEnd
  1397. }
  1398. catch (Exception $e)
  1399. {
  1400. // Unlock the tables and rethrow.
  1401. $this->_unlock();
  1402. throw $e;
  1403. }
  1404. }
  1405. }