1<?php
2
3/* Copyright (c) 1998-2009 ILIAS open source, Extended GPL, see docs/LICENSE */
4
5include_once "Services/Tracking/classes/class.ilLPObjSettings.php";
6
7/**
8 * Base class for object lp connectors
9 *
10 * @author Jörg Lützenkirchen <luetzenkirchen@leifos.com>
11 * @version $Id: class.ilLPStatusPlugin.php 43734 2013-07-29 15:27:58Z jluetzen $
12 * @package ServicesTracking
13 */
14class ilObjectLP
15{
16    /**
17     * @var ilTree
18     */
19    protected $tree;
20
21    /**
22     * @var ilDB
23     */
24    protected $db;
25
26    protected $obj_id; // [int]
27    protected $collection_instance; // [ilLPCollection]
28    protected $mode; // [int]
29
30    protected static $type_defaults; // [array]
31
32    protected function __construct($a_obj_id)
33    {
34        global $DIC;
35
36        $this->tree = $DIC->repositoryTree();
37        $this->db = $DIC->database();
38        $this->obj_id = (int) $a_obj_id;
39    }
40
41    /**
42     * @param int $a_obj_id
43     * @return ilObjectLP
44     */
45    public static function getInstance($a_obj_id)
46    {
47        static $instances = array();
48
49        if (!isset($instances[$a_obj_id])) {
50            $type = ilObject::_lookupType($a_obj_id);
51            $class = self::getTypeClass($type);
52            if ($class) {
53                $instance = new $class($a_obj_id);
54            } else {
55                // :TODO: should we return anything?
56                $instance = new self($a_obj_id);
57            }
58            $instances[$a_obj_id] = $instance;
59        }
60
61        return $instances[$a_obj_id];
62    }
63
64    public static function getTypeClass($a_type)
65    {
66        global $DIC;
67
68        $objDefinition = $DIC["objDefinition"];
69
70        if (self::isSupportedObjectType($a_type)) {
71            switch ($a_type) {
72                // container
73
74                case "crs":
75                    include_once "Modules/Course/classes/class.ilCourseLP.php";
76                    return "ilCourseLP";
77
78                case 'crsr':
79                    return 'ilCourseReferenceLP';
80
81                case "grp":
82                    include_once "Modules/Group/classes/class.ilGroupLP.php";
83                    return "ilGroupLP";
84
85                case "fold":
86                    include_once "Modules/Folder/classes/class.ilFolderLP.php";
87                    return "ilFolderLP";
88
89                case "lso":
90                    include_once "Modules/LearningSequence/classes/LearnerProgress/class.ilLSLP.php";
91                    return "ilLSLP";
92
93
94                // learning resources
95
96                case "lm":
97                    include_once "Modules/LearningModule/classes/class.ilLearningModuleLP.php";
98                    return "ilLearningModuleLP";
99
100                case "htlm":
101                    include_once "Modules/HTMLLearningModule/classes/class.ilHTMLLearningModuleLP.php";
102                    return "ilHTMLLearningModuleLP";
103
104                case "sahs":
105                    include_once "Modules/ScormAicc/classes/class.ilScormLP.php";
106                    return "ilScormLP";
107
108
109                // misc
110
111                case "tst":
112                    include_once "Modules/Test/classes/class.ilTestLP.php";
113                    return "ilTestLP";
114
115                case "exc":
116                    include_once "Modules/Exercise/classes/class.ilExerciseLP.php";
117                    return "ilExerciseLP";
118
119                case 'file':
120                    require_once 'Modules/File/classes/class.ilFileLP.php';
121                    return 'ilFileLP';
122
123                case "mcst":
124                    require_once "Modules/MediaCast/classes/class.ilMediaCastLP.php";
125                    return "ilMediaCastLP";
126
127                case "sess":
128                    include_once "Modules/Session/classes/class.ilSessionLP.php";
129                    return  "ilSessionLP";
130
131                case "svy":
132                    return  "ilSurveyLP";
133
134                case "prg":
135                    include_once "Modules/StudyProgramme/classes/class.ilStudyProgrammeLP.php";
136                    return "ilStudyProgrammeLP";
137
138                case "iass":
139                    include_once "Modules/IndividualAssessment/classes/class.ilIndividualAssessmentLP.php";
140                    return "ilIndividualAssessmentLP";
141
142                case "copa":
143                    return "ilContentPageLP";
144
145                case 'cmix':
146                    return ilCmiXapiLP::class;
147
148                case 'lti':
149                    return ilLTIConsumerLP::class;
150
151                // plugin
152                case $objDefinition->isPluginTypeName($a_type):
153                    include_once "Services/Component/classes/class.ilPluginLP.php";
154                    return "ilPluginLP";
155            }
156        }
157    }
158    public static function isSupportedObjectType($a_type)
159    {
160        global $DIC;
161
162        $objDefinition = $DIC["objDefinition"];
163
164        $valid = array("crs", "grp", "fold", "lm", "htlm", "sahs", "tst", "exc",
165            "sess", "svy", "file", "mcst", "prg", "iass", "copa", "lso", 'cmix', 'lti', 'crsr');
166
167        if (in_array($a_type, $valid)) {
168            return true;
169        }
170
171        if ($objDefinition->isPluginTypeName($a_type)) {
172            include_once 'Services/Repository/classes/class.ilRepositoryObjectPluginSlot.php';
173            return ilRepositoryObjectPluginSlot::isTypePluginWithLP($a_type);
174        }
175
176        return false;
177    }
178
179    public function resetCaches()
180    {
181        $this->mode = null;
182        $this->collection_instance = null;
183    }
184
185    public function isAnonymized()
186    {
187        // see ilLPCollectionOfRepositoryObjects::validateEntry()
188        return false;
189    }
190
191
192    //
193    // MODE
194    //
195
196    public function getDefaultMode()
197    {
198        return ilLPObjSettings::LP_MODE_UNDEFINED;
199    }
200
201    public function getValidModes()
202    {
203        return array();
204    }
205
206    public function getCurrentMode()
207    {
208        if ($this->mode === null) {
209            // using global type default if LP is inactive
210            include_once "Services/Tracking/classes/class.ilObjUserTracking.php";
211            if (!ilObjUserTracking::_enabledLearningProgress()) {
212                $mode = self::getTypeDefaultFromDB(ilObject::_lookupType($this->obj_id));
213                if ($mode === null) {
214                    // fallback: inactive as type default may not be suitable
215                    $mode = ilLPObjSettings::LP_MODE_DEACTIVATED;
216                }
217            }
218            // use object LP setting
219            else {
220                $mode = ilLPObjSettings::_lookupDBMode($this->obj_id);
221                if ($mode === null) {
222                    // fallback: object type default
223                    $mode = $this->getDefaultMode();
224                }
225            }
226            $this->mode = (int) $mode;
227        }
228
229        return $this->mode;
230    }
231
232    public function isActive()
233    {
234        // :TODO: check LP activation?
235
236        $mode = $this->getCurrentMode();
237        if ($mode == ilLPObjSettings::LP_MODE_DEACTIVATED ||
238            $mode == ilLPObjSettings::LP_MODE_UNDEFINED) {
239            return false;
240        }
241        return true;
242    }
243
244    public function getModeText($a_mode)
245    {
246        return ilLPObjSettings::_mode2Text($a_mode);
247    }
248
249    public function getModeInfoText($a_mode)
250    {
251        return ilLPObjSettings::_mode2InfoText($a_mode);
252    }
253
254    public function getSettingsInfo()
255    {
256        // type-specific
257    }
258
259
260    //
261    // COLLECTION
262    //
263
264    public function getCollectionInstance()
265    {
266        if ($this->collection_instance === null) {
267            include_once "Services/Tracking/classes/collection/class.ilLPCollection.php";
268            $this->collection_instance = ilLPCollection::getInstanceByMode($this->obj_id, $this->getCurrentMode());
269        }
270
271        return $this->collection_instance;
272    }
273
274
275    //
276    // MEMBERS
277    //
278
279    public function getMembers($a_search = true)
280    {
281        $tree = $this->tree;
282
283        if (!$a_search) {
284            return;
285        }
286
287        $ref_ids = ilObject::_getAllReferences($this->obj_id);
288        $ref_id = current($ref_ids);
289
290        // walk path to find parent with specific members
291        $path = $tree->getPathId($ref_id);
292        array_pop($path);
293        foreach (array_reverse($path) as $path_ref_id) {
294            $olp = self::getInstance(ilObject::_lookupObjId($path_ref_id));
295            $all = $olp->getMembers(false);
296            if (is_array($all)) {
297                return $all;
298            }
299        }
300    }
301
302
303    //
304    // RESET
305    //
306
307    final public function resetLPDataForCompleteObject($a_recursive = true)
308    {
309        $user_ids = $this->gatherLPUsers();
310        if (sizeof($user_ids)) {
311            $this->resetLPDataForUserIds(array_unique($user_ids), $a_recursive);
312        }
313    }
314
315    final public function resetLPDataForUserIds(array $a_user_ids, $a_recursive = true)
316    {
317        if ((bool) $a_recursive &&
318            method_exists($this, "getPossibleCollectionItems")) { // #15203
319            $subitems = $this->getPossibleCollectionItems();
320            if (is_array($subitems)) {
321                foreach ($subitems as $sub_ref_id) {
322                    $olp = self::getInstance(ilObject::_lookupObjId($sub_ref_id));
323                    $olp->resetLPDataForUserIds($a_user_ids, false);
324                }
325            }
326        }
327
328        $this->resetCustomLPDataForUserIds($a_user_ids, (bool) $a_recursive);
329
330        include_once "Services/Tracking/classes/class.ilLPMarks.php";
331        ilLPMarks::_deleteForUsers($this->obj_id, $a_user_ids);
332
333        include_once "Services/Tracking/classes/class.ilChangeEvent.php";
334        ilChangeEvent::_deleteReadEventsForUsers($this->obj_id, $a_user_ids);
335
336        // update LP status to get collections up-to-date
337        include_once "Services/Tracking/classes/class.ilLPStatusWrapper.php";
338        foreach ($a_user_ids as $user_id) {
339            ilLPStatusWrapper::_updateStatus($this->obj_id, $user_id);
340        }
341    }
342
343    protected function resetCustomLPDataForUserIds(array $a_user_ids, $a_recursive = true)
344    {
345        // this should delete all data that is relevant for the supported LP modes
346    }
347
348    protected function gatherLPUsers()
349    {
350        include_once "Services/Tracking/classes/class.ilLPMarks.php";
351        $user_ids = ilLPMarks::_getAllUserIds($this->obj_id);
352
353        include_once "Services/Tracking/classes/class.ilChangeEvent.php";
354        $user_ids = array_merge($user_ids, ilChangeEvent::_getAllUserIds($this->obj_id));
355
356        return $user_ids;
357    }
358
359
360    //
361    // EVENTS
362    //
363
364    final public static function handleMove($a_source_ref_id)
365    {
366        global $DIC;
367
368        $tree = $DIC->repositoryTree();
369        $ilDB = $DIC->database();
370
371        $ref_ids = $tree->getSubTreeIds($a_source_ref_id);
372        $ref_ids[] = $a_source_ref_id;
373
374        // get "parent" path to source node (not including source node)
375        $new_path = $tree->getPathId($a_source_ref_id);
376        array_pop($new_path);
377        $new_path = implode("/", $new_path);
378
379        include_once("./Services/Tracking/classes/class.ilLPStatusWrapper.php");
380
381        // find collections with ref_ids
382        $set = $ilDB->query("SELECT DISTINCT(ut_lp_collections.obj_id) obj_id" .
383            " FROM object_reference" .
384            " JOIN ut_lp_collections ON" .
385            " (" . $ilDB->in("object_reference.ref_id", $ref_ids, "", "integer") .
386            " AND object_reference.ref_id = ut_lp_collections.item_id)");
387        while ($rec = $ilDB->fetchAssoc($set)) {
388            if (in_array(ilObject::_lookupType($rec["obj_id"]), array("crs", "grp", "fold"))) {
389                $coll_ref_id = ilObject::_getAllReferences($rec["obj_id"]);
390                $coll_ref_id = array_pop($coll_ref_id);
391
392                // #13402
393                if ($coll_ref_id == $a_source_ref_id) {
394                    continue;
395                }
396
397                // #17703 - collection has also been moved - nothing todo
398                if ($tree->isGrandChild($a_source_ref_id, $coll_ref_id)) {
399                    continue;
400                }
401
402                // get path to collection (including collection "parent")
403                $coll_path = $tree->getPathId($coll_ref_id);
404                $coll_path = implode("/", $coll_path);
405
406                // collection path is not inside new path
407                if (!stristr($new_path, $coll_path)) {
408                    // delete all items of moved (sub-)tree
409                    $query = "DELETE FROM ut_lp_collections" .
410                        " WHERE obj_id = " . $ilDB->quote($rec["obj_id"], "integer") .
411                        " AND " . $ilDB->in("item_id", $ref_ids, "", "integer");
412                    $ilDB->manipulate($query);
413
414                    ilLPStatusWrapper::_refreshStatus($rec["obj_id"]);
415                }
416            }
417        }
418    }
419
420    final public function handleToTrash()
421    {
422        $this->updateParentCollections();
423    }
424
425    final public function handleDelete()
426    {
427        include_once "Services/Tracking/classes/class.ilLPMarks.php";
428        ilLPMarks::deleteObject($this->obj_id);
429
430        include_once "Services/Tracking/classes/class.ilChangeEvent.php";
431        ilChangeEvent::_delete($this->obj_id);
432
433        $collection = $this->getCollectionInstance();
434        if ($collection) {
435            $collection->delete();
436        }
437
438        $this->updateParentCollections();
439    }
440
441    final protected function updateParentCollections()
442    {
443        $ilDB = $this->db;
444
445        include_once("./Services/Tracking/classes/class.ilLPStatusWrapper.php");
446
447        // update parent collections?
448        $set = $ilDB->query("SELECT ut_lp_collections.obj_id obj_id FROM " .
449                "object_reference JOIN ut_lp_collections ON " .
450                "(object_reference.obj_id = " . $ilDB->quote($this->obj_id, "integer") .
451                " AND object_reference.ref_id = ut_lp_collections.item_id)");
452        while ($rec = $ilDB->fetchAssoc($set)) {
453            if (in_array(ilObject::_lookupType($rec["obj_id"]), array("crs", "grp", "fold"))) {
454                // remove from parent collection
455                $query = "DELETE FROM ut_lp_collections" .
456                    " WHERE obj_id = " . $ilDB->quote($rec["obj_id"], "integer") .
457                    " AND item_id = " . $ilDB->quote($this->obj_id, "integer");
458                $ilDB->manipulate($query);
459
460                ilLPStatusWrapper::_refreshStatus($rec["obj_id"]);
461            }
462        }
463    }
464
465
466    //
467    // LP-relevant memberships
468    //
469
470    /**
471     * Find (lp-relevant) members for given object ids
472     *
473     * @param array $a_res
474     * @param int $a_usr_id
475     * @param array $a_obj_ids
476     */
477    protected static function isLPMember(array &$a_res, $a_usr_id, $a_obj_ids)
478    {
479        // should be overwritten by object-type-specific class
480        return false;
481    }
482
483    /**
484     * Find (lp-relevant) memberships by path
485     *
486     * @param array $a_res
487     * @param int $a_usr_id
488     * @param int $a_parent_ref_id
489     * @param array $a_obj_ids
490     * @param bool $a_mapped_ref_ids
491     * @return array
492     */
493    protected static function findMembershipsByPath(array &$a_res, $a_usr_id, $a_parent_ref_id, array $a_obj_ids, $a_mapped_ref_ids = false)
494    {
495        global $DIC;
496
497        $tree = $DIC->repositoryTree();
498
499        $found = array();
500
501        // walk path to find course or group object and check members of that object
502        $path = $tree->getPathId($a_parent_ref_id);
503        foreach (array_reverse($path) as $path_ref_id) {
504            $type = ilObject::_lookupType($path_ref_id, true);
505            if ($type == "crs" ||
506                $type == "grp") {
507                $class = self::getTypeClass($type);
508                $path_ob_id = ilObject::_lookupObjId($path_ref_id);
509                $chk = array();
510                $class::isLPMember($chk, $a_usr_id, array($path_ob_id));
511                if (!$a_mapped_ref_ids) {
512                    // we found a grp/crs in path of (single) parent - mark all objects
513                    foreach ($a_obj_ids as $obj_id) {
514                        $found[] = $obj_id;
515                        if ($chk[$path_ob_id]) {
516                            $a_res[$obj_id] = true;
517                        }
518                    }
519                } else {
520                    // all children from current node are "lp-valid"
521                    foreach ($a_obj_ids as $obj_id => $ref_ids) {
522                        foreach ($ref_ids as $ref_id) {
523                            if ($tree->isGrandChild($path_ref_id, $ref_id)) {
524                                $found[$obj_id][] = $ref_id;
525                                if ($chk[$path_ob_id]) {
526                                    $a_res[$obj_id] = true;
527                                }
528                                break;
529                            }
530                        }
531                    }
532                }
533                break;
534            }
535        }
536
537        return $found;
538    }
539
540    /**
541     * Get all objects where given user is member (from LP POV)
542     *
543     * @param int $a_usr_id
544     * @param array $a_obj_ids
545     * @param int $a_parent_ref_id
546     * @param bool $a_mapped_ref_ids
547     * @return array
548     */
549    public static function getLPMemberships($a_usr_id, array $a_obj_ids, $a_parent_ref_id = null, $a_mapped_ref_ids = false)
550    {
551        global $DIC;
552
553        $ilDB = $DIC->database();
554        $tree = $DIC->repositoryTree();
555
556        // see ilTrQuery::getParticipantsForObject() [single object only]
557        // this is optimized for larger number of objects, e.g. list GUIs
558
559        $ref_map = [];
560        if ((bool) $a_mapped_ref_ids) {
561            $ref_map = $a_obj_ids;
562            $a_obj_ids = array_keys($a_obj_ids);
563        }
564
565        $res = array();
566
567        // get object types
568        $types_map = array();
569        $query = " SELECT obj_id, type" .
570            " FROM object_data" .
571            " WHERE " . $ilDB->in("obj_id", $a_obj_ids, "", "integer");
572        $set = $ilDB->query($query);
573        while ($row = $ilDB->fetchAssoc($set)) {
574            $types_map[$row["type"]][] = $row["obj_id"];
575            $res[$row["obj_id"]] = false;
576        }
577
578        $find_by_parent = array();
579        foreach ($types_map as $type => $type_obj_ids) {
580            $class = self::getTypeClass($type);
581            if ($class) {
582                // lp-supported type?
583                if (!$class::isLPMember($res, $a_usr_id, $type_obj_ids)) {
584                    $find_by_parent = array_merge($find_by_parent, $type_obj_ids);
585                }
586            }
587        }
588
589        if (sizeof($find_by_parent)) {
590            // single parent for all objects (repository/ilObjectListGUI)
591            if ($a_parent_ref_id) {
592                if (self::findMembershipsByPath($res, $a_usr_id, $a_parent_ref_id, $find_by_parent)) {
593                    // we found a crs/grp in path, so no need to check read_events
594                    $find_by_parent = null;
595                }
596            }
597            // different parents (PD > LP)
598            elseif (is_array($ref_map) && count($ref_map) > 0) {
599                foreach ($find_by_parent as $obj_id) {
600                    // maybe already found by path search from other object/reference
601                    if ($res[$obj_id] === false) {
602                        if (isset($ref_map[$obj_id]) && is_array($ref_map[$obj_id])) {
603                            // check all references
604                            foreach ($ref_map[$obj_id] as $ref_id) {
605                                $parent_ref_id = $tree->getParentId($ref_id);
606                                if ($parent_ref_id == ROOT_FOLDER_ID) {
607                                    continue;
608                                }
609
610                                // we are checking the complete ref_map
611                                // to find all relevant objects in subtree of current ref_id
612                                $found = self::findMembershipsByPath($res, $a_usr_id, $parent_ref_id, $ref_map, true);
613                                if (is_array($found) && count($found) > 0) {
614                                    // if any references were found in a crs/grp-subtree
615                                    // remove from "read-event"-last-resort-pool
616                                    foreach ($found as $found_obj_id => $found_ref_ids) {
617                                        $diff = array_diff($ref_map[$found_obj_id], $found_ref_ids);
618                                        if ($diff) {
619                                            // 1-n refs are in another subtree
620                                            // have to be checked separately
621                                            $ref_map[$found_obj_id] = $diff;
622                                        } else {
623                                            // all references found in subtree
624                                            // no need to check again
625                                            unset($ref_map[$found_obj_id]);
626                                        }
627                                    }
628                                    break;
629                                }
630                            }
631                        }
632                    }
633                }
634
635                $find_by_parent = array_keys($ref_map);
636            }
637
638            // last resort: use read_event?
639            if (is_array($find_by_parent) && count($find_by_parent) > 0) {
640                $set = $ilDB->query("SELECT obj_id" .
641                    " FROM read_event" .
642                    " WHERE " . $ilDB->in("obj_id", $find_by_parent, "", "integer") .
643                    " AND usr_id = " . $ilDB->quote($a_usr_id, "integer"));
644                while ($row = $ilDB->fetchAssoc($set)) {
645                    $res[$row["obj_id"]] = true;
646                }
647            }
648        }
649
650        return $res;
651    }
652
653    public function getMailTemplateId()
654    {
655        // type-specific
656    }
657
658
659    //
660    // type-specific support of features (should be enhanced)
661    //
662
663    public static function supportsSpentSeconds($a_obj_type)
664    {
665        return !in_array($a_obj_type, array("exc", "file", "mcst", "mob", "htlm", "copa", 'cmix', 'lti'));
666    }
667
668    public static function supportsMark($a_obj_type)
669    {
670        return !in_array($a_obj_type, array("lm", "dbk"));
671    }
672
673    public static function supportsMatrixView($a_obj_type)
674    {
675        return !in_array($a_obj_type, array('svy', 'tst', 'htlm', 'exc', 'sess', 'file', 'prg', 'copa', 'cmix', 'lti','crsr'));
676    }
677
678
679    // type-wide default
680
681    /**
682     * Get available type-specific default modes (no administration needed)
683     * @param bool $a_lp_active
684     * @return array
685     */
686    public static function getDefaultModes($a_lp_active)
687    {
688        return array(ilLPObjSettings::LP_MODE_UNDEFINED);
689    }
690
691    protected static function getTypeDefaultFromDB($a_type)
692    {
693        global $DIC;
694
695        $ilDB = $DIC->database();
696
697        if (!is_array(self::$type_defaults)) {
698            self::$type_defaults = array();
699            $set = $ilDB->query("SELECT * FROM ut_lp_defaults");
700            while ($row = $ilDB->fetchAssoc($set)) {
701                self::$type_defaults[$row["type_id"]] = $row["lp_mode"];
702            }
703        }
704        return self::$type_defaults[$a_type];
705    }
706
707    public static function saveTypeDefaults(array $a_data)
708    {
709        global $DIC;
710
711        $ilDB = $DIC->database();
712
713        $ilDB->manipulate("DELETE FROM ut_lp_defaults");
714        foreach ($a_data as $type => $mode) {
715            $ilDB->insert("ut_lp_defaults", array(
716                "type_id" => array("text", $type),
717                "lp_mode" => array("integer", $mode)
718            ));
719        }
720    }
721
722    /**
723     * Get current type default
724     *
725     * @param string $a_type
726     * @return int
727     */
728    public static function getTypeDefault($a_type)
729    {
730        $db = self::getTypeDefaultFromDB($a_type);
731        if ($db !== null) {
732            return $db;
733        }
734
735        $class = self::getTypeClass($a_type);
736        $olp = new $class(0);
737        return $olp->getDefaultMode();
738    }
739}
740