This repository has been archived by the owner on Nov 26, 2017. It is now read-only.
/
nested.php
1618 lines (1417 loc) · 44.1 KB
/
nested.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
/**
* @package Joomla.Platform
* @subpackage Table
*
* @copyright Copyright (C) 2005 - 2012 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE
*/
defined('JPATH_PLATFORM') or die;
/**
* Table class supporting modified pre-order tree traversal behavior.
*
* @package Joomla.Platform
* @subpackage Table
* @link http://docs.joomla.org/JTableNested
* @since 11.1
*/
class JTableNested extends JTable
{
/**
* Object property holding the primary key of the parent node. Provides
* adjacency list data for nodes.
*
* @var integer
* @since 11.1
*/
public $parent_id;
/**
* Object property holding the depth level of the node in the tree.
*
* @var integer
* @since 11.1
*/
public $level;
/**
* Object property holding the left value of the node for managing its
* placement in the nested sets tree.
*
* @var integer
* @since 11.1
*/
public $lft;
/**
* Object property holding the right value of the node for managing its
* placement in the nested sets tree.
*
* @var integer
* @since 11.1
*/
public $rgt;
/**
* Object property holding the alias of this node used to constuct the
* full text path, forward-slash delimited.
*
* @var string
* @since 11.1
*/
public $alias;
/**
* Object property to hold the location type to use when storing the row.
* Possible values are: ['before', 'after', 'first-child', 'last-child'].
*
* @var string
* @since 11.1
*/
protected $_location;
/**
* Object property to hold the primary key of the location reference node to
* use when storing the row. A combination of location type and reference
* node describes where to store the current node in the tree.
*
* @var integer
* @since 11.1
*/
protected $_location_id;
/**
* An array to cache values in recursive processes.
*
* @var array
* @since 11.1
*/
protected $_cache = array();
/**
* Debug level
*
* @var integer
* @since 11.1
*/
protected $_debug = 0;
/**
* Sets the debug level on or off
*
* @param integer $level 0 = off, 1 = on
*
* @return void
*
* @since 11.1
*/
public function debug($level)
{
$this->_debug = intval($level);
}
/**
* Method to get an array of nodes from a given node to its root.
*
* @param integer $pk Primary key of the node for which to get the path.
* @param boolean $diagnostic Only select diagnostic data for the nested sets.
*
* @return mixed An array of node objects including the start node.
*
* @since 11.1
* @throws RuntimeException on database error
*/
public function getPath($pk = null, $diagnostic = false)
{
// Initialise variables.
$k = $this->_tbl_key;
$pk = (is_null($pk)) ? $this->$k : $pk;
// Get the path from the node to the root.
$query = $this->_db->getQuery(true);
$select = ($diagnostic) ? 'p.' . $k . ', p.parent_id, p.level, p.lft, p.rgt' : 'p.*';
$query->select($select)
->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p')
->where('n.lft BETWEEN p.lft AND p.rgt')
->where('n.' . $k . ' = ' . (int) $pk)
->order('p.lft');
$this->_db->setQuery($query);
return $this->_db->loadObjectList();
}
/**
* Method to get a node and all its child nodes.
*
* @param integer $pk Primary key of the node for which to get the tree.
* @param boolean $diagnostic Only select diagnostic data for the nested sets.
*
* @return mixed Boolean false on failure or array of node objects on success.
*
* @since 11.1
* @throws RuntimeException on database error.
*/
public function getTree($pk = null, $diagnostic = false)
{
// Initialise variables.
$k = $this->_tbl_key;
$pk = (is_null($pk)) ? $this->$k : $pk;
// Get the node and children as a tree.
$query = $this->_db->getQuery(true);
$select = ($diagnostic) ? 'n.' . $k . ', n.parent_id, n.level, n.lft, n.rgt' : 'n.*';
$query->select($select)
->from($this->_tbl . ' AS n, ' . $this->_tbl . ' AS p')
->where('n.lft BETWEEN p.lft AND p.rgt')
->where('p.' . $k . ' = ' . (int) $pk)
->order('n.lft');
return $this->_db->setQuery($query)->loadObjectList();
}
/**
* Method to determine if a node is a leaf node in the tree (has no children).
*
* @param integer $pk Primary key of the node to check.
*
* @return boolean True if a leaf node, false if not or null if the node does not exist.
*
* @note Since 12.1 this method returns null if the node does not exist.
* @since 11.1
* @throws RuntimeException on database error.
*/
public function isLeaf($pk = null)
{
// Initialise variables.
$k = $this->_tbl_key;
$pk = (is_null($pk)) ? $this->$k : $pk;
$node = $this->_getNode($pk);
// Get the node by primary key.
if (empty($node))
{
// Error message set in getNode method.
return null;
}
// The node is a leaf node.
return (($node->rgt - $node->lft) == 1);
}
/**
* Method to set the location of a node in the tree object. This method does not
* save the new location to the database, but will set it in the object so
* that when the node is stored it will be stored in the new location.
*
* @param integer $referenceId The primary key of the node to reference new location by.
* @param string $position Location type string. ['before', 'after', 'first-child', 'last-child']
*
* @return void
*
* @note Since 12.1 this method returns void and throws an InvalidArgumentException when an invalid position is passed.
* @since 11.1
* @throws InvalidArgumentException
*/
public function setLocation($referenceId, $position = 'after')
{
// Make sure the location is valid.
if (($position != 'before') && ($position != 'after') && ($position != 'first-child') && ($position != 'last-child'))
{
throw new InvalidArgumentException(sprintf('%s::setLocation(%d, *%s*)', get_class($this), $referenceId, $position));
}
// Set the location properties.
$this->_location = $position;
$this->_location_id = $referenceId;
}
/**
* Method to move a row in the ordering sequence of a group of rows defined by an SQL WHERE clause.
* Negative numbers move the row up in the sequence and positive numbers move it down.
*
* @param integer $delta The direction and magnitude to move the row in the ordering sequence.
* @param string $where WHERE clause to use for limiting the selection of rows to compact the
* ordering values.
*
* @return mixed Boolean true on success.
*
* @link http://docs.joomla.org/JTable/move
* @since 11.1
*/
public function move($delta, $where = '')
{
// Initialise variables.
$k = $this->_tbl_key;
$pk = $this->$k;
$query = $this->_db->getQuery(true);
$query->select($k);
$query->from($this->_tbl);
$query->where('parent_id = ' . $this->parent_id);
if ($where)
{
$query->where($where);
}
$position = 'after';
if ($delta > 0)
{
$query->where('rgt > ' . $this->rgt);
$query->order('rgt ASC');
$position = 'after';
}
else
{
$query->where('lft < ' . $this->lft);
$query->order('lft DESC');
$position = 'before';
}
$this->_db->setQuery($query);
$referenceId = $this->_db->loadResult();
if ($referenceId)
{
return $this->moveByReference($referenceId, $position, $pk);
}
else
{
return false;
}
}
/**
* Method to move a node and its children to a new location in the tree.
*
* @param integer $referenceId The primary key of the node to reference new location by.
* @param string $position Location type string. ['before', 'after', 'first-child', 'last-child']
* @param integer $pk The primary key of the node to move.
*
* @return boolean True on success.
*
* @link http://docs.joomla.org/JTableNested/moveByReference
* @since 11.1
* @throws RuntimeException on database error.
*/
public function moveByReference($referenceId, $position = 'after', $pk = null)
{
// @codeCoverageIgnoreStart
if ($this->_debug)
{
echo "\nMoving ReferenceId:$referenceId, Position:$position, PK:$pk";
}
// @codeCoverageIgnoreEnd
// Initialise variables.
$k = $this->_tbl_key;
$pk = (is_null($pk)) ? $this->$k : $pk;
// Get the node by id.
if (!$node = $this->_getNode($pk))
{
// Error message set in getNode method.
return false;
}
// Get the ids of child nodes.
$query = $this->_db->getQuery(true);
$query->select($k)
->from($this->_tbl)
->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
$children = $this->_db->setQuery($query)->loadColumn();
// @codeCoverageIgnoreStart
if ($this->_debug)
{
$this->_logtable(false);
}
// @codeCoverageIgnoreEnd
// Cannot move the node to be a child of itself.
if (in_array($referenceId, $children))
{
$e = new UnexpectedValueException(
sprintf('%s::moveByReference(%d, %s, %d) parenting to child.', get_class($this), $referenceId, $position, $pk)
);
$this->setError($e);
return false;
}
// Lock the table for writing.
if (!$this->_lock())
{
return false;
}
/*
* Move the sub-tree out of the nested sets by negating its left and right values.
*/
$query = $this->_db->getQuery(true);
$query->update($this->_tbl)
->set('lft = lft * (-1), rgt = rgt * (-1)')
->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
$this->_db->setQuery($query);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
/*
* Close the hole in the tree that was opened by removing the sub-tree from the nested sets.
*/
// Compress the left values.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl)
->set('lft = lft - ' . (int) $node->width)
->where('lft > ' . (int) $node->rgt);
$this->_db->setQuery($query);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
// Compress the right values.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl)
->set('rgt = rgt - ' . (int) $node->width)
->where('rgt > ' . (int) $node->rgt);
$this->_db->setQuery($query);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
// We are moving the tree relative to a reference node.
if ($referenceId)
{
// Get the reference node by primary key.
if (!$reference = $this->_getNode($referenceId))
{
// Error message set in getNode method.
$this->_unlock();
return false;
}
// Get the reposition data for shifting the tree and re-inserting the node.
if (!$repositionData = $this->_getTreeRepositionData($reference, $node->width, $position))
{
// Error message set in getNode method.
$this->_unlock();
return false;
}
}
// We are moving the tree to be the last child of the root node
else
{
// Get the last root node as the reference node.
$query = $this->_db->getQuery(true);
$query->select($this->_tbl_key . ', parent_id, level, lft, rgt')
->from($this->_tbl)
->where('parent_id = 0')
->order('lft DESC');
$this->_db->setQuery($query, 0, 1);
$reference = $this->_db->loadObject();
// @codeCoverageIgnoreStart
if ($this->_debug)
{
$this->_logtable(false);
}
// @codeCoverageIgnoreEnd
// Get the reposition data for re-inserting the node after the found root.
if (!$repositionData = $this->_getTreeRepositionData($reference, $node->width, 'last-child'))
{
// Error message set in getNode method.
$this->_unlock();
return false;
}
}
/*
* Create space in the nested sets at the new location for the moved sub-tree.
*/
// Shift left values.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl)
->set('lft = lft + ' . (int) $node->width)
->where($repositionData->left_where);
$this->_db->setQuery($query);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
// Shift right values.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl)
->set('rgt = rgt + ' . (int) $node->width)
->where($repositionData->right_where);
$this->_db->setQuery($query);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
/*
* Calculate the offset between where the node used to be in the tree and
* where it needs to be in the tree for left ids (also works for right ids).
*/
$offset = $repositionData->new_lft - $node->lft;
$levelOffset = $repositionData->new_level - $node->level;
// Move the nodes back into position in the tree using the calculated offsets.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl)
->set('rgt = ' . (int) $offset . ' - rgt')
->set('lft = ' . (int) $offset . ' - lft')
->set('level = level + ' . (int) $levelOffset)
->where('lft < 0');
$this->_db->setQuery($query);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
// Set the correct parent id for the moved node if required.
if ($node->parent_id != $repositionData->new_parent_id)
{
$query = $this->_db->getQuery(true);
$query->update($this->_tbl);
// Update the title and alias fields if they exist for the table.
if (property_exists($this, 'title') && $this->title !== null)
{
$query->set('title = ' . $this->_db->Quote($this->title));
}
if (property_exists($this, 'alias') && $this->alias !== null)
{
$query->set('alias = ' . $this->_db->Quote($this->alias));
}
$query->set('parent_id = ' . (int) $repositionData->new_parent_id)
->where($this->_tbl_key . ' = ' . (int) $node->$k);
$this->_db->setQuery($query);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_MOVE_FAILED');
}
// Unlock the table for writing.
$this->_unlock();
// Set the object values.
$this->parent_id = $repositionData->new_parent_id;
$this->level = $repositionData->new_level;
$this->lft = $repositionData->new_lft;
$this->rgt = $repositionData->new_rgt;
return true;
}
/**
* Method to delete a node and, optionally, its child nodes from the table.
*
* @param integer $pk The primary key of the node to delete.
* @param boolean $children True to delete child nodes, false to move them up a level.
*
* @return boolean True on success.
*
* @since 11.1
*/
public function delete($pk = null, $children = true)
{
// Initialise variables.
$k = $this->_tbl_key;
$pk = (is_null($pk)) ? $this->$k : $pk;
// Lock the table for writing.
if (!$this->_lock())
{
// Error message set in lock method.
return false;
}
// If tracking assets, remove the asset first.
if ($this->_trackAssets)
{
$name = $this->_getAssetName();
$asset = JTable::getInstance('Asset');
// Lock the table for writing.
if (!$asset->_lock())
{
// Error message set in lock method.
return false;
}
if ($asset->loadByName($name))
{
// Delete the node in assets table.
if (!$asset->delete(null, $children))
{
$this->setError($asset->getError());
$asset->_unlock();
return false;
}
$asset->_unlock();
}
else
{
$this->setError($asset->getError());
$asset->_unlock();
return false;
}
}
// Get the node by id.
$node = $this->_getNode($pk);
if (empty($node))
{
// Error message set in getNode method.
$this->_unlock();
return false;
}
// Should we delete all children along with the node?
if ($children)
{
// Delete the node and all of its children.
$query = $this->_db->getQuery(true);
$query->delete()
->from($this->_tbl)
->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
// Compress the left values.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl)
->set('lft = lft - ' . (int) $node->width)
->where('lft > ' . (int) $node->rgt);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
// Compress the right values.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl)
->set('rgt = rgt - ' . (int) $node->width)
->where('rgt > ' . (int) $node->rgt);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
}
// Leave the children and move them up a level.
else
{
// Delete the node.
$query = $this->_db->getQuery(true);
$query->delete()
->from($this->_tbl)
->where('lft = ' . (int) $node->lft);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
// Shift all node's children up a level.
$query->clear()
->update($this->_tbl)
->set('lft = lft - 1')
->set('rgt = rgt - 1')
->set('level = level - 1')
->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
// Adjust all the parent values for direct children of the deleted node.
$query->clear()
->update($this->_tbl)
->set('parent_id = ' . (int) $node->parent_id)
->where('parent_id = ' . (int) $node->$k);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
// Shift all of the left values that are right of the node.
$query->clear()
->update($this->_tbl)
->set('lft = lft - 2')
->where('lft > ' . (int) $node->rgt);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
// Shift all of the right values that are right of the node.
$query->clear()
->update($this->_tbl)
->set('rgt = rgt - 2')
->where('rgt > ' . (int) $node->rgt);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_DELETE_FAILED');
}
// Unlock the table for writing.
$this->_unlock();
return true;
}
/**
* Checks that the object is valid and able to be stored.
*
* This method checks that the parent_id is non-zero and exists in the database.
* Note that the root node (parent_id = 0) cannot be manipulated with this class.
*
* @return boolean True if all checks pass.
*
* @since 11.1
* @throws RuntimeException on database error.
*/
public function check()
{
$this->parent_id = (int) $this->parent_id;
// Set up a mini exception handler.
try
{
// Check that the parent_id field is valid.
if ($this->parent_id == 0)
{
throw new UnexpectedValueException(sprintf('Invalid `parent_id` [%d] in %s', $this->parent_id, get_class($this)));
}
$query = $this->_db->getQuery(true);
$query->select('COUNT(' . $this->_tbl_key . ')')
->from($this->_tbl)
->where($this->_tbl_key . ' = ' . $this->parent_id);
if (!$this->_db->setQuery($query)->loadResult())
{
throw new UnexpectedValueException(sprintf('Invalid `parent_id` [%d] in %s', $this->parent_id, get_class($this)));
}
}
catch (UnexpectedValueException $e)
{
// Validation error - record it and return false.
$this->setError($e);
return false;
}
// @codeCoverageIgnoreStart
catch (Exception $e)
{
// Database error - rethrow.
throw $e;
}
// @codeCoverageIgnoreEnd
return true;
}
/**
* Method to store a node in the database table.
*
* @param boolean $updateNulls True to update null values as well.
*
* @return boolean True on success.
*
* @link http://docs.joomla.org/JTableNested/store
* @since 11.1
*/
public function store($updateNulls = false)
{
// Initialise variables.
$k = $this->_tbl_key;
// @codeCoverageIgnoreStart
if ($this->_debug)
{
echo "\n" . get_class($this) . "::store\n";
$this->_logtable(true, false);
}
// @codeCoverageIgnoreEnd
/*
* If the primary key is empty, then we assume we are inserting a new node into the
* tree. From this point we would need to determine where in the tree to insert it.
*/
if (empty($this->$k))
{
/*
* We are inserting a node somewhere in the tree with a known reference
* node. We have to make room for the new node and set the left and right
* values before we insert the row.
*/
if ($this->_location_id >= 0)
{
// Lock the table for writing.
if (!$this->_lock())
{
// Error message set in lock method.
return false;
}
// We are inserting a node relative to the last root node.
if ($this->_location_id == 0)
{
// Get the last root node as the reference node.
$query = $this->_db->getQuery(true);
$query->select($this->_tbl_key . ', parent_id, level, lft, rgt')
->from($this->_tbl)
->where('parent_id = 0')
->order('lft DESC');
$this->_db->setQuery($query, 0, 1);
$reference = $this->_db->loadObject();
// @codeCoverageIgnoreStart
if ($this->_debug)
{
$this->_logtable(false);
}
// @codeCoverageIgnoreEnd
}
// We have a real node set as a location reference.
else
{
// Get the reference node by primary key.
if (!$reference = $this->_getNode($this->_location_id))
{
// Error message set in getNode method.
$this->_unlock();
return false;
}
}
// Get the reposition data for shifting the tree and re-inserting the node.
if (!($repositionData = $this->_getTreeRepositionData($reference, 2, $this->_location)))
{
// Error message set in getNode method.
$this->_unlock();
return false;
}
// Create space in the tree at the new location for the new node in left ids.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl)
->set('lft = lft + 2')
->where($repositionData->left_where);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED');
// Create space in the tree at the new location for the new node in right ids.
$query = $this->_db->getQuery(true);
$query->update($this->_tbl)
->set('rgt = rgt + 2')
->where($repositionData->right_where);
$this->_runQuery($query, 'JLIB_DATABASE_ERROR_STORE_FAILED');
// Set the object values.
$this->parent_id = $repositionData->new_parent_id;
$this->level = $repositionData->new_level;
$this->lft = $repositionData->new_lft;
$this->rgt = $repositionData->new_rgt;
}
else
{
// Negative parent ids are invalid
$e = new UnexpectedValueException(sprintf('%s::store() used a negative _location_id', get_class($this)));
$this->setError($e);
return false;
}
}
/*
* If we have a given primary key then we assume we are simply updating this
* node in the tree. We should assess whether or not we are moving the node
* or just updating its data fields.
*/
else
{
// If the location has been set, move the node to its new location.
if ($this->_location_id > 0)
{
if (!$this->moveByReference($this->_location_id, $this->_location, $this->$k))
{
// Error message set in move method.
return false;
}
}
// Lock the table for writing.
if (!$this->_lock())
{
// Error message set in lock method.
return false;
}
}
// Store the row to the database.
if (!parent::store($updateNulls))
{
$this->_unlock();
return false;
}
// @codeCoverageIgnoreStart
if ($this->_debug)
{
$this->_logtable();
}
// @codeCoverageIgnoreEnd
// Unlock the table for writing.
$this->_unlock();
return true;
}
/**
* Method to set the publishing state for a node or list of nodes in the database
* table. The method respects rows checked out by other users and will attempt
* to checkin rows that it can after adjustments are made. The method will not
* allow you to set a publishing state higher than any ancestor node and will
* not allow you to set a publishing state on a node with a checked out child.
*
* @param mixed $pks An optional array of primary key values to update. If not
* set the instance property value is used.
* @param integer $state The publishing state. eg. [0 = unpublished, 1 = published]
* @param integer $userId The user id of the user performing the operation.
*
* @return boolean True on success.
*
* @link http://docs.joomla.org/JTableNested/publish
* @since 11.1
*/
public function publish($pks = null, $state = 1, $userId = 0)
{
// Initialise variables.
$k = $this->_tbl_key;
// Sanitize input.
JArrayHelper::toInteger($pks);
$userId = (int) $userId;
$state = (int) $state;
// If $state > 1, then we allow state changes even if an ancestor has lower state
// (for example, can change a child state to Archived (2) if an ancestor is Published (1)
$compareState = ($state > 1) ? 1 : $state;
// If there are no primary keys set check to see if the instance key is set.
if (empty($pks))
{
if ($this->$k)
{
$pks = explode(',', $this->$k);
}
// Nothing to set publishing state on, return false.
else
{
$e = new UnexpectedValueException(sprintf('%s::publish(%s, %d, %d) empty.', get_class($this), $pks, $state, $userId));
$this->setError($e);
return false;
}
}
// Determine if there is checkout support for the table.
$checkoutSupport = (property_exists($this, 'checked_out') || property_exists($this, 'checked_out_time'));
// Iterate over the primary keys to execute the publish action if possible.
foreach ($pks as $pk)
{
// Get the node by primary key.
if (!$node = $this->_getNode($pk))
{
// Error message set in getNode method.
return false;
}
// If the table has checkout support, verify no children are checked out.
if ($checkoutSupport)
{
// Ensure that children are not checked out.
$query = $this->_db->getQuery(true);
$query->select('COUNT(' . $k . ')');
$query->from($this->_tbl);
$query->where('lft BETWEEN ' . (int) $node->lft . ' AND ' . (int) $node->rgt);
$query->where('(checked_out <> 0 AND checked_out <> ' . (int) $userId . ')');
$this->_db->setQuery($query);
// Check for checked out children.
if ($this->_db->loadResult())
{
// TODO Convert to a conflict exception when available.
$e = new RuntimeException(sprintf('%s::publish(%s, %d, %d) checked-out conflict.', get_class($this), $pks, $state, $userId));
$this->setError($e);
return false;
}
}
// If any parent nodes have lower published state values, we cannot continue.
if ($node->parent_id)
{
// Get any ancestor nodes that have a lower publishing state.
$query = $this->_db->getQuery(true)->select('n.' . $k)->from($this->_db->quoteName($this->_tbl) . ' AS n')
->where('n.lft < ' . (int) $node->lft)->where('n.rgt > ' . (int) $node->rgt)->where('n.parent_id > 0')
->where('n.published < ' . (int) $compareState);
// Just fetch one row (one is one too many).
$this->_db->setQuery($query, 0, 1);
$rows = $this->_db->loadColumn();
if (!empty($rows))
{
$e = new UnexpectedValueException(
sprintf('%s::publish(%s, %d, %d) ancestors have lower state.', get_class($this), $pks, $state, $userId)
);
$this->setError($e);
return false;
}
}
// Update and cascade the publishing state.
$query = $this->_db->getQuery(true)->update($this->_db->quoteName($this->_tbl))->set('published = ' . (int) $state)
->where('(lft > ' . (int) $node->lft . ' AND rgt < ' . (int) $node->rgt . ')' . ' OR ' . $k . ' = ' . (int) $pk);
$this->_db->setQuery($query)->execute();
// If checkout support exists for the object, check the row in.
if ($checkoutSupport)
{
$this->checkin($pk);
}
}
// If the JTable instance value is in the list of primary keys that were set, set the instance.
if (in_array($this->$k, $pks))
{
$this->published = $state;
}
$this->setError('');
return true;
}
/**
* Method to move a node one position to the left in the same level.
*
* @param integer $pk Primary key of the node to move.
*
* @return boolean True on success.
*
* @since 11.1
* @throws RuntimeException on database error.
*/
public function orderUp($pk)
{
// Initialise variables.
$k = $this->_tbl_key;
$pk = (is_null($pk)) ? $this->$k : $pk;
// Lock the table for writing.
if (!$this->_lock())
{
// Error message set in lock method.
return false;
}
// Get the node by primary key.
$node = $this->_getNode($pk);
if (empty($node))
{
// Error message set in getNode method.
$this->_unlock();
return false;