1<?php
2
3/* Copyright (c) 1998-2009 ILIAS open source, Extended GPL, see docs/LICENSE */
4
5/**
6 * Helper class to create new object types (object_data, RBAC)
7 *
8 * @author Jörg Lützenkirchen <luetzenkirchen@leifos.com>
9 * $Id: class.ilObjFolderGUI.php 25134 2010-08-13 14:22:11Z smeyer $
10 *
11 * @ingroup ServicesMigration
12 */
13class ilDBUpdateNewObjectType
14{
15    const RBAC_OP_EDIT_PERMISSIONS = 1;
16    const RBAC_OP_VISIBLE = 2;
17    const RBAC_OP_READ = 3;
18    const RBAC_OP_WRITE = 4;
19    const RBAC_OP_DELETE = 6;
20    const RBAC_OP_COPY = 99;
21
22    protected static $initialPermissionDefinition = [
23        'role' => [
24            'User' => [
25                'id' => 4,
26                'ignore_for_authoring_objects' => true,
27                'object' => [
28                    self::RBAC_OP_VISIBLE,
29                    self::RBAC_OP_READ,
30                ]
31            ]
32        ],
33        'rolt' => [
34            'il_crs_admin' => [
35                'object' => [
36                    self::RBAC_OP_VISIBLE,
37                    self::RBAC_OP_READ,
38                    self::RBAC_OP_WRITE,
39                    self::RBAC_OP_DELETE,
40                    self::RBAC_OP_COPY,
41                    self::RBAC_OP_EDIT_PERMISSIONS,
42                ],
43                'lp' => true,
44                'create' => [
45                    'crs',
46                    'grp',
47                    'fold',
48                ]
49            ],
50            'il_crs_tutor' => [
51                'object' => [
52                    self::RBAC_OP_VISIBLE,
53                    self::RBAC_OP_READ,
54                    self::RBAC_OP_WRITE,
55                    self::RBAC_OP_COPY,
56                ],
57                'create' => [
58                    'crs',
59                    'fold',
60                ]
61            ],
62            'il_crs_member' => [
63                'ignore_for_authoring_objects' => true,
64                'object' => [
65                    self::RBAC_OP_VISIBLE,
66                    self::RBAC_OP_READ,
67                ]
68            ],
69            'il_grp_admin' => [
70                'object' => [
71                    self::RBAC_OP_VISIBLE,
72                    self::RBAC_OP_READ,
73                    self::RBAC_OP_WRITE,
74                    self::RBAC_OP_DELETE,
75                    self::RBAC_OP_COPY,
76                    self::RBAC_OP_EDIT_PERMISSIONS,
77                ],
78                'lp' => true,
79                'create' => [
80                    'grp',
81                    'fold',
82                ]
83            ],
84            'il_grp_member' => [
85                'ignore_for_authoring_objects' => true,
86                'object' => [
87                    self::RBAC_OP_VISIBLE,
88                    self::RBAC_OP_READ,
89                ]
90            ],
91            'Author' => [
92                'object' => [
93                    self::RBAC_OP_VISIBLE,
94                    self::RBAC_OP_READ,
95                    self::RBAC_OP_WRITE,
96                    self::RBAC_OP_DELETE,
97                    self::RBAC_OP_COPY,
98                    self::RBAC_OP_EDIT_PERMISSIONS,
99                ],
100                'lp' => true,
101                'create' => [
102                    'cat',
103                    'crs',
104                    'grp',
105                    'fold',
106                ]
107            ],
108            'Local Administrator' => [
109                'object' => [
110                    self::RBAC_OP_VISIBLE,
111                    self::RBAC_OP_DELETE,
112                    self::RBAC_OP_EDIT_PERMISSIONS,
113                ],
114                'create' => [
115                    'cat',
116                ]
117            ],
118        ]
119    ];
120
121    /**
122     * Add new type to object data
123     *
124     * @param string $a_type_id
125     * @param string $a_type_title
126     * @return int insert id
127     */
128    public static function addNewType($a_type_id, $a_type_title)
129    {
130        global $ilDB;
131
132        // check if it already exists
133        $type_id = self::getObjectTypeId($a_type_id);
134        if ($type_id) {
135            return $type_id;
136        }
137
138        $type_id = $ilDB->nextId('object_data');
139
140        $fields = array(
141            'obj_id' => array('integer', $type_id),
142            'type' => array('text', 'typ'),
143            'title' => array('text', $a_type_id),
144            'description' => array('text', $a_type_title),
145            'owner' => array('integer', -1),
146            'create_date' => array('timestamp', ilUtil::now()),
147            'last_update' => array('timestamp', ilUtil::now())
148        );
149        $ilDB->insert('object_data', $fields);
150
151        return $type_id;
152    }
153
154    /**
155     * Add RBAC operations for type
156     *
157     * @param int $a_type_id
158     * @param array $a_operations
159     */
160    public static function addRBACOperations($a_type_id, array $a_operations)
161    {
162        foreach ($a_operations as $ops_id) {
163            if (self::isValidRBACOperation($ops_id)) {
164                if ($ops_id == self::RBAC_OP_COPY) {
165                    $ops_id = self::getCustomRBACOperationId('copy');
166                }
167
168                self::addRBACOperation($a_type_id, $ops_id);
169            }
170        }
171    }
172
173    /**
174     * Add RBAC operation
175     *
176     * @param int $a_type_id
177     * @param int $a_ops_id
178     * @return bool
179     */
180    public static function addRBACOperation($a_type_id, $a_ops_id)
181    {
182        global $ilDB;
183
184        // check if it already exists
185        $set = $ilDB->query('SELECT * FROM rbac_ta' .
186            ' WHERE typ_id = ' . $ilDB->quote($a_type_id, 'integer') .
187            ' AND ops_id = ' . $ilDB->quote($a_ops_id, 'integer'));
188        if ($ilDB->numRows($set)) {
189            return false;
190        }
191
192        $fields = array(
193            'typ_id' => array('integer', $a_type_id),
194            'ops_id' => array('integer', $a_ops_id)
195        );
196        $ilDB->insert('rbac_ta', $fields);
197        return true;
198    }
199
200    /**
201     * Check if rbac operation exists
202     *
203     * @param int $a_type_id type id
204     * @param int $a_ops_id operation id
205     * @return bool
206     */
207    public static function isRBACOperation($a_type_id, $a_ops_id)
208    {
209        global $ilDB;
210
211        // check if it already exists
212        $set = $ilDB->query('SELECT * FROM rbac_ta' .
213            ' WHERE typ_id = ' . $ilDB->quote($a_type_id, 'integer') .
214            ' AND ops_id = ' . $ilDB->quote($a_ops_id, 'integer'));
215        if ($ilDB->numRows($set)) {
216            return true;
217        }
218        return false;
219    }
220
221    /**
222     * Delete rbac operation
223     *
224     * @param int $a_type
225     * @param int $a_ops_id
226     */
227    public static function deleteRBACOperation($a_type, $a_ops_id)
228    {
229        global $ilDB;
230
231        if (!$a_type || !$a_ops_id) {
232            return;
233        }
234
235        $type_id = self::getObjectTypeId($a_type);
236        if (!$type_id) {
237            return;
238        }
239
240        $query = 'DELETE FROM rbac_ta WHERE ' .
241            'typ_id = ' . $ilDB->quote($type_id, 'integer') . ' AND ' .
242            'ops_id = ' . $ilDB->quote($a_ops_id, 'integer');
243        $GLOBALS['ilLog']->write(__METHOD__ . ': ' . $query);
244        $ilDB->manipulate($query);
245
246        self::deleteRBACTemplateOperation($a_type, $a_ops_id);
247    }
248
249    /**
250     * Delete operation for type in templates
251     *
252     * @param string $a_type
253     * @param int $a_ops_id
254     */
255    public static function deleteRBACTemplateOperation($a_type, $a_ops_id)
256    {
257        global $ilDB;
258
259        if (!$a_type || !$a_ops_id) {
260            return;
261        }
262
263        $query = 'DELETE FROM rbac_templates WHERE ' .
264            'type = ' . $ilDB->quote($a_type, 'text') . ' AND ' .
265            'ops_id = ' . $ilDB->quote($a_ops_id, 'integer');
266        $GLOBALS['ilLog']->write(__METHOD__ . ': ' . $query);
267        $ilDB->manipulate($query);
268    }
269
270    /**
271     * Check if given RBAC operation id is valid
272     *
273     * @param int $a_ops_id
274     * @return bool
275     */
276    protected static function isValidRBACOperation($a_ops_id)
277    {
278        $valid = array(
279            self::RBAC_OP_EDIT_PERMISSIONS,
280            self::RBAC_OP_VISIBLE,
281            self::RBAC_OP_READ,
282            self::RBAC_OP_WRITE,
283            self::RBAC_OP_DELETE,
284            self::RBAC_OP_COPY
285        );
286        if (in_array($a_ops_id, $valid)) {
287            return true;
288        }
289        return false;
290    }
291
292    /**
293     * Get id of RBAC operation
294     *
295     * @param string $a_operation
296     * @return int
297     */
298    public static function getCustomRBACOperationId($a_operation)
299    {
300        global $ilDB;
301
302        $sql = 'SELECT ops_id' .
303            ' FROM rbac_operations' .
304            ' WHERE operation = ' . $ilDB->quote($a_operation, 'text');
305        $res = $ilDB->query($sql);
306        $row = $ilDB->fetchAssoc($res);
307        return $row['ops_id'];
308    }
309
310    /**
311     * Add custom RBAC operation
312     *
313     * @param string $a_id
314     * @param string $a_title
315     * @param string $a_class
316     * @param string $a_pos
317     * @return int ops_id
318     */
319    public static function addCustomRBACOperation($a_id, $a_title, $a_class, $a_pos)
320    {
321        global $ilDB;
322
323        // check if it already exists
324        $ops_id = self::getCustomRBACOperationId($a_id);
325        if ($ops_id) {
326            return $ops_id;
327        }
328
329        if (!in_array($a_class, array('create', 'object', 'general'))) {
330            return;
331        }
332        if ($a_class == 'create') {
333            $a_pos = 9999;
334        }
335
336        $ops_id = $ilDB->nextId('rbac_operations');
337
338        $fields = array(
339            'ops_id' => array('integer', $ops_id),
340            'operation' => array('text', $a_id),
341            'description' => array('text', $a_title),
342            'class' => array('text', $a_class),
343            'op_order' => array('integer', $a_pos),
344        );
345        $ilDB->insert('rbac_operations', $fields);
346
347        return $ops_id;
348    }
349
350    /**
351     * Get id for object data type entry
352     *
353     * @param string $a_type
354     * @return int
355     */
356    public static function getObjectTypeId($a_type)
357    {
358        global $ilDB;
359
360        $sql = 'SELECT obj_id FROM object_data' .
361            ' WHERE type = ' . $ilDB->quote('typ', 'text') .
362            ' AND title = ' . $ilDB->quote($a_type, 'text');
363        $res = $ilDB->query($sql);
364        $row = $ilDB->fetchAssoc($res);
365        return $row['obj_id'];
366    }
367
368    /**
369     * Add create RBAC operations for parent object types
370     *
371     * @param string  $a_id
372     * @param string $a_title
373     * @param array $a_parent_types
374     */
375    public static function addRBACCreate($a_id, $a_title, array $a_parent_types)
376    {
377        $ops_id = self::addCustomRBACOperation($a_id, $a_title, 'create', 9999);
378
379        foreach ($a_parent_types as $type) {
380            $type_id = self::getObjectTypeId($type);
381            if ($type_id) {
382                self::addRBACOperation($type_id, $ops_id);
383            }
384        }
385    }
386
387    /**
388     * Change order of operations
389     *
390     * @param string $a_operation
391     * @param int $a_pos
392     */
393    public static function updateOperationOrder($a_operation, $a_pos)
394    {
395        global $ilDB;
396
397        $ilDB->update(
398            'rbac_operations',
399            array('op_order' => array('integer', $a_pos)),
400            array('operation' => array('text', $a_operation))
401        );
402    }
403
404    /**
405     * Create new admin object node
406     *
407     * @param string $a_id
408     * @param string $a_title
409     */
410    public static function addAdminNode($a_obj_type, $a_title)
411    {
412        global $ilDB, $tree;
413
414        if (self::getObjectTypeId($a_obj_type)) {
415            return;
416        }
417
418        $obj_type_id = self::addNewType($a_obj_type, $a_title);
419
420        $obj_id = $ilDB->nextId('object_data');
421        $ilDB->manipulate("INSERT INTO object_data " .
422            "(obj_id, type, title, description, owner, create_date, last_update) VALUES (" .
423            $ilDB->quote($obj_id, "integer") . "," .
424            $ilDB->quote($a_obj_type, "text") . "," .
425            $ilDB->quote($a_title, "text") . "," .
426            $ilDB->quote($a_title, "text") . "," .
427            $ilDB->quote(-1, "integer") . "," .
428            $ilDB->now() . "," .
429            $ilDB->now() .
430            ")");
431
432        $ref_id = $ilDB->nextId('object_reference');
433        $ilDB->manipulate("INSERT INTO object_reference " .
434            "(obj_id, ref_id) VALUES (" .
435            $ilDB->quote($obj_id, "integer") . "," .
436            $ilDB->quote($ref_id, "integer") .
437            ")");
438
439        // put in tree
440        require_once("Services/Tree/classes/class.ilTree.php");
441        $tree = new ilTree(ROOT_FOLDER_ID);
442        $tree->insertNode($ref_id, SYSTEM_FOLDER_ID);
443
444        $rbac_ops = array(
445            self::RBAC_OP_EDIT_PERMISSIONS,
446            self::RBAC_OP_VISIBLE,
447            self::RBAC_OP_READ,
448            self::RBAC_OP_WRITE
449        );
450        self::addRBACOperations($obj_type_id, $rbac_ops);
451    }
452
453    /**
454     * Clone RBAC-settings between operations
455     *
456     * @param string $a_obj_type
457     * @param int $a_source_op_id
458     * @param int $a_target_op_id
459     */
460    public static function cloneOperation($a_obj_type, $a_source_op_id, $a_target_op_id)
461    {
462        global $ilDB;
463
464        // rbac_pa
465        $sql = "SELECT rpa.*" .
466            " FROM rbac_pa rpa" .
467            " JOIN object_reference ref ON (ref.ref_id = rpa.ref_id)" .
468            " JOIN object_data od ON (od.obj_id = ref.obj_id AND od.type = " . $ilDB->quote($a_obj_type) . ")" .
469            // see ilUtil::_getObjectsByOperations()
470            " WHERE (" . $ilDB->like("ops_id", "text", "%i:" . $a_source_op_id . "%") .
471            " OR " . $ilDB->like("ops_id", "text", "%:\"" . $a_source_op_id . "\";%") . ")";
472        $set = $ilDB->query($sql);
473        while ($row = $ilDB->fetchAssoc($set)) {
474            $ops = unserialize($row["ops_id"]);
475            // the query above could match by array KEY, we need extra checks
476            if (in_array($a_source_op_id, $ops) && !in_array($a_target_op_id, $ops)) {
477                $ops[] = $a_target_op_id;
478
479                $ilDB->manipulate("UPDATE rbac_pa" .
480                    " SET ops_id = " . $ilDB->quote(serialize($ops), "text") .
481                    " WHERE rol_id = " . $ilDB->quote($row["rol_id"], "integer") .
482                    " AND ref_id = " . $ilDB->quote($row["ref_id"], "integer"));
483            }
484        }
485
486        // rbac_templates
487        $tmp = array();
488        $sql = "SELECT rol_id, parent, ops_id" .
489            " FROM rbac_templates" .
490            " WHERE type = " . $ilDB->quote($a_obj_type, "text") .
491            " AND (ops_id = " . $ilDB->quote($a_source_op_id, "integer") .
492            " OR ops_id = " . $ilDB->quote($a_target_op_id) . ")";
493        $set = $ilDB->query($sql);
494        while ($row = $ilDB->fetchAssoc($set)) {
495            $tmp[$row["rol_id"]][$row["parent"]][] = $row["ops_id"];
496        }
497
498        foreach ($tmp as $role_id => $parents) {
499            foreach ($parents as $parent_id => $ops_ids) {
500                // only if the target op is missing
501                if (sizeof($ops_ids) < 2 && in_array($a_source_op_id, $ops_ids)) {
502                    $ilDB->manipulate("INSERT INTO rbac_templates" .
503                        " (rol_id, type, ops_id, parent)" .
504                        " VALUES " .
505                        "(" . $ilDB->quote($role_id, "integer") .
506                        "," . $ilDB->quote($a_obj_type, "text") .
507                        "," . $ilDB->quote($a_target_op_id, "integer") .
508                        "," . $ilDB->quote($parent_id, "integer") .
509                        ")");
510                }
511            }
512        }
513    }
514
515    /**
516     * Migrate varchar column to text/clob
517     *
518     * @param string $a_table_name
519     * @param string $a_column_name
520     * @return bool
521     */
522    public static function varchar2text($a_table_name, $a_column_name)
523    {
524        global $ilDB;
525
526        $tmp_column_name = $a_column_name . "_tmp_clob";
527
528        if (!$ilDB->tableColumnExists($a_table_name, $a_column_name) ||
529            $ilDB->tableColumnExists($a_table_name, $tmp_column_name)) {
530            return false;
531        }
532
533        // oracle does not support ALTER TABLE varchar2 to CLOB
534
535        $ilAtomQuery = $ilDB->buildAtomQuery();
536        $ilAtomQuery->addTableLock($a_table_name);
537
538        $ilAtomQuery->addQueryCallable(
539            function (ilDBInterface $ilDB) use ($a_table_name, $a_column_name, $tmp_column_name) {
540                $def = array(
541                    'type' => 'clob',
542                    'notnull' => false
543                );
544                $ilDB->addTableColumn($a_table_name, $tmp_column_name, $def);
545
546                $ilDB->manipulate('UPDATE ' . $a_table_name . ' SET ' . $tmp_column_name . ' = ' . $a_column_name);
547
548                $ilDB->dropTableColumn($a_table_name, $a_column_name);
549
550                $ilDB->renameTableColumn($a_table_name, $tmp_column_name, $a_column_name);
551            }
552        );
553
554        $ilAtomQuery->run();
555
556        return true;
557    }
558
559    /**
560     * Add new RBAC template
561     *
562     * @param string $a_obj_type
563     * @param string $a_id
564     * @param string $a_description
565     * @param int|array $a_op_ids
566     */
567    public static function addRBACTemplate($a_obj_type, $a_id, $a_description, $a_op_ids)
568    {
569        global $ilDB;
570
571        $new_tpl_id = $ilDB->nextId('object_data');
572
573        $ilDB->manipulateF(
574            "INSERT INTO object_data (obj_id, type, title, description," .
575            " owner, create_date, last_update) VALUES (%s, %s, %s, %s, %s, %s, %s)",
576            array("integer", "text", "text", "text", "integer", "timestamp", "timestamp"),
577            array($new_tpl_id, "rolt", $a_id, $a_description, -1, ilUtil::now(), ilUtil::now())
578        );
579
580        $ilDB->manipulateF(
581            "INSERT INTO rbac_fa (rol_id, parent, assign, protected)" .
582            " VALUES (%s, %s, %s, %s)",
583            array("integer", "integer", "text", "text"),
584            array($new_tpl_id, 8, "n", "n")
585        );
586
587        if ($a_op_ids) {
588            if (!is_array($a_op_ids)) {
589                $a_op_ids = array($a_op_ids);
590            }
591            foreach ($a_op_ids as $op_id) {
592                $ilDB->manipulateF(
593                    "INSERT INTO rbac_templates (rol_id, type, ops_id, parent)" .
594                " VALUES (%s, %s, %s, %s)",
595                    array("integer", "text", "integer", "integer"),
596                    array($new_tpl_id, $a_obj_type, $op_id, 8)
597                );
598            }
599        }
600    }
601
602    public static function setRolePermission(int $a_rol_id, string $a_type, array $a_ops, int $a_ref_id)
603    {
604        global $DIC;
605
606        $ilDB = $DIC['ilDB'];
607
608        foreach ($a_ops as $ops_id) {
609            if ($ops_id == self::RBAC_OP_COPY) {
610                $ops_id = self::getCustomRBACOperationId('copy');
611            }
612
613            $ilDB->replace(
614                'rbac_templates',
615                [
616                    'rol_id' => ['integer', $a_rol_id],
617                    'type' => ['text', $a_type],
618                    'ops_id' => ['integer', $ops_id],
619                    'parent' => ['integer', $a_ref_id]
620                ],
621                []
622            );
623        }
624    }
625
626
627    /**
628     * This method will apply the 'Initial Permissions Guideline' when introducing new object types.
629     * This method does not apply permissions to existing obejcts in the ILIAS repository ('change existing objects').
630     * @param string $objectType
631     * @param bool $hasLearningProgress A boolean flag whether or not the object type supports learning progress
632     * @param bool $usedForAuthoring A boolean flag to tell whether or not the object type is mainly used for authoring
633     * @see https://www.ilias.de/docu/goto_docu_wiki_wpage_2273_1357.html
634     */
635    public static function applyInitialPermissionGuideline(string $objectType, bool $hasLearningProgress = false, bool $usedForAuthoring = false)
636    {
637        global $DIC;
638
639        $ilDB = $DIC['ilDB'];
640
641        $objectTypeId = self::getObjectTypeId($objectType);
642        if (!$objectTypeId) {
643            die("Something went wrong, there MUST be valid id for object_type " . $objectType);
644        }
645
646        $objectCreateOperationId = ilDBUpdateNewObjectType::getCustomRBACOperationId('create_' . $objectType);
647        if (!$objectCreateOperationId) {
648            die("Something went wrong, missing CREATE operation id for object type " . $objectType);
649        }
650
651        $globalRoleFolderId = 8; // Maybe there is another way to determine this id
652
653        $learningProgressPermissions = [];
654        if ($hasLearningProgress) {
655            $learningProgressPermissions = array_filter([
656                self::getCustomRBACOperationId('read_learning_progress'),
657                self::getCustomRBACOperationId('edit_learning_progress'),
658            ]);
659        }
660
661        foreach (self::$initialPermissionDefinition as $roleType => $roles) {
662            foreach ($roles as $roleTitle => $definition) {
663                if (
664                    true === $usedForAuthoring &&
665                    array_key_exists('ignore_for_authoring_objects', $definition) &&
666                    true === $definition['ignore_for_authoring_objects']
667                ) {
668                    continue;
669                }
670
671                if (array_key_exists('id', $definition) && is_numeric($definition['id'])) {
672                    // According to JF (2018-07-02), some roles have to be selected by if, not by title
673                    $query = "SELECT obj_id FROM object_data WHERE type = %s AND obj_id = %s";
674                    $queryTypes = ['text', 'integer'];
675                    $queryValues = [$roleType, $definition['id']];
676                } else {
677                    $query = "SELECT obj_id FROM object_data WHERE type = %s AND title = %s";
678                    $queryTypes = ['text', 'text'];
679                    $queryValues = [$roleType, $roleTitle];
680                }
681
682                $res = $ilDB->queryF($query, $queryTypes, $queryValues);
683                if (1 == $ilDB->numRows($res)) {
684                    $row = $ilDB->fetchAssoc($res);
685                    $roleId = (int) $row['obj_id'];
686
687                    $operationIds = [];
688
689                    if (array_key_exists('object', $definition) && is_array($definition['object'])) {
690                        $operationIds = array_merge($operationIds, (array) $definition['object']);
691                    }
692
693                    if (array_key_exists('lp', $definition) && true === $definition['lp']) {
694                        $operationIds = array_merge($operationIds, $learningProgressPermissions);
695                    }
696
697                    self::setRolePermission(
698                        $roleId,
699                        $objectType,
700                        array_filter(array_map('intval', $operationIds)),
701                        $globalRoleFolderId
702                    );
703
704                    if (array_key_exists('create', $definition) && is_array($definition['create'])) {
705                        foreach ($definition['create'] as $containerObjectType) {
706                            self::setRolePermission(
707                                $roleId,
708                                $containerObjectType,
709                                [
710                                    $objectCreateOperationId
711                                ],
712                                $globalRoleFolderId
713                            );
714                        }
715                    }
716                }
717            }
718        }
719    }
720}
721