52 "adm",
"root",
"mail",
"usrf",
"objf",
"lngf",
53 "trac",
"taxf",
"auth",
"rolf",
"assf",
"svyf",
"extt",
"adve",
"fold"
62 "restore_trash" =>
false,
63 "purge_trash" =>
false
93 $this->obj_definition =
$DIC[
"objDefinition"];
94 $this->db =
$DIC->database();
95 $this->
lng = $DIC->language();
96 $this->log =
$DIC[
"ilLog"];
97 $this->rbacadmin =
$DIC->rbac()->admin();
98 $this->tree =
$DIC->repositoryTree();
99 $this->
user = $DIC->user();
100 $objDefinition =
$DIC[
"objDefinition"];
104 $this->rbac_object_types = $objDefinition->getAllRBACObjects();
106 if ($a_log ===
true) {
107 $this->logging =
true;
116 $this->scan_log->setLogFormat(
"");
127 return array_keys($this->mode);
139 public function setMode(
string $a_mode,
bool $a_value): bool
141 if ((!array_key_exists($a_mode, $this->mode) && $a_mode !==
"all") || !is_bool($a_value)) {
142 $this->throwError(INVALID_PARAM, FATAL, DEBUG);
146 if ($a_mode ===
"all") {
147 foreach ($this->mode as
$mode => $value) {
148 $this->mode[
$mode] = $a_value;
151 $this->mode[$a_mode] = $a_value;
163 if (!array_key_exists($a_mode, $this->mode)) {
164 $this->throwError(VALIDATER_UNKNOWN_MODE, WARNING, DEBUG);
168 return $this->mode[$a_mode];
173 return $this->logging;
185 if ($this->mode[
"restore"] ===
true) {
186 $this->mode[
"scan"] =
true;
187 $this->mode[
"purge"] =
false;
190 if ($this->mode[
"purge"] ===
true) {
191 $this->mode[
"scan"] =
true;
192 $this->mode[
"restore"] =
false;
195 if ($this->mode[
"restore_trash"] ===
true) {
196 $this->mode[
"scan"] =
true;
197 $this->mode[
"purge_trash"] =
false;
200 if ($this->mode[
"purge_trash"] ===
true) {
201 $this->mode[
"scan"] =
true;
202 $this->mode[
"restore_trash"] =
false;
205 if ($this->mode[
"clean"] ===
true) {
206 $this->mode[
"scan"] =
true;
224 $summary .=
$lng->txt(
"scanning_system");
225 if (!$this->isModeEnabled(
"scan")) {
226 $summary .=
$lng->txt(
"disabled");
228 $summary .=
"<br/>" .
$lng->txt(
"searching_invalid_refs");
229 if ($this->findInvalidReferences()) {
230 $summary .= count($this->getInvalidReferences()) .
" " .
$lng->txt(
"found");
232 $summary .=
$lng->txt(
"found_none");
235 $summary .=
"<br/>" .
$lng->txt(
"searching_invalid_childs");
236 if ($this->findInvalidChilds()) {
237 $summary .= count($this->getInvalidChilds()) .
" " .
$lng->txt(
"found");
239 $summary .=
$lng->txt(
"found_none");
242 $summary .=
"<br/>" .
$lng->txt(
"searching_missing_objs");
243 if ($this->findMissingObjects()) {
244 $summary .= count($this->getMissingObjects()) .
" " .
$lng->txt(
"found");
246 $summary .=
$lng->txt(
"found_none");
249 $summary .=
"<br/>" .
$lng->txt(
"searching_unbound_objs");
250 if ($this->findUnboundObjects()) {
251 $summary .= count($this->getUnboundObjects()) .
" " .
$lng->txt(
"found");
253 $summary .=
$lng->txt(
"found_none");
256 $summary .=
"<br/>" .
$lng->txt(
"searching_deleted_objs");
257 if ($this->findDeletedObjects()) {
258 $summary .= count($this->getDeletedObjects()) .
" " .
$lng->txt(
"found");
260 $summary .=
$lng->txt(
"found_none");
263 $summary .=
"<br/>" .
$lng->txt(
"searching_invalid_rolfs");
264 if ($this->findInvalidRolefolders()) {
265 $summary .= count($this->getInvalidRolefolders()) .
" " .
$lng->txt(
"found");
267 $summary .=
$lng->txt(
"found_none");
270 $summary .=
"<br/><br/>" .
$lng->txt(
"analyzing_tree_structure");
271 if ($this->checkTreeStructure()) {
272 $summary .=
$lng->txt(
"tree_corrupt");
274 $summary .=
$lng->txt(
"done");
280 $summary .=
"<br /><br />" .
$lng->txt(
"dumping_tree");
281 if (!$this->isModeEnabled(
"dump_tree")) {
282 $summary .=
$lng->txt(
"disabled");
284 $error_count = $this->dumpTree();
285 if ($error_count > 0) {
286 $summary .=
$lng->txt(
"tree_corrupt");
288 $summary .=
$lng->txt(
"done");
294 $summary .=
"<br /><br />" .
$lng->txt(
"cleaning");
295 if (!$this->isModeEnabled(
"clean")) {
296 $summary .=
$lng->txt(
"disabled");
298 $summary .=
"<br />" .
$lng->txt(
"removing_invalid_refs");
299 if ($this->removeInvalidReferences()) {
300 $summary .= strtolower(
$lng->txt(
"done"));
302 $summary .=
$lng->txt(
"nothing_to_remove") .
$lng->txt(
"skipped");
305 $summary .=
"<br />" .
$lng->txt(
"removing_invalid_childs");
306 if ($this->removeInvalidChilds()) {
307 $summary .= strtolower(
$lng->txt(
"done"));
309 $summary .=
$lng->txt(
"nothing_to_remove") .
$lng->txt(
"skipped");
312 $summary .=
"<br />" .
$lng->txt(
"removing_invalid_rolfs");
313 if ($this->removeInvalidRolefolders()) {
314 $summary .= strtolower(
$lng->txt(
"done"));
316 $summary .=
$lng->txt(
"nothing_to_remove") .
$lng->txt(
"skipped");
322 $this->findUnboundObjects();
326 $summary .=
"<br /><br />" .
$lng->txt(
"restoring");
328 if (!$this->isModeEnabled(
"restore")) {
329 $summary .=
$lng->txt(
"disabled");
331 $summary .=
"<br />" .
$lng->txt(
"restoring_missing_objs");
332 if ($this->restoreMissingObjects()) {
333 $summary .= strtolower(
$lng->txt(
"done"));
335 $summary .=
$lng->txt(
"nothing_to_restore") .
$lng->txt(
"skipped");
338 $summary .=
"<br />" .
$lng->txt(
"restoring_unbound_objs");
339 if ($this->restoreUnboundObjects()) {
340 $summary .= strtolower(
$lng->txt(
"done"));
342 $summary .=
$lng->txt(
"nothing_to_restore") .
$lng->txt(
"skipped");
347 $summary .=
"<br /><br />" .
$lng->txt(
"restoring_trash");
349 if (!$this->isModeEnabled(
"restore_trash")) {
350 $summary .=
$lng->txt(
"disabled");
351 } elseif ($this->restoreTrash()) {
352 $summary .= strtolower(
$lng->txt(
"done"));
354 $summary .=
$lng->txt(
"nothing_to_restore") .
$lng->txt(
"skipped");
358 $summary .=
"<br /><br />" .
$lng->txt(
"purging");
360 if (!$this->isModeEnabled(
"purge")) {
361 $summary .=
$lng->txt(
"disabled");
363 $summary .=
"<br />" .
$lng->txt(
"purging_missing_objs");
364 if ($this->purgeMissingObjects()) {
365 $summary .= strtolower(
$lng->txt(
"done"));
367 $summary .=
$lng->txt(
"nothing_to_purge") .
$lng->txt(
"skipped");
370 $summary .=
"<br />" .
$lng->txt(
"purging_unbound_objs");
371 if ($this->purgeUnboundObjects()) {
372 $summary .= strtolower(
$lng->txt(
"done"));
374 $summary .=
$lng->txt(
"nothing_to_purge") .
$lng->txt(
"skipped");
379 $summary .=
"<br /><br />" .
$lng->txt(
"purging_trash");
381 if (!$this->isModeEnabled(
"purge_trash")) {
382 $summary .=
$lng->txt(
"disabled");
383 } elseif ($this->purgeTrash()) {
384 $summary .= strtolower(
$lng->txt(
"done"));
386 $summary .=
$lng->txt(
"nothing_to_purge") .
$lng->txt(
"skipped");
390 if ($this->isModeEnabled(
"clean")) {
391 $summary .=
"<br /><br />" .
$lng->txt(
"cleaning_final");
392 if ($this->initGapsInTree()) {
393 $summary .=
"<br />" .
$lng->txt(
"initializing_gaps") .
" " . strtolower(
$lng->txt(
"done"));
401 foreach ($this->mode as $mode => $value) {
402 $arr[] = $mode .
"[" . (
int) $value .
"]";
418 if ($this->mode[
"scan"] !==
true) {
423 $this->missing_objects = [];
425 $this->writeScanLogLine(
"\nfindMissingObjects:");
432 $q =
"SELECT object_data.*, ref_id FROM object_data " .
433 "LEFT JOIN object_reference ON object_data.obj_id = object_reference.obj_id " .
434 "LEFT JOIN tree ON object_reference.ref_id = tree.child " .
435 "WHERE tree.child IS NULL " .
436 "AND (object_reference.obj_id IS NOT NULL " .
437 " OR object_data.type <> 'file' AND " .
438 $ilDB->in(
'object_data.type', $this->rbac_object_types,
false,
'text') .
440 $r = $this->db->query($q);
443 #if (!in_array($row->type,$this->object_types_exclude))
444 if (!$this->isExcludedFromRecovery($row->type, $row->obj_id)) {
445 $this->missing_objects[] = [
446 "obj_id" => $row->obj_id,
447 "type" => $row->type,
448 "ref_id" => $row->ref_id,
449 "child" => $row->child,
450 "title" => $row->title,
451 "desc" => $row->description,
452 "owner" => $row->owner,
453 "create_date" => $row->create_date,
454 "last_update" => $row->last_update
459 $this->filterWorkspaceObjects($this->missing_objects);
460 if (count($this->missing_objects) > 0) {
461 $this->writeScanLogLine(
"obj_id\ttype\tref_id\tchild\ttitle\tdesc\towner\tcreate_date\tlast_update");
462 $this->writeScanLogArray($this->missing_objects);
466 $this->writeScanLogLine(
"none");
482 if ($this->mode[
"scan"] !==
true) {
487 $this->invalid_rolefolders = [];
489 $this->writeScanLogLine(
"\nfindInvalidRolefolders:");
492 $q =
"SELECT object_data.*, ref_id FROM object_data " .
493 "LEFT JOIN object_reference ON object_data.obj_id = object_reference.obj_id " .
494 "LEFT JOIN tree ON object_reference.ref_id = tree.child " .
495 "WHERE (object_reference.obj_id IS NULL OR tree.child IS NULL) " .
496 "AND object_data.type='rolf'";
497 $r = $this->db->query($q);
500 $this->invalid_rolefolders[] = [
501 "obj_id" => $row->obj_id,
502 "type" => $row->type,
503 "ref_id" => $row->ref_id,
504 "child" => $row->child,
505 "title" => $row->title,
506 "desc" => $row->description,
507 "owner" => $row->owner,
508 "create_date" => $row->create_date,
509 "last_update" => $row->last_update
514 $q =
"SELECT object_data.*, ref_id FROM object_data " .
515 "LEFT JOIN object_reference ON object_data.obj_id = object_reference.obj_id " .
516 "LEFT JOIN tree ON object_reference.ref_id = tree.child " .
518 "AND object_data.type='rolf'";
519 $r = $this->db->query($q);
522 $this->invalid_rolefolders[] = [
523 "obj_id" => $row->obj_id,
524 "type" => $row->type,
525 "ref_id" => $row->ref_id,
526 "child" => $row->child,
527 "title" => $row->title,
528 "desc" => $row->description,
529 "owner" => $row->owner,
530 "create_date" => $row->create_date,
531 "last_update" => $row->last_update
535 $this->filterWorkspaceObjects($this->invalid_rolefolders);
536 if (count($this->invalid_rolefolders) > 0) {
537 $this->writeScanLogLine(
"obj_id\ttype\tref_id\tchild\ttitle\tdesc\towner\tcreate_date\tlast_update");
538 $this->writeScanLogArray($this->invalid_rolefolders);
542 $this->writeScanLogLine(
"none");
556 if ($this->mode[
"scan"] !==
true) {
561 $this->invalid_rbac_entries = [];
563 $this->writeScanLogLine(
"\nfindInvalidRBACEntries:");
565 $q =
"SELECT object_data.*, ref_id FROM object_data " .
566 "LEFT JOIN object_reference ON object_data.obj_id = object_reference.obj_id " .
567 "LEFT JOIN tree ON object_reference.ref_id = tree.child " .
568 "WHERE (object_reference.obj_id IS NULL OR tree.child IS NULL) " .
569 "AND object_data.type='rolf'";
570 $r = $this->db->query($q);
573 $this->invalid_rolefolders[] = [
574 "obj_id" => $row->obj_id,
575 "type" => $row->type,
576 "ref_id" => $row->ref_id,
577 "child" => $row->child,
578 "title" => $row->title,
579 "desc" => $row->description,
580 "owner" => $row->owner,
581 "create_date" => $row->create_date,
582 "last_update" => $row->last_update
587 $q =
"SELECT object_data.*, ref_id FROM object_data " .
588 "LEFT JOIN object_reference ON object_data.obj_id = object_reference.obj_id " .
589 "LEFT JOIN tree ON object_reference.ref_id = tree.child " .
591 "AND object_data.type='rolf'";
592 $r = $this->db->query($q);
595 $this->invalid_rolefolders[] = [
596 "obj_id" => $row->obj_id,
597 "type" => $row->type,
598 "ref_id" => $row->ref_id,
599 "child" => $row->child,
600 "title" => $row->title,
601 "desc" => $row->description,
602 "owner" => $row->owner,
603 "create_date" => $row->create_date,
604 "last_update" => $row->last_update
608 $this->filterWorkspaceObjects($this->invalid_rolefolders);
609 if (count($this->invalid_rolefolders) > 0) {
610 $this->writeScanLogLine(
"obj_id\ttype\tref_id\tchild\ttitle\tdesc\towner\tcreate_date\tlast_update");
611 $this->writeScanLogArray($this->invalid_rolefolders);
615 $this->writeScanLogLine(
"none");
629 return $this->missing_objects;
642 if ($this->mode[
"scan"] !==
true) {
647 $this->invalid_references = [];
649 $this->writeScanLogLine(
"\nfindInvalidReferences:");
650 $q =
"SELECT object_reference.* FROM object_reference " .
651 "LEFT JOIN object_data ON object_data.obj_id = object_reference.obj_id " .
652 "WHERE object_data.obj_id IS NULL " .
653 "OR " .
$ilDB->in(
'object_data.type', $this->rbac_object_types,
true,
'text');
654 $r = $this->db->query($q);
657 $this->invalid_references[] = [
658 "ref_id" => $row->ref_id,
659 "obj_id" => $row->obj_id,
660 "msg" =>
"Object does not exist."
664 $this->filterWorkspaceObjects($this->invalid_references);
665 if (count($this->invalid_references) > 0) {
666 $this->writeScanLogLine(
"ref_id\t\tobj_id");
667 $this->writeScanLogArray($this->invalid_references);
671 $this->writeScanLogLine(
"none");
680 return $this->invalid_references;
691 if ($this->mode[
"scan"] !==
true) {
696 $this->invalid_childs = [];
698 $this->writeScanLogLine(
"\nfindInvalidChilds:");
700 $q =
"SELECT tree.*,object_reference.ref_id FROM tree " .
701 "LEFT JOIN object_reference ON tree.child = object_reference.ref_id " .
702 "LEFT JOIN object_data ON object_reference.obj_id = object_data.obj_id " .
703 "WHERE object_reference.ref_id IS NULL or object_data.obj_id IS NULL";
704 $r = $this->db->query($q);
706 $this->invalid_childs[] = [
707 "child" => $row->child,
708 "ref_id" => $row->ref_id,
709 "msg" =>
"No object found"
713 if (count($this->invalid_childs) > 0) {
714 $this->writeScanLogLine(
"child\t\tref_id");
715 $this->writeScanLogArray($this->invalid_childs);
719 $this->writeScanLogLine(
"none");
728 return $this->invalid_childs;
741 if ($this->mode[
"scan"] !==
true) {
746 $this->unbound_objects = [];
748 $this->writeScanLogLine(
"\nfindUnboundObjects:");
750 $q =
"SELECT T1.tree,T1.child,T1.parent," .
751 "T2.tree deleted,T2.parent grandparent " .
753 "LEFT JOIN tree T2 ON T2.child=T1.parent " .
754 "WHERE (T2.tree!=1 OR T2.tree IS NULL) AND T1.parent!=0";
755 $r = $this->db->query($q);
759 if ($row->deleted ===
null) {
760 $this->unbound_objects[] = [
761 "child" => $row->child,
762 "parent" => $row->parent,
763 "tree" => $row->tree,
764 "msg" =>
"No valid parent node found"
769 if (count($this->unbound_objects) > 0) {
770 $this->writeScanLogLine(
"child\t\tparent\ttree");
771 $this->writeScanLogArray($this->unbound_objects);
775 $this->writeScanLogLine(
"none");
789 if ($this->mode[
"scan"] !==
true) {
794 $this->deleted_objects = [];
796 $this->writeScanLogLine(
"\nfindDeletedObjects:");
799 $query =
"SELECT object_data.*,tree.tree,tree.child,tree.parent,deleted " .
800 "FROM object_data " .
801 "LEFT JOIN object_reference ON object_data.obj_id=object_reference.obj_id " .
802 "LEFT JOIN tree ON tree.child=object_reference.ref_id " .
803 " WHERE tree != 1 " .
805 $r = $this->db->query(
$query);
810 $this->deleted_objects[] = [
811 "child" => $row->child,
812 "parent" => $row->parent,
813 "tree" => $row->tree,
814 "type" => $row->type,
815 "title" => $row->title,
816 "desc" => $row->description,
817 "owner" => $row->owner,
818 "deleted" => $row->deleted,
819 "deleted_timestamp" => $tmp_date->get(
IL_CAL_UNIX),
820 "create_date" => $row->create_date,
821 "last_update" => $row->last_update
825 if (count($this->deleted_objects) > 0) {
826 $this->writeScanLogArray([array_keys($this->deleted_objects[0])]);
827 $this->writeScanLogArray($this->deleted_objects);
830 $this->writeScanLogLine(
"none");
845 return $this->unbound_objects;
853 return $this->deleted_objects;
861 return $this->invalid_rolefolders;
870 array $a_invalid_refs =
null
876 if ($this->mode[
"clean"] !==
true) {
880 $this->writeScanLogLine(
"\nremoveInvalidReferences:");
882 if ($a_invalid_refs ===
null && isset($this->invalid_references)) {
883 $a_invalid_refs = &$this->invalid_references;
887 if (!is_array($a_invalid_refs)) {
888 $this->throwError(INVALID_PARAM, WARNING, DEBUG);
892 if (count($a_invalid_refs) === 0) {
893 $this->writeScanLogLine(
"none");
902 '%s::removeInvalidReferences(): Started...',
905 $ilLog->write(
$message, $ilLog->WARNING);
908 $this->filterWorkspaceObjects($a_invalid_refs);
910 foreach ($a_invalid_refs as $entry) {
911 $query =
"DELETE FROM object_reference WHERE ref_id= " . $this->db->quote($entry[
"ref_id"],
'integer') .
912 " AND obj_id = " . $this->db->quote($entry[
"obj_id"],
'integer') .
" ";
916 '%s::removeInvalidReferences(): Reference %s removed',
920 $ilLog->write(
$message, $ilLog->WARNING);
922 $this->writeScanLogLine(
"Entry " . $entry[
"ref_id"] .
" removed");
934 array $a_invalid_childs =
null
939 if ($this->mode[
"clean"] !==
true) {
943 $this->writeScanLogLine(
"\nremoveInvalidChilds:");
945 if ($a_invalid_childs ===
null && isset($this->invalid_childs)) {
946 $a_invalid_childs = &$this->invalid_childs;
950 if (!is_array($a_invalid_childs)) {
951 $this->throwError(INVALID_PARAM, WARNING, DEBUG);
956 if (count($a_invalid_childs) === 0) {
957 $this->writeScanLogLine(
"none");
966 '%s::removeInvalidChilds(): Started...',
969 $ilLog->write(
$message, $ilLog->WARNING);
971 foreach ($a_invalid_childs as $entry) {
972 $q =
"DELETE FROM tree WHERE child='" . $entry[
"child"] .
"'";
973 $this->db->query($q);
976 '%s::removeInvalidChilds(): Entry child=%s removed',
980 $ilLog->write(
$message, $ilLog->WARNING);
982 $this->writeScanLogLine(
"Entry " . $entry[
"child"] .
" removed");
995 array $a_invalid_rolefolders =
null
1000 if ($this->mode[
"clean"] !==
true) {
1004 $this->writeScanLogLine(
"\nremoveInvalidRolefolders:");
1006 if ($a_invalid_rolefolders ===
null && isset($this->invalid_rolefolders)) {
1007 $a_invalid_rolefolders = $this->invalid_rolefolders;
1011 if (!is_array($a_invalid_rolefolders)) {
1012 $this->throwError(INVALID_PARAM, WARNING, DEBUG);
1017 if (count($a_invalid_rolefolders) === 0) {
1018 $this->writeScanLogLine(
"none");
1029 '%s::removeInvalidRolefolders(): Started...',
1032 $ilLog->write(
$message, $ilLog->WARNING);
1035 $this->filterWorkspaceObjects($a_invalid_rolefolders);
1037 foreach ($a_invalid_rolefolders as $rolf) {
1039 if ($rolf[
"ref_id"] ===
null) {
1040 $rolf[
"ref_id"] = $this->restoreReference($rolf[
"obj_id"]);
1042 $this->writeScanLogLine(
"Created missing reference '" . $rolf[
"ref_id"] .
"' for rolefolder object '" . $rolf[
"obj_id"] .
"'");
1047 $obj_data->delete();
1050 $this->writeScanLogLine(
"Removed invalid rolefolder '" . $rolf[
"title"] .
"' (id=" . $rolf[
"obj_id"] .
",ref=" . $rolf[
"ref_id"] .
") from system");
1065 array $a_missing_objects =
null
1067 $rbacadmin = $this->rbacadmin;
1071 if ($this->mode[
"restore"] !==
true) {
1075 $this->writeScanLogLine(
"\nrestoreMissingObjects:");
1077 if ($a_missing_objects ===
null && isset($this->missing_objects)) {
1078 $a_missing_objects = $this->missing_objects;
1082 if (!is_array($a_missing_objects)) {
1083 $this->throwError(INVALID_PARAM, WARNING, DEBUG);
1088 if (count($a_missing_objects) === 0) {
1089 $this->writeScanLogLine(
"none");
1100 '%s::restoreMissingObjects(): Started...',
1103 $ilLog->write(
$message, $ilLog->WARNING);
1106 $this->filterWorkspaceObjects($a_missing_objects);
1108 foreach ($a_missing_objects as $missing_obj) {
1110 if ($missing_obj[
"ref_id"] ===
null) {
1111 $missing_obj[
"ref_id"] = $this->restoreReference($missing_obj[
"obj_id"]);
1113 $this->writeScanLogLine(
"Created missing reference '" . $missing_obj[
"ref_id"] .
"' for object '" . $missing_obj[
"obj_id"] .
"'");
1117 #if (!in_array($missing_obj["type"],$this->object_types_exclude))
1118 if (!$this->isExcludedFromRecovery($missing_obj[
'type'], $missing_obj[
'obj_id'])) {
1119 $rbacadmin->revokePermission((
int) $missing_obj[
"ref_id"]);
1126 $this->writeScanLogLine(
"Restored object '" . $missing_obj[
"title"] .
"' (id=" . $missing_obj[
"obj_id"] .
",ref=" . $missing_obj[
"ref_id"] .
") in 'Restored objects folder'");
1145 if (empty($a_obj_id)) {
1146 $this->throwError(INVALID_PARAM, WARNING, DEBUG);
1150 $next_id =
$ilDB->nextId(
'object_reference');
1151 $query =
"INSERT INTO object_reference (ref_id, obj_id) " .
1152 "VALUES (" . $this->db->quote($next_id,
'integer') .
", " . $this->db->quote($a_obj_id,
'integer') .
")";
1156 '%s::restoreReference(): new reference %s for obj_id %s created',
1161 $ilLog->write(
$message, $ilLog->WARNING);
1172 array $a_unbound_objects =
null
1174 $ilLog = $this->log;
1177 if ($this->mode[
"restore"] !==
true) {
1181 $this->writeScanLogLine(
"\nrestoreUnboundObjects:");
1183 if ($a_unbound_objects ===
null && isset($this->unbound_objects)) {
1184 $a_unbound_objects = $this->unbound_objects;
1188 if (!is_array($a_unbound_objects)) {
1189 $this->throwError(INVALID_PARAM, WARNING, DEBUG);
1194 '%s::restoreUnboundObjects(): Started...',
1197 $ilLog->write(
$message, $ilLog->WARNING);
1200 return $this->restoreSubTrees($a_unbound_objects);
1210 array $a_deleted_objects =
null
1212 $ilLog = $this->log;
1215 if ($this->mode[
"restore_trash"] !==
true) {
1219 $this->writeScanLogLine(
"\nrestoreTrash:");
1221 if ($a_deleted_objects ===
null && isset($this->deleted_objects)) {
1222 $a_deleted_objects = $this->deleted_objects;
1226 if (!is_array($a_deleted_objects)) {
1227 $this->throwError(INVALID_PARAM, WARNING, DEBUG);
1232 '%s::restoreTrash(): Started...',
1235 $ilLog->write(
$message, $ilLog->WARNING);
1238 $restored = $this->restoreDeletedObjects($a_deleted_objects);
1241 $q =
"DELETE FROM tree WHERE tree!=1";
1242 $this->db->query($q);
1245 '%s::restoreTrash(): Removed all trees with tree id <> 1',
1248 $ilLog->write(
$message, $ilLog->WARNING);
1250 $this->writeScanLogLine(
"Old tree entries removed");
1267 $tree = $this->tree;
1268 $rbacadmin = $this->rbacadmin;
1271 if (!is_array($a_nodes)) {
1272 $this->throwError(INVALID_PARAM, WARNING, DEBUG);
1277 if (count($a_nodes) === 0) {
1278 $this->writeScanLogLine(
"none");
1283 '%s::restoreDeletedObjects()): Started...',
1286 $ilLog->write(
$message, $ilLog->WARNING);
1291 foreach ($a_nodes as
$key => $node) {
1292 if ($node[
"type"] ===
"rolf") {
1294 $tree->deleteTree($node);
1297 $obj_data->delete();
1298 unset($a_nodes[
$key]);
1303 foreach ($a_nodes as $node) {
1305 $tree->deleteTree($node);
1307 $rbacadmin->revokePermission((
int) $node[
"child"]);
1324 $tree = $this->tree;
1325 $rbacadmin = $this->rbacadmin;
1329 if (!is_array($a_nodes)) {
1330 $this->throwError(INVALID_PARAM, WARNING, DEBUG);
1335 if (count($a_nodes) === 0) {
1336 $this->writeScanLogLine(
"none");
1348 '%s::restoreSubTrees(): Started...',
1351 $ilLog->write(
$message, $ilLog->WARNING);
1354 foreach ($a_nodes as $node) {
1356 $topnode = $tree->getNodeData($node[
"child"], $node[
'tree']);
1360 if ($topnode[
"type"] ===
"rolf") {
1363 unset($top_node, $rolfObj);
1368 $subnodes[$node[
"child"]] = $tree->getSubTree($topnode);
1371 $tree->deleteTree($topnode);
1376 foreach ($subnodes as
$key => $subnode) {
1379 $rbacadmin->revokePermission((
int)
$key);
1384 $this->writeScanLogLine(
"Object '" . $obj_data->getId() .
"' restored.");
1387 array_shift($subnode);
1390 if (count($subnode) > 0) {
1391 foreach ($subnode as $node) {
1392 $rbacadmin->revokePermission((
int) $node[
"child"]);
1394 $obj_data->putInTree($node[
"parent"]);
1395 $obj_data->setPermissions($node[
"parent"]);
1397 $this->writeScanLogLine(
"Object '" . $obj_data->getId() .
"' restored.");
1403 $this->findInvalidChilds();
1404 $this->removeInvalidChilds();
1415 array $a_nodes =
null
1417 $ilLog = $this->log;
1420 if ($this->mode[
"purge_trash"] !==
true) {
1424 $this->writeScanLogLine(
"\npurgeTrash:");
1426 if ($a_nodes ===
null && isset($this->deleted_objects)) {
1427 $a_nodes = $this->deleted_objects;
1430 '%s::purgeTrash(): Started...',
1433 $ilLog->write(
$message, $ilLog->WARNING);
1436 return $this->purgeObjects($a_nodes);
1445 array $a_nodes =
null
1447 $ilLog = $this->log;
1450 if ($this->mode[
"purge"] !==
true) {
1454 $this->writeScanLogLine(
"\npurgeUnboundObjects:");
1456 if ($a_nodes ===
null && isset($this->unbound_objects)) {
1457 $a_nodes = $this->unbound_objects;
1461 '%s::purgeUnboundObjects(): Started...',
1464 $ilLog->write(
$message, $ilLog->WARNING);
1467 return $this->purgeObjects($a_nodes);
1476 array $a_nodes =
null
1478 $ilLog = $this->log;
1481 if ($this->mode[
"purge"] !==
true) {
1485 $this->writeScanLogLine(
"\npurgeMissingObjects:");
1487 if ($a_nodes ===
null && isset($this->missing_objects)) {
1488 $a_nodes = $this->missing_objects;
1492 '%s::purgeMissingObjects(): Started...',
1495 $ilLog->write(
$message, $ilLog->WARNING);
1498 return $this->purgeObjects($a_nodes);
1509 $ilLog = $this->log;
1513 $count_limit =
$ilUser->getPref(
"systemcheck_count_limit");
1514 if (!is_numeric($count_limit) || $count_limit < 0) {
1515 $count_limit = count($a_nodes);
1517 $timestamp_limit = time();
1518 $age_limit =
$ilUser->getPref(
"systemcheck_age_limit");
1519 if (is_numeric($age_limit) && $age_limit > 0) {
1520 $timestamp_limit -= $age_limit * 60 * 60 * 24;
1522 $type_limit =
$ilUser->getPref(
"systemcheck_type_limit");
1524 $type_limit = trim($type_limit);
1525 if ($type_limit ===
'') {
1531 if (!is_array($a_nodes)) {
1532 $this->throwError(INVALID_PARAM, WARNING, DEBUG);
1537 $this->writeScanLogLine(
"action\tref_id\tobj_id\ttype\telapsed\ttitle");
1539 foreach ($a_nodes as $node) {
1540 if ($type_limit && $node[
'type'] != $type_limit) {
1541 $this->writeScanLogLine(
1543 $node[
'child'] .
"\t\t" . $node[
'type'] .
"\t\t" . $node[
'title']
1550 if ($count > $count_limit) {
1551 $this->writeScanLogLine(
"Stopped purging after " . ($count - 1) .
" objects, because count limit was reached: " . $count_limit);
1554 if ($node[
"deleted_timestamp"] > $timestamp_limit) {
1555 $this->writeScanLogLine(
"Stopped purging after " . ($count - 1) .
" objects, because timestamp limit was reached: " . date(
"c", $timestamp_limit));
1559 $ref_id = ($node[
"child"]) ?: $node[
"ref_id"];
1562 if ($node_obj ===
false) {
1563 $this->invalid_objects[] = $node;
1568 '%s::purgeObjects(): Removing object (id:%s ref:%s)',
1573 $ilLog->write(
$message, $ilLog->WARNING);
1575 $startTime = microtime(
true);
1576 $node_obj->delete();
1578 $endTime = microtime(
true);
1580 $this->writeScanLogLine(
"purged\t" .
$ref_id .
"\t" . $node_obj->getId() .
1581 "\t" . $node[
'type'] .
"\t" . round($endTime - $startTime, 1) .
"\t" . $node[
'title']);
1584 $this->findInvalidChilds();
1585 $this->removeInvalidChilds();
1602 $tree = $this->tree;
1606 '%s::initGapsInTree(): Started...',
1609 $ilLog->write(
$message, $ilLog->WARNING);
1612 if ($this->mode[
"clean"] !==
true) {
1615 $this->writeScanLogLine(
"\nrenumberTree:");
1619 $this->writeScanLogLine(
"done");
1633 $call_loc = $error->backtrace[count($error->backtrace) - 1];
1634 $num_args = count($call_loc[
"args"]);
1637 if ($num_args > 0) {
1638 foreach ($call_loc[
"args"] as $arg) {
1639 $type = gettype($arg);
1643 $value = strlen($arg);
1647 $value = count($arg);
1651 $value = get_class($arg);
1655 $value = ($arg) ?
"true" :
"false";
1665 "value" =>
"(" . $value .
")"
1669 foreach ($arg_list as $arg) {
1670 $arg_str .= implode(
"", $arg) .
" ";
1674 $err_msg =
"<br/><b>" . $error->getCode() .
":</b> " . $error->getMessage() .
" in " . $call_loc[
"class"] . $call_loc[
"type"] . $call_loc[
"function"] .
"()" .
1675 "<br/>Called from: " . basename($call_loc[
"file"]) .
" , line " . $call_loc[
"line"] .
1676 "<br/>Passed parameters: [" . $num_args .
"] " . $arg_str .
"<br/>";
1687 if ($error->getCode() == FATAL) {
1694 if (!$this->isLogEnabled()) {
1698 foreach ($a_arr as $entry) {
1699 $this->scan_log->write(implode(
"\t", $entry));
1705 if (!$this->isLogEnabled()) {
1709 $this->scan_log->write($a_msg);
1732 if (!$this->hasScanLog()) {
1737 if (!$scan_log = $this->get_last_scan($scanfile)) {
1748 $logs = array_keys($a_scan_log, $this->scan_log_separator .
"\n");
1750 if (count($logs) > 0) {
1751 return array_slice($a_scan_log, array_pop($logs) + 2);
1759 $this->writeScanLogLine(
"\nchecking tree structure is disabled");
1770 $this->writeScanLogLine(
"BEGIN dumpTree:");
1775 $q =
'SELECT child FROM tree GROUP BY child HAVING COUNT(*) > 1';
1776 $r = $this->db->query($q);
1777 $duplicateNodes = [];
1779 $duplicateNodes[] = $row->child;
1783 $q =
"SELECT tree.*,ref.ref_id,dat.obj_id objobj_id,ref.obj_id refobj_id,ref.deleted,dat.* "
1785 .
"RIGHT JOIN object_reference ref ON tree.child = ref.ref_id "
1786 .
"RIGHT JOIN object_data dat ON ref.obj_id = dat.obj_id "
1788 .
"ORDER BY tree, lft, type, dat.title";
1789 $r = $this->db->query($q);
1791 $this->writeScanLogLine(
1793 .
'<td>tree, child, parent, lft, rgt, depth</td>'
1794 .
'<td>ref_id, ref.obj_id, deleted</td>'
1795 .
'<td>obj_id, type, owner, title</td>'
1804 $repository_tree_count = 0;
1805 $trash_trees_count = 0;
1806 $other_trees_count = 0;
1807 $not_in_tree_count = 0;
1810 $previousNumber = 0;
1812 $this->initWorkspaceObjects();
1816 if ($this->workspace_object_ids &&
1817 in_array($row->objobj_id, $this->workspace_object_ids)) {
1822 if (is_null($row->child)) {
1823 switch ($row->type) {
1837 if (is_null($row->ref_id)) {
1848 $isParentOkay =
false;
1851 $isDepthOkay =
false;
1856 if ($row->type ===
"fold" && $this->isMediaFolder($row->obj_id)) {
1861 $isParentOkay =
false;
1864 $isDepthOkay =
false;
1869 $not_in_tree_count++;
1871 $this->writeScanLogLine(
1874 . (($isRowOkay) ?
'' :
'<font color=#ff0000>')
1876 . $row->child .
', '
1877 . (($isParentOkay) ?
'' :
'parent:<b>')
1879 . (($isParentOkay) ?
'' :
'</b>')
1881 . (($isLftOkay) ?
'' :
'lft:<b>')
1883 . (($isLftOkay) ?
'' :
'</b>')
1885 . (($isRgtOkay) ?
'' :
'rgt:<b>')
1887 . (($isRgtOkay) ?
'' :
'</b>')
1889 . (($isDepthOkay) ?
'' :
'depth:<b>')
1891 . (($isDepthOkay) ?
'' :
'</b>')
1892 . (($isRowOkay) ?
'' :
'</font>')
1894 . (($isRowOkay) ?
'' :
'<font color=#ff0000>')
1895 . (($isRefRefOkay && $isChildOkay) ?
'' :
'ref.ref_id:<b>')
1897 . (($isRefRefOkay && $isChildOkay) ?
'' :
'</b>')
1899 . (($isRefObjOkay) ?
'' :
'ref.obj_id:<b>')
1901 . (($isRefObjOkay) ?
'' :
'</b>')
1902 . (($isRowOkay) ?
'' :
'<font color=#ff0000>')
1903 . (($row->deleted !=
null) ?
', ' . $row->deleted :
'')
1905 . (($isRowOkay) ?
'' :
'<font color=#ff0000>')
1907 . $row->obj_id .
', '
1909 . $row->login .
', '
1911 . (($isRowOkay) ?
'' :
' <b>*ERROR*</b><font color=#ff0000>')
1921 for (
$i = 1;
$i < $row->depth;
$i++) {
1926 if (count($stack) === 0 || $stack[0]->tree != $row->tree) {
1928 $previousNumber = $row->lft - 1;
1929 $this->writeScanLogLine(
'<tr><td> </td></tr>');
1934 while (count($stack) > 0 && $stack[count($stack) - 1]->rgt < $row->lft) {
1935 $popped = array_pop($stack);
1938 $gap = $popped->rgt - $previousNumber - 1;
1941 for (
$i = 1;
$i < $popped->depth;
$i++) {
1942 $poppedIndent .=
". ";
1944 $this->writeScanLogLine(
1946 .
'<td colspan=2><div align="right">'
1947 .
'<font color=#00cc00>*gap* for ' . ($gap / 2) .
' nodes at end of </font>'
1950 .
'<font color=#00cc00>'
1952 . $popped->obj_id .
', '
1953 . $popped->type .
', '
1954 . $popped->login .
', '
1961 $previousNumber = $popped->rgt;
1970 $isChildOkay =
true;
1971 $isParentOkay =
true;
1974 $isDepthOkay =
true;
1977 if (count($stack) > 0) {
1978 $parent = $stack[count($stack) - 1];
1979 if ($parent->depth + 1 != $row->depth) {
1980 $isDepthOkay =
false;
1983 if ($parent->child != $row->parent) {
1984 $isParentOkay =
false;
1987 if (
$GLOBALS[
'ilSetting']->
get(
'main_tree_impl',
'ns') ===
'ns') {
1988 if ($parent->lft >= $row->lft) {
1992 if ($parent->rgt <= $row->rgt) {
2000 if ($row->lft >= $row->rgt &&
$GLOBALS[
'ilSetting']->get(
'main_tree_impl',
'ns') ===
'ns') {
2005 if (in_array($row->child, $duplicateNodes)) {
2006 $isChildOkay =
false;
2011 $isRefRefOkay =
true;
2012 $isRefObjOkay =
true;
2013 if ($row->ref_id ==
null) {
2014 $isRefRefOkay =
false;
2017 if ($row->obj_id ==
null) {
2018 $isRefObjOkay =
false;
2028 if (
$GLOBALS[
'ilSetting']->
get(
'main_tree_impl',
'ns') ===
'ns') {
2029 $gap = $row->lft - $previousNumber - 1;
2030 $previousNumber = $row->lft;
2032 $this->writeScanLogLine(
2034 .
'<td colspan=2><div align="right">'
2035 .
'<font color=#00cc00>*gap* for ' . ($gap / 2) .
' nodes between </font>'
2038 .
'<font color=#00cc00>siblings</font>'
2047 $this->writeScanLogLine(
2050 . (($isRowOkay) ?
'' :
'<font color=#ff0000>')
2052 . $row->child .
', '
2053 . (($isParentOkay) ?
'' :
'parent:<b>')
2055 . (($isParentOkay) ?
'' :
'</b>')
2057 . (($isLftOkay) ?
'' :
'lft:<b>')
2059 . (($isLftOkay) ?
'' :
'</b>')
2061 . (($isRgtOkay) ?
'' :
'rgt:<b>')
2063 . (($isRgtOkay) ?
'' :
'</b>')
2065 . (($isDepthOkay) ?
'' :
'depth:<b>')
2067 . (($isDepthOkay) ?
'' :
'</b>')
2068 . (($isRowOkay) ?
'' :
'</font>')
2070 . (($isRowOkay) ?
'' :
'<font color=#ff0000>')
2071 . (($isRefRefOkay && $isChildOkay) ?
'' :
'ref.ref_id:<b>')
2073 . (($isRefRefOkay && $isChildOkay) ?
'' :
'</b>')
2075 . (($isRefObjOkay) ?
'' :
'ref.obj_id:<b>')
2077 . (($isRefObjOkay) ?
'' :
'</b>')
2078 . (($isRowOkay) ?
'' :
'<font color=#ff0000>')
2079 . (($row->tree < 0) ?
', ' . $row->deleted :
'')
2081 . (($isRowOkay) ?
'' :
'<font color=#ff0000>')
2083 . $row->obj_id .
', '
2085 . $row->login .
', '
2087 . (($isRowOkay) ?
'' :
' <b>*ERROR*</b><font color=#ff0000>')
2099 if ($row->tree == 1) {
2100 $repository_tree_count++;
2101 } elseif ($row->tree < 0) {
2102 $trash_trees_count++;
2104 $other_trees_count++;
2110 while (count($stack) > 0) {
2111 $popped = array_pop($stack);
2114 $gap = $popped->rgt - $previousNumber - 1;
2117 for (
$i = 1;
$i < $popped->depth;
$i++) {
2118 $poppedIndent .=
". ";
2120 $this->writeScanLogLine(
2122 .
'<td colspan=2><div align="right">'
2123 .
'<font color=#00cc00>*gap* for ' . ($gap / 2) .
' nodes at end of </font>'
2126 .
'<font color=#00cc00>'
2128 . $popped->obj_id .
', '
2129 . $popped->type .
', '
2130 . $popped->login .
', '
2137 $previousNumber = $popped->rgt;
2142 $this->writeScanLogLine(
"</table>");
2144 if ($error_count > 0) {
2145 $this->writeScanLogLine(
'<font color=#ff0000>' . $error_count .
' errors found while dumping tree.</font>');
2147 $this->writeScanLogLine(
'No errors found while dumping tree.');
2149 $this->writeScanLogLine(
"$repository_tree_count nodes in repository tree");
2150 $this->writeScanLogLine(
"$trash_trees_count nodes in trash trees");
2151 $this->writeScanLogLine(
"$other_trees_count nodes in other trees");
2152 $this->writeScanLogLine(
"$not_in_tree_count nodes are not in a tree");
2153 $this->writeScanLogLine(
"END dumpTree");
2155 return $error_count;
2162 if (!is_array($this->media_pool_ids)) {
2163 $this->media_pool_ids = [];
2164 $query =
"SELECT child FROM mep_tree ";
2166 while ($row =
$ilDB->fetchObject(
$res)) {
2167 $this->media_pool_ids[] = $row->child;
2171 return in_array($a_obj_id, $this->media_pool_ids);
2181 if (!$this->isMediaFolder($a_obj_id)) {
2185 return in_array($a_type, $this->object_types_exclude);
2192 if ($this->workspace_object_ids ===
null) {
2193 $this->workspace_object_ids = [];
2196 $set =
$ilDB->query(
"SELECT DISTINCT(obj_id) FROM object_reference_ws");
2197 while ($row =
$ilDB->fetchAssoc($set)) {
2198 $this->workspace_object_ids[] = $row[
"obj_id"];
2202 $set =
$ilDB->query(
"SELECT id FROM usr_portfolio");
2203 while ($row =
$ilDB->fetchAssoc($set)) {
2204 $this->workspace_object_ids[] = $row[
"id"];
2211 string $a_index =
"obj_id"
2213 if (count($a_data)) {
2214 $this->initWorkspaceObjects();
2217 if (is_array($this->workspace_object_ids)) {
2218 foreach ($a_data as $idx => $item) {
2219 if (in_array($item[$a_index], $this->workspace_object_ids)) {
2220 unset($a_data[$idx]);
if(!defined('PATH_SEPARATOR')) $GLOBALS['_PEAR_default_error_mode']
@classDescription Date and time handling
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Component logger with individual log levels by component id.
parses the objects.xml it handles the xml-description of all ilias objects
static getInstanceByRefId(int $ref_id, bool $stop_on_error=true)
get an instance of an Ilias object by reference id
Class ilRbacAdmin Core functions for role based access control.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _removeEntry(int $a_tree, int $a_child, string $a_db_table="tree")
STATIC METHOD Removes a single entry from a tree.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
writeScanLogArray(array $a_arr)
deleteScanLog()
Delete scan log.
findUnboundObjects()
Search database for all tree entries having no valid parent (=> no valid path to root node) and store...
purgeTrash(array $a_nodes=null)
Removes all objects in trash from system.
string $scan_log_separator
purgeUnboundObjects(array $a_nodes=null)
Removes all invalid objects from system.
getInvalidChilds()
Gets all tree entries without any link to a valid object.
getDeletedObjects()
Gets all object in trash.
getInvalidReferences()
Gets all reference entries that are not linked with a valid object id.
get_last_scan(array $a_scan_log)
setMode(string $a_mode, bool $a_value)
set mode of ilValidator Usage: setMode("restore",true) => enable object restorey setMode("all",...
restoreSubTrees(array $a_nodes)
Restore objects (and their subobjects) to RecoveryFolder.
restoreDeletedObjects(array $a_nodes)
Restore deleted objects (and their subobjects) to RecoveryFolder.
restoreMissingObjects(array $a_missing_objects=null)
Restores missing reference and/or tree entry for all objects found by this::getMissingObjects() Resto...
array $invalid_rolefolders
contains missing objects that are rolefolders.
array $invalid_rbac_entries
isMediaFolder(int $a_obj_id)
array $invalid_objects
contains correct registrated objects but data are corrupted (experimental)
array $object_types_exclude
list of object types to exclude from recovering
array $invalid_references
getInvalidRolefolders()
Gets invalid rolefolders (same as missing objects)
filterWorkspaceObjects(array &$a_data, string $a_index="obj_id")
ilObjectDefinition $obj_definition
handleErr(object $error)
Callback function handles PEAR_error and outputs detailed infos about error TODO: implement that in g...
initGapsInTree()
Initializes gaps in lft/rgt values of a tree.
setModeDependencies()
Sets modes by considering mode dependencies; some modes require other modes to be activated.
getUnboundObjects()
Gets all tree entries having no valid parent (=> no valid path to root node) Returns an array with ch...
purgeMissingObjects(array $a_nodes=null)
Removes all missing objects from system.
removeInvalidRolefolders(array $a_invalid_rolefolders=null)
Removes invalid rolefolders.
restoreReference(int $a_obj_id)
restore a reference for an object Creates a new reference entry in DB table object_reference for $a_o...
writeScanLogLine(string $a_msg)
getPossibleModes()
get possible ilValidator modes
findDeletedObjects()
Search database for all tree entries having no valid parent (=> no valid path to root node) and store...
isModeEnabled(string $a_mode)
hasScanLog()
Quickly determine if there is a scan log.
dumpTree()
Dumps the Tree structure into the scan log.
restoreUnboundObjects(array $a_unbound_objects=null)
Restore objects (and their subobjects) to RecoveryFolder that are valid but not linked correctly in t...
removeInvalidReferences(array $a_invalid_refs=null)
Removes all reference entries that are linked with invalid object IDs.
restoreTrash(array $a_deleted_objects=null)
Restore all objects in trash to RecoveryFolder NOTE: All objects will be restored to top of RecoveryF...
findInvalidChilds()
Search database for all tree entries without any link to a valid object and stores result in $this->i...
findMissingObjects()
Search database for all object entries with missing reference and/or tree entry and stores result in ...
findInvalidRBACEntries()
Search database for all role entries that are linked to invalid ref_ids, stores results in $this->inv...
isExcludedFromRecovery(string $a_type, int $a_obj_id)
__construct(bool $a_log=false)
findInvalidReferences()
Search database for all reference entries that are not linked with a valid object id and stores resul...
findInvalidRolefolders()
Search database for all rolefolder object entries with missing reference entry.
validate()
Performs the validation for each enabled mode.
purgeObjects(array $a_nodes)
removes objects from system
array $workspace_object_ids
getMissingObjects()
Gets all object entries with missing reference and/or tree entry.
removeInvalidChilds(array $a_invalid_childs=null)
Removes all tree entries without any link to a valid object.
if(!file_exists(getcwd() . '/ilias.ini.php'))
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...