ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.ilTree.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
29class ilTree
30{
31 public const TREE_TYPE_MATERIALIZED_PATH = 'mp';
32 public const TREE_TYPE_NESTED_SET = 'ns';
33
34 public const POS_LAST_NODE = -2;
35 public const POS_FIRST_NODE = -1;
36
37 public const RELATION_CHILD = 1;
38 public const RELATION_PARENT = 2;
39 public const RELATION_SIBLING = 3;
40 public const RELATION_EQUALS = 4;
41 public const RELATION_NONE = 5;
42
43 protected const DEFAULT_LANGUAGE = 'en';
44 protected const DEFAULT_GAP = 50;
45
46 protected ilLogger $logger;
47 protected ilDBInterface $db;
49
54 private string $lang_code;
55
59 protected int $root_id;
60
64 protected int $tree_id;
65
69 protected string $table_tree;
70
74 protected string $table_obj_data;
75
79 protected string $table_obj_reference;
80
84 protected string $ref_pk;
85
89 protected string $obj_pk;
90
94 protected string $tree_pk;
95
114 private int $gap;
115
116 protected bool $use_cache;
118 protected array $oc_preloaded = [];
119 protected array $depth_cache = [];
120 protected array $parent_cache = [];
121 protected array $in_tree_cache = [];
122 protected array $translation_cache = [];
123 protected array $parent_type_cache = [];
124 protected array $is_saved_cache = [];
125
127
128 private array $path_id_cache = [];
129
133 public function __construct(
134 int $a_tree_id,
135 int $a_root_id = 0,
136 ?ilDBInterface $db = null
137 ) {
138 global $DIC;
139
140 $this->db = $db ?? $DIC->database();
141 $this->logger = ilLoggerFactory::getLogger('tree');
142 //$this->logger = $DIC->logger()->tree();
143 if (isset($DIC['ilAppEventHandler'])) {
144 $this->eventHandler = $DIC['ilAppEventHandler'];
145 }
146
147 $this->lang_code = self::DEFAULT_LANGUAGE;
148
149 if (func_num_args() > 3) {
150 $this->logger->error("Wrong parameter count!");
151 $this->logger->logStack(ilLogLevel::ERROR);
152 throw new InvalidArgumentException("Wrong parameter count!");
153 }
154
155 if ($a_root_id > 0) {
156 $this->root_id = $a_root_id;
157 } else {
158 $this->root_id = ROOT_FOLDER_ID;
159 }
160
161 $this->tree_id = $a_tree_id;
162 $this->table_tree = 'tree';
163 $this->table_obj_data = 'object_data';
164 $this->table_obj_reference = 'object_reference';
165 $this->ref_pk = 'ref_id';
166 $this->obj_pk = 'obj_id';
167 $this->tree_pk = 'tree';
168
169 $this->use_cache = true;
170
171 // By default, we create gaps in the tree sequence numbering for 50 nodes
172 $this->gap = self::DEFAULT_GAP;
173
174 // init tree implementation
175 $this->initTreeImplementation();
176 }
177
182 public static function lookupTreesForNode(int $node_id): array
183 {
184 global $DIC;
185
186 $db = $DIC->database();
187
188 $query = 'select tree from tree ' .
189 'where child = ' . $db->quote($node_id, \ilDBConstants::T_INTEGER);
190 $res = $db->query($query);
191
192 $trees = [];
193 while ($row = $res->fetchRow(\ilDBConstants::FETCHMODE_OBJECT)) {
194 $trees[] = (int) $row->tree;
195 }
196 return $trees;
197 }
198
202 public function initTreeImplementation(): void
203 {
204 global $DIC;
205
206 if (!$DIC->isDependencyAvailable('settings') || $DIC->settings()->getModule() != 'common') {
207 $setting = new ilSetting('common');
208 } else {
209 $setting = $DIC->settings();
210 }
211
212 if ($this->__isMainTree()) {
213 if ($setting->get('main_tree_impl', 'ns') == 'ns') {
214 $this->tree_impl = new ilNestedSetTree($this);
215 } else {
216 $this->tree_impl = new ilMaterializedPathTree($this);
217 }
218 } else {
219 $this->tree_impl = new ilNestedSetTree($this);
220 }
221 }
222
228 {
229 return $this->tree_impl;
230 }
231
235 public function useCache(bool $a_use = true): void
236 {
237 $this->use_cache = $a_use;
238 }
239
243 protected function isCacheUsed(): bool
244 {
245 return $this->__isMainTree() && $this->use_cache;
246 }
247
251 public function getDepthCache(): array
252 {
253 return $this->depth_cache;
254 }
255
259 public function getParentCache(): array
260 {
261 return $this->parent_cache;
262 }
263
270 public function initLangCode(): void
271 {
272 global $DIC;
273
274 if ($DIC->offsetExists('ilUser')) {
275 $this->lang_code = $DIC->user()->getCurrentLanguage() ?
276 $DIC->user()->getCurrentLanguage() : self::DEFAULT_LANGUAGE;
277 } else {
278 $this->lang_code = self::DEFAULT_LANGUAGE;
279 }
280 }
281
285 public function getTreeTable(): string
286 {
287 return $this->table_tree;
288 }
289
293 public function getObjectDataTable(): string
294 {
296 }
297
301 public function getTreePk(): string
302 {
303 return $this->tree_pk;
304 }
305
309 public function getTableReference(): string
310 {
312 }
313
317 public function getGap(): int
318 {
319 return $this->gap;
320 }
321
325 protected function resetInTreeCache(): void
326 {
327 $this->in_tree_cache = array();
328 }
329
338 public function setTableNames(
339 string $a_table_tree,
340 string $a_table_obj_data,
341 string $a_table_obj_reference = ""
342 ): void {
343 $this->table_tree = $a_table_tree;
344 $this->table_obj_data = $a_table_obj_data;
345 $this->table_obj_reference = $a_table_obj_reference;
346
347 // reconfigure tree implementation
348 $this->initTreeImplementation();
349 }
350
354 public function setReferenceTablePK(string $a_column_name): void
355 {
356 $this->ref_pk = $a_column_name;
357 }
358
362 public function setObjectTablePK(string $a_column_name): void
363 {
364 $this->obj_pk = $a_column_name;
365 }
366
370 public function setTreeTablePK(string $a_column_name): void
371 {
372 $this->tree_pk = $a_column_name;
373 }
374
380 public function buildJoin(): string
381 {
382 if ($this->table_obj_reference) {
383 // Use inner join instead of left join to improve performance
384 return "JOIN " . $this->table_obj_reference . " ON " . $this->table_tree . ".child=" . $this->table_obj_reference . "." . $this->ref_pk . " " .
385 "JOIN " . $this->table_obj_data . " ON " . $this->table_obj_reference . "." . $this->obj_pk . "=" . $this->table_obj_data . "." . $this->obj_pk . " ";
386 } else {
387 // Use inner join instead of left join to improve performance
388 return "JOIN " . $this->table_obj_data . " ON " . $this->table_tree . ".child=" . $this->table_obj_data . "." . $this->obj_pk . " ";
389 }
390 }
391
396 public function getRelation(int $a_node_a, int $a_node_b): int
397 {
398 return $this->getRelationOfNodes(
399 $this->getNodeTreeData($a_node_a),
400 $this->getNodeTreeData($a_node_b)
401 );
402 }
403
407 public function getRelationOfNodes(array $a_node_a_arr, array $a_node_b_arr): int
408 {
409 return $this->getTreeImplementation()->getRelation($a_node_a_arr, $a_node_b_arr);
410 }
411
416 public function getChildIds(int $a_node): array
417 {
418 $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
419 'WHERE parent = ' . $this->db->quote($a_node, 'integer') . ' ' .
420 'AND tree = ' . $this->db->quote($this->tree_id, 'integer' . ' ' .
421 'ORDER BY lft');
422 $res = $this->db->query($query);
423
424 $childs = array();
425 while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) {
426 $childs[] = (int) $row->child;
427 }
428 return $childs;
429 }
430
435 public function getChilds(int $a_node_id, string $a_order = "", string $a_direction = "ASC"): array
436 {
437 global $DIC;
438
439 $ilObjDataCache = $DIC['ilObjDataCache'];
440 $ilUser = $DIC['ilUser'];
441
442 // init childs
443 $childs = [];
444
445 // number of childs
446 $count = 0;
447
448 // init order_clause
449 $order_clause = "";
450
451 // set order_clause if sort order parameter is given
452 if (!empty($a_order)) {
453 $order_clause = "ORDER BY " . $a_order . " " . $a_direction;
454 } else {
455 $order_clause = "ORDER BY " . $this->table_tree . ".lft";
456 }
457
458 $query = sprintf(
459 'SELECT * FROM ' . $this->table_tree . ' ' .
460 $this->buildJoin() .
461 "WHERE parent = %s " .
462 "AND " . $this->table_tree . "." . $this->tree_pk . " = %s " .
463 $order_clause,
464 $this->db->quote($a_node_id, 'integer'),
465 $this->db->quote($this->tree_id, 'integer')
466 );
467
468 $res = $this->db->query($query);
469
470 if (!$count = $res->numRows()) {
471 return [];
472 }
473
474 // get rows and object ids
475 $rows = [];
476 $obj_ids = [];
477 while ($r = $this->db->fetchAssoc($res)) {
478 $rows[] = $r;
479 $obj_ids[] = (int) $r["obj_id"];
480 }
481
482 // preload object translation information
483 if ($this->__isMainTree() && $this->isCacheUsed() && is_object($ilObjDataCache) &&
484 is_object($ilUser) && $this->lang_code == $ilUser->getLanguage() && !isset($this->oc_preloaded[$a_node_id])) {
485 // $ilObjDataCache->preloadTranslations($obj_ids, $this->lang_code);
486 $ilObjDataCache->preloadObjectCache($obj_ids, $this->lang_code);
487 $this->fetchTranslationFromObjectDataCache($obj_ids);
488 $this->oc_preloaded[$a_node_id] = true;
489 }
490
491 foreach ($rows as $row) {
492 $childs[] = $this->fetchNodeData($row);
493
494 // Update cache of main tree
495 if ($this->__isMainTree()) {
496 #$GLOBALS['DIC']['ilLog']->write(__METHOD__.': Storing in tree cache '.$row['child'].' = true');
497 $this->in_tree_cache[$row['child']] = $row['tree'] == 1;
498 }
499 }
500 $childs[$count - 1]["last"] = true;
501 return $childs;
502 }
503
512 public function getFilteredChilds(
513 array $a_filter,
514 int $a_node,
515 string $a_order = "",
516 string $a_direction = "ASC"
517 ): array {
518 $childs = $this->getChilds($a_node, $a_order, $a_direction);
519
520 $filtered = [];
521 foreach ($childs as $child) {
522 if (!in_array($child["type"], $a_filter)) {
523 $filtered[] = $child;
524 }
525 }
526 return $filtered;
527 }
528
533 public function getChildsByType(int $a_node_id, string $a_type): array
534 {
535 if ($a_type == 'rolf' && $this->table_obj_reference) {
536 // Performance optimization: A node can only have exactly one
537 // role folder as its child. Therefore we don't need to sort the
538 // results, and we can let the database know about the expected limit.
539 $this->db->setLimit(1, 0);
540 $query = sprintf(
541 "SELECT * FROM " . $this->table_tree . " " .
542 $this->buildJoin() .
543 "WHERE parent = %s " .
544 "AND " . $this->table_tree . "." . $this->tree_pk . " = %s " .
545 "AND " . $this->table_obj_data . ".type = %s ",
546 $this->db->quote($a_node_id, 'integer'),
547 $this->db->quote($this->tree_id, 'integer'),
548 $this->db->quote($a_type, 'text')
549 );
550 } else {
551 $query = sprintf(
552 "SELECT * FROM " . $this->table_tree . " " .
553 $this->buildJoin() .
554 "WHERE parent = %s " .
555 "AND " . $this->table_tree . "." . $this->tree_pk . " = %s " .
556 "AND " . $this->table_obj_data . ".type = %s " .
557 "ORDER BY " . $this->table_tree . ".lft",
558 $this->db->quote($a_node_id, 'integer'),
559 $this->db->quote($this->tree_id, 'integer'),
560 $this->db->quote($a_type, 'text')
561 );
562 }
563 $res = $this->db->query($query);
564
565 // init childs
566 $childs = [];
567 while ($row = $this->db->fetchAssoc($res)) {
568 $childs[] = $this->fetchNodeData($row);
569 }
570 return $childs;
571 }
572
576 public function getChildsByTypeFilter(
577 int $a_node_id,
578 array $a_types,
579 string $a_order = "",
580 string $a_direction = "ASC"
581 ): array {
582 $filter = ' ';
583 if ($a_types) {
584 $filter = 'AND ' . $this->table_obj_data . '.type IN(' . implode(',', ilArrayUtil::quoteArray($a_types)) . ') ';
585 }
586
587 // set order_clause if sort order parameter is given
588 if (!empty($a_order)) {
589 $order_clause = "ORDER BY " . $a_order . " " . $a_direction;
590 } else {
591 $order_clause = "ORDER BY " . $this->table_tree . ".lft";
592 }
593
594 $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
595 $this->buildJoin() .
596 'WHERE parent = ' . $this->db->quote($a_node_id, 'integer') . ' ' .
597 'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = ' . $this->db->quote(
598 $this->tree_id,
599 'integer'
600 ) . ' ' .
601 $filter .
602 $order_clause;
603
604 $res = $this->db->query($query);
605
606 $childs = [];
607 while ($row = $this->db->fetchAssoc($res)) {
608 $childs[] = $this->fetchNodeData($row);
609 }
610
611 return $childs;
612 }
613
620 public function insertNodeFromTrash(
621 int $a_source_id,
622 int $a_target_id,
623 int $a_tree_id,
624 int $a_pos = self::POS_LAST_NODE,
625 bool $a_reset_deleted_date = false
626 ): void {
627 if ($this->__isMainTree()) {
628 if ($a_source_id <= 1 || $a_target_id <= 0) {
629 $this->logger->logStack(ilLogLevel::WARNING);
630 throw new InvalidArgumentException('Invalid parameter given for ilTree::insertNodeFromTrash');
631 }
632 }
633 if ($this->isInTree($a_source_id)) {
634 ilLoggerFactory::getLogger('tree')->error('Node already in tree');
636 throw new InvalidArgumentException('Node already in tree.');
637 }
638
639 ilTree::_removeEntry($a_tree_id, $a_source_id, 'tree');
640 $this->insertNode($a_source_id, $a_target_id, self::POS_LAST_NODE, $a_reset_deleted_date);
641 }
642
647 public function insertNode(
648 int $a_node_id,
649 int $a_parent_id,
650 int $a_pos = self::POS_LAST_NODE,
651 bool $a_reset_deletion_date = false
652 ): void {
653 // CHECK node_id and parent_id > 0 if in main tree
654 if ($this->__isMainTree()) {
655 if ($a_node_id <= 1 || $a_parent_id <= 0) {
656 $message = sprintf(
657 'Invalid parameters! $a_node_id: %s $a_parent_id: %s',
658 $a_node_id,
659 $a_parent_id
660 );
661 $this->logger->logStack(ilLogLevel::ERROR, $message);
662 throw new InvalidArgumentException($message);
663 }
664 }
665 if ($this->isInTree($a_node_id)) {
666 throw new InvalidArgumentException("Node " . $a_node_id . " already in tree " .
667 $this->table_tree . "!");
668 }
669
670 $this->getTreeImplementation()->insertNode($a_node_id, $a_parent_id, $a_pos);
671
672 $this->in_tree_cache[$a_node_id] = true;
673
674 // reset deletion date
675 if ($a_reset_deletion_date) {
676 ilObject::_resetDeletedDate($a_node_id);
677 }
678 if (isset($this->eventHandler) && ($this->eventHandler instanceof ilAppEventHandler) && $this->__isMainTree()) {
679 $this->eventHandler->raise(
680 'components/ILIAS/Tree',
681 'insertNode',
682 [
683 'tree' => $this->table_tree,
684 'node_id' => $a_node_id,
685 'parent_id' => $a_parent_id
686 ]
687 );
688 }
689 }
690
697 public function getFilteredSubTree(int $a_node_id, array $a_filter = []): array
698 {
699 $node = $this->getNodeData($a_node_id);
700
701 $first = true;
702 $depth = 0;
703 $filtered = [];
704 foreach ($this->getSubTree($node) as $subnode) {
705 if ($depth && $subnode['depth'] > $depth) {
706 continue;
707 }
708 if (!$first && in_array($subnode['type'], $a_filter)) {
709 $depth = $subnode['depth'];
710 $first = false;
711 continue;
712 }
713 $depth = 0;
714 $first = false;
715 $filtered[] = $subnode;
716 }
717 return $filtered;
718 }
719
725 public function getSubTreeIds(int $a_ref_id): array
726 {
727 return $this->getTreeImplementation()->getSubTreeIds($a_ref_id);
728 }
729
735 public function getSubTree(array $a_node, bool $a_with_data = true, array $a_type = []): array
736 {
737 $query = $this->getTreeImplementation()->getSubTreeQuery($a_node, $a_type);
738
739 $res = $this->db->query($query);
740 $subtree = [];
741 while ($row = $this->db->fetchAssoc($res)) {
742 if ($a_with_data) {
743 $subtree[] = $this->fetchNodeData($row);
744 } else {
745 $subtree[] = (int) $row['child'];
746 }
747 // the lm_data "hack" should be removed in the trunk during an alpha
748 if ($this->__isMainTree() || $this->table_tree == "lm_tree") {
749 $this->in_tree_cache[$row['child']] = true;
750 }
751 }
752 return $subtree;
753 }
754
758 public function deleteTree(array $a_node): void
759 {
760 if ($this->__isMainTree()) {
761 // moved to trash and then deleted.
762 if (!$this->__checkDelete($a_node)) {
763 $this->logger->logStack(ilLogLevel::ERROR);
764 throw new ilInvalidTreeStructureException('Deletion canceled due to invalid tree structure.' . print_r(
765 $a_node,
766 true
767 ));
768 }
769 }
770 $this->getTreeImplementation()->deleteTree((int) $a_node['child']);
771 $this->resetInTreeCache();
772 }
773
778 public function validateParentRelations(): array
779 {
780 return $this->getTreeImplementation()->validateParentRelations();
781 }
782
788 public function getPathFull(int $a_endnode_id, int $a_startnode_id = 0): array
789 {
790 $pathIds = $this->getPathId($a_endnode_id, $a_startnode_id);
791
792 // We retrieve the full path in a single query to improve performance
793 // Abort if no path ids were found
794 if (count($pathIds) == 0) {
795 return [];
796 }
797
798 $inClause = 'child IN (';
799 for ($i = 0; $i < count($pathIds); $i++) {
800 if ($i > 0) {
801 $inClause .= ',';
802 }
803 $inClause .= $this->db->quote($pathIds[$i], 'integer');
804 }
805 $inClause .= ')';
806
807 $q = 'SELECT * ' .
808 'FROM ' . $this->table_tree . ' ' .
809 $this->buildJoin() . ' ' .
810 'WHERE ' . $inClause . ' ' .
811 'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = ' . $this->db->quote(
812 $this->tree_id,
813 'integer'
814 ) . ' ' .
815 'ORDER BY depth';
816 $r = $this->db->query($q);
817
818 $pathFull = [];
819 while ($row = $r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
820 $pathFull[] = $this->fetchNodeData($row);
821
822 // Update cache
823 if ($this->__isMainTree()) {
824 $this->in_tree_cache[$row['child']] = $row['tree'] == 1;
825 }
826 }
827 return $pathFull;
828 }
829
834 public function preloadDepthParent(array $a_node_ids): void
835 {
836 global $DIC;
837
838 if (!$this->__isMainTree() || !$this->isCacheUsed()) {
839 return;
840 }
841
842 $res = $this->db->query('SELECT t.depth, t.parent, t.child ' .
843 'FROM ' . $this->table_tree . ' t ' .
844 'WHERE ' . $this->db->in("child", $a_node_ids, false, "integer") .
845 'AND ' . $this->tree_pk . ' = ' . $this->db->quote($this->tree_id, "integer"));
846 while ($row = $this->db->fetchAssoc($res)) {
847 $this->depth_cache[$row["child"]] = (int) $row["depth"];
848 $this->parent_cache[$row["child"]] = (int) $row["parent"];
849 }
850 }
851
857 public function getPathId(int $a_endnode_id, int $a_startnode_id = 0): array
858 {
859 if (!$a_endnode_id) {
860 $this->logger->logStack(ilLogLevel::ERROR);
861 throw new InvalidArgumentException(__METHOD__ . ': No endnode given!');
862 }
863
864 // path id cache
865 if ($this->isCacheUsed() && isset($this->path_id_cache[$a_endnode_id][$a_startnode_id])) {
866 return $this->path_id_cache[$a_endnode_id][$a_startnode_id];
867 }
868
869 $pathIds = $this->getTreeImplementation()->getPathIds($a_endnode_id, $a_startnode_id);
870
871 if ($this->__isMainTree()) {
872 $this->path_id_cache[$a_endnode_id][$a_startnode_id] = $pathIds;
873 }
874 return $pathIds;
875 }
876
887 public function getNodePath(int $a_endnode_id, int $a_startnode_id = 0): array
888 {
889 $pathIds = $this->getPathId($a_endnode_id, $a_startnode_id);
890
891 // Abort if no path ids were found
892 if (count($pathIds) == 0) {
893 return [];
894 }
895
896 $types = [];
897 $data = [];
898 for ($i = 0; $i < count($pathIds); $i++) {
899 $types[] = 'integer';
900 $data[] = $pathIds[$i];
901 }
902
903 $query = 'SELECT t.depth,t.parent,t.child,d.obj_id,d.type,d.title ' .
904 'FROM ' . $this->table_tree . ' t ' .
905 'JOIN ' . $this->table_obj_reference . ' r ON r.ref_id = t.child ' .
906 'JOIN ' . $this->table_obj_data . ' d ON d.obj_id = r.obj_id ' .
907 'WHERE ' . $this->db->in('t.child', $data, false, 'integer') . ' ' .
908 'ORDER BY t.depth ';
909
910 $res = $this->db->queryF($query, $types, $data);
911
912 $titlePath = [];
913 while ($row = $this->db->fetchAssoc($res)) {
914 $titlePath[] = $row;
915 }
916 return $titlePath;
917 }
918
924 public function checkTree(): bool
925 {
926 $types = array('integer');
927 $query = 'SELECT lft,rgt FROM ' . $this->table_tree . ' ' .
928 'WHERE ' . $this->tree_pk . ' = %s ';
929
930 $res = $this->db->queryF($query, $types, array($this->tree_id));
931 $lft = $rgt = [];
932 while ($row = $this->db->fetchObject($res)) {
933 $lft[] = $row->lft;
934 $rgt[] = $row->rgt;
935 }
936
937 $all = array_merge($lft, $rgt);
938 $uni = array_unique($all);
939
940 if (count($all) != count($uni)) {
941 $message = 'Tree is corrupted!';
942 $this->logger->error($message);
944 }
945 return true;
946 }
947
952 public function checkTreeChilds(bool $a_no_zero_child = true): bool
953 {
954 $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
955 'WHERE ' . $this->tree_pk . ' = %s ' .
956 'ORDER BY lft';
957 $r1 = $this->db->queryF($query, array('integer'), array($this->tree_id));
958
959 while ($row = $this->db->fetchAssoc($r1)) {
960 //echo "tree:".$row[$this->tree_pk].":lft:".$row["lft"].":rgt:".$row["rgt"].":child:".$row["child"].":<br>";
961 if (($row["child"] == 0) && $a_no_zero_child) {
962 $message = "Tree contains child with ID 0!";
963 $this->logger->error($message);
965 }
966
967 if ($this->table_obj_reference) {
968 // get object reference data
969 $query = 'SELECT * FROM ' . $this->table_obj_reference . ' WHERE ' . $this->ref_pk . ' = %s ';
970 $r2 = $this->db->queryF($query, array('integer'), array($row['child']));
971
972 //echo "num_childs:".$r2->numRows().":<br>";
973 if ($r2->numRows() == 0) {
974 $message = "No Object-to-Reference entry found for ID " . $row["child"] . "!";
975 $this->logger->error($message);
977 }
978 if ($r2->numRows() > 1) {
979 $message = "More Object-to-Reference entries found for ID " . $row["child"] . "!";
980 $this->logger->error($message);
982 }
983
984 // get object data
985 $obj_ref = $this->db->fetchAssoc($r2);
986
987 $query = 'SELECT * FROM ' . $this->table_obj_data . ' WHERE ' . $this->obj_pk . ' = %s';
988 $r3 = $this->db->queryF($query, array('integer'), array($obj_ref[$this->obj_pk]));
989 if ($r3->numRows() == 0) {
990 $message = " No child found for ID " . $obj_ref[$this->obj_pk] . "!";
991 $this->logger->error($message);
993 }
994 if ($r3->numRows() > 1) {
995 $message = "More childs found for ID " . $obj_ref[$this->obj_pk] . "!";
996 $this->logger->error($message);
998 }
999 } else {
1000 // get only object data
1001 $query = 'SELECT * FROM ' . $this->table_obj_data . ' WHERE ' . $this->obj_pk . ' = %s';
1002 $r2 = $this->db->queryF($query, array('integer'), array($row['child']));
1003 //echo "num_childs:".$r2->numRows().":<br>";
1004 if ($r2->numRows() == 0) {
1005 $message = "No child found for ID " . $row["child"] . "!";
1006 $this->logger->error($message);
1008 }
1009 if ($r2->numRows() > 1) {
1010 $message = "More childs found for ID " . $row["child"] . "!";
1011 $this->logger->error($message);
1013 }
1014 }
1015 }
1016 return true;
1017 }
1018
1022 public function getMaximumDepth(): int
1023 {
1024 global $DIC;
1025
1026 $query = 'SELECT MAX(depth) depth FROM ' . $this->table_tree;
1027 $res = $this->db->query($query);
1028
1029 $row = $this->db->fetchAssoc($res);
1030 return (int) $row['depth'];
1031 }
1032
1036 public function getDepth(int $a_node_id): int
1037 {
1038 global $DIC;
1039
1040 if ($a_node_id) {
1041 if ($this->__isMainTree()) {
1042 $query = 'SELECT depth FROM ' . $this->table_tree . ' ' .
1043 'WHERE child = %s ';
1044 $res = $this->db->queryF($query, array('integer'), array($a_node_id));
1045 $row = $this->db->fetchObject($res);
1046 } else {
1047 $query = 'SELECT depth FROM ' . $this->table_tree . ' ' .
1048 'WHERE child = %s ' .
1049 'AND ' . $this->tree_pk . ' = %s ';
1050 $res = $this->db->queryF($query, array('integer', 'integer'), array($a_node_id, $this->tree_id));
1051 $row = $this->db->fetchObject($res);
1052 }
1053 return (int) ($row->depth ?? 0);
1054 }
1055 return 1;
1056 }
1057
1062 public function getNodeTreeData(int $a_node_id): array
1063 {
1064 global $DIC;
1065
1066 if (!$a_node_id) {
1067 $this->logger->logStack(ilLogLevel::ERROR);
1068 throw new InvalidArgumentException('Missing or empty parameter $a_node_id: ' . $a_node_id);
1069 }
1070
1071 $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1072 'WHERE child = ' . $this->db->quote($a_node_id, 'integer');
1073 $res = $this->db->query($query);
1074 while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
1075 return $row;
1076 }
1077 return [];
1078 }
1079
1085 public function getNodeData(int $a_node_id, ?int $a_tree_pk = null): array
1086 {
1087 if ($this->__isMainTree()) {
1088 if ($a_node_id < 1) {
1089 $message = 'No valid parameter given! $a_node_id: %s' . $a_node_id;
1090 $this->logger->error($message);
1091 throw new InvalidArgumentException($message);
1092 }
1093 }
1094
1095 $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1096 $this->buildJoin() .
1097 'WHERE ' . $this->table_tree . '.child = %s ' .
1098 'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s ';
1099 $res = $this->db->queryF($query, array('integer', 'integer'), array(
1100 $a_node_id,
1101 $a_tree_pk === null ? $this->tree_id : $a_tree_pk
1102 ));
1103 $row = $this->db->fetchAssoc($res);
1104 $row[$this->tree_pk] = $this->tree_id;
1105
1106 return $this->fetchNodeData($row);
1107 }
1108
1112 public function fetchNodeData(array $a_row): array
1113 {
1114 global $DIC;
1115
1116 $objDefinition = $DIC['objDefinition'];
1117 $lng = $DIC['lng'];
1118
1119 $data = $a_row;
1120 $data["desc"] = (string) ($a_row["description"] ?? ''); // for compability
1121
1122 // multilingual support systemobjects (sys) & categories (db)
1123 $translation_type = '';
1124 if (is_object($objDefinition)) {
1125 $translation_type = $objDefinition->getTranslationType($data["type"] ?? '');
1126 }
1127
1128 if ($translation_type == "sys") {
1129 if ($data["type"] == "rolf" && $data["obj_id"] != ROLE_FOLDER_ID) {
1130 $data["description"] = (string) $lng->txt("obj_" . $data["type"] . "_local_desc") . $data["title"] . $data["desc"];
1131 $data["desc"] = $lng->txt("obj_" . $data["type"] . "_local_desc") . $data["title"] . $data["desc"];
1132 $data["title"] = $lng->txt("obj_" . $data["type"] . "_local");
1133 } else {
1134 $data["title"] = $lng->txt("obj_" . $data["type"]);
1135 $data["description"] = $lng->txt("obj_" . $data["type"] . "_desc");
1136 $data["desc"] = $lng->txt("obj_" . $data["type"] . "_desc");
1137 }
1138 } elseif ($translation_type == "db") {
1139 // Try to retrieve object translation from cache
1140 $lang_code = ''; // This did never work, because it was undefined before
1141 if ($this->isCacheUsed() &&
1142 array_key_exists($data["obj_id"] . '.' . $lang_code, $this->translation_cache)) {
1143 $key = $data["obj_id"] . '.' . $lang_code;
1144 $data["title"] = $this->translation_cache[$key]['title'];
1145 $data["description"] = $this->translation_cache[$key]['description'];
1146 $data["desc"] = $this->translation_cache[$key]['desc'];
1147 } else {
1148 // Object translation is not in cache, read it from database
1149 $query = 'SELECT title,description FROM object_translation ' .
1150 'WHERE obj_id = %s ' .
1151 'AND lang_code = %s ';
1152
1153 $res = $this->db->queryF($query, array('integer', 'text'), array(
1154 $data['obj_id'],
1155 $this->lang_code
1156 ));
1157 $row = $this->db->fetchObject($res);
1158
1159 if ($row) {
1160 $data["title"] = (string) $row->title;
1161 $data["description"] = ilStr::shortenTextExtended((string) $row->description, ilObject::DESC_LENGTH, true);
1162 $data["desc"] = (string) $row->description;
1163 }
1164
1165 // Store up to 1000 object translations in cache
1166 if ($this->isCacheUsed() && count($this->translation_cache) < 1000) {
1167 $key = $data["obj_id"] . '.' . $lang_code;
1168 $this->translation_cache[$key] = [];
1169 $this->translation_cache[$key]['title'] = $data["title"];
1170 $this->translation_cache[$key]['description'] = $data["description"];
1171 $this->translation_cache[$key]['desc'] = $data["desc"];
1172 }
1173 }
1174 }
1175
1176 // TODO: Handle this switch by module.xml definitions
1177 if (isset($data['type']) && ($data['type'] == 'crsr' || $data['type'] == 'catr' || $data['type'] == 'grpr' || $data['type'] === 'prgr')) {
1178 $data['title'] = ilContainerReference::_lookupTitle((int) $data['obj_id']);
1179 }
1180 return $data;
1181 }
1182
1188 protected function fetchTranslationFromObjectDataCache(array $a_obj_ids): void
1189 {
1190 global $DIC;
1191
1192 $ilObjDataCache = $DIC['ilObjDataCache'];
1193
1194 if ($this->isCacheUsed() && is_array($a_obj_ids) && is_object($ilObjDataCache)) {
1195 foreach ($a_obj_ids as $id) {
1196 $this->translation_cache[$id . '.']['title'] = $ilObjDataCache->lookupTitle((int) $id);
1197 $this->translation_cache[$id . '.']['description'] = $ilObjDataCache->lookupDescription((int) $id);
1198 $this->translation_cache[$id . '.']['desc'] =
1199 $this->translation_cache[$id . '.']['description'];
1200 }
1201 }
1202 }
1203
1208 public function isInTree(?int $a_node_id): bool
1209 {
1210 if (is_null($a_node_id) || !$a_node_id) {
1211 return false;
1212 }
1213 // is in tree cache
1214 if ($this->isCacheUsed() && isset($this->in_tree_cache[$a_node_id])) {
1215 return $this->in_tree_cache[$a_node_id];
1216 }
1217
1218 $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1219 'WHERE ' . $this->table_tree . '.child = %s ' .
1220 'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s';
1221
1222 $res = $this->db->queryF($query, array('integer', 'integer'), array(
1223 $a_node_id,
1224 $this->tree_id
1225 ));
1226
1227 if ($res->numRows() > 0) {
1228 if ($this->__isMainTree()) {
1229 $this->in_tree_cache[$a_node_id] = true;
1230 }
1231 return true;
1232 } else {
1233 if ($this->__isMainTree()) {
1234 $this->in_tree_cache[$a_node_id] = false;
1235 }
1236 return false;
1237 }
1238 }
1239
1243 public function getParentNodeData(int $a_node_id): array
1244 {
1245 global $DIC;
1246
1247 $ilLog = $DIC['ilLog'];
1248 if ($this->table_obj_reference) {
1249 // Use inner join instead of left join to improve performance
1250 $innerjoin = "JOIN " . $this->table_obj_reference . " ON v.child=" . $this->table_obj_reference . "." . $this->ref_pk . " " .
1251 "JOIN " . $this->table_obj_data . " ON " . $this->table_obj_reference . "." . $this->obj_pk . "=" . $this->table_obj_data . "." . $this->obj_pk . " ";
1252 } else {
1253 // Use inner join instead of left join to improve performance
1254 $innerjoin = "JOIN " . $this->table_obj_data . " ON v.child=" . $this->table_obj_data . "." . $this->obj_pk . " ";
1255 }
1256
1257 $query = 'SELECT * FROM ' . $this->table_tree . ' s, ' . $this->table_tree . ' v ' .
1258 $innerjoin .
1259 'WHERE s.child = %s ' .
1260 'AND s.parent = v.child ' .
1261 'AND s.' . $this->tree_pk . ' = %s ' .
1262 'AND v.' . $this->tree_pk . ' = %s';
1263 $res = $this->db->queryF($query, array('integer', 'integer', 'integer'), array(
1264 $a_node_id,
1265 $this->tree_id,
1266 $this->tree_id
1267 ));
1268 $row = $this->db->fetchAssoc($res);
1269 if (is_array($row)) {
1270 return $this->fetchNodeData($row);
1271 }
1272 return [];
1273 }
1274
1278 public function isGrandChild(int $a_startnode_id, int $a_querynode_id): bool
1279 {
1280 return $this->getRelation($a_startnode_id, $a_querynode_id) == self::RELATION_PARENT;
1281 }
1282
1287 public function addTree(int $a_tree_id, int $a_node_id = -1): bool
1288 {
1289 global $DIC;
1290
1291 // FOR SECURITY addTree() IS NOT ALLOWED ON MAIN TREE
1292 if ($this->__isMainTree()) {
1293 $message = sprintf(
1294 'Operation not allowed on main tree! $a_tree_if: %s $a_node_id: %s',
1295 $a_tree_id,
1296 $a_node_id
1297 );
1298 $this->logger->error($message);
1299 throw new InvalidArgumentException($message);
1300 }
1301
1302 if ($a_node_id <= 0) {
1303 $a_node_id = $a_tree_id;
1304 }
1305
1306 $query = 'INSERT INTO ' . $this->table_tree . ' (' .
1307 $this->tree_pk . ', child,parent,lft,rgt,depth) ' .
1308 'VALUES ' .
1309 '(%s,%s,%s,%s,%s,%s)';
1310 $res = $this->db->manipulateF(
1311 $query,
1312 array('integer', 'integer', 'integer', 'integer', 'integer', 'integer'),
1313 array(
1314 $a_tree_id,
1315 $a_node_id,
1316 0,
1317 1,
1318 2,
1319 1
1320 )
1321 );
1322
1323 return true;
1324 }
1325
1329 public function removeTree(int $a_tree_id): bool
1330 {
1331 if ($this->__isMainTree()) {
1332 $this->logger->logStack(ilLogLevel::ERROR);
1333 throw new InvalidArgumentException('Operation not allowed on main tree');
1334 }
1335 if (!$a_tree_id) {
1336 $this->logger->logStack(ilLogLevel::ERROR);
1337 throw new InvalidArgumentException('Missing parameter tree id');
1338 }
1339
1340 $query = 'DELETE FROM ' . $this->table_tree .
1341 ' WHERE ' . $this->tree_pk . ' = %s ';
1342 $this->db->manipulateF($query, array('integer'), array($a_tree_id));
1343 return true;
1344 }
1345
1351 public function moveToTrash(int $a_node_id, bool $a_set_deleted = false, int $a_deleted_by = 0): bool
1352 {
1353 global $DIC;
1354
1355 $user = $DIC->user();
1356 if (!$a_deleted_by) {
1357 $a_deleted_by = $user->getId();
1358 }
1359
1360 if (!$a_node_id) {
1361 $this->logger->logStack(ilLogLevel::ERROR);
1362 throw new InvalidArgumentException('No valid parameter given! $a_node_id: ' . $a_node_id);
1363 }
1364
1365 $query = $this->getTreeImplementation()->getSubTreeQuery($this->getNodeTreeData($a_node_id), [], false);
1366 $res = $this->db->query($query);
1367
1368 $subnodes = [];
1369 while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
1370 $subnodes[] = (int) $row['child'];
1371 }
1372
1373 if (!count($subnodes)) {
1374 // Possibly already deleted
1375 return false;
1376 }
1377
1378 if ($a_set_deleted) {
1379 ilObject::setDeletedDates($subnodes, $a_deleted_by);
1380 }
1381 // netsted set <=> mp
1382 $this->getTreeImplementation()->moveToTrash($a_node_id);
1383 return true;
1384 }
1385
1390 public function isDeleted(int $a_node_id): bool
1391 {
1392 if ($this->isCacheUsed() && isset($this->is_saved_cache[$a_node_id])) {
1393 return $this->is_saved_cache[$a_node_id];
1394 }
1395
1396 $query = 'SELECT ' . $this->tree_pk . ' FROM ' . $this->table_tree . ' ' .
1397 'WHERE child = %s ';
1398 $res = $this->db->queryF($query, array('integer'), array($a_node_id));
1399 $row = $this->db->fetchAssoc($res);
1400
1401 $tree_id = $row[$this->tree_pk] ?? 0;
1402 if ($tree_id < 0) {
1403 if ($this->__isMainTree()) {
1404 $this->is_saved_cache[$a_node_id] = true;
1405 }
1406 return true;
1407 } else {
1408 if ($this->__isMainTree()) {
1409 $this->is_saved_cache[$a_node_id] = false;
1410 }
1411 return false;
1412 }
1413 }
1414
1418 public function preloadDeleted(array $a_node_ids): void
1419 {
1420 if (!is_array($a_node_ids) || !$this->isCacheUsed()) {
1421 return;
1422 }
1423
1424 $query = 'SELECT ' . $this->tree_pk . ', child FROM ' . $this->table_tree . ' ' .
1425 'WHERE ' . $this->db->in("child", $a_node_ids, false, "integer");
1426
1427 $res = $this->db->query($query);
1428 while ($row = $this->db->fetchAssoc($res)) {
1429 if ($row[$this->tree_pk] < 0) {
1430 if ($this->__isMainTree()) {
1431 $this->is_saved_cache[$row["child"]] = true;
1432 }
1433 } else {
1434 if ($this->__isMainTree()) {
1435 $this->is_saved_cache[$row["child"]] = false;
1436 }
1437 }
1438 }
1439 }
1440
1445 public function getSavedNodeData(int $a_parent_id): array
1446 {
1447 global $DIC;
1448
1449 if (!isset($a_parent_id)) {
1450 $message = "No node_id given!";
1451 $this->logger->error($message);
1452 throw new InvalidArgumentException($message);
1453 }
1454
1455 $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1456 $this->buildJoin() .
1457 'WHERE ' . $this->table_tree . '.' . $this->tree_pk . ' < %s ' .
1458 'AND ' . $this->table_tree . '.parent = %s';
1459 $res = $this->db->queryF($query, array('integer', 'integer'), array(
1460 0,
1461 $a_parent_id
1462 ));
1463
1464 $saved = [];
1465 while ($row = $this->db->fetchAssoc($res)) {
1466 $saved[] = $this->fetchNodeData($row);
1467 }
1468
1469 return $saved;
1470 }
1471
1475 public function getSavedNodeObjIds(array $a_obj_ids): array
1476 {
1477 global $DIC;
1478
1479 $query = 'SELECT ' . $this->table_obj_data . '.obj_id FROM ' . $this->table_tree . ' ' .
1480 $this->buildJoin() .
1481 'WHERE ' . $this->table_tree . '.' . $this->tree_pk . ' < ' . $this->db->quote(0, 'integer') . ' ' .
1482 'AND ' . $this->db->in($this->table_obj_data . '.obj_id', $a_obj_ids, false, 'integer');
1483 $res = $this->db->query($query);
1484 $saved = [];
1485 while ($row = $this->db->fetchAssoc($res)) {
1486 $saved[] = (int) $row['obj_id'];
1487 }
1488
1489 return $saved;
1490 }
1491
1496 public function getParentId(int $a_node_id): ?int
1497 {
1498 global $DIC;
1499 if ($this->__isMainTree()) {
1500 $query = 'SELECT parent FROM ' . $this->table_tree . ' ' .
1501 'WHERE child = %s ';
1502 $res = $this->db->queryF(
1503 $query,
1504 ['integer'],
1505 [$a_node_id]
1506 );
1507 } else {
1508 $query = 'SELECT parent FROM ' . $this->table_tree . ' ' .
1509 'WHERE child = %s ' .
1510 'AND ' . $this->tree_pk . ' = %s ';
1511 $res = $this->db->queryF($query, array('integer', 'integer'), array(
1512 $a_node_id,
1513 $this->tree_id
1514 ));
1515 }
1516
1517 if ($row = $this->db->fetchObject($res)) {
1518 return (int) $row->parent;
1519 }
1520 return null;
1521 }
1522
1527 public function getChildSequenceNumber(array $a_node, string $type = ""): int
1528 {
1529 $tree_implementation = $this->getTreeImplementation();
1530 if ($tree_implementation instanceof ilNestedSetTree) {
1531 return $tree_implementation->getChildSequenceNumber($a_node, $type);
1532 } else {
1533 $message = "This tree is not part of ilNestedSetTree.";
1534 $this->logger->error($message);
1535 throw new LogicException($message);
1536 }
1537 }
1538
1539 public function readRootId(): int
1540 {
1541 $query = 'SELECT child FROM ' . $this->table_tree . ' ' .
1542 'WHERE parent = %s ' .
1543 'AND ' . $this->tree_pk . ' = %s ';
1544 $res = $this->db->queryF($query, array('integer', 'integer'), array(
1545 0,
1546 $this->tree_id
1547 ));
1548 $this->root_id = 0;
1549 if ($row = $this->db->fetchObject($res)) {
1550 $this->root_id = (int) $row->child;
1551 }
1552 return $this->root_id;
1553 }
1554
1555 public function getRootId(): int
1556 {
1557 return $this->root_id;
1558 }
1559
1560 public function setRootId(int $a_root_id): void
1561 {
1562 $this->root_id = $a_root_id;
1563 }
1564
1565 public function getTreeId(): int
1566 {
1567 return $this->tree_id;
1568 }
1569
1574 public function fetchSuccessorNode(int $a_node_id, string $a_type = ""): ?array
1575 {
1576 $tree_implementation = $this->getTreeImplementation();
1577 if ($tree_implementation instanceof ilNestedSetTree) {
1578 return $tree_implementation->fetchSuccessorNode($a_node_id, $a_type);
1579 } else {
1580 $message = "This tree is not part of ilNestedSetTree.";
1581 $this->logger->error($message);
1582 throw new LogicException($message);
1583 }
1584 }
1585
1590 public function fetchPredecessorNode(int $a_node_id, string $a_type = ""): ?array
1591 {
1592 $tree_implementation = $this->getTreeImplementation();
1593 if ($tree_implementation instanceof ilNestedSetTree) {
1594 return $tree_implementation->fetchPredecessorNode($a_node_id, $a_type);
1595 } else {
1596 $message = "This tree is not part of ilNestedSetTree.";
1597 $this->logger->error($message);
1598 throw new LogicException($message);
1599 }
1600 }
1601
1606 public function renumber(int $node_id = 1, int $i = 1): int
1607 {
1608 $renumber_callable = function (ilDBInterface $db) use ($node_id, $i, &$return) {
1609 $return = $this->__renumber($node_id, $i);
1610 };
1611
1612 // LOCKED ###################################
1613 if ($this->__isMainTree()) {
1614 $ilAtomQuery = $this->db->buildAtomQuery();
1615 $ilAtomQuery->addTableLock($this->table_tree);
1616
1617 $ilAtomQuery->addQueryCallable($renumber_callable);
1618 $ilAtomQuery->run();
1619 } else {
1620 $renumber_callable($this->db);
1621 }
1622 return $return;
1623 }
1624
1630 protected function __renumber(int $node_id = 1, int $i = 1): int
1631 {
1632 if ($this->isRepositoryTree()) {
1633 $query = 'UPDATE ' . $this->table_tree . ' SET lft = %s WHERE child = %s';
1634 $this->db->manipulateF(
1635 $query,
1636 array('integer', 'integer'),
1637 array(
1638 $i,
1639 $node_id
1640 )
1641 );
1642 } else {
1643 $query = 'UPDATE ' . $this->table_tree . ' SET lft = %s WHERE child = %s AND tree = %s';
1644 $this->db->manipulateF(
1645 $query,
1646 array('integer', 'integer', 'integer'),
1647 array(
1648 $i,
1649 $node_id,
1650 $this->tree_id
1651 )
1652 );
1653 }
1654
1655 $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1656 'WHERE parent = ' . $this->db->quote($node_id, 'integer') . ' ' .
1657 'ORDER BY lft';
1658 $res = $this->db->query($query);
1659
1660 $childs = [];
1661 while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) {
1662 $childs[] = (int) $row->child;
1663 }
1664
1665 foreach ($childs as $child) {
1666 $i = $this->__renumber($child, $i + 1);
1667 }
1668 $i++;
1669
1670 // Insert a gap at the end of node, if the node has children
1671 if (count($childs) > 0) {
1672 $i += $this->gap * 2;
1673 }
1674
1675 if ($this->isRepositoryTree()) {
1676 $query = 'UPDATE ' . $this->table_tree . ' SET rgt = %s WHERE child = %s';
1677 $res = $this->db->manipulateF(
1678 $query,
1679 array('integer', 'integer'),
1680 array(
1681 $i,
1682 $node_id
1683 )
1684 );
1685 } else {
1686 $query = 'UPDATE ' . $this->table_tree . ' SET rgt = %s WHERE child = %s AND tree = %s';
1687 $res = $this->db->manipulateF($query, array('integer', 'integer', 'integer'), array(
1688 $i,
1689 $node_id,
1690 $this->tree_id
1691 ));
1692 }
1693 return $i;
1694 }
1695
1700 public function checkForParentType(int $a_ref_id, string $a_type, bool $a_exclude_source_check = false): int
1701 {
1702 // #12577
1703 $cache_key = $a_ref_id . '.' . $a_type . '.' . ((int) $a_exclude_source_check);
1704
1705 // Try to return a cached result
1706 if ($this->isCacheUsed() &&
1707 array_key_exists($cache_key, $this->parent_type_cache)) {
1708 return (int) $this->parent_type_cache[$cache_key];
1709 }
1710
1711 // Store up to 1000 results in cache
1712 $do_cache = ($this->__isMainTree() && count($this->parent_type_cache) < 1000);
1713
1714 // ref_id is not in tree
1715 if (!$this->isInTree($a_ref_id)) {
1716 if ($do_cache) {
1717 $this->parent_type_cache[$cache_key] = false;
1718 }
1719 return 0;
1720 }
1721
1722 $path = array_reverse($this->getPathFull($a_ref_id));
1723
1724 // remove first path entry as it is requested node
1725 if ($a_exclude_source_check) {
1726 array_shift($path);
1727 }
1728
1729 foreach ($path as $node) {
1730 // found matching parent
1731 if ($node["type"] == $a_type) {
1732 if ($do_cache) {
1733 $this->parent_type_cache[$cache_key] = (int) $node["child"];
1734 }
1735 return (int) $node["child"];
1736 }
1737 }
1738
1739 if ($do_cache) {
1740 $this->parent_type_cache[$cache_key] = false;
1741 }
1742 return 0;
1743 }
1744
1750 public static function _removeEntry(int $a_tree, int $a_child, string $a_db_table = "tree"): void
1751 {
1752 global $DIC;
1753
1754 $db = $DIC->database();
1755
1756 if ($a_db_table === 'tree') {
1757 if ($a_tree == 1 && $a_child == ROOT_FOLDER_ID) {
1758 $message = sprintf(
1759 'Tried to delete root node! $a_tree: %s $a_child: %s',
1760 $a_tree,
1761 $a_child
1762 );
1763 ilLoggerFactory::getLogger('tree')->error($message);
1764 throw new InvalidArgumentException($message);
1765 }
1766 }
1767
1768 $query = 'DELETE FROM ' . $a_db_table . ' ' .
1769 'WHERE tree = %s ' .
1770 'AND child = %s ';
1771 $res = $db->manipulateF($query, array('integer', 'integer'), array(
1772 $a_tree,
1773 $a_child
1774 ));
1775 }
1776
1780 public function __isMainTree(): bool
1781 {
1782 return $this->table_tree === 'tree';
1783 }
1784
1791 protected function __checkDelete(array $a_node): bool
1792 {
1793 $query = $this->getTreeImplementation()->getSubTreeQuery($a_node, [], false);
1794 $this->logger->debug($query);
1795 $res = $this->db->query($query);
1796
1797 $counter = (int) $lft_childs = [];
1798 while ($row = $this->db->fetchObject($res)) {
1799 $lft_childs[$row->child] = (int) $row->parent;
1800 ++$counter;
1801 }
1802
1803 // CHECK FOR DUPLICATE CHILD IDS
1804 if ($counter != count($lft_childs)) {
1805 $message = 'Duplicate entries for "child" in maintree! $a_node_id: ' . $a_node['child'];
1806
1807 $this->logger->error($message);
1809 }
1810
1811 // GET SUBTREE BY PARENT RELATION
1812 $parent_childs = [];
1813 $this->__getSubTreeByParentRelation((int)$a_node['child'], $parent_childs);
1814 $this->__validateSubtrees($lft_childs, $parent_childs);
1815
1816 return true;
1817 }
1818
1823 protected function __getSubTreeByParentRelation(int $a_node_id, array &$parent_childs): bool
1824 {
1825 // GET PARENT ID
1826 $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1827 'WHERE child = %s ' .
1828 'AND tree = %s ';
1829 $res = $this->db->queryF($query, array('integer', 'integer'), array(
1830 $a_node_id,
1831 $this->tree_id
1832 ));
1833
1834 $counter = 0;
1835 while ($row = $this->db->fetchObject($res)) {
1836 $parent_childs[$a_node_id] = (int) $row->parent;
1837 ++$counter;
1838 }
1839 // MULTIPLE ENTRIES
1840 if ($counter > 1) {
1841 $message = 'Multiple entries in maintree! $a_node_id: ' . $a_node_id;
1842
1843 $this->logger->error($message);
1845 }
1846
1847 // GET ALL CHILDS
1848 $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1849 'WHERE parent = %s ';
1850 $res = $this->db->queryF($query, array('integer'), array($a_node_id));
1851
1852 while ($row = $this->db->fetchObject($res)) {
1853 // RECURSION
1854 $this->__getSubTreeByParentRelation((int) $row->child, $parent_childs);
1855 }
1856 return true;
1857 }
1858
1866 protected function __validateSubtrees(array &$lft_childs, array $parent_childs): bool
1867 {
1868 // SORT BY KEY
1869 ksort($lft_childs);
1870 ksort($parent_childs);
1871
1872 $this->logger->debug('left childs ' . print_r($lft_childs, true));
1873 $this->logger->debug('parent childs ' . print_r($parent_childs, true));
1874
1875 if (count($lft_childs) != count($parent_childs)) {
1876 $message = '(COUNT) Tree is corrupted! Left/Right subtree does not comply with parent relation';
1877 $this->logger->error($message);
1879 }
1880
1881 foreach ($lft_childs as $key => $value) {
1882 if ($parent_childs[$key] != $value) {
1883 $message = '(COMPARE) Tree is corrupted! Left/Right subtree does not comply with parent relation';
1884 $this->logger->error($message);
1886 }
1887 if ($key == ROOT_FOLDER_ID) {
1888 $message = '(ROOT_FOLDER) Tree is corrupted! Tried to delete root folder';
1889 $this->logger->error($message);
1891 }
1892 }
1893 return true;
1894 }
1895
1904 public function moveTree(int $a_source_id, int $a_target_id, int $a_location = self::POS_LAST_NODE): void
1905 {
1906 $old_parent_id = $this->getParentId($a_source_id);
1907 $this->getTreeImplementation()->moveTree($a_source_id, $a_target_id, $a_location);
1908 if (isset($GLOBALS['DIC']["ilAppEventHandler"]) && $this->__isMainTree()) {
1909 $GLOBALS['DIC']['ilAppEventHandler']->raise(
1910 "components/ILIAS/Tree",
1911 "moveTree",
1912 array(
1913 'tree' => $this->table_tree,
1914 'source_id' => $a_source_id,
1915 'target_id' => $a_target_id,
1916 'old_parent_id' => $old_parent_id
1917 )
1918 );
1919 }
1920 }
1921
1927 public function getRbacSubtreeInfo(int $a_endnode_id): array
1928 {
1929 return $this->getTreeImplementation()->getSubtreeInfo($a_endnode_id);
1930 }
1931
1935 public function getSubTreeQuery(
1936 int $a_node_id,
1937 array $a_fields = [],
1938 array $a_types = [],
1939 bool $a_force_join_reference = false
1940 ): string {
1941 return $this->getTreeImplementation()->getSubTreeQuery(
1942 $this->getNodeTreeData($a_node_id),
1943 $a_types,
1944 $a_force_join_reference,
1945 $a_fields
1946 );
1947 }
1948
1949 public function getTrashSubTreeQuery(
1950 int $a_node_id,
1951 array $a_fields = [],
1952 array $a_types = [],
1953 bool $a_force_join_reference = false
1954 ): string {
1955 return $this->getTreeImplementation()->getTrashSubTreeQuery(
1956 $this->getNodeTreeData($a_node_id),
1957 $a_types,
1958 $a_force_join_reference,
1959 $a_fields
1960 );
1961 }
1962
1969 public function getSubTreeFilteredByObjIds(int $a_node_id, array $a_obj_ids, array $a_fields = []): array
1970 {
1971 $node = $this->getNodeData($a_node_id);
1972 if (!count($node)) {
1973 return [];
1974 }
1975
1976 $res = [];
1977
1978 $query = $this->getTreeImplementation()->getSubTreeQuery($node, [], true, array($this->ref_pk));
1979
1980 $fields = '*';
1981 if (count($a_fields)) {
1982 $fields = implode(',', $a_fields);
1983 }
1984
1985 $query = "SELECT " . $fields .
1986 " FROM " . $this->getTreeTable() .
1987 " " . $this->buildJoin() .
1988 " WHERE " . $this->getTableReference() . "." . $this->ref_pk . " IN (" . $query . ")" .
1989 " AND " . $this->db->in($this->getObjectDataTable() . "." . $this->obj_pk, $a_obj_ids, false, "integer");
1990 $set = $this->db->query($query);
1991 while ($row = $this->db->fetchAssoc($set)) {
1992 $res[] = $row;
1993 }
1994
1995 return $res;
1996 }
1997
1998 public function deleteNode(int $a_tree_id, int $a_node_id): void
1999 {
2000 $query = 'DELETE FROM tree where ' .
2001 'child = ' . $this->db->quote($a_node_id, 'integer') . ' ' .
2002 'AND tree = ' . $this->db->quote($a_tree_id, 'integer');
2003 $this->db->manipulate($query);
2004
2005 $this->eventHandler->raise(
2006 "components/ILIAS/Tree",
2007 "deleteNode",
2008 [
2009 'tree' => $this->table_tree,
2010 'node_id' => $a_node_id,
2011 'tree_id' => $a_tree_id
2012 ]
2013 );
2014 }
2015
2020 public function lookupTrashedObjectTypes(): array
2021 {
2022 $query = 'SELECT DISTINCT(o.type) ' . $this->db->quoteIdentifier('type') .
2023 ' FROM tree t JOIN object_reference r ON child = r.ref_id ' .
2024 'JOIN object_data o on r.obj_id = o.obj_id ' .
2025 'WHERE tree < ' . $this->db->quote(0, 'integer') . ' ' .
2026 'AND child = -tree ' .
2027 'GROUP BY o.type';
2028 $res = $this->db->query($query);
2029
2030 $types_deleted = [];
2031 while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) {
2032 $types_deleted[] = (string) $row->type;
2033 }
2034 return $types_deleted;
2035 }
2036
2040 protected function isRepositoryTree(): bool
2041 {
2042 return $this->table_tree == 'tree';
2043 }
2044} // END class.tree
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
Global event handler.
static quoteArray(array $a_array)
Quotes all members of an array for usage in DB query statement.
static _lookupTitle(int $obj_id)
Thrown if invalid tree strucutes are found.
static getLogger(string $a_component_id)
Get component logger.
Component logger with individual log levels by component id.
Base class for materialize path based trees Based on implementation of Werner Randelshofer.
Base class for nested set path based trees.
static setDeletedDates(array $ref_ids, int $user_id)
const DESC_LENGTH
static _resetDeletedDate(int $ref_id)
ILIAS Setting Class.
static shortenTextExtended(string $a_str, int $a_len, bool $a_dots=false, bool $a_next_blank=false, bool $a_keep_extension=false)
Tree class data representation in hierachical trees using the Nested Set Model with Gaps by Joe Celco...
isCacheUsed()
Check if cache is active.
lookupTrashedObjectTypes()
Lookup object types in trash.
string $tree_pk
column name containing tree id in tree table
string $table_tree
table name of tree table
fetchNodeData(array $a_row)
get data of parent node from tree and object_data
getRbacSubtreeInfo(int $a_endnode_id)
This method is used for change existing objects and returns all necessary information for this action...
setReferenceTablePK(string $a_column_name)
set column containing primary key in reference table
getTreePk()
Get tree primary key.
getParentCache()
Get parent cache.
getChildSequenceNumber(array $a_node, string $type="")
get sequence number of node in sibling sequence
fetchPredecessorNode(int $a_node_id, string $a_type="")
get node data of predecessor node
insertNodeFromTrash(int $a_source_id, int $a_target_id, int $a_tree_id, int $a_pos=self::POS_LAST_NODE, bool $a_reset_deleted_date=false)
Insert node from trash deletes trash entry.
isInTree(?int $a_node_id)
get all information of a node.
setRootId(int $a_root_id)
isGrandChild(int $a_startnode_id, int $a_querynode_id)
checks if a node is in the path of an other node
getPathFull(int $a_endnode_id, int $a_startnode_id=0)
get path from a given startnode to a given endnode if startnode is not given the rootnode is startnod...
isDeleted(int $a_node_id)
This is a wrapper for isSaved() with a more useful name.
getNodeTreeData(int $a_node_id)
return all columns of tabel tree
fetchTranslationFromObjectDataCache(array $a_obj_ids)
Get translation data from object cache (trigger in object cache on preload)
isRepositoryTree()
check if current tree instance operates on repository tree table
array $parent_cache
const RELATION_EQUALS
array $parent_type_cache
const DEFAULT_GAP
insertNode(int $a_node_id, int $a_parent_id, int $a_pos=self::POS_LAST_NODE, bool $a_reset_deletion_date=false)
insert new node with node_id under parent node with parent_id
addTree(int $a_tree_id, int $a_node_id=-1)
create a new tree to do: ???
getDepthCache()
Get depth cache.
static _removeEntry(int $a_tree, int $a_child, string $a_db_table="tree")
STATIC METHOD Removes a single entry from a tree.
getNodeData(int $a_node_id, ?int $a_tree_pk=null)
get all information of a node.
string $ref_pk
column name containing primary key in reference table
renumber(int $node_id=1, int $i=1)
Wrapper for renumber.
getTreeTable()
Get tree table name.
array $path_id_cache
useCache(bool $a_use=true)
Use Cache (usually activated)
getRelationOfNodes(array $a_node_a_arr, array $a_node_b_arr)
get relation of two nodes by node data
moveTree(int $a_source_id, int $a_target_id, int $a_location=self::POS_LAST_NODE)
Move Tree Implementation @access public.
ilDBInterface $db
array $is_saved_cache
const POS_LAST_NODE
string $obj_pk
column name containing primary key in object table
bool $use_cache
array $oc_preloaded
int $root_id
points to root node (may be a subtree)
setTreeTablePK(string $a_column_name)
set column containing primary key in tree table
ilTreeImplementation $tree_impl
setObjectTablePK(string $a_column_name)
set column containing primary key in object table
getTableReference()
Get reference table if available.
const TREE_TYPE_NESTED_SET
getSubTreeQuery(int $a_node_id, array $a_fields=[], array $a_types=[], bool $a_force_join_reference=false)
Get tree subtree query.
__renumber(int $node_id=1, int $i=1)
This method is private.
array $depth_cache
__validateSubtrees(array &$lft_childs, array $parent_childs)
setTableNames(string $a_table_tree, string $a_table_obj_data, string $a_table_obj_reference="")
set table names The primary key of the table containing your object_data must be 'obj_id' You may use...
getSubTreeIds(int $a_ref_id)
Get all ids of subnodes.
int $gap
Size of the gaps to be created in the nested sets sequence numbering of the tree nodes.
array $in_tree_cache
getChilds(int $a_node_id, string $a_order="", string $a_direction="ASC")
get child nodes of given node
static lookupTreesForNode(int $node_id)
checkTree()
check consistence of tree all left & right values are checked if they are exists only once
ilLogger $logger
getSubTree(array $a_node, bool $a_with_data=true, array $a_type=[])
get all nodes in the subtree under specified node
resetInTreeCache()
reset in tree cache
getParentNodeData(int $a_node_id)
get data of parent node from tree and object_data
checkForParentType(int $a_ref_id, string $a_type, bool $a_exclude_source_check=false)
Check for parent type e.g check if a folder (ref_id 3) is in a parent course obj => checkForParentTyp...
getSavedNodeObjIds(array $a_obj_ids)
get object id of saved/deleted nodes
deleteTree(array $a_node)
delete node and the whole subtree under this node
getMaximumDepth()
Return the current maximum depth in the tree.
getChildsByType(int $a_node_id, string $a_type)
get child nodes of given node by object type
moveToTrash(int $a_node_id, bool $a_set_deleted=false, int $a_deleted_by=0)
Move node to trash bin.
__construct(int $a_tree_id, int $a_root_id=0, ?ilDBInterface $db=null)
const POS_FIRST_NODE
const RELATION_PARENT
deleteNode(int $a_tree_id, int $a_node_id)
initLangCode()
Do not use it Store user language.
const RELATION_NONE
buildJoin()
build join depending on table settings @access private
getTreeImplementation()
Get tree implementation.
getPathId(int $a_endnode_id, int $a_startnode_id=0)
get path from a given startnode to a given endnode if startnode is not given the rootnode is startnod...
initTreeImplementation()
Init tree implementation.
const RELATION_SIBLING
getChildsByTypeFilter(int $a_node_id, array $a_types, string $a_order="", string $a_direction="ASC")
get child nodes of given node by object type
getObjectDataTable()
Get object data table.
string $lang_code
getRelation(int $a_node_a, int $a_node_b)
Get relation of two nodes.
getChildIds(int $a_node)
getNodePath(int $a_endnode_id, int $a_startnode_id=0)
Returns the node path for the specified object reference.
const TREE_TYPE_MATERIALIZED_PATH
const DEFAULT_LANGUAGE
string $table_obj_data
table name of object_data table
getTrashSubTreeQuery(int $a_node_id, array $a_fields=[], array $a_types=[], bool $a_force_join_reference=false)
preloadDeleted(array $a_node_ids)
Preload deleted information.
array $translation_cache
__getSubTreeByParentRelation(int $a_node_id, array &$parent_childs)
__isMainTree()
Check if operations are done on main tree.
getFilteredSubTree(int $a_node_id, array $a_filter=[])
get filtered subtree get all subtree nodes beginning at a specific node excluding specific object typ...
string $table_obj_reference
table name of object_reference table
checkTreeChilds(bool $a_no_zero_child=true)
check, if all childs of tree nodes exist in object table
getGap()
Get default gap.
const RELATION_CHILD
int $tree_id
to use different trees in one db-table
removeTree(int $a_tree_id)
remove an existing tree
getDepth(int $a_node_id)
return depth of a node in tree
getParentId(int $a_node_id)
get parent id of given node
preloadDepthParent(array $a_node_ids)
Preload depth/parent.
getFilteredChilds(array $a_filter, int $a_node, string $a_order="", string $a_direction="ASC")
get child nodes of given node (exclude filtered obj_types)
getSavedNodeData(int $a_parent_id)
get data saved/deleted nodes
fetchSuccessorNode(int $a_node_id, string $a_type="")
get node data of successor node
__checkDelete(array $a_node)
Check for deleteTree() compares a subtree of a given node by checking lft, rgt against parent relatio...
validateParentRelations()
Validate parent relations of tree.
ilAppEventHandler $eventHandler
getSubTreeFilteredByObjIds(int $a_node_id, array $a_obj_ids, array $a_fields=[])
get all node ids in the subtree under specified node id, filter by object ids
const ROLE_FOLDER_ID
Definition: constants.php:34
const ROOT_FOLDER_ID
Definition: constants.php:32
return['delivery_method'=> 'php',]
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Interface ilDBInterface.
quote($value, string $type)
manipulateF(string $query, array $types, array $values)
query(string $query)
Run a (read-only) Query on the database.
Interface for tree implementations Currrently nested set or materialized path.
$path
Definition: ltiservices.php:30
$res
Definition: ltiservices.php:69
global $lng
Definition: privfeed.php:31
if(!file_exists('../ilias.ini.php'))
global $DIC
Definition: shib_login.php:26
$q
Definition: shib_logout.php:23
$counter
$GLOBALS["DIC"]
Definition: wac.php:54
$message
Definition: xapiexit.php:31