PageRenderTime 59ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/libraries/joomla/table/nested.php

https://bitbucket.org/biojazzard/joomla-eboracast
PHP | 1637 lines | 906 code | 211 blank | 520 comment | 103 complexity | d446ec3fce78333a0dba826dafbe750e MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, MIT, BSD-3-Clause
  1. <?php
  2. /**
  3. * @package Joomla.Platform
  4. * @subpackage Table
  5. *
  6. * @copyright Copyright (C) 2005 - 2013 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. $select = ($diagnostic) ? 'p.' . $k . ', p.parent_id, p.level, p.lft, p.rgt' : 'p.*';
  120. $query = $this->_db->getQuery(true)
  121. ->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. $select = ($diagnostic) ? 'n.' . $k . ', n.parent_id, n.level, n.lft, n.rgt' : 'n.*';
  146. $query = $this->_db->getQuery(true)
  147. ->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. ->select($k)
  223. ->from($this->_tbl)
  224. ->where('parent_id = ' . $this->parent_id);
  225. if ($where)
  226. {
  227. $query->where($where);
  228. }
  229. if ($delta > 0)
  230. {
  231. $query->where('rgt > ' . $this->rgt)
  232. ->order('rgt ASC');
  233. $position = 'after';
  234. }
  235. else
  236. {
  237. $query->where('lft < ' . $this->lft)
  238. ->order('lft DESC');
  239. $position = 'before';
  240. }
  241. $this->_db->setQuery($query);
  242. $referenceId = $this->_db->loadResult();
  243. if ($referenceId)
  244. {
  245. return $this->moveByReference($referenceId, $position, $pk);
  246. }
  247. else
  248. {
  249. return false;
  250. }
  251. }
  252. /**
  253. * Method to move a node and its children to a new location in the tree.
  254. *
  255. * @param integer $referenceId The primary key of the node to reference new location by.
  256. * @param string $position Location type string. ['before', 'after', 'first-child', 'last-child']
  257. * @param integer $pk The primary key of the node to move.
  258. *
  259. * @return boolean True on success.
  260. *
  261. * @link http://docs.joomla.org/JTableNested/moveByReference
  262. * @since 11.1
  263. * @throws RuntimeException on database error.
  264. */
  265. public function moveByReference($referenceId, $position = 'after', $pk = null)
  266. {
  267. // @codeCoverageIgnoreStart
  268. if ($this->_debug)
  269. {
  270. echo "\nMoving ReferenceId:$referenceId, Position:$position, PK:$pk";
  271. }
  272. // @codeCoverageIgnoreEnd
  273. $k = $this->_tbl_key;
  274. $pk = (is_null($pk)) ? $this->$k : $pk;
  275. // Get the node by id.
  276. if (!$node = $this->_getNode($pk))
  277. {
  278. // Error message set in getNode method.
  279. return false;
  280. }
  281. // Get the ids of child nodes.
  282. $query = $this->_db->getQuery(true)
  283. ->select($k)
  284. ->from($this->_tbl)
  285. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  286. $children = $this->_db->setQuery($query)->loadColumn();
  287. // @codeCoverageIgnoreStart
  288. if ($this->_debug)
  289. {
  290. $this->_logtable(false);
  291. }
  292. // @codeCoverageIgnoreEnd
  293. // Cannot move the node to be a child of itself.
  294. if (in_array($referenceId, $children))
  295. {
  296. $e = new UnexpectedValueException(
  297. sprintf('%s::moveByReference(%d, %s, %d) parenting to child.', get_class($this), $referenceId, $position, $pk)
  298. );
  299. $this->setError($e);
  300. return false;
  301. }
  302. // Lock the table for writing.
  303. if (!$this->_lock())
  304. {
  305. return false;
  306. }
  307. /*
  308. * Move the sub-tree out of the nested sets by negating its left and right values.
  309. */
  310. $query->clear()
  311. ->update($this->_tbl)
  312. ->set('lft = lft * (-1), rgt = rgt * (-1)')
  313. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  314. $this->_db->setQuery($query);
  315. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  316. /*
  317. * Close the hole in the tree that was opened by removing the sub-tree from the nested sets.
  318. */
  319. // Compress the left values.
  320. $query->clear()
  321. ->update($this->_tbl)
  322. ->set('lft = lft - ' . (int) $node->width)
  323. ->where('lft > ' . (int) $node->rgt);
  324. $this->_db->setQuery($query);
  325. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  326. // Compress the right values.
  327. $query->clear()
  328. ->update($this->_tbl)
  329. ->set('rgt = rgt - ' . (int) $node->width)
  330. ->where('rgt > ' . (int) $node->rgt);
  331. $this->_db->setQuery($query);
  332. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  333. // We are moving the tree relative to a reference node.
  334. if ($referenceId)
  335. {
  336. // Get the reference node by primary key.
  337. if (!$reference = $this->_getNode($referenceId))
  338. {
  339. // Error message set in getNode method.
  340. $this->_unlock();
  341. return false;
  342. }
  343. // Get the reposition data for shifting the tree and re-inserting the node.
  344. if (!$repositionData = $this->_getTreeRepositionData($reference, $node->width, $position))
  345. {
  346. // Error message set in getNode method.
  347. $this->_unlock();
  348. return false;
  349. }
  350. }
  351. // We are moving the tree to be the last child of the root node
  352. else
  353. {
  354. // Get the last root node as the reference node.
  355. $query->clear()
  356. ->select($this->_tbl_key . ', parent_id, level, lft, rgt')
  357. ->from($this->_tbl)
  358. ->where('parent_id = 0')
  359. ->order('lft DESC');
  360. $this->_db->setQuery($query, 0, 1);
  361. $reference = $this->_db->loadObject();
  362. // @codeCoverageIgnoreStart
  363. if ($this->_debug)
  364. {
  365. $this->_logtable(false);
  366. }
  367. // @codeCoverageIgnoreEnd
  368. // Get the reposition data for re-inserting the node after the found root.
  369. if (!$repositionData = $this->_getTreeRepositionData($reference, $node->width, 'last-child'))
  370. {
  371. // Error message set in getNode method.
  372. $this->_unlock();
  373. return false;
  374. }
  375. }
  376. /*
  377. * Create space in the nested sets at the new location for the moved sub-tree.
  378. */
  379. // Shift left values.
  380. $query->clear()
  381. ->update($this->_tbl)
  382. ->set('lft = lft + ' . (int) $node->width)
  383. ->where($repositionData->left_where);
  384. $this->_db->setQuery($query);
  385. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  386. // Shift right values.
  387. $query->clear()
  388. ->update($this->_tbl)
  389. ->set('rgt = rgt + ' . (int) $node->width)
  390. ->where($repositionData->right_where);
  391. $this->_db->setQuery($query);
  392. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  393. /*
  394. * Calculate the offset between where the node used to be in the tree and
  395. * where it needs to be in the tree for left ids (also works for right ids).
  396. */
  397. $offset = $repositionData->new_lft - $node->lft;
  398. $levelOffset = $repositionData->new_level - $node->level;
  399. // Move the nodes back into position in the tree using the calculated offsets.
  400. $query->clear()
  401. ->update($this->_tbl)
  402. ->set('rgt = ' . (int) $offset . ' - rgt')
  403. ->set('lft = ' . (int) $offset . ' - lft')
  404. ->set('level = level + ' . (int) $levelOffset)
  405. ->where('lft < 0');
  406. $this->_db->setQuery($query);
  407. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
  408. // Set the correct parent id for the moved node if required.
  409. if ($node->parent_id != $repositionData->new_parent_id)
  410. {
  411. $query = $this->_db->getQuery(true)
  412. ->update($this->_tbl);
  413. // Update the title and alias fields if they exist for the table.
  414. $fields = $this->getFields();
  415. if (property_exists($this, 'title') && $this->title !== null)
  416. {
  417. $query->set('title = ' . $this->_db->quote($this->title));
  418. }
  419. if (array_key_exists('alias', $fields) && $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. $query = $this->_db->getQuery(true);
  495. // Should we delete all children along with the node?
  496. if ($children)
  497. {
  498. // Delete the node and all of its children.
  499. $query->clear()
  500. ->delete($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->clear()
  505. ->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->clear()
  511. ->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->clear()
  521. ->delete($this->_tbl)
  522. ->where('lft = ' . (int) $node->lft);
  523. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  524. // Shift all node's children up a level.
  525. $query->clear()
  526. ->update($this->_tbl)
  527. ->set('lft = lft - 1')
  528. ->set('rgt = rgt - 1')
  529. ->set('level = level - 1')
  530. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  531. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  532. // Adjust all the parent values for direct children of the deleted node.
  533. $query->clear()
  534. ->update($this->_tbl)
  535. ->set('parent_id = ' . (int) $node->parent_id)
  536. ->where('parent_id = ' . (int) $node->$k);
  537. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  538. // Shift all of the left values that are right of the node.
  539. $query->clear()
  540. ->update($this->_tbl)
  541. ->set('lft = lft - 2')
  542. ->where('lft > ' . (int) $node->rgt);
  543. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  544. // Shift all of the right values that are right of the node.
  545. $query->clear()
  546. ->update($this->_tbl)
  547. ->set('rgt = rgt - 2')
  548. ->where('rgt > ' . (int) $node->rgt);
  549. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
  550. }
  551. // Unlock the table for writing.
  552. $this->_unlock();
  553. return true;
  554. }
  555. /**
  556. * Checks that the object is valid and able to be stored.
  557. *
  558. * This method checks that the parent_id is non-zero and exists in the database.
  559. * Note that the root node (parent_id = 0) cannot be manipulated with this class.
  560. *
  561. * @return boolean True if all checks pass.
  562. *
  563. * @since 11.1
  564. * @throws RuntimeException on database error.
  565. */
  566. public function check()
  567. {
  568. $this->parent_id = (int) $this->parent_id;
  569. // Set up a mini exception handler.
  570. try
  571. {
  572. // Check that the parent_id field is valid.
  573. if ($this->parent_id == 0)
  574. {
  575. throw new UnexpectedValueException(sprintf('Invalid `parent_id` [%d] in %s', $this->parent_id, get_class($this)));
  576. }
  577. $query = $this->_db->getQuery(true)
  578. ->select('COUNT(' . $this->_tbl_key . ')')
  579. ->from($this->_tbl)
  580. ->where($this->_tbl_key . ' = ' . $this->parent_id);
  581. if (!$this->_db->setQuery($query)->loadResult())
  582. {
  583. throw new UnexpectedValueException(sprintf('Invalid `parent_id` [%d] in %s', $this->parent_id, get_class($this)));
  584. }
  585. }
  586. catch (UnexpectedValueException $e)
  587. {
  588. // Validation error - record it and return false.
  589. $this->setError($e);
  590. return false;
  591. }
  592. // @codeCoverageIgnoreStart
  593. catch (Exception $e)
  594. {
  595. // Database error - rethrow.
  596. throw $e;
  597. }
  598. // @codeCoverageIgnoreEnd
  599. return true;
  600. }
  601. /**
  602. * Method to store a node in the database table.
  603. *
  604. * @param boolean $updateNulls True to update null values as well.
  605. *
  606. * @return boolean True on success.
  607. *
  608. * @link http://docs.joomla.org/JTableNested/store
  609. * @since 11.1
  610. */
  611. public function store($updateNulls = false)
  612. {
  613. $k = $this->_tbl_key;
  614. // Implement JObservableInterface: Pre-processing by observers
  615. $this->_observers->update('onBeforeStore', array($updateNulls, $k));
  616. // @codeCoverageIgnoreStart
  617. if ($this->_debug)
  618. {
  619. echo "\n" . get_class($this) . "::store\n";
  620. $this->_logtable(true, false);
  621. }
  622. // @codeCoverageIgnoreEnd
  623. /*
  624. * If the primary key is empty, then we assume we are inserting a new node into the
  625. * tree. From this point we would need to determine where in the tree to insert it.
  626. */
  627. if (empty($this->$k))
  628. {
  629. /*
  630. * We are inserting a node somewhere in the tree with a known reference
  631. * node. We have to make room for the new node and set the left and right
  632. * values before we insert the row.
  633. */
  634. if ($this->_location_id >= 0)
  635. {
  636. // Lock the table for writing.
  637. if (!$this->_lock())
  638. {
  639. // Error message set in lock method.
  640. return false;
  641. }
  642. // We are inserting a node relative to the last root node.
  643. if ($this->_location_id == 0)
  644. {
  645. // Get the last root node as the reference node.
  646. $query = $this->_db->getQuery(true)
  647. ->select($this->_tbl_key . ', parent_id, level, lft, rgt')
  648. ->from($this->_tbl)
  649. ->where('parent_id = 0')
  650. ->order('lft DESC');
  651. $this->_db->setQuery($query, 0, 1);
  652. $reference = $this->_db->loadObject();
  653. // @codeCoverageIgnoreStart
  654. if ($this->_debug)
  655. {
  656. $this->_logtable(false);
  657. }
  658. // @codeCoverageIgnoreEnd
  659. }
  660. // We have a real node set as a location reference.
  661. else
  662. {
  663. // Get the reference node by primary key.
  664. if (!$reference = $this->_getNode($this->_location_id))
  665. {
  666. // Error message set in getNode method.
  667. $this->_unlock();
  668. return false;
  669. }
  670. }
  671. // Get the reposition data for shifting the tree and re-inserting the node.
  672. if (!($repositionData = $this->_getTreeRepositionData($reference, 2, $this->_location)))
  673. {
  674. // Error message set in getNode method.
  675. $this->_unlock();
  676. return false;
  677. }
  678. // Create space in the tree at the new location for the new node in left ids.
  679. $query = $this->_db->getQuery(true)
  680. ->update($this->_tbl)
  681. ->set('lft = lft + 2')
  682. ->where($repositionData->left_where);
  683. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED');
  684. // Create space in the tree at the new location for the new node in right ids.
  685. $query->clear()
  686. ->update($this->_tbl)
  687. ->set('rgt = rgt + 2')
  688. ->where($repositionData->right_where);
  689. $this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED');
  690. // Set the object values.
  691. $this->parent_id = $repositionData->new_parent_id;
  692. $this->level = $repositionData->new_level;
  693. $this->lft = $repositionData->new_lft;
  694. $this->rgt = $repositionData->new_rgt;
  695. }
  696. else
  697. {
  698. // Negative parent ids are invalid
  699. $e = new UnexpectedValueException(sprintf('%s::store() used a negative _location_id', get_class($this)));
  700. $this->setError($e);
  701. return false;
  702. }
  703. }
  704. /*
  705. * If we have a given primary key then we assume we are simply updating this
  706. * node in the tree. We should assess whether or not we are moving the node
  707. * or just updating its data fields.
  708. */
  709. else
  710. {
  711. // If the location has been set, move the node to its new location.
  712. if ($this->_location_id > 0)
  713. {
  714. if (!$this->moveByReference($this->_location_id, $this->_location, $this->$k))
  715. {
  716. // Error message set in move method.
  717. return false;
  718. }
  719. }
  720. // Lock the table for writing.
  721. if (!$this->_lock())
  722. {
  723. // Error message set in lock method.
  724. return false;
  725. }
  726. }
  727. // Store the row to the database.
  728. // Implement JObservableInterface: We do not want parent::store to update observers,
  729. // since tables are locked and we are updating it from this level of store():
  730. $oldCallObservers = $this->_observers->doCallObservers(false);
  731. $result = parent::store($updateNulls);
  732. // Implement JObservableInterface: Restore previous callable observers state:
  733. $this->_observers->doCallObservers($oldCallObservers);
  734. if ($result)
  735. {
  736. // @codeCoverageIgnoreStart
  737. if ($this->_debug)
  738. {
  739. $this->_logtable();
  740. }
  741. // @codeCoverageIgnoreEnd
  742. }
  743. // Unlock the table for writing.
  744. $this->_unlock();
  745. // Implement JObservableInterface: Post-processing by observers
  746. $this->_observers->update('onAfterStore', array(&$result));
  747. return $result;
  748. }
  749. /**
  750. * Method to set the publishing state for a node or list of nodes in the database
  751. * table. The method respects rows checked out by other users and will attempt
  752. * to checkin rows that it can after adjustments are made. The method will not
  753. * allow you to set a publishing state higher than any ancestor node and will
  754. * not allow you to set a publishing state on a node with a checked out child.
  755. *
  756. * @param mixed $pks An optional array of primary key values to update. If not
  757. * set the instance property value is used.
  758. * @param integer $state The publishing state. eg. [0 = unpublished, 1 = published]
  759. * @param integer $userId The user id of the user performing the operation.
  760. *
  761. * @return boolean True on success.
  762. *
  763. * @link http://docs.joomla.org/JTableNested/publish
  764. * @since 11.1
  765. * @throws UnexpectedValueException
  766. */
  767. public function publish($pks = null, $state = 1, $userId = 0)
  768. {
  769. $k = $this->_tbl_key;
  770. $query = $this->_db->getQuery(true);
  771. // Sanitize input.
  772. JArrayHelper::toInteger($pks);
  773. $userId = (int) $userId;
  774. $state = (int) $state;
  775. // If $state > 1, then we allow state changes even if an ancestor has lower state
  776. // (for example, can change a child state to Archived (2) if an ancestor is Published (1)
  777. $compareState = ($state > 1) ? 1 : $state;
  778. // If there are no primary keys set check to see if the instance key is set.
  779. if (empty($pks))
  780. {
  781. if ($this->$k)
  782. {
  783. $pks = explode(',', $this->$k);
  784. }
  785. // Nothing to set publishing state on, return false.
  786. else
  787. {
  788. $e = new UnexpectedValueException(sprintf(__CLASS__ . '::' . __FUNCTION__ . '(%s, %d, %d) empty.', get_class($this), $state, $userId));
  789. $this->setError($e);
  790. return false;
  791. }
  792. }
  793. // Determine if there is checkout support for the table.
  794. $checkoutSupport = (property_exists($this, 'checked_out') || property_exists($this, 'checked_out_time'));
  795. // Iterate over the primary keys to execute the publish action if possible.
  796. foreach ($pks as $pk)
  797. {
  798. // Get the node by primary key.
  799. if (!$node = $this->_getNode($pk))
  800. {
  801. // Error message set in getNode method.
  802. return false;
  803. }
  804. // If the table has checkout support, verify no children are checked out.
  805. if ($checkoutSupport)
  806. {
  807. // Ensure that children are not checked out.
  808. $query->clear()
  809. ->select('COUNT(' . $k . ')')
  810. ->from($this->_tbl)
  811. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt)
  812. ->where('(checked_out <> 0 AND checked_out <> ' . (int) $userId . ')');
  813. $this->_db->setQuery($query);
  814. // Check for checked out children.
  815. if ($this->_db->loadResult())
  816. {
  817. // TODO Convert to a conflict exception when available.
  818. $implodedPks = implode(',', $pks);
  819. $e = new RuntimeException(sprintf(__CLASS__ . '::' . __FUNCTION__ . '(%s, %d, %d) checked-out conflict.', get_class($this), $implodedPks, $state, $userId));
  820. $this->setError($e);
  821. return false;
  822. }
  823. }
  824. // If any parent nodes have lower published state values, we cannot continue.
  825. if ($node->parent_id)
  826. {
  827. // Get any ancestor nodes that have a lower publishing state.
  828. $query->clear()
  829. ->select('n.' . $k)
  830. ->from($this->_db->quoteName($this->_tbl) . ' AS n')
  831. ->where('n.lft < ' . (int) $node->lft)
  832. ->where('n.rgt > ' . (int) $node->rgt)
  833. ->where('n.parent_id > 0')
  834. ->where('n.published < ' . (int) $compareState);
  835. // Just fetch one row (one is one too many).
  836. $this->_db->setQuery($query, 0, 1);
  837. $rows = $this->_db->loadColumn();
  838. if (!empty($rows))
  839. {
  840. $pksImploded = implode(',', $pks);
  841. throw new UnexpectedValueException(
  842. sprintf(__CLASS__ . '::' . __FUNCTION__ . '(%s, %d, %d) ancestors have lower state.', $pksImploded, $state, $userId)
  843. );
  844. }
  845. }
  846. // Update and cascade the publishing state.
  847. $query->clear()
  848. ->update($this->_db->quoteName($this->_tbl))
  849. ->set('published = ' . (int) $state)
  850. ->where('(lft > ' . (int) $node->lft . ' AND rgt < ' . (int) $node->rgt . ') OR ' . $k . ' = ' . (int) $pk);
  851. $this->_db->setQuery($query)->execute();
  852. // If checkout support exists for the object, check the row in.
  853. if ($checkoutSupport)
  854. {
  855. $this->checkin($pk);
  856. }
  857. }
  858. // If the JTable instance value is in the list of primary keys that were set, set the instance.
  859. if (in_array($this->$k, $pks))
  860. {
  861. $this->published = $state;
  862. }
  863. $this->setError('');
  864. return true;
  865. }
  866. /**
  867. * Method to move a node one position to the left in the same level.
  868. *
  869. * @param integer $pk Primary key of the node to move.
  870. *
  871. * @return boolean True on success.
  872. *
  873. * @since 11.1
  874. * @throws RuntimeException on database error.
  875. */
  876. public function orderUp($pk)
  877. {
  878. $k = $this->_tbl_key;
  879. $pk = (is_null($pk)) ? $this->$k : $pk;
  880. // Lock the table for writing.
  881. if (!$this->_lock())
  882. {
  883. // Error message set in lock method.
  884. return false;
  885. }
  886. // Get the node by primary key.
  887. $node = $this->_getNode($pk);
  888. if (empty($node))
  889. {
  890. // Error message set in getNode method.
  891. $this->_unlock();
  892. return false;
  893. }
  894. // Get the left sibling node.
  895. $sibling = $this->_getNode($node->lft - 1, 'right');
  896. if (empty($sibling))
  897. {
  898. // Error message set in getNode method.
  899. $this->_unlock();
  900. return false;
  901. }
  902. try
  903. {
  904. // Get the primary keys of child nodes.
  905. $query = $this->_db->getQuery(true)
  906. ->select($this->_tbl_key)
  907. ->from($this->_tbl)
  908. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  909. $children = $this->_db->setQuery($query)->loadColumn();
  910. // Shift left and right values for the node and it's children.
  911. $query->clear()
  912. ->update($this->_tbl)
  913. ->set('lft = lft - ' . (int) $sibling->width)
  914. ->set('rgt = rgt - ' . (int) $sibling->width)
  915. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  916. $this->_db->setQuery($query)->execute();
  917. // Shift left and right values for the sibling and it's children.
  918. $query->clear()
  919. ->update($this->_tbl)
  920. ->set('lft = lft + ' . (int) $node->width)
  921. ->set('rgt = rgt + ' . (int) $node->width)
  922. ->where('lft BETWEEN ' . (int) $sibling->lft . ' AND ' . (int) $sibling->rgt)
  923. ->where($this->_tbl_key . ' NOT IN (' . implode(',', $children) . ')');
  924. $this->_db->setQuery($query)->execute();
  925. }
  926. catch (RuntimeException $e)
  927. {
  928. $this->_unlock();
  929. throw $e;
  930. }
  931. // Unlock the table for writing.
  932. $this->_unlock();
  933. return true;
  934. }
  935. /**
  936. * Method to move a node one position to the right in the same level.
  937. *
  938. * @param integer $pk Primary key of the node to move.
  939. *
  940. * @return boolean True on success.
  941. *
  942. * @since 11.1
  943. * @throws RuntimeException on database error.
  944. */
  945. public function orderDown($pk)
  946. {
  947. $k = $this->_tbl_key;
  948. $pk = (is_null($pk)) ? $this->$k : $pk;
  949. // Lock the table for writing.
  950. if (!$this->_lock())
  951. {
  952. // Error message set in lock method.
  953. return false;
  954. }
  955. // Get the node by primary key.
  956. $node = $this->_getNode($pk);
  957. if (empty($node))
  958. {
  959. // Error message set in getNode method.
  960. $this->_unlock();
  961. return false;
  962. }
  963. $query = $this->_db->getQuery(true);
  964. // Get the right sibling node.
  965. $sibling = $this->_getNode($node->rgt + 1, 'left');
  966. if (empty($sibling))
  967. {
  968. // Error message set in getNode method.
  969. $query->_unlock($this->_db);
  970. $this->_locked = false;
  971. return false;
  972. }
  973. try
  974. {
  975. // Get the primary keys of child nodes.
  976. $query->clear()
  977. ->select($this->_tbl_key)
  978. ->from($this->_tbl)
  979. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  980. $this->_db->setQuery($query);
  981. $children = $this->_db->loadColumn();
  982. // Shift left and right values for the node and it's children.
  983. $query->clear()
  984. ->update($this->_tbl)
  985. ->set('lft = lft + ' . (int) $sibling->width)
  986. ->set('rgt = rgt + ' . (int) $sibling->width)
  987. ->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
  988. $this->_db->setQuery($query)->execute();
  989. // Shift left and right values for the sibling and it's children.
  990. $query->clear()
  991. ->update($this->_tbl)
  992. ->set('lft = lft - ' . (int) $node->width)
  993. ->set('rgt = rgt - ' . (int) $node->width)
  994. ->where('lft BETWEEN ' . (int) $sibling->lft . ' AND ' . (int) $sibling->rgt)
  995. ->where($this->_tbl_key . ' NOT IN (' . implode(',', $children) . ')');
  996. $this->_db->setQuery($query)->execute();
  997. }
  998. catch (RuntimeException $e)
  999. {
  1000. $this->_unlock();
  1001. throw $e;
  1002. }
  1003. // Unlock the table for writing.
  1004. $this->_unlock();
  1005. return true;
  1006. }
  1007. /**
  1008. * Gets the ID of the root item in the tree
  1009. *
  1010. * @return mixed The primary id of the root row, or false if not found and the internal error is set.
  1011. *
  1012. * @since 11.1
  1013. */
  1014. public function getRootId()
  1015. {
  1016. // Get the root item.
  1017. $k = $this->_tbl_key;
  1018. // Test for a unique record with parent_id = 0
  1019. $query = $this->_db->getQuery(true)
  1020. ->select($k)
  1021. ->from($this->_tbl)
  1022. ->where('parent_id = 0');
  1023. $result = $this->_db->setQuery($query)->loadColumn();
  1024. if (count($result) == 1)
  1025. {
  1026. return $result[0];
  1027. }
  1028. // Test for a unique record with lft = 0
  1029. $query->clear()
  1030. ->select($k)
  1031. ->from($this->_tbl)
  1032. ->where('lft = 0');
  1033. $result = $this->_db->setQuery($query)->loadColumn();
  1034. if (count($result) == 1)
  1035. {
  1036. return $result[0];
  1037. }
  1038. $fields = $this->getFields();
  1039. if (array_key_exists('alias', $fields))
  1040. {
  1041. // Test for a unique record alias = root
  1042. $query->clear()
  1043. ->select($k)
  1044. ->from($this->_tbl)
  1045. ->where('alias = ' . $this->_db->quote('root'));
  1046. $result = $this->_db->setQuery($query)->loadColumn();
  1047. if (count($result) == 1)
  1048. {
  1049. return $result[0];
  1050. }
  1051. }
  1052. $e = new UnexpectedValueException(sprintf('%s::getRootId', get_class($this)));
  1053. $this->setError($e);
  1054. return false;
  1055. }
  1056. /**
  1057. * Method to recursively rebuild the whole nested set tree.
  1058. *
  1059. * @param integer $parentId The root of the tree to rebuild.
  1060. * @param integer $leftId The left id to start with in building the tree.
  1061. * @param integer $level The level to assign to the current nodes.
  1062. * @param string $path The path to the current nodes.
  1063. *
  1064. * @return integer 1 + value of root rgt on success, false on failure
  1065. *
  1066. * @link http://docs.joomla.org/JTableNested/rebuild
  1067. * @since 11.1
  1068. * @throws RuntimeException on database error.
  1069. */
  1070. public function rebuild($parentId = null, $leftId = 0, $level = 0, $path = '')
  1071. {
  1072. // If no parent is provided, try to find it.
  1073. if ($parentId === null)
  1074. {
  1075. // Get the root item.
  1076. $parentId = $this->getRootId();
  1077. if ($parentId === false)
  1078. {
  1079. return false;
  1080. }
  1081. }
  1082. $query = $this->_db->getQuery(true);
  1083. // Build the structure of the recursive query.
  1084. if (!isset($this->_cache['rebuild.sql']))
  1085. {
  1086. $query->clear()
  1087. ->select($this->_tbl_key . ', alias')
  1088. ->from($this->_tbl)
  1089. ->where('parent_id = %d');
  1090. // If the table has an ordering field, use that for ordering.
  1091. if (property_exists($this, 'ordering'))
  1092. {
  1093. $query->order('parent_id, ordering, lft');
  1094. }
  1095. else
  1096. {
  1097. $query->order('parent_id, lft');
  1098. }
  1099. $this->_cache['rebuild.sql'] = (string) $query;
  1100. }
  1101. // Make a shortcut to database object.
  1102. // Assemble the query to find all children of this node.
  1103. $this->_db->setQuery(sprintf($this->_cache['rebuild.sql'], (int) $parentId));
  1104. $children = $this->_db->loadObjectList();
  1105. // The right value of this node is the left value + 1
  1106. $rightId = $leftId + 1;
  1107. // Execute this function recursively over all children
  1108. foreach ($children as $node)
  1109. {
  1110. /*
  1111. * $rightId is the current right value, which is incremented on recursion return.
  1112. * Increment the level for the children.
  1113. * Add this item's alias to the path (but avoid a leading /)
  1114. */
  1115. $rightId = $this->rebuild($node->{$this->_tbl_key}, $rightId, $level + 1, $path . (empty($path) ? '' : '/') . $node->alias);
  1116. // If there is an update failure, return false to break out of the recursion.
  1117. if ($rightId === false)
  1118. {
  1119. return false;
  1120. }
  1121. }
  1122. // We've got the left value, and now that we've processed
  1123. // the children of this node we also know the right value.
  1124. $query->clear()
  1125. ->update($this->_tbl)
  1126. ->set('lft = ' . (int) $leftId)
  1127. ->set('rgt = ' . (int) $rightId)
  1128. ->set('level = ' . (int) $level)
  1129. ->set('path = ' . $this->_db->quote($path))
  1130. ->where($this->_tbl_key . ' = ' . (int) $parentId);
  1131. $this->_db->setQuery($query)->execute();
  1132. // Return the right value of this node + 1.
  1133. return $rightId + 1;
  1134. }
  1135. /**
  1136. * Method to rebuild the node's path field from the alias values of the
  1137. * nodes from the current node to the root node of the tree.
  1138. *
  1139. * @param integer $pk Primary key of the node for which to get the path.
  1140. *
  1141. * @return boolean True on success.
  1142. *
  1143. * @link http://docs.joomla.org/JTableNested/rebuildPath
  1144. * @since 11.1
  1145. */
  1146. public function rebuildPath($pk = null)
  1147. {
  1148. $fields = $this->getFields();
  1149. // If there is no alias or path field, just return true.
  1150. if (!array_key_exists('alias', $fields) || !array_key_exists('path', $fields))
  1151. {
  1152. return true;
  1153. }
  1154. $k = $this->_tbl_key;
  1155. $pk = (is_null($pk)) ? $this->$k : $pk;
  1156. // Get the aliases for the path from the node to the root node.
  1157. $query = $this->_db->getQuery(true)
  1158. ->select('p.alias')
  1159. ->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p')
  1160. ->where('n.lft BETWEEN p.lft AND p.rgt')
  1161. ->where('n.' . $this->_tbl_key . ' = ' . (int) $pk)
  1162. ->order('p.lft');
  1163. $this->_db->setQuery($query);
  1164. $segments = $this->_db->loadColumn();
  1165. // Make sure to remove the root path if it exists in the list.
  1166. if ($segments[0] == 'root')
  1167. {
  1168. array_shift($segments);
  1169. }
  1170. // Build the path.
  1171. $path = trim(implode('/', $segments), ' /\\');
  1172. // Update the path field for the node.
  1173. $query->clear()
  1174. ->update($this->_tbl)
  1175. ->set('path = ' . $this->_db->quote($path))
  1176. ->where($this->_tbl_key . ' = ' . (int) $pk);
  1177. $this->_db->setQuery($query)->execute();
  1178. // Update the current record's path to the new one:
  1179. $this->path = $path;
  1180. return true;
  1181. }
  1182. /**
  1183. * Method to update order of table rows
  1184. *
  1185. * @param array $idArray id numbers of rows to be reordered.
  1186. * @param array $lft_array lft values of rows to be reordered.
  1187. *
  1188. * @return integer 1 + value of root rgt on success, false on failure.
  1189. *
  1190. * @since 11.1
  1191. * @throws RuntimeException on database error.
  1192. */
  1193. public function saveorder($idArray = null, $lft_array = null)
  1194. {
  1195. try
  1196. {
  1197. $query = $this->_db->getQuery(true);
  1198. // Validate arguments
  1199. if (is_array($idArray) && is_array($lft_array) && count($idArray) == count($lft_array))
  1200. {
  1201. for ($i = 0, $count = count($idArray); $i < $count; $i++)
  1202. {
  1203. // Do an update to change the lft values in the table for each id
  1204. $query->clear()
  1205. ->update($this->_tbl)
  1206. ->where($this->_tbl_key . ' = ' . (int) $idArray[$i])
  1207. ->set('lft = ' . (int) $lft_array[$i]);
  1208. $this->_db->setQuery($query)->execute();
  1209. // @codeCoverageIgnoreStart
  1210. if ($this->_debug)
  1211. {
  1212. $this->_logtable();
  1213. }
  1214. // @codeCoverageIgnoreEnd
  1215. }
  1216. return $this->rebuild();
  1217. }
  1218. else
  1219. {
  1220. return false;
  1221. }
  1222. }
  1223. catch (Exception $e)
  1224. {
  1225. $this->_unlock();
  1226. throw $e;
  1227. }
  1228. }
  1229. /**
  1230. * Method to get nested set properties for a node in the tree.
  1231. *
  1232. * @param integer $id Value to look up the node by.
  1233. * @param string $key An optional key to look up the node by (parent | left | right).
  1234. * If omitted, the primary key of the table is used.
  1235. *
  1236. * @return mixed Boolean false on failure or node object on success.
  1237. *
  1238. * @since 11.1
  1239. * @throws RuntimeException on database error.
  1240. */
  1241. protected function _getNode($id, $key = null)
  1242. {
  1243. // Determine which key to get the node base on.
  1244. switch ($key)
  1245. {
  1246. case 'parent':
  1247. $k = 'parent_id';
  1248. break;
  1249. case 'left':
  1250. $k = 'lft';
  1251. break;
  1252. case 'right':
  1253. $k = 'rgt';
  1254. break;
  1255. default:
  1256. $k = $this->_tbl_key;
  1257. break;
  1258. }
  1259. // Get the node data.
  1260. $query = $this->_db->getQuery(true)
  1261. ->select($this->_tbl_key . ', parent_id, level, lft, rgt')
  1262. ->from($this->_tbl)
  1263. ->where($k . ' = ' . (int) $id);
  1264. $row = $this->_db->setQuery($query, 0, 1)->loadObject();
  1265. // Check for no $row returned
  1266. if (empty($row))
  1267. {
  1268. $e = new UnexpectedValueException(sprintf('%s::_getNode(%d, %s) failed.', get_class($this), $id, $key));
  1269. $this->setError($e);
  1270. return false;
  1271. }
  1272. // Do some simple calculations.
  1273. $row->numChildren = (int) ($row->rgt - $row->lft - 1) / 2;
  1274. $row->width = (int) $row->rgt - $row->lft + 1;
  1275. return $row;
  1276. }
  1277. /**
  1278. * Method to get various data necessary to make room in the tree at a location
  1279. * for a node and its children. The returned data object includes conditions
  1280. * for SQL WHERE clauses for updating left and right id values to make room for
  1281. * the node as well as the new left and right ids for the node.
  1282. *
  1283. * @param object $referenceNode A node object with at least a 'lft' and 'rgt' with
  1284. * which to make room in the tree around for a new node.
  1285. * @param integer $nodeWidth The width of the node for which to make room in the tree.
  1286. * @param string $position The position relative to the reference node where the room
  1287. * should be made.
  1288. *
  1289. * @return mixed Boolean false on failure or data object on success.
  1290. *
  1291. * @since 11.1
  1292. */
  1293. protected function _getTreeRepositionData($referenceNode, $nodeWidth, $position = 'before')
  1294. {
  1295. // Make sure the reference an object with a left and right id.
  1296. if (!is_object($referenceNode) || !(isset($referenceNode->lft) && isset($referenceNode->rgt)))
  1297. {
  1298. return false;
  1299. }
  1300. // A valid node cannot have a width less than 2.
  1301. if ($nodeWidth < 2)
  1302. {
  1303. return false;
  1304. }
  1305. $k = $this->_tbl_key;
  1306. $data = new stdClass;
  1307. // Run the calculations and build the data object by reference position.
  1308. switch ($position)
  1309. {
  1310. case 'first-child':
  1311. $data->left_where = 'lft > ' . $referenceNode->lft;
  1312. $data->right_where = 'rgt >= ' . $referenceNode->lft;
  1313. $data->new_lft = $referenceNode->lft + 1;
  1314. $data->new_rgt = $referenceNode->lft + $nodeWidth;
  1315. $data->new_parent_id = $referenceNode->$k;
  1316. $data->new_level = $referenceNode->level + 1;
  1317. break;
  1318. case 'last-child':
  1319. $data->left_where = 'lft > ' . ($referenceNode->rgt);
  1320. $data->right_where = 'rgt >= ' . ($referenceNode->rgt);
  1321. $data->new_lft = $referenceNode->rgt;
  1322. $data->new_rgt = $referenceNode->rgt + $nodeWidth - 1;
  1323. $data->new_parent_id = $referenceNode->$k;
  1324. $data->new_level = $referenceNode->level + 1;
  1325. break;
  1326. case 'before':
  1327. $data->left_where = 'lft >= ' . $referenceNode->lft;
  1328. $data->right_where = 'rgt >= ' . $referenceNode->lft;
  1329. $data->new_lft = $referenceNode->lft;
  1330. $data->new_rgt = $referenceNode->lft + $nodeWidth - 1;
  1331. $data->new_parent_id = $referenceNode->parent_id;
  1332. $data->new_level = $referenceNode->level;
  1333. break;
  1334. default:
  1335. case 'after':
  1336. $data->left_where = 'lft > ' . $referenceNode->rgt;
  1337. $data->right_where = 'rgt > ' . $referenceNode->rgt;
  1338. $data->new_lft = $referenceNode->rgt + 1;
  1339. $data->new_rgt = $referenceNode->rgt + $nodeWidth;
  1340. $data->new_parent_id = $referenceNode->parent_id;
  1341. $data->new_level = $referenceNode->level;
  1342. break;
  1343. }
  1344. // @codeCoverageIgnoreStart
  1345. if ($this->_debug)
  1346. {
  1347. echo "\nRepositioning Data for $position" . "\n-----------------------------------" . "\nLeft Where: $data->left_where"
  1348. . "\nRight Where: $data->right_where" . "\nNew Lft: $data->new_lft" . "\nNew Rgt: $data->new_rgt"
  1349. . "\nNew Parent ID: $data->new_parent_id" . "\nNew Level: $data->new_level" . "\n";
  1350. }
  1351. // @codeCoverageIgnoreEnd
  1352. return $data;
  1353. }
  1354. /**
  1355. * Method to create a log table in the buffer optionally showing the query and/or data.
  1356. *
  1357. * @param boolean $showData True to show data
  1358. * @param boolean $showQuery True to show query
  1359. *
  1360. * @return void
  1361. *
  1362. * @codeCoverageIgnore
  1363. * @since 11.1
  1364. */
  1365. protected function _logtable($showData = true, $showQuery = true)
  1366. {
  1367. $sep = "\n" . str_pad('', 40, '-');
  1368. $buffer = '';
  1369. if ($showQuery)
  1370. {
  1371. $buffer .= "\n" . $this->_db->getQuery() . $sep;
  1372. }
  1373. if ($showData)
  1374. {
  1375. $query = $this->_db->getQuery(true)
  1376. ->select($this->_tbl_key . ', parent_id, lft, rgt, level')
  1377. ->from($this->_tbl)
  1378. ->order($this->_tbl_key);
  1379. $this->_db->setQuery($query);
  1380. $rows = $this->_db->loadRowList();
  1381. $buffer .= sprintf("\n| %4s | %4s | %4s | %4s |", $this->_tbl_key, 'par', 'lft', 'rgt');
  1382. $buffer .= $sep;
  1383. foreach ($rows as $row)
  1384. {
  1385. $buffer .= sprintf("\n| %4s | %4s | %4s | %4s |", $row[0], $row[1], $row[2], $row[3]);
  1386. }
  1387. $buffer .= $sep;
  1388. }
  1389. echo $buffer;
  1390. }
  1391. /**
  1392. * Runs a query and unlocks the database on an error.
  1393. *
  1394. * @param mixed $query A string or JDatabaseQuery object.
  1395. * @param string $errorMessage Unused.
  1396. *
  1397. * @return boolean void
  1398. *
  1399. * @note Since 12.1 this method returns void and will rethrow the database exception.
  1400. * @since 11.1
  1401. * @throws RuntimeException on database error.
  1402. */
  1403. protected function _runQuery($query, $errorMessage)
  1404. {
  1405. // Prepare to catch an exception.
  1406. try
  1407. {
  1408. $this->_db->setQuery($query)->execute();
  1409. // @codeCoverageIgnoreStart
  1410. if ($this->_debug)
  1411. {
  1412. $this->_logtable();
  1413. }
  1414. // @codeCoverageIgnoreEnd
  1415. }
  1416. catch (Exception $e)
  1417. {
  1418. // Unlock the tables and rethrow.
  1419. $this->_unlock();
  1420. throw $e;
  1421. }
  1422. }
  1423. }