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