ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.ilTree.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
29 class 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  public 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 
264  protected function getLangCode(): string
265  {
266  return $this->lang_code;
267  }
268 
275  public function initLangCode(): void
276  {
277  global $DIC;
278 
279  if ($DIC->offsetExists('ilUser')) {
280  $this->lang_code = $DIC->user()->getCurrentLanguage() ?
281  $DIC->user()->getCurrentLanguage() : self::DEFAULT_LANGUAGE;
282  } else {
283  $this->lang_code = self::DEFAULT_LANGUAGE;
284  }
285  }
286 
290  public function getTreeTable(): string
291  {
292  return $this->table_tree;
293  }
294 
298  public function getObjectDataTable(): string
299  {
300  return $this->table_obj_data;
301  }
302 
306  public function getTreePk(): string
307  {
308  return $this->tree_pk;
309  }
310 
314  public function getTableReference(): string
315  {
317  }
318 
322  public function getGap(): int
323  {
324  return $this->gap;
325  }
326 
330  public function resetInTreeCache(): void
331  {
332  $this->in_tree_cache = array();
333  }
334 
343  public function setTableNames(
344  string $a_table_tree,
345  string $a_table_obj_data,
346  string $a_table_obj_reference = ""
347  ): void {
348  $this->table_tree = $a_table_tree;
349  $this->table_obj_data = $a_table_obj_data;
350  $this->table_obj_reference = $a_table_obj_reference;
351 
352  // reconfigure tree implementation
353  $this->initTreeImplementation();
354  }
355 
359  public function setReferenceTablePK(string $a_column_name): void
360  {
361  $this->ref_pk = $a_column_name;
362  }
363 
367  public function setObjectTablePK(string $a_column_name): void
368  {
369  $this->obj_pk = $a_column_name;
370  }
371 
375  public function setTreeTablePK(string $a_column_name): void
376  {
377  $this->tree_pk = $a_column_name;
378  }
379 
385  public function buildJoin(): string
386  {
387  if ($this->table_obj_reference) {
388  // Use inner join instead of left join to improve performance
389  return "JOIN " . $this->table_obj_reference . " ON " . $this->table_tree . ".child=" . $this->table_obj_reference . "." . $this->ref_pk . " " .
390  "JOIN " . $this->table_obj_data . " ON " . $this->table_obj_reference . "." . $this->obj_pk . "=" . $this->table_obj_data . "." . $this->obj_pk . " ";
391  } else {
392  // Use inner join instead of left join to improve performance
393  return "JOIN " . $this->table_obj_data . " ON " . $this->table_tree . ".child=" . $this->table_obj_data . "." . $this->obj_pk . " ";
394  }
395  }
396 
401  public function getRelation(int $a_node_a, int $a_node_b): int
402  {
403  return $this->getRelationOfNodes(
404  $this->getNodeTreeData($a_node_a),
405  $this->getNodeTreeData($a_node_b)
406  );
407  }
408 
412  public function getRelationOfNodes(array $a_node_a_arr, array $a_node_b_arr): int
413  {
414  return $this->getTreeImplementation()->getRelation($a_node_a_arr, $a_node_b_arr);
415  }
416 
421  public function getChildIds(int $a_node): array
422  {
423  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
424  'WHERE parent = ' . $this->db->quote($a_node, 'integer') . ' ' .
425  'AND tree = ' . $this->db->quote($this->tree_id, 'integer' . ' ' .
426  'ORDER BY lft');
427  $res = $this->db->query($query);
428 
429  $childs = array();
430  while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) {
431  $childs[] = (int) $row->child;
432  }
433  return $childs;
434  }
435 
440  public function getChilds(int $a_node_id, string $a_order = "", string $a_direction = "ASC"): array
441  {
442  global $DIC;
443 
444  $ilObjDataCache = $DIC['ilObjDataCache'];
445  $ilUser = $DIC['ilUser'];
446 
447  // init childs
448  $childs = [];
449 
450  // number of childs
451  $count = 0;
452 
453  // init order_clause
454  $order_clause = "";
455 
456  // set order_clause if sort order parameter is given
457  if (!empty($a_order)) {
458  $order_clause = "ORDER BY " . $a_order . " " . $a_direction;
459  } else {
460  $order_clause = "ORDER BY " . $this->table_tree . ".lft";
461  }
462 
463  $query = sprintf(
464  'SELECT * FROM ' . $this->table_tree . ' ' .
465  $this->buildJoin() .
466  "WHERE parent = %s " .
467  "AND " . $this->table_tree . "." . $this->tree_pk . " = %s " .
468  $order_clause,
469  $this->db->quote($a_node_id, 'integer'),
470  $this->db->quote($this->tree_id, 'integer')
471  );
472 
473  $res = $this->db->query($query);
474 
475  if (!$count = $res->numRows()) {
476  return [];
477  }
478 
479  // get rows and object ids
480  $rows = [];
481  $obj_ids = [];
482  while ($r = $this->db->fetchAssoc($res)) {
483  $rows[] = $r;
484  $obj_ids[] = (int) $r["obj_id"];
485  }
486 
487  // preload object translation information
488  if ($this->__isMainTree() && $this->isCacheUsed() && is_object($ilObjDataCache) &&
489  is_object($ilUser) && $this->lang_code == $ilUser->getLanguage() && !isset($this->oc_preloaded[$a_node_id])) {
490  // $ilObjDataCache->preloadTranslations($obj_ids, $this->lang_code);
491  $ilObjDataCache->preloadObjectCache($obj_ids, $this->lang_code);
492  $this->fetchTranslationFromObjectDataCache($obj_ids);
493  $this->oc_preloaded[$a_node_id] = true;
494  }
495 
496  foreach ($rows as $row) {
497  $childs[] = $this->fetchNodeData($row);
498 
499  // Update cache of main tree
500  if ($this->__isMainTree()) {
501  #$GLOBALS['DIC']['ilLog']->write(__METHOD__.': Storing in tree cache '.$row['child'].' = true');
502  $this->in_tree_cache[$row['child']] = $row['tree'] == 1;
503  }
504  }
505  $childs[$count - 1]["last"] = true;
506  return $childs;
507  }
508 
517  public function getFilteredChilds(
518  array $a_filter,
519  int $a_node,
520  string $a_order = "",
521  string $a_direction = "ASC"
522  ): array {
523  $childs = $this->getChilds($a_node, $a_order, $a_direction);
524 
525  $filtered = [];
526  foreach ($childs as $child) {
527  if (!in_array($child["type"], $a_filter)) {
528  $filtered[] = $child;
529  }
530  }
531  return $filtered;
532  }
533 
538  public function getChildsByType(int $a_node_id, string $a_type): array
539  {
540  if ($a_type == 'rolf' && $this->table_obj_reference) {
541  // Performance optimization: A node can only have exactly one
542  // role folder as its child. Therefore we don't need to sort the
543  // results, and we can let the database know about the expected limit.
544  $this->db->setLimit(1, 0);
545  $query = sprintf(
546  "SELECT * FROM " . $this->table_tree . " " .
547  $this->buildJoin() .
548  "WHERE parent = %s " .
549  "AND " . $this->table_tree . "." . $this->tree_pk . " = %s " .
550  "AND " . $this->table_obj_data . ".type = %s ",
551  $this->db->quote($a_node_id, 'integer'),
552  $this->db->quote($this->tree_id, 'integer'),
553  $this->db->quote($a_type, 'text')
554  );
555  } else {
556  $query = sprintf(
557  "SELECT * FROM " . $this->table_tree . " " .
558  $this->buildJoin() .
559  "WHERE parent = %s " .
560  "AND " . $this->table_tree . "." . $this->tree_pk . " = %s " .
561  "AND " . $this->table_obj_data . ".type = %s " .
562  "ORDER BY " . $this->table_tree . ".lft",
563  $this->db->quote($a_node_id, 'integer'),
564  $this->db->quote($this->tree_id, 'integer'),
565  $this->db->quote($a_type, 'text')
566  );
567  }
568  $res = $this->db->query($query);
569 
570  // init childs
571  $childs = [];
572  while ($row = $this->db->fetchAssoc($res)) {
573  $childs[] = $this->fetchNodeData($row);
574  }
575  return $childs;
576  }
577 
581  public function getChildsByTypeFilter(
582  int $a_node_id,
583  array $a_types,
584  string $a_order = "",
585  string $a_direction = "ASC"
586  ): array {
587  $filter = ' ';
588  if ($a_types) {
589  $filter = 'AND ' . $this->table_obj_data . '.type IN(' . implode(',', ilArrayUtil::quoteArray($a_types)) . ') ';
590  }
591 
592  // set order_clause if sort order parameter is given
593  if (!empty($a_order)) {
594  $order_clause = "ORDER BY " . $a_order . " " . $a_direction;
595  } else {
596  $order_clause = "ORDER BY " . $this->table_tree . ".lft";
597  }
598 
599  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
600  $this->buildJoin() .
601  'WHERE parent = ' . $this->db->quote($a_node_id, 'integer') . ' ' .
602  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = ' . $this->db->quote(
603  $this->tree_id,
604  'integer'
605  ) . ' ' .
606  $filter .
607  $order_clause;
608 
609  $res = $this->db->query($query);
610 
611  $childs = [];
612  while ($row = $this->db->fetchAssoc($res)) {
613  $childs[] = $this->fetchNodeData($row);
614  }
615 
616  return $childs;
617  }
618 
625  public function insertNodeFromTrash(
626  int $a_source_id,
627  int $a_target_id,
628  int $a_tree_id,
629  int $a_pos = self::POS_LAST_NODE,
630  bool $a_reset_deleted_date = false
631  ): void {
632  if ($this->__isMainTree()) {
633  if ($a_source_id <= 1 || $a_target_id <= 0) {
634  $this->logger->logStack(ilLogLevel::WARNING);
635  throw new InvalidArgumentException('Invalid parameter given for ilTree::insertNodeFromTrash');
636  }
637  }
638  if ($this->isInTree($a_source_id)) {
639  ilLoggerFactory::getLogger('tree')->error('Node already in tree');
641  throw new InvalidArgumentException('Node already in tree.');
642  }
643 
644  $query = 'DELETE from tree ' .
645  'WHERE tree = ' . $this->db->quote($a_tree_id, 'integer') . ' ' .
646  'AND child = ' . $this->db->quote($a_source_id, 'integer');
647  $this->db->manipulate($query);
648 
649  $this->insertNode($a_source_id, $a_target_id, self::POS_LAST_NODE, $a_reset_deleted_date);
650  }
651 
656  public function insertNode(
657  int $a_node_id,
658  int $a_parent_id,
659  int $a_pos = self::POS_LAST_NODE,
660  bool $a_reset_deletion_date = false
661  ): void {
662  // CHECK node_id and parent_id > 0 if in main tree
663  if ($this->__isMainTree()) {
664  if ($a_node_id <= 1 || $a_parent_id <= 0) {
665  $message = sprintf(
666  'Invalid parameters! $a_node_id: %s $a_parent_id: %s',
667  $a_node_id,
668  $a_parent_id
669  );
670  $this->logger->logStack(ilLogLevel::ERROR, $message);
672  }
673  }
674  if ($this->isInTree($a_node_id)) {
675  throw new InvalidArgumentException("Node " . $a_node_id . " already in tree " .
676  $this->table_tree . "!");
677  }
678 
679  $this->getTreeImplementation()->insertNode($a_node_id, $a_parent_id, $a_pos);
680 
681  $this->in_tree_cache[$a_node_id] = true;
682 
683  // reset deletion date
684  if ($a_reset_deletion_date) {
685  ilObject::_resetDeletedDate($a_node_id);
686  }
687  if (isset($this->eventHandler) && ($this->eventHandler instanceof ilAppEventHandler) && $this->__isMainTree()) {
688  $this->eventHandler->raise(
689  'Services/Tree',
690  'insertNode',
691  [
692  'tree' => $this->table_tree,
693  'node_id' => $a_node_id,
694  'parent_id' => $a_parent_id
695  ]
696  );
697  }
698  }
699 
706  public function getFilteredSubTree(int $a_node_id, array $a_filter = []): array
707  {
708  $node = $this->getNodeData($a_node_id);
709 
710  $first = true;
711  $depth = 0;
712  $filtered = [];
713  foreach ($this->getSubTree($node) as $subnode) {
714  if ($depth && $subnode['depth'] > $depth) {
715  continue;
716  }
717  if (!$first && in_array($subnode['type'], $a_filter)) {
718  $depth = $subnode['depth'];
719  $first = false;
720  continue;
721  }
722  $depth = 0;
723  $first = false;
724  $filtered[] = $subnode;
725  }
726  return $filtered;
727  }
728 
734  public function getSubTreeIds(int $a_ref_id): array
735  {
736  return $this->getTreeImplementation()->getSubTreeIds($a_ref_id);
737  }
738 
744  public function getSubTree(array $a_node, bool $a_with_data = true, array $a_type = []): array
745  {
746  $query = $this->getTreeImplementation()->getSubTreeQuery($a_node, $a_type);
747 
748  $res = $this->db->query($query);
749  $subtree = [];
750  while ($row = $this->db->fetchAssoc($res)) {
751  if ($a_with_data) {
752  $subtree[] = $this->fetchNodeData($row);
753  } else {
754  $subtree[] = (int) $row['child'];
755  }
756  // the lm_data "hack" should be removed in the trunk during an alpha
757  if ($this->__isMainTree() || $this->table_tree == "lm_tree") {
758  $this->in_tree_cache[$row['child']] = true;
759  }
760  }
761  return $subtree;
762  }
763 
767  public function deleteTree(array $a_node): void
768  {
769  if ($this->__isMainTree()) {
770  // moved to trash and then deleted.
771  if (!$this->__checkDelete($a_node)) {
772  $this->logger->logStack(ilLogLevel::ERROR);
773  throw new ilInvalidTreeStructureException('Deletion canceled due to invalid tree structure.' . print_r(
774  $a_node,
775  true
776  ));
777  }
778  }
779  $this->getTreeImplementation()->deleteTree((int) $a_node['child']);
780  $this->resetInTreeCache();
781  }
782 
787  public function validateParentRelations(): array
788  {
789  return $this->getTreeImplementation()->validateParentRelations();
790  }
791 
797  public function getPathFull(int $a_endnode_id, int $a_startnode_id = 0): array
798  {
799  $pathIds = $this->getPathId($a_endnode_id, $a_startnode_id);
800 
801  // We retrieve the full path in a single query to improve performance
802  // Abort if no path ids were found
803  if (count($pathIds) == 0) {
804  return [];
805  }
806 
807  $inClause = 'child IN (';
808  for ($i = 0; $i < count($pathIds); $i++) {
809  if ($i > 0) {
810  $inClause .= ',';
811  }
812  $inClause .= $this->db->quote($pathIds[$i], 'integer');
813  }
814  $inClause .= ')';
815 
816  $q = 'SELECT * ' .
817  'FROM ' . $this->table_tree . ' ' .
818  $this->buildJoin() . ' ' .
819  'WHERE ' . $inClause . ' ' .
820  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = ' . $this->db->quote(
821  $this->tree_id,
822  'integer'
823  ) . ' ' .
824  'ORDER BY depth';
825  $r = $this->db->query($q);
826 
827  $pathFull = [];
828  while ($row = $r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
829  $pathFull[] = $this->fetchNodeData($row);
830 
831  // Update cache
832  if ($this->__isMainTree()) {
833  $this->in_tree_cache[$row['child']] = $row['tree'] == 1;
834  }
835  }
836  return $pathFull;
837  }
838 
843  public function preloadDepthParent(array $a_node_ids): void
844  {
845  global $DIC;
846 
847  if (!$this->__isMainTree() || !$this->isCacheUsed()) {
848  return;
849  }
850 
851  $res = $this->db->query('SELECT t.depth, t.parent, t.child ' .
852  'FROM ' . $this->table_tree . ' t ' .
853  'WHERE ' . $this->db->in("child", $a_node_ids, false, "integer") .
854  'AND ' . $this->tree_pk . ' = ' . $this->db->quote($this->tree_id, "integer"));
855  while ($row = $this->db->fetchAssoc($res)) {
856  $this->depth_cache[$row["child"]] = (int) $row["depth"];
857  $this->parent_cache[$row["child"]] = (int) $row["parent"];
858  }
859  }
860 
866  public function getPathId(int $a_endnode_id, int $a_startnode_id = 0): array
867  {
868  if (!$a_endnode_id) {
869  $this->logger->logStack(ilLogLevel::ERROR);
870  throw new InvalidArgumentException(__METHOD__ . ': No endnode given!');
871  }
872 
873  // path id cache
874  if ($this->isCacheUsed() && isset($this->path_id_cache[$a_endnode_id][$a_startnode_id])) {
875  return $this->path_id_cache[$a_endnode_id][$a_startnode_id];
876  }
877 
878  $pathIds = $this->getTreeImplementation()->getPathIds($a_endnode_id, $a_startnode_id);
879 
880  if ($this->__isMainTree()) {
881  $this->path_id_cache[$a_endnode_id][$a_startnode_id] = $pathIds;
882  }
883  return $pathIds;
884  }
885 
896  public function getNodePath(int $a_endnode_id, int $a_startnode_id = 0): array
897  {
898  $pathIds = $this->getPathId($a_endnode_id, $a_startnode_id);
899 
900  // Abort if no path ids were found
901  if (count($pathIds) == 0) {
902  return [];
903  }
904 
905  $types = [];
906  $data = [];
907  for ($i = 0; $i < count($pathIds); $i++) {
908  $types[] = 'integer';
909  $data[] = $pathIds[$i];
910  }
911 
912  $query = 'SELECT t.depth,t.parent,t.child,d.obj_id,d.type,d.title ' .
913  'FROM ' . $this->table_tree . ' t ' .
914  'JOIN ' . $this->table_obj_reference . ' r ON r.ref_id = t.child ' .
915  'JOIN ' . $this->table_obj_data . ' d ON d.obj_id = r.obj_id ' .
916  'WHERE ' . $this->db->in('t.child', $data, false, 'integer') . ' ' .
917  'ORDER BY t.depth ';
918 
919  $res = $this->db->queryF($query, $types, $data);
920 
921  $titlePath = [];
922  while ($row = $this->db->fetchAssoc($res)) {
923  $titlePath[] = $row;
924  }
925  return $titlePath;
926  }
927 
933  public function checkTree(): bool
934  {
935  $types = array('integer');
936  $query = 'SELECT lft,rgt FROM ' . $this->table_tree . ' ' .
937  'WHERE ' . $this->tree_pk . ' = %s ';
938 
939  $res = $this->db->queryF($query, $types, array($this->tree_id));
940  $lft = $rgt = [];
941  while ($row = $this->db->fetchObject($res)) {
942  $lft[] = $row->lft;
943  $rgt[] = $row->rgt;
944  }
945 
946  $all = array_merge($lft, $rgt);
947  $uni = array_unique($all);
948 
949  if (count($all) != count($uni)) {
950  $message = 'Tree is corrupted!';
951  $this->logger->error($message);
953  }
954  return true;
955  }
956 
961  public function checkTreeChilds(bool $a_no_zero_child = true): bool
962  {
963  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
964  'WHERE ' . $this->tree_pk . ' = %s ' .
965  'ORDER BY lft';
966  $r1 = $this->db->queryF($query, array('integer'), array($this->tree_id));
967 
968  while ($row = $this->db->fetchAssoc($r1)) {
969  //echo "tree:".$row[$this->tree_pk].":lft:".$row["lft"].":rgt:".$row["rgt"].":child:".$row["child"].":<br>";
970  if (($row["child"] == 0) && $a_no_zero_child) {
971  $message = "Tree contains child with ID 0!";
972  $this->logger->error($message);
974  }
975 
976  if ($this->table_obj_reference) {
977  // get object reference data
978  $query = 'SELECT * FROM ' . $this->table_obj_reference . ' WHERE ' . $this->ref_pk . ' = %s ';
979  $r2 = $this->db->queryF($query, array('integer'), array($row['child']));
980 
981  //echo "num_childs:".$r2->numRows().":<br>";
982  if ($r2->numRows() == 0) {
983  $message = "No Object-to-Reference entry found for ID " . $row["child"] . "!";
984  $this->logger->error($message);
986  }
987  if ($r2->numRows() > 1) {
988  $message = "More Object-to-Reference entries found for ID " . $row["child"] . "!";
989  $this->logger->error($message);
991  }
992 
993  // get object data
994  $obj_ref = $this->db->fetchAssoc($r2);
995 
996  $query = 'SELECT * FROM ' . $this->table_obj_data . ' WHERE ' . $this->obj_pk . ' = %s';
997  $r3 = $this->db->queryF($query, array('integer'), array($obj_ref[$this->obj_pk]));
998  if ($r3->numRows() == 0) {
999  $message = " No child found for ID " . $obj_ref[$this->obj_pk] . "!";
1000  $this->logger->error($message);
1002  }
1003  if ($r3->numRows() > 1) {
1004  $message = "More childs found for ID " . $obj_ref[$this->obj_pk] . "!";
1005  $this->logger->error($message);
1007  }
1008  } else {
1009  // get only object data
1010  $query = 'SELECT * FROM ' . $this->table_obj_data . ' WHERE ' . $this->obj_pk . ' = %s';
1011  $r2 = $this->db->queryF($query, array('integer'), array($row['child']));
1012  //echo "num_childs:".$r2->numRows().":<br>";
1013  if ($r2->numRows() == 0) {
1014  $message = "No child found for ID " . $row["child"] . "!";
1015  $this->logger->error($message);
1017  }
1018  if ($r2->numRows() > 1) {
1019  $message = "More childs found for ID " . $row["child"] . "!";
1020  $this->logger->error($message);
1022  }
1023  }
1024  }
1025  return true;
1026  }
1027 
1031  public function getMaximumDepth(): int
1032  {
1033  global $DIC;
1034 
1035  $query = 'SELECT MAX(depth) depth FROM ' . $this->table_tree;
1036  $res = $this->db->query($query);
1037 
1038  $row = $this->db->fetchAssoc($res);
1039  return (int) $row['depth'];
1040  }
1041 
1045  public function getDepth(int $a_node_id): int
1046  {
1047  global $DIC;
1048 
1049  if ($a_node_id) {
1050  if ($this->__isMainTree()) {
1051  $query = 'SELECT depth FROM ' . $this->table_tree . ' ' .
1052  'WHERE child = %s ';
1053  $res = $this->db->queryF($query, array('integer'), array($a_node_id));
1054  $row = $this->db->fetchObject($res);
1055  } else {
1056  $query = 'SELECT depth FROM ' . $this->table_tree . ' ' .
1057  'WHERE child = %s ' .
1058  'AND ' . $this->tree_pk . ' = %s ';
1059  $res = $this->db->queryF($query, array('integer', 'integer'), array($a_node_id, $this->tree_id));
1060  $row = $this->db->fetchObject($res);
1061  }
1062  return (int) ($row->depth ?? 0);
1063  }
1064  return 1;
1065  }
1066 
1071  public function getNodeTreeData(int $a_node_id): array
1072  {
1073  global $DIC;
1074 
1075  if (!$a_node_id) {
1076  $this->logger->logStack(ilLogLevel::ERROR);
1077  throw new InvalidArgumentException('Missing or empty parameter $a_node_id: ' . $a_node_id);
1078  }
1079 
1080  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1081  'WHERE child = ' . $this->db->quote($a_node_id, 'integer');
1082  $res = $this->db->query($query);
1083  while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
1084  return $row;
1085  }
1086  return [];
1087  }
1088 
1094  public function getNodeData(int $a_node_id, ?int $a_tree_pk = null): array
1095  {
1096  if ($this->__isMainTree()) {
1097  if ($a_node_id < 1) {
1098  $message = 'No valid parameter given! $a_node_id: %s' . $a_node_id;
1099  $this->logger->error($message);
1101  }
1102  }
1103 
1104  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1105  $this->buildJoin() .
1106  'WHERE ' . $this->table_tree . '.child = %s ' .
1107  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s ';
1108  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1109  $a_node_id,
1110  $a_tree_pk === null ? $this->tree_id : $a_tree_pk
1111  ));
1112  $row = $this->db->fetchAssoc($res);
1114 
1115  return $this->fetchNodeData($row);
1116  }
1117 
1121  public function fetchNodeData(array $a_row): array
1122  {
1123  global $DIC;
1124 
1125  $objDefinition = $DIC['objDefinition'];
1126  $lng = $DIC['lng'];
1127 
1128  $data = $a_row;
1129  $data["desc"] = (string) ($a_row["description"] ?? ''); // for compability
1130 
1131  // multilingual support systemobjects (sys) & categories (db)
1132  $translation_type = '';
1133  if (is_object($objDefinition)) {
1134  $translation_type = $objDefinition->getTranslationType($data["type"] ?? '');
1135  }
1136 
1137  if ($translation_type == "sys") {
1138  if ($data["type"] == "rolf" && $data["obj_id"] != ROLE_FOLDER_ID) {
1139  $data["description"] = (string) $lng->txt("obj_" . $data["type"] . "_local_desc") . $data["title"] . $data["desc"];
1140  $data["desc"] = $lng->txt("obj_" . $data["type"] . "_local_desc") . $data["title"] . $data["desc"];
1141  $data["title"] = $lng->txt("obj_" . $data["type"] . "_local");
1142  } else {
1143  $data["title"] = $lng->txt("obj_" . $data["type"]);
1144  $data["description"] = $lng->txt("obj_" . $data["type"] . "_desc");
1145  $data["desc"] = $lng->txt("obj_" . $data["type"] . "_desc");
1146  }
1147  } elseif ($translation_type == "db") {
1148  // Try to retrieve object translation from cache
1149  $lang_code = ''; // This did never work, because it was undefined before
1150  if ($this->isCacheUsed() &&
1151  array_key_exists($data["obj_id"] . '.' . $lang_code, $this->translation_cache)) {
1152  $key = $data["obj_id"] . '.' . $lang_code;
1153  $data["title"] = $this->translation_cache[$key]['title'];
1154  $data["description"] = $this->translation_cache[$key]['description'];
1155  $data["desc"] = $this->translation_cache[$key]['desc'];
1156  } else {
1157  // Object translation is not in cache, read it from database
1158  $query = 'SELECT title,description FROM object_translation ' .
1159  'WHERE obj_id = %s ' .
1160  'AND lang_code = %s ';
1161 
1162  $res = $this->db->queryF($query, array('integer', 'text'), array(
1163  $data['obj_id'],
1164  $this->lang_code
1165  ));
1166  $row = $this->db->fetchObject($res);
1167 
1168  if ($row) {
1169  $data["title"] = (string) $row->title;
1170  $data["description"] = ilStr::shortenTextExtended((string) $row->description, ilObject::DESC_LENGTH, true);
1171  $data["desc"] = (string) $row->description;
1172  }
1173 
1174  // Store up to 1000 object translations in cache
1175  if ($this->isCacheUsed() && count($this->translation_cache) < 1000) {
1176  $key = $data["obj_id"] . '.' . $lang_code;
1177  $this->translation_cache[$key] = [];
1178  $this->translation_cache[$key]['title'] = $data["title"];
1179  $this->translation_cache[$key]['description'] = $data["description"];
1180  $this->translation_cache[$key]['desc'] = $data["desc"];
1181  }
1182  }
1183  }
1184 
1185  // TODO: Handle this switch by module.xml definitions
1186  if (isset($data['type']) && ($data['type'] == 'crsr' || $data['type'] == 'catr' || $data['type'] == 'grpr' || $data['type'] === 'prgr')) {
1187  $data['title'] = ilContainerReference::_lookupTitle((int) $data['obj_id']);
1188  }
1189  return $data;
1190  }
1191 
1197  protected function fetchTranslationFromObjectDataCache(array $a_obj_ids): void
1198  {
1199  global $DIC;
1200 
1201  $ilObjDataCache = $DIC['ilObjDataCache'];
1202 
1203  if ($this->isCacheUsed() && is_array($a_obj_ids) && is_object($ilObjDataCache)) {
1204  foreach ($a_obj_ids as $id) {
1205  $this->translation_cache[$id . '.']['title'] = $ilObjDataCache->lookupTitle((int) $id);
1206  $this->translation_cache[$id . '.']['description'] = $ilObjDataCache->lookupDescription((int) $id);
1207  $this->translation_cache[$id . '.']['desc'] =
1208  $this->translation_cache[$id . '.']['description'];
1209  }
1210  }
1211  }
1212 
1217  public function isInTree(?int $a_node_id): bool
1218  {
1219  if (is_null($a_node_id) || !$a_node_id) {
1220  return false;
1221  }
1222  // is in tree cache
1223  if ($this->isCacheUsed() && isset($this->in_tree_cache[$a_node_id])) {
1224  return $this->in_tree_cache[$a_node_id];
1225  }
1226 
1227  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1228  'WHERE ' . $this->table_tree . '.child = %s ' .
1229  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s';
1230 
1231  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1232  $a_node_id,
1233  $this->tree_id
1234  ));
1235 
1236  if ($res->numRows() > 0) {
1237  if ($this->__isMainTree()) {
1238  $this->in_tree_cache[$a_node_id] = true;
1239  }
1240  return true;
1241  } else {
1242  if ($this->__isMainTree()) {
1243  $this->in_tree_cache[$a_node_id] = false;
1244  }
1245  return false;
1246  }
1247  }
1248 
1252  public function getParentNodeData(int $a_node_id): array
1253  {
1254  global $DIC;
1255 
1256  $ilLog = $DIC['ilLog'];
1257 
1258  if ($this->table_obj_reference) {
1259  // Use inner join instead of left join to improve performance
1260  $innerjoin = "JOIN " . $this->table_obj_reference . " ON v.child=" . $this->table_obj_reference . "." . $this->ref_pk . " " .
1261  "JOIN " . $this->table_obj_data . " ON " . $this->table_obj_reference . "." . $this->obj_pk . "=" . $this->table_obj_data . "." . $this->obj_pk . " ";
1262  } else {
1263  // Use inner join instead of left join to improve performance
1264  $innerjoin = "JOIN " . $this->table_obj_data . " ON v.child=" . $this->table_obj_data . "." . $this->obj_pk . " ";
1265  }
1266 
1267  $query = 'SELECT * FROM ' . $this->table_tree . ' s, ' . $this->table_tree . ' v ' .
1268  $innerjoin .
1269  'WHERE s.child = %s ' .
1270  'AND s.parent = v.child ' .
1271  'AND s.' . $this->tree_pk . ' = %s ' .
1272  'AND v.' . $this->tree_pk . ' = %s';
1273  $res = $this->db->queryF($query, array('integer', 'integer', 'integer'), array(
1274  $a_node_id,
1275  $this->tree_id,
1276  $this->tree_id
1277  ));
1278  $row = $this->db->fetchAssoc($res);
1279  if (is_array($row)) {
1280  return $this->fetchNodeData($row);
1281  }
1282  return [];
1283  }
1284 
1288  public function isGrandChild(int $a_startnode_id, int $a_querynode_id): bool
1289  {
1290  return $this->getRelation($a_startnode_id, $a_querynode_id) == self::RELATION_PARENT;
1291  }
1292 
1297  public function addTree(int $a_tree_id, int $a_node_id = -1): bool
1298  {
1299  global $DIC;
1300 
1301  // FOR SECURITY addTree() IS NOT ALLOWED ON MAIN TREE
1302  if ($this->__isMainTree()) {
1303  $message = sprintf(
1304  'Operation not allowed on main tree! $a_tree_if: %s $a_node_id: %s',
1305  $a_tree_id,
1306  $a_node_id
1307  );
1308  $this->logger->error($message);
1310  }
1311 
1312  if ($a_node_id <= 0) {
1313  $a_node_id = $a_tree_id;
1314  }
1315 
1316  $query = 'INSERT INTO ' . $this->table_tree . ' (' .
1317  $this->tree_pk . ', child,parent,lft,rgt,depth) ' .
1318  'VALUES ' .
1319  '(%s,%s,%s,%s,%s,%s)';
1320  $res = $this->db->manipulateF(
1321  $query,
1322  array('integer', 'integer', 'integer', 'integer', 'integer', 'integer'),
1323  array(
1324  $a_tree_id,
1325  $a_node_id,
1326  0,
1327  1,
1328  2,
1329  1
1330  )
1331  );
1332 
1333  return true;
1334  }
1335 
1339  public function removeTree(int $a_tree_id): bool
1340  {
1341  if ($this->__isMainTree()) {
1342  $this->logger->logStack(ilLogLevel::ERROR);
1343  throw new InvalidArgumentException('Operation not allowed on main tree');
1344  }
1345  if (!$a_tree_id) {
1346  $this->logger->logStack(ilLogLevel::ERROR);
1347  throw new InvalidArgumentException('Missing parameter tree id');
1348  }
1349 
1350  $query = 'DELETE FROM ' . $this->table_tree .
1351  ' WHERE ' . $this->tree_pk . ' = %s ';
1352  $this->db->manipulateF($query, array('integer'), array($a_tree_id));
1353  return true;
1354  }
1355 
1361  public function moveToTrash(int $a_node_id, bool $a_set_deleted = false, int $a_deleted_by = 0): bool
1362  {
1363  global $DIC;
1364 
1365  $user = $DIC->user();
1366  if (!$a_deleted_by) {
1367  $a_deleted_by = $user->getId();
1368  }
1369 
1370  if (!$a_node_id) {
1371  $this->logger->logStack(ilLogLevel::ERROR);
1372  throw new InvalidArgumentException('No valid parameter given! $a_node_id: ' . $a_node_id);
1373  }
1374 
1375  $query = $this->getTreeImplementation()->getSubTreeQuery($this->getNodeTreeData($a_node_id), [], false);
1376  $res = $this->db->query($query);
1377 
1378  $subnodes = [];
1379  while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) {
1380  $subnodes[] = (int) $row['child'];
1381  }
1382 
1383  if (!count($subnodes)) {
1384  // Possibly already deleted
1385  return false;
1386  }
1387 
1388  if ($a_set_deleted) {
1389  ilObject::setDeletedDates($subnodes, $a_deleted_by);
1390  }
1391  // netsted set <=> mp
1392  $this->getTreeImplementation()->moveToTrash($a_node_id);
1393  return true;
1394  }
1395 
1399  public function isDeleted(int $a_node_id): bool
1400  {
1401  return $this->isSaved($a_node_id);
1402  }
1403 
1408  public function isSaved(int $a_node_id): bool
1409  {
1410  if ($this->isCacheUsed() && isset($this->is_saved_cache[$a_node_id])) {
1411  return $this->is_saved_cache[$a_node_id];
1412  }
1413 
1414  $query = 'SELECT ' . $this->tree_pk . ' FROM ' . $this->table_tree . ' ' .
1415  'WHERE child = %s ';
1416  $res = $this->db->queryF($query, array('integer'), array($a_node_id));
1417  $row = $this->db->fetchAssoc($res);
1418 
1419  $tree_id = $row[$this->tree_pk] ?? 0;
1420  if ($tree_id < 0) {
1421  if ($this->__isMainTree()) {
1422  $this->is_saved_cache[$a_node_id] = true;
1423  }
1424  return true;
1425  } else {
1426  if ($this->__isMainTree()) {
1427  $this->is_saved_cache[$a_node_id] = false;
1428  }
1429  return false;
1430  }
1431  }
1432 
1436  public function preloadDeleted(array $a_node_ids): void
1437  {
1438  if (!is_array($a_node_ids) || !$this->isCacheUsed()) {
1439  return;
1440  }
1441 
1442  $query = 'SELECT ' . $this->tree_pk . ', child FROM ' . $this->table_tree . ' ' .
1443  'WHERE ' . $this->db->in("child", $a_node_ids, false, "integer");
1444 
1445  $res = $this->db->query($query);
1446  while ($row = $this->db->fetchAssoc($res)) {
1447  if ($row[$this->tree_pk] < 0) {
1448  if ($this->__isMainTree()) {
1449  $this->is_saved_cache[$row["child"]] = true;
1450  }
1451  } else {
1452  if ($this->__isMainTree()) {
1453  $this->is_saved_cache[$row["child"]] = false;
1454  }
1455  }
1456  }
1457  }
1458 
1463  public function getSavedNodeData(int $a_parent_id): array
1464  {
1465  global $DIC;
1466 
1467  if (!isset($a_parent_id)) {
1468  $message = "No node_id given!";
1469  $this->logger->error($message);
1471  }
1472 
1473  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1474  $this->buildJoin() .
1475  'WHERE ' . $this->table_tree . '.' . $this->tree_pk . ' < %s ' .
1476  'AND ' . $this->table_tree . '.parent = %s';
1477  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1478  0,
1479  $a_parent_id
1480  ));
1481 
1482  $saved = [];
1483  while ($row = $this->db->fetchAssoc($res)) {
1484  $saved[] = $this->fetchNodeData($row);
1485  }
1486 
1487  return $saved;
1488  }
1489 
1493  public function getSavedNodeObjIds(array $a_obj_ids): array
1494  {
1495  global $DIC;
1496 
1497  $query = 'SELECT ' . $this->table_obj_data . '.obj_id FROM ' . $this->table_tree . ' ' .
1498  $this->buildJoin() .
1499  'WHERE ' . $this->table_tree . '.' . $this->tree_pk . ' < ' . $this->db->quote(0, 'integer') . ' ' .
1500  'AND ' . $this->db->in($this->table_obj_data . '.obj_id', $a_obj_ids, false, 'integer');
1501  $res = $this->db->query($query);
1502  $saved = [];
1503  while ($row = $this->db->fetchAssoc($res)) {
1504  $saved[] = (int) $row['obj_id'];
1505  }
1506 
1507  return $saved;
1508  }
1509 
1514  public function getParentId(int $a_node_id): ?int
1515  {
1516  global $DIC;
1517  if ($this->__isMainTree()) {
1518  $query = 'SELECT parent FROM ' . $this->table_tree . ' ' .
1519  'WHERE child = %s ';
1520  $res = $this->db->queryF(
1521  $query,
1522  ['integer'],
1523  [$a_node_id]
1524  );
1525  } else {
1526  $query = 'SELECT parent FROM ' . $this->table_tree . ' ' .
1527  'WHERE child = %s ' .
1528  'AND ' . $this->tree_pk . ' = %s ';
1529  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1530  $a_node_id,
1531  $this->tree_id
1532  ));
1533  }
1534 
1535  if ($row = $this->db->fetchObject($res)) {
1536  return (int) $row->parent;
1537  }
1538  return null;
1539  }
1540 
1546  public function getLeftValue(int $a_node_id): int
1547  {
1548  global $DIC;
1549 
1550  if (!isset($a_node_id)) {
1551  $message = "No node_id given!";
1552  $this->logger->error($message);
1554  }
1555 
1556  $query = 'SELECT lft FROM ' . $this->table_tree . ' ' .
1557  'WHERE child = %s ' .
1558  'AND ' . $this->tree_pk . ' = %s ';
1559  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1560  $a_node_id,
1561  $this->tree_id
1562  ));
1563  $row = $this->db->fetchObject($res);
1564  return (int) $row->lft;
1565  }
1566 
1572  public function getChildSequenceNumber(array $a_node, string $type = ""): int
1573  {
1574  if (!isset($a_node)) {
1575  $message = "No node_id given!";
1576  $this->logger->error($message);
1578  }
1579 
1580  if ($type) {
1581  $query = 'SELECT count(*) cnt FROM ' . $this->table_tree . ' ' .
1582  $this->buildJoin() .
1583  'WHERE lft <= %s ' .
1584  'AND type = %s ' .
1585  'AND parent = %s ' .
1586  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s ';
1587 
1588  $res = $this->db->queryF($query, array('integer', 'text', 'integer', 'integer'), array(
1589  $a_node['lft'],
1590  $type,
1591  $a_node['parent'],
1592  $this->tree_id
1593  ));
1594  } else {
1595  $query = 'SELECT count(*) cnt FROM ' . $this->table_tree . ' ' .
1596  $this->buildJoin() .
1597  'WHERE lft <= %s ' .
1598  'AND parent = %s ' .
1599  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s ';
1600 
1601  $res = $this->db->queryF($query, array('integer', 'integer', 'integer'), array(
1602  $a_node['lft'],
1603  $a_node['parent'],
1604  $this->tree_id
1605  ));
1606  }
1607  $row = $this->db->fetchAssoc($res);
1608  return (int) $row["cnt"];
1609  }
1610 
1611  public function readRootId(): int
1612  {
1613  $query = 'SELECT child FROM ' . $this->table_tree . ' ' .
1614  'WHERE parent = %s ' .
1615  'AND ' . $this->tree_pk . ' = %s ';
1616  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1617  0,
1618  $this->tree_id
1619  ));
1620  $this->root_id = 0;
1621  if ($row = $this->db->fetchObject($res)) {
1622  $this->root_id = (int) $row->child;
1623  }
1624  return $this->root_id;
1625  }
1626 
1627  public function getRootId(): int
1628  {
1629  return $this->root_id;
1630  }
1631 
1632  public function setRootId(int $a_root_id): void
1633  {
1634  $this->root_id = $a_root_id;
1635  }
1636 
1637  public function getTreeId(): int
1638  {
1639  return $this->tree_id;
1640  }
1641 
1642  public function setTreeId(int $a_tree_id): void
1643  {
1644  $this->tree_id = $a_tree_id;
1645  }
1646 
1653  public function fetchSuccessorNode(int $a_node_id, string $a_type = ""): ?array
1654  {
1655  // get lft value for current node
1656  $query = 'SELECT lft FROM ' . $this->table_tree . ' ' .
1657  'WHERE ' . $this->table_tree . '.child = %s ' .
1658  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s ';
1659  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1660  $a_node_id,
1661  $this->tree_id
1662  ));
1663  $curr_node = $this->db->fetchAssoc($res);
1664 
1665  if ($a_type) {
1666  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1667  $this->buildJoin() .
1668  'WHERE lft > %s ' .
1669  'AND ' . $this->table_obj_data . '.type = %s ' .
1670  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s ' .
1671  'ORDER BY lft ';
1672  $this->db->setLimit(1, 0);
1673  $res = $this->db->queryF($query, array('integer', 'text', 'integer'), array(
1674  $curr_node['lft'],
1675  $a_type,
1676  $this->tree_id
1677  ));
1678  } else {
1679  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1680  $this->buildJoin() .
1681  'WHERE lft > %s ' .
1682  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s ' .
1683  'ORDER BY lft ';
1684  $this->db->setLimit(1, 0);
1685  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1686  $curr_node['lft'],
1687  $this->tree_id
1688  ));
1689  }
1690 
1691  if ($res->numRows() < 1) {
1692  return null;
1693  } else {
1694  $row = $this->db->fetchAssoc($res);
1695  return $this->fetchNodeData($row);
1696  }
1697  }
1698 
1705  public function fetchPredecessorNode(int $a_node_id, string $a_type = ""): ?array
1706  {
1707  if (!isset($a_node_id)) {
1708  $message = "No node_id given!";
1709  $this->logger->error($message);
1711  }
1712 
1713  // get lft value for current node
1714  $query = 'SELECT lft FROM ' . $this->table_tree . ' ' .
1715  'WHERE ' . $this->table_tree . '.child = %s ' .
1716  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s ';
1717  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1718  $a_node_id,
1719  $this->tree_id
1720  ));
1721 
1722  $curr_node = $this->db->fetchAssoc($res);
1723 
1724  if ($a_type) {
1725  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1726  $this->buildJoin() .
1727  'WHERE lft < %s ' .
1728  'AND ' . $this->table_obj_data . '.type = %s ' .
1729  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s ' .
1730  'ORDER BY lft DESC';
1731  $this->db->setLimit(1, 0);
1732  $res = $this->db->queryF($query, array('integer', 'text', 'integer'), array(
1733  $curr_node['lft'],
1734  $a_type,
1735  $this->tree_id
1736  ));
1737  } else {
1738  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1739  $this->buildJoin() .
1740  'WHERE lft < %s ' .
1741  'AND ' . $this->table_tree . '.' . $this->tree_pk . ' = %s ' .
1742  'ORDER BY lft DESC';
1743  $this->db->setLimit(1, 0);
1744  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1745  $curr_node['lft'],
1746  $this->tree_id
1747  ));
1748  }
1749 
1750  if ($res->numRows() < 1) {
1751  return null;
1752  } else {
1753  $row = $this->db->fetchAssoc($res);
1754  return $this->fetchNodeData($row);
1755  }
1756  }
1757 
1762  public function renumber(int $node_id = 1, int $i = 1): int
1763  {
1764  $renumber_callable = function (ilDBInterface $db) use ($node_id, $i, &$return) {
1765  $return = $this->__renumber($node_id, $i);
1766  };
1767 
1768  // LOCKED ###################################
1769  if ($this->__isMainTree()) {
1770  $ilAtomQuery = $this->db->buildAtomQuery();
1771  $ilAtomQuery->addTableLock($this->table_tree);
1772 
1773  $ilAtomQuery->addQueryCallable($renumber_callable);
1774  $ilAtomQuery->run();
1775  } else {
1776  $renumber_callable($this->db);
1777  }
1778  return $return;
1779  }
1780 
1786  private function __renumber(int $node_id = 1, int $i = 1): int
1787  {
1788  if ($this->isRepositoryTree()) {
1789  $query = 'UPDATE ' . $this->table_tree . ' SET lft = %s WHERE child = %s';
1790  $this->db->manipulateF(
1791  $query,
1792  array('integer', 'integer'),
1793  array(
1794  $i,
1795  $node_id
1796  )
1797  );
1798  } else {
1799  $query = 'UPDATE ' . $this->table_tree . ' SET lft = %s WHERE child = %s AND tree = %s';
1800  $this->db->manipulateF(
1801  $query,
1802  array('integer', 'integer', 'integer'),
1803  array(
1804  $i,
1805  $node_id,
1806  $this->tree_id
1807  )
1808  );
1809  }
1810 
1811  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1812  'WHERE parent = ' . $this->db->quote($node_id, 'integer') . ' ' .
1813  'ORDER BY lft';
1814  $res = $this->db->query($query);
1815 
1816  $childs = [];
1817  while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) {
1818  $childs[] = (int) $row->child;
1819  }
1820 
1821  foreach ($childs as $child) {
1822  $i = $this->__renumber($child, $i + 1);
1823  }
1824  $i++;
1825 
1826  // Insert a gap at the end of node, if the node has children
1827  if (count($childs) > 0) {
1828  $i += $this->gap * 2;
1829  }
1830 
1831  if ($this->isRepositoryTree()) {
1832  $query = 'UPDATE ' . $this->table_tree . ' SET rgt = %s WHERE child = %s';
1833  $res = $this->db->manipulateF(
1834  $query,
1835  array('integer', 'integer'),
1836  array(
1837  $i,
1838  $node_id
1839  )
1840  );
1841  } else {
1842  $query = 'UPDATE ' . $this->table_tree . ' SET rgt = %s WHERE child = %s AND tree = %s';
1843  $res = $this->db->manipulateF($query, array('integer', 'integer', 'integer'), array(
1844  $i,
1845  $node_id,
1846  $this->tree_id
1847  ));
1848  }
1849  return $i;
1850  }
1851 
1856  public function checkForParentType(int $a_ref_id, string $a_type, bool $a_exclude_source_check = false): int
1857  {
1858  // #12577
1859  $cache_key = $a_ref_id . '.' . $a_type . '.' . ((int) $a_exclude_source_check);
1860 
1861  // Try to return a cached result
1862  if ($this->isCacheUsed() &&
1863  array_key_exists($cache_key, $this->parent_type_cache)) {
1864  return (int) $this->parent_type_cache[$cache_key];
1865  }
1866 
1867  // Store up to 1000 results in cache
1868  $do_cache = ($this->__isMainTree() && count($this->parent_type_cache) < 1000);
1869 
1870  // ref_id is not in tree
1871  if (!$this->isInTree($a_ref_id)) {
1872  if ($do_cache) {
1873  $this->parent_type_cache[$cache_key] = false;
1874  }
1875  return 0;
1876  }
1877 
1878  $path = array_reverse($this->getPathFull($a_ref_id));
1879 
1880  // remove first path entry as it is requested node
1881  if ($a_exclude_source_check) {
1882  array_shift($path);
1883  }
1884 
1885  foreach ($path as $node) {
1886  // found matching parent
1887  if ($node["type"] == $a_type) {
1888  if ($do_cache) {
1889  $this->parent_type_cache[$cache_key] = (int) $node["child"];
1890  }
1891  return (int) $node["child"];
1892  }
1893  }
1894 
1895  if ($do_cache) {
1896  $this->parent_type_cache[$cache_key] = false;
1897  }
1898  return 0;
1899  }
1900 
1906  public static function _removeEntry(int $a_tree, int $a_child, string $a_db_table = "tree"): void
1907  {
1908  global $DIC;
1909 
1910  $db = $DIC->database();
1911 
1912  if ($a_db_table === 'tree') {
1913  if ($a_tree == 1 && $a_child == ROOT_FOLDER_ID) {
1914  $message = sprintf(
1915  'Tried to delete root node! $a_tree: %s $a_child: %s',
1916  $a_tree,
1917  $a_child
1918  );
1919  ilLoggerFactory::getLogger('tree')->error($message);
1921  }
1922  }
1923 
1924  $query = 'DELETE FROM ' . $a_db_table . ' ' .
1925  'WHERE tree = %s ' .
1926  'AND child = %s ';
1927  $res = $db->manipulateF($query, array('integer', 'integer'), array(
1928  $a_tree,
1929  $a_child
1930  ));
1931  }
1932 
1936  public function __isMainTree(): bool
1937  {
1938  return $this->table_tree === 'tree';
1939  }
1940 
1947  public function __checkDelete(array $a_node): bool
1948  {
1949  $query = $this->getTreeImplementation()->getSubTreeQuery($a_node, [], false);
1950  $this->logger->debug($query);
1951  $res = $this->db->query($query);
1952 
1953  $counter = (int) $lft_childs = [];
1954  while ($row = $this->db->fetchObject($res)) {
1955  $lft_childs[$row->child] = (int) $row->parent;
1956  ++$counter;
1957  }
1958 
1959  // CHECK FOR DUPLICATE CHILD IDS
1960  if ($counter != count($lft_childs)) {
1961  $message = 'Duplicate entries for "child" in maintree! $a_node_id: ' . $a_node['child'];
1962 
1963  $this->logger->error($message);
1965  }
1966 
1967  // GET SUBTREE BY PARENT RELATION
1968  $parent_childs = [];
1969  $this->__getSubTreeByParentRelation((int)$a_node['child'], $parent_childs);
1970  $this->__validateSubtrees($lft_childs, $parent_childs);
1971 
1972  return true;
1973  }
1974 
1979  public function __getSubTreeByParentRelation(int $a_node_id, array &$parent_childs): bool
1980  {
1981  // GET PARENT ID
1982  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
1983  'WHERE child = %s ' .
1984  'AND tree = %s ';
1985  $res = $this->db->queryF($query, array('integer', 'integer'), array(
1986  $a_node_id,
1987  $this->tree_id
1988  ));
1989 
1990  $counter = 0;
1991  while ($row = $this->db->fetchObject($res)) {
1992  $parent_childs[$a_node_id] = (int) $row->parent;
1993  ++$counter;
1994  }
1995  // MULTIPLE ENTRIES
1996  if ($counter > 1) {
1997  $message = 'Multiple entries in maintree! $a_node_id: ' . $a_node_id;
1998 
1999  $this->logger->error($message);
2001  }
2002 
2003  // GET ALL CHILDS
2004  $query = 'SELECT * FROM ' . $this->table_tree . ' ' .
2005  'WHERE parent = %s ';
2006  $res = $this->db->queryF($query, array('integer'), array($a_node_id));
2007 
2008  while ($row = $this->db->fetchObject($res)) {
2009  // RECURSION
2010  $this->__getSubTreeByParentRelation((int) $row->child, $parent_childs);
2011  }
2012  return true;
2013  }
2014 
2022  public function __validateSubtrees(array &$lft_childs, array $parent_childs): bool
2023  {
2024  // SORT BY KEY
2025  ksort($lft_childs);
2026  ksort($parent_childs);
2027 
2028  $this->logger->debug('left childs ' . print_r($lft_childs, true));
2029  $this->logger->debug('parent childs ' . print_r($parent_childs, true));
2030 
2031  if (count($lft_childs) != count($parent_childs)) {
2032  $message = '(COUNT) Tree is corrupted! Left/Right subtree does not comply with parent relation';
2033  $this->logger->error($message);
2035  }
2036 
2037  foreach ($lft_childs as $key => $value) {
2038  if ($parent_childs[$key] != $value) {
2039  $message = '(COMPARE) Tree is corrupted! Left/Right subtree does not comply with parent relation';
2040  $this->logger->error($message);
2042  }
2043  if ($key == ROOT_FOLDER_ID) {
2044  $message = '(ROOT_FOLDER) Tree is corrupted! Tried to delete root folder';
2045  $this->logger->error($message);
2047  }
2048  }
2049  return true;
2050  }
2051 
2060  public function moveTree(int $a_source_id, int $a_target_id, int $a_location = self::POS_LAST_NODE): void
2061  {
2062  $old_parent_id = $this->getParentId($a_source_id);
2063  $this->getTreeImplementation()->moveTree($a_source_id, $a_target_id, $a_location);
2064  if (isset($GLOBALS['DIC']["ilAppEventHandler"]) && $this->__isMainTree()) {
2065  $GLOBALS['DIC']['ilAppEventHandler']->raise(
2066  "Services/Tree",
2067  "moveTree",
2068  array(
2069  'tree' => $this->table_tree,
2070  'source_id' => $a_source_id,
2071  'target_id' => $a_target_id,
2072  'old_parent_id' => $old_parent_id
2073  )
2074  );
2075  }
2076  }
2077 
2083  public function getRbacSubtreeInfo(int $a_endnode_id): array
2084  {
2085  return $this->getTreeImplementation()->getSubtreeInfo($a_endnode_id);
2086  }
2087 
2091  public function getSubTreeQuery(
2092  int $a_node_id,
2093  array $a_fields = [],
2094  array $a_types = [],
2095  bool $a_force_join_reference = false
2096  ): string {
2097  return $this->getTreeImplementation()->getSubTreeQuery(
2098  $this->getNodeTreeData($a_node_id),
2099  $a_types,
2100  $a_force_join_reference,
2101  $a_fields
2102  );
2103  }
2104 
2105  public function getTrashSubTreeQuery(
2106  int $a_node_id,
2107  array $a_fields = [],
2108  array $a_types = [],
2109  bool $a_force_join_reference = false
2110  ): string {
2111  return $this->getTreeImplementation()->getTrashSubTreeQuery(
2112  $this->getNodeTreeData($a_node_id),
2113  $a_types,
2114  $a_force_join_reference,
2115  $a_fields
2116  );
2117  }
2118 
2125  public function getSubTreeFilteredByObjIds(int $a_node_id, array $a_obj_ids, array $a_fields = []): array
2126  {
2127  $node = $this->getNodeData($a_node_id);
2128  if (!count($node)) {
2129  return [];
2130  }
2131 
2132  $res = [];
2133 
2134  $query = $this->getTreeImplementation()->getSubTreeQuery($node, [], true, array($this->ref_pk));
2135 
2136  $fields = '*';
2137  if (count($a_fields)) {
2138  $fields = implode(',', $a_fields);
2139  }
2140 
2141  $query = "SELECT " . $fields .
2142  " FROM " . $this->getTreeTable() .
2143  " " . $this->buildJoin() .
2144  " WHERE " . $this->getTableReference() . "." . $this->ref_pk . " IN (" . $query . ")" .
2145  " AND " . $this->db->in($this->getObjectDataTable() . "." . $this->obj_pk, $a_obj_ids, false, "integer");
2146  $set = $this->db->query($query);
2147  while ($row = $this->db->fetchAssoc($set)) {
2148  $res[] = $row;
2149  }
2150 
2151  return $res;
2152  }
2153 
2154  public function deleteNode(int $a_tree_id, int $a_node_id): void
2155  {
2156  $query = 'DELETE FROM tree where ' .
2157  'child = ' . $this->db->quote($a_node_id, 'integer') . ' ' .
2158  'AND tree = ' . $this->db->quote($a_tree_id, 'integer');
2159  $this->db->manipulate($query);
2160 
2161  $this->eventHandler->raise(
2162  "Services/Tree",
2163  "deleteNode",
2164  [
2165  'tree' => $this->table_tree,
2166  'node_id' => $a_node_id,
2167  'tree_id' => $a_tree_id
2168  ]
2169  );
2170  }
2171 
2176  public function lookupTrashedObjectTypes(): array
2177  {
2178  $query = 'SELECT DISTINCT(o.type) ' . $this->db->quoteIdentifier('type') .
2179  ' FROM tree t JOIN object_reference r ON child = r.ref_id ' .
2180  'JOIN object_data o on r.obj_id = o.obj_id ' .
2181  'WHERE tree < ' . $this->db->quote(0, 'integer') . ' ' .
2182  'AND child = -tree ' .
2183  'GROUP BY o.type';
2184  $res = $this->db->query($query);
2185 
2186  $types_deleted = [];
2187  while ($row = $res->fetchRow(ilDBConstants::FETCHMODE_OBJECT)) {
2188  $types_deleted[] = (string) $row->type;
2189  }
2190  return $types_deleted;
2191  }
2192 
2196  public function isRepositoryTree(): bool
2197  {
2198  return $this->table_tree == 'tree';
2199  }
2200 } // END class.tree
isRepositoryTree()
check if current tree instance operates on repository tree table
moveTree(int $a_source_id, int $a_target_id, int $a_location=self::POS_LAST_NODE)
Move Tree Implementation public.
Thrown if invalid tree strucutes are found.
$res
Definition: ltiservices.php:69
Global event handler.
setObjectTablePK(string $a_column_name)
set column containing primary key in object table
static setDeletedDates(array $ref_ids, int $user_id)
initTreeImplementation()
Init tree implementation.
fetchSuccessorNode(int $a_node_id, string $a_type="")
get node data of successor node
getNodeData(int $a_node_id, ?int $a_tree_pk=null)
get all information of a node.
static _lookupTitle(int $obj_id)
getNodeTreeData(int $a_node_id)
return all columns of tabel tree
array $parent_cache
static quoteArray(array $a_array)
Quotes all members of an array for usage in DB query statement.
ilAppEventHandler $eventHandler
manipulateF(string $query, array $types, array $values)
static getLogger(string $a_component_id)
Get component logger.
string $table_obj_data
table name of object_data table
isSaved(int $a_node_id)
Use method isDeleted.
const ROOT_FOLDER_ID
Definition: constants.php:32
getFilteredChilds(array $a_filter, int $a_node, string $a_order="", string $a_direction="ASC")
get child nodes of given node (exclude filtered obj_types)
getChildIds(int $a_node)
array $in_tree_cache
deleteNode(int $a_tree_id, int $a_node_id)
checkTreeChilds(bool $a_no_zero_child=true)
check, if all childs of tree nodes exist in object table
getChilds(int $a_node_id, string $a_order="", string $a_direction="ASC")
get child nodes of given node
getNodePath(int $a_endnode_id, int $a_startnode_id=0)
Returns the node path for the specified object reference.
getSavedNodeObjIds(array $a_obj_ids)
get object id of saved/deleted nodes
const DESC_LENGTH
isInTree(?int $a_node_id)
get all information of a node.
getParentCache()
Get parent cache.
isDeleted(int $a_node_id)
This is a wrapper for isSaved() with a more useful name.
buildJoin()
build join depending on table settings private
useCache(bool $a_use=true)
Use Cache (usually activated)
setTreeTablePK(string $a_column_name)
set column containing primary key in tree table
const RELATION_PARENT
static lookupTreesForNode(int $node_id)
getSubTreeIds(int $a_ref_id)
Get all ids of subnodes.
deleteTree(array $a_node)
delete node and the whole subtree under this node
ilLogger $logger
quote($value, string $type)
getTreePk()
Get tree primary key.
Base class for nested set path based trees.
addTree(int $a_tree_id, int $a_node_id=-1)
create a new tree to do: ???
ilDBInterface $db
const TREE_TYPE_MATERIALIZED_PATH
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...
__getSubTreeByParentRelation(int $a_node_id, array &$parent_childs)
$path
Definition: ltiservices.php:32
int $tree_id
to use different trees in one db-table
getRelationOfNodes(array $a_node_a_arr, array $a_node_b_arr)
get relation of two nodes by node data
isGrandChild(int $a_startnode_id, int $a_querynode_id)
checks if a node is in the path of an other node
getObjectDataTable()
Get object data table.
static _resetDeletedDate(int $ref_id)
global $DIC
Definition: feed.php:28
validateParentRelations()
Validate parent relations of tree.
const POS_FIRST_NODE
getChildsByType(int $a_node_id, string $a_type)
get child nodes of given node by object type
__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 &#39;obj_id&#39; You may use...
__checkDelete(array $a_node)
Check for deleteTree() compares a subtree of a given node by checking lft, rgt against parent relatio...
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...
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
fetchTranslationFromObjectDataCache(array $a_obj_ids)
Get translation data from object cache (trigger in object cache on preload)
const DEFAULT_GAP
array $parent_type_cache
getDepth(int $a_node_id)
return depth of a node in tree
removeTree(int $a_tree_id)
remove an existing tree
$GLOBALS["DIC"]
Definition: wac.php:31
getParentNodeData(int $a_node_id)
get data of parent node from tree and object_data
$lng
getTreeTable()
Get tree table name.
getRelation(int $a_node_a, int $a_node_b)
Get relation of two nodes.
checkTree()
check consistence of tree all left & right values are checked if they are exists only once ...
getSavedNodeData(int $a_parent_id)
get data saved/deleted nodes
preloadDeleted(array $a_node_ids)
Preload deleted information.
int $root_id
points to root node (may be a subtree)
const DEFAULT_LANGUAGE
renumber(int $node_id=1, int $i=1)
Wrapper for renumber.
lookupTrashedObjectTypes()
Lookup object types in trash.
string $key
Consumer key/client ID value.
Definition: System.php:193
__renumber(int $node_id=1, int $i=1)
This method is private.
query(string $query)
Run a (read-only) Query on the database.
getRbacSubtreeInfo(int $a_endnode_id)
This method is used for change existing objects and returns all necessary information for this action...
getLeftValue(int $a_node_id)
get left value of given node
getTableReference()
Get reference table if available.
getParentId(int $a_node_id)
get parent id of given node
resetInTreeCache()
reset in tree cache
ilTreeImplementation $tree_impl
const RELATION_EQUALS
getChildsByTypeFilter(int $a_node_id, array $a_types, string $a_order="", string $a_direction="ASC")
get child nodes of given node by object type
const RELATION_CHILD
const RELATION_NONE
const ROLE_FOLDER_ID
Definition: constants.php:34
setRootId(int $a_root_id)
getFilteredSubTree(int $a_node_id, array $a_filter=[])
get filtered subtree get all subtree nodes beginning at a specific node excluding specific object typ...
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.
getDepthCache()
Get depth cache.
const POS_LAST_NODE
string $tree_pk
column name containing tree id in tree table
fetchNodeData(array $a_row)
get data of parent node from tree and object_data
initLangCode()
Do not use it Store user language.
array $oc_preloaded
preloadDepthParent(array $a_node_ids)
Preload depth/parent.
const TREE_TYPE_NESTED_SET
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...
getTrashSubTreeQuery(int $a_node_id, array $a_fields=[], array $a_types=[], bool $a_force_join_reference=false)
getSubTreeQuery(int $a_node_id, array $a_fields=[], array $a_types=[], bool $a_force_join_reference=false)
Get tree subtree query.
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
bool $use_cache
getMaximumDepth()
Return the current maximum depth in the tree.
static shortenTextExtended(string $a_str, int $a_len, bool $a_dots=false, bool $a_next_blank=false, bool $a_keep_extension=false)
getGap()
Get default gap.
string $obj_pk
column name containing primary key in object table
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
$q
Definition: shib_logout.php:21
setTreeId(int $a_tree_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$message
Definition: xapiexit.php:32
const RELATION_SIBLING
string $ref_pk
column name containing primary key in reference table
isCacheUsed()
Check if cache is active.
array $depth_cache
getChildSequenceNumber(array $a_node, string $type="")
get sequence number of node in sibling sequence
getSubTree(array $a_node, bool $a_with_data=true, array $a_type=[])
get all nodes in the subtree under specified node
__construct(int $a_tree_id, int $a_root_id=0, ilDBInterface $db=null)
__isMainTree()
Check if operations are done on main tree.
string $table_tree
table name of tree table
string $table_obj_reference
table name of object_reference table
getTreeImplementation()
Get tree implementation.
array $is_saved_cache
static _removeEntry(int $a_tree, int $a_child, string $a_db_table="tree")
STATIC METHOD Removes a single entry from a tree.
array $translation_cache
Interface for tree implementations Currrently nested set or materialized path.
string $lang_code
array $path_id_cache
moveToTrash(int $a_node_id, bool $a_set_deleted=false, int $a_deleted_by=0)
Move node to trash bin.
setReferenceTablePK(string $a_column_name)
set column containing primary key in reference table
int $gap
Size of the gaps to be created in the nested sets sequence numbering of the tree nodes.
$r
fetchPredecessorNode(int $a_node_id, string $a_type="")
get node data of predecessor node