1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Data registry renderable.
19 *
20 * @package    tool_dataprivacy
21 * @copyright  2018 David Monllao
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24namespace tool_dataprivacy\output;
25defined('MOODLE_INTERNAL') || die();
26
27use renderable;
28use renderer_base;
29use stdClass;
30use templatable;
31use tool_dataprivacy\data_registry;
32
33require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/dataprivacy/lib.php');
34require_once($CFG->libdir . '/blocklib.php');
35
36/**
37 * Class containing the data registry renderable
38 *
39 * @copyright  2018 David Monllao
40 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
41 */
42class data_registry_page implements renderable, templatable {
43
44    /**
45     * @var int
46     */
47    private $defaultcontextlevel;
48
49    /**
50     * @var int
51     */
52    private $defaultcontextid;
53
54    /**
55     * Constructor.
56     *
57     * @param int $defaultcontextlevel
58     * @param int $defaultcontextid
59     * @return null
60     */
61    public function __construct($defaultcontextlevel = false, $defaultcontextid = false) {
62        $this->defaultcontextlevel = $defaultcontextlevel;
63        $this->defaultcontextid = $defaultcontextid;
64    }
65
66    /**
67     * Export this data so it can be used as the context for a mustache template.
68     *
69     * @param renderer_base $output
70     * @return stdClass
71     */
72    public function export_for_template(renderer_base $output) {
73        global $PAGE;
74
75        $params = [\context_system::instance()->id, $this->defaultcontextlevel, $this->defaultcontextid];
76        $PAGE->requires->js_call_amd('tool_dataprivacy/data_registry', 'init', $params);
77
78        $data = new stdClass();
79        $defaultsbutton = new \action_link(
80            new \moodle_url('/admin/tool/dataprivacy/defaults.php'),
81            get_string('setdefaults', 'tool_dataprivacy'),
82            null,
83            ['class' => 'btn btn-primary']
84        );
85        $data->defaultsbutton = $defaultsbutton->export_for_template($output);
86
87        $actionmenu = new \action_menu();
88        $actionmenu->set_menu_trigger(get_string('edit'), 'btn btn-primary');
89        $actionmenu->set_owner_selector('dataregistry-actions');
90        $actionmenu->set_alignment(\action_menu::TL, \action_menu::BL);
91
92        $url = new \moodle_url('/admin/tool/dataprivacy/categories.php');
93        $categories = new \action_menu_link_secondary($url, null, get_string('categories', 'tool_dataprivacy'));
94        $actionmenu->add($categories);
95
96        $url = new \moodle_url('/admin/tool/dataprivacy/purposes.php');
97        $purposes = new \action_menu_link_secondary($url, null, get_string('purposes', 'tool_dataprivacy'));
98        $actionmenu->add($purposes);
99
100        $data->actions = $actionmenu->export_for_template($output);
101
102        if (!data_registry::defaults_set()) {
103            $data->info = (object)[
104                    'message' => get_string('dataregistryinfo', 'tool_dataprivacy'),
105                    'announce' => 1
106            ];
107            $data->nosystemdefaults = (object)[
108                'message' => get_string('nosystemdefaults', 'tool_dataprivacy'),
109                'announce' => 1
110            ];
111        }
112
113        $data->tree = $this->get_default_tree_structure();
114
115        return $data;
116    }
117
118    /**
119     * Returns the tree default structure.
120     *
121     * @return array
122     */
123    private function get_default_tree_structure() {
124
125        $frontpage = \context_course::instance(SITEID);
126
127        $categorybranches = $this->get_all_category_branches();
128
129        $elements = [
130            'text' => get_string('contextlevelname' . CONTEXT_SYSTEM, 'tool_dataprivacy'),
131            'contextlevel' => CONTEXT_SYSTEM,
132            'branches' => [
133                [
134                    'text' => get_string('user'),
135                    'contextlevel' => CONTEXT_USER,
136                ], [
137                    'text' => get_string('categories'),
138                    'branches' => $categorybranches,
139                    'expandelement' => 'category',
140                ], [
141                    'text' => get_string('frontpagecourse', 'tool_dataprivacy'),
142                    'contextid' => $frontpage->id,
143                    'branches' => [
144                        [
145                            'text' => get_string('activitiesandresources', 'tool_dataprivacy'),
146                            'expandcontextid' => $frontpage->id,
147                            'expandelement' => 'module',
148                            'expanded' => 0,
149                        ], [
150                            'text' => get_string('blocks'),
151                            'expandcontextid' => $frontpage->id,
152                            'expandelement' => 'block',
153                            'expanded' => 0,
154                        ],
155                    ]
156                ]
157            ]
158        ];
159
160        // Returned as an array to follow a common array format.
161        return [self::complete($elements, $this->defaultcontextlevel, $this->defaultcontextid)];
162    }
163
164    /**
165     * Returns the hierarchy of system course categories.
166     *
167     * @return array
168     */
169    private function get_all_category_branches() {
170
171        $categories = data_registry::get_site_categories();
172
173        $categoriesbranch = [];
174        while (count($categories) > 0) {
175            foreach ($categories as $key => $category) {
176
177                $context = \context_coursecat::instance($category->id);
178                $newnode = [
179                    'text' => shorten_text(format_string($category->name, true, ['context' => $context])),
180                    'categoryid' => $category->id,
181                    'contextid' => $context->id,
182                ];
183                if ($category->coursecount > 0) {
184                    $newnode['branches'] = [
185                        [
186                            'text' => get_string('courses'),
187                            'expandcontextid' => $context->id,
188                            'expandelement' => 'course',
189                            'expanded' => 0,
190                        ]
191                    ];
192                }
193
194                $added = false;
195                if ($category->parent == 0) {
196                    // New categories root-level node.
197                    $categoriesbranch[] = $newnode;
198                    $added = true;
199
200                } else {
201                    // Add the new node under the appropriate parent.
202                    if ($this->add_to_parent_category_branch($category, $newnode, $categoriesbranch)) {
203                        $added = true;
204                    }
205                }
206
207                if ($added) {
208                    unset($categories[$key]);
209                }
210            }
211        }
212
213        return $categoriesbranch;
214    }
215
216    /**
217     * Gets the courses branch for the provided category.
218     *
219     * @param \context $catcontext
220     * @return array
221     */
222    public static function get_courses_branch(\context $catcontext) {
223
224        if ($catcontext->contextlevel !== CONTEXT_COURSECAT) {
225            throw new \coding_exception('A course category context should be provided');
226        }
227
228        $coursecat = \core_course_category::get($catcontext->instanceid);
229        $courses = $coursecat->get_courses();
230
231        $branches = [];
232
233        foreach ($courses as $course) {
234
235            $coursecontext = \context_course::instance($course->id);
236
237            $coursenode = [
238                'text' => shorten_text(format_string($course->shortname, true, ['context' => $coursecontext])),
239                'contextid' => $coursecontext->id,
240                'branches' => [
241                    [
242                        'text' => get_string('activitiesandresources', 'tool_dataprivacy'),
243                        'expandcontextid' => $coursecontext->id,
244                        'expandelement' => 'module',
245                        'expanded' => 0,
246                    ], [
247                        'text' => get_string('blocks'),
248                        'expandcontextid' => $coursecontext->id,
249                        'expandelement' => 'block',
250                        'expanded' => 0,
251                    ],
252                ]
253            ];
254            $branches[] = self::complete($coursenode);
255        }
256
257        return $branches;
258    }
259
260    /**
261     * Gets the modules branch for the provided course.
262     *
263     * @param \context $coursecontext
264     * @return array
265     */
266    public static function get_modules_branch(\context $coursecontext) {
267
268        if ($coursecontext->contextlevel !== CONTEXT_COURSE) {
269            throw new \coding_exception('A course context should be provided');
270        }
271
272        $branches = [];
273
274        // Using the current user.
275        $modinfo = get_fast_modinfo($coursecontext->instanceid);
276        foreach ($modinfo->get_instances() as $moduletype => $instances) {
277            foreach ($instances as $cm) {
278
279                if (!$cm->uservisible) {
280                    continue;
281                }
282
283                $a = (object)[
284                    'instancename' => shorten_text($cm->get_formatted_name()),
285                    'modulename' => get_string('pluginname', 'mod_' . $moduletype),
286                ];
287
288                $text = get_string('moduleinstancename', 'tool_dataprivacy', $a);
289                $branches[] = self::complete([
290                    'text' => $text,
291                    'contextid' => $cm->context->id,
292                ]);
293            }
294        }
295
296        return $branches;
297    }
298
299    /**
300     * Gets the blocks branch for the provided course.
301     *
302     * @param \context $coursecontext
303     * @return null
304     */
305    public static function get_blocks_branch(\context $coursecontext) {
306        global $DB;
307
308        if ($coursecontext->contextlevel !== CONTEXT_COURSE) {
309            throw new \coding_exception('A course context should be provided');
310        }
311
312        $branches = [];
313
314        $children = $coursecontext->get_child_contexts();
315        foreach ($children as $childcontext) {
316
317            if ($childcontext->contextlevel !== CONTEXT_BLOCK) {
318                continue;
319            }
320
321            $blockinstance = block_instance_by_id($childcontext->instanceid);
322            $displayname = shorten_text(format_string($blockinstance->get_title(), true, ['context' => $childcontext]));
323            $branches[] = self::complete([
324                'text' => $displayname,
325                'contextid' => $childcontext->id,
326            ]);
327
328        }
329
330        return $branches;
331    }
332
333    /**
334     * Adds the provided category to the categories branch.
335     *
336     * @param stdClass $category
337     * @param array $newnode
338     * @param array $categoriesbranch
339     * @return bool
340     */
341    private function add_to_parent_category_branch($category, $newnode, &$categoriesbranch) {
342
343        foreach ($categoriesbranch as $key => $branch) {
344            if (!empty($branch['categoryid']) && $branch['categoryid'] == $category->parent) {
345                // It may be empty (if it does not contain courses and this is the first child cat).
346                if (!isset($categoriesbranch[$key]['branches'])) {
347                    $categoriesbranch[$key]['branches'] = [];
348                }
349                $categoriesbranch[$key]['branches'][] = $newnode;
350                return true;
351            }
352            if (!empty($branch['branches'])) {
353                $parent = $this->add_to_parent_category_branch($category, $newnode, $categoriesbranch[$key]['branches']);
354                if ($parent) {
355                    return true;
356                }
357            }
358        }
359
360        return false;
361    }
362
363    /**
364     * Completes tree nodes with default values.
365     *
366     * @param array $node
367     * @param int|false $currentcontextlevel
368     * @param int|false $currentcontextid
369     * @return array
370     */
371    private static function complete($node, $currentcontextlevel = false, $currentcontextid = false) {
372        if (!isset($node['active'])) {
373            if ($currentcontextlevel && !empty($node['contextlevel']) &&
374                    $currentcontextlevel == $node['contextlevel'] &&
375                    empty($currentcontextid)) {
376                // This is the active context level, we also checked that there
377                // is no default contextid set.
378                $node['active'] = true;
379            } else if ($currentcontextid && !empty($node['contextid']) &&
380                    $currentcontextid == $node['contextid']) {
381                $node['active'] = true;
382            } else {
383                $node['active'] = null;
384            }
385        }
386
387        if (!isset($node['branches'])) {
388            $node['branches'] = [];
389        } else {
390            foreach ($node['branches'] as $key => $childnode) {
391                $node['branches'][$key] = self::complete($childnode, $currentcontextlevel, $currentcontextid);
392            }
393        }
394
395        if (!isset($node['expandelement'])) {
396            $node['expandelement'] = null;
397        }
398
399        if (!isset($node['expandcontextid'])) {
400            $node['expandcontextid'] = null;
401        }
402
403        if (!isset($node['contextid'])) {
404            $node['contextid'] = null;
405        }
406
407        if (!isset($node['contextlevel'])) {
408            $node['contextlevel'] = null;
409        }
410
411        if (!isset($node['expanded'])) {
412            if (!empty($node['branches'])) {
413                $node['expanded'] = 1;
414            } else {
415                $node['expanded'] = 0;
416            }
417        }
418        return $node;
419    }
420
421    /**
422     * From a list of purpose persistents to a list of id => name purposes.
423     *
424     * @param \tool_dataprivacy\purpose[] $purposes
425     * @param bool $includenotset
426     * @param bool $includeinherit
427     * @return string[]
428     */
429    public static function purpose_options($purposes, $includenotset = true, $includeinherit = true) {
430        $options = self::base_options($includenotset, $includeinherit);
431        foreach ($purposes as $purpose) {
432            $options[$purpose->get('id')] = $purpose->get('name');
433        }
434
435        return $options;
436    }
437
438    /**
439     * From a list of category persistents to a list of id => name categories.
440     *
441     * @param \tool_dataprivacy\category[] $categories
442     * @param bool $includenotset
443     * @param bool $includeinherit
444     * @return string[]
445     */
446    public static function category_options($categories, $includenotset = true, $includeinherit = true) {
447        $options = self::base_options($includenotset, $includeinherit);
448        foreach ($categories as $category) {
449            $options[$category->get('id')] = $category->get('name');
450        }
451
452        return $options;
453    }
454
455    /**
456     * Base not set and inherit options.
457     *
458     * @param bool $includenotset
459     * @param bool $includeinherit
460     * @return array
461     */
462    private static function base_options($includenotset = true, $includeinherit = true) {
463
464        $options = [];
465
466        if ($includenotset) {
467            $options[\tool_dataprivacy\context_instance::NOTSET] = get_string('notset', 'tool_dataprivacy');
468        }
469
470        if ($includeinherit) {
471            $options[\tool_dataprivacy\context_instance::INHERIT] = get_string('inherit', 'tool_dataprivacy');
472        }
473
474        return $options;
475    }
476}
477