MDL-52370 role: purge coursecontacts when role is edited
[moodle.git] / admin / roles / classes / define_role_table_advanced.php
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/>.
17 /**
18  * Advanced role definition form.
19  *
20  * @package    core_role
21  * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
28 /**
29  * As well as tracking the permissions information about the role we are creating
30  * or editing, we also track the other information about the role. (This class is
31  * starting to be more and more like a formslib form in some respects.)
32  */
33 class core_role_define_role_table_advanced extends core_role_capability_table_with_risks {
34     /** @var stdClass Used to store other information (besides permissions) about the role we are creating/editing. */
35     protected $role;
36     /** @var array Used to store errors found when validating the data. */
37     protected $errors;
38     protected $contextlevels;
39     protected $allcontextlevels;
40     protected $disabled = '';
42     protected $allowassign;
43     protected $allowoverride;
44     protected $allowswitch;
46     public function __construct($context, $roleid) {
47         $this->roleid = $roleid;
48         parent::__construct($context, 'defineroletable', $roleid);
49         $this->displaypermissions = $this->allpermissions;
50         $this->strperms[$this->allpermissions[CAP_INHERIT]] = get_string('notset', 'core_role');
52         $this->allcontextlevels = array();
53         $levels = context_helper::get_all_levels();
54         foreach ($levels as $level => $classname) {
55             $this->allcontextlevels[$level] = context_helper::get_level_name($level);
56         }
57     }
59     protected function load_current_permissions() {
60         global $DB;
61         if ($this->roleid) {
62             if (!$this->role = $DB->get_record('role', array('id' => $this->roleid))) {
63                 throw new moodle_exception('invalidroleid');
64             }
65             $contextlevels = get_role_contextlevels($this->roleid);
66             // Put the contextlevels in the array keys, as well as the values.
67             if (!empty($contextlevels)) {
68                 $this->contextlevels = array_combine($contextlevels, $contextlevels);
69             } else {
70                 $this->contextlevels = array();
71             }
72             $this->allowassign = array_keys($this->get_allow_roles_list('assign'));
73             $this->allowoverride = array_keys($this->get_allow_roles_list('override'));
74             $this->allowswitch = array_keys($this->get_allow_roles_list('switch'));
76         } else {
77             $this->role = new stdClass;
78             $this->role->name = '';
79             $this->role->shortname = '';
80             $this->role->description = '';
81             $this->role->archetype = '';
82             $this->contextlevels = array();
83             $this->allowassign = array();
84             $this->allowoverride = array();
85             $this->allowswitch = array();
86         }
87         parent::load_current_permissions();
88     }
90     public function read_submitted_permissions() {
91         global $DB;
92         $this->errors = array();
94         // Role short name. We clean this in a special way. We want to end up
95         // with only lowercase safe ASCII characters.
96         $shortname = optional_param('shortname', null, PARAM_RAW);
97         if (!is_null($shortname)) {
98             $this->role->shortname = $shortname;
99             $this->role->shortname = core_text::specialtoascii($this->role->shortname);
100             $this->role->shortname = core_text::strtolower(clean_param($this->role->shortname, PARAM_ALPHANUMEXT));
101             if (empty($this->role->shortname)) {
102                 $this->errors['shortname'] = get_string('errorbadroleshortname', 'core_role');
103             }
104         }
105         if ($DB->record_exists_select('role', 'shortname = ? and id <> ?', array($this->role->shortname, $this->roleid))) {
106             $this->errors['shortname'] = get_string('errorexistsroleshortname', 'core_role');
107         }
109         // Role name.
110         $name = optional_param('name', null, PARAM_TEXT);
111         if (!is_null($name)) {
112             $this->role->name = $name;
113             // Hack: short names of standard roles are equal to archetypes, empty name means localised via lang packs.
114             $archetypes = get_role_archetypes();
115             if (!isset($archetypes[$shortname]) and html_is_blank($this->role->name)) {
116                 $this->errors['name'] = get_string('errorbadrolename', 'core_role');
117             }
118         }
119         if ($this->role->name !== '' and $DB->record_exists_select('role', 'name = ? and id <> ?', array($this->role->name, $this->roleid))) {
120             $this->errors['name'] = get_string('errorexistsrolename', 'core_role');
121         }
123         // Description.
124         $description = optional_param('description', null, PARAM_RAW);
125         if (!is_null($description)) {
126             $this->role->description = $description;
127         }
129         // Legacy type.
130         $archetype = optional_param('archetype', null, PARAM_RAW);
131         if (isset($archetype)) {
132             $archetypes = get_role_archetypes();
133             if (isset($archetypes[$archetype])) {
134                 $this->role->archetype = $archetype;
135             } else {
136                 $this->role->archetype = '';
137             }
138         }
140         // Assignable context levels.
141         foreach ($this->allcontextlevels as $cl => $notused) {
142             $assignable = optional_param('contextlevel' . $cl, null, PARAM_BOOL);
143             if (!is_null($assignable)) {
144                 if ($assignable) {
145                     $this->contextlevels[$cl] = $cl;
146                 } else {
147                     unset($this->contextlevels[$cl]);
148                 }
149             }
150         }
152         // Allowed roles.
153         $allow = optional_param_array('allowassign', null, PARAM_INT);
154         if (!is_null($allow)) {
155             $this->allowassign = $allow;
156         }
157         $allow = optional_param_array('allowoverride', null, PARAM_INT);
158         if (!is_null($allow)) {
159             $this->allowoverride = $allow;
160         }
161         $allow = optional_param_array('allowswitch', null, PARAM_INT);
162         if (!is_null($allow)) {
163             $this->allowswitch = $allow;
164         }
166         // Now read the permissions for each capability.
167         parent::read_submitted_permissions();
168     }
170     public function is_submission_valid() {
171         return empty($this->errors);
172     }
174     /**
175      * Call this after the table has been initialised,
176      * this resets everything to that role.
177      *
178      * @param int $roleid role id or 0 for no role
179      * @param array $options array with following keys:
180      *      'name', 'shortname', 'description', 'permissions', 'archetype',
181      *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch'
182      */
183     public function force_duplicate($roleid, array $options) {
184         global $DB;
186         if ($roleid == 0) {
187             // This means reset to nothing == remove everything.
189             if ($options['shortname']) {
190                 $this->role->shortname = '';
191             }
193             if ($options['name']) {
194                 $this->role->name = '';
195             }
197             if ($options['description']) {
198                 $this->role->description = '';
199             }
201             if ($options['archetype']) {
202                 $this->role->archetype = '';
203             }
205             if ($options['contextlevels']) {
206                 $this->contextlevels = array();
207             }
209             if ($options['allowassign']) {
210                 $this->allowassign = array();
211             }
212             if ($options['allowoverride']) {
213                 $this->allowoverride = array();
214             }
215             if ($options['allowswitch']) {
216                 $this->allowswitch = array();
217             }
219             if ($options['permissions']) {
220                 foreach ($this->capabilities as $capid => $cap) {
221                     $this->permissions[$cap->name] = CAP_INHERIT;
222                 }
223             }
225             return;
226         }
228         $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
230         if ($options['shortname']) {
231             $this->role->shortname = $role->shortname;
232         }
234         if ($options['name']) {
235             $this->role->name = $role->name;
236         }
238         if ($options['description']) {
239             $this->role->description = $role->description;
240         }
242         if ($options['archetype']) {
243             $this->role->archetype = $role->archetype;
244         }
246         if ($options['contextlevels']) {
247             $this->contextlevels = array();
248             $levels = get_role_contextlevels($roleid);
249             foreach ($levels as $cl) {
250                 $this->contextlevels[$cl] = $cl;
251             }
252         }
254         if ($options['allowassign']) {
255             $this->allowassign = array_keys($this->get_allow_roles_list('assign', $roleid));
256         }
257         if ($options['allowoverride']) {
258             $this->allowoverride = array_keys($this->get_allow_roles_list('override', $roleid));
259         }
260         if ($options['allowswitch']) {
261             $this->allowswitch = array_keys($this->get_allow_roles_list('switch', $roleid));
262         }
264         if ($options['permissions']) {
265             $this->permissions = $DB->get_records_menu('role_capabilities',
266                 array('roleid' => $roleid, 'contextid' => context_system::instance()->id),
267                 '', 'capability,permission');
269             foreach ($this->capabilities as $capid => $cap) {
270                 if (!isset($this->permissions[$cap->name])) {
271                     $this->permissions[$cap->name] = CAP_INHERIT;
272                 }
273             }
274         }
275     }
277     /**
278      * Change the role definition to match given archetype.
279      *
280      * @param string $archetype
281      * @param array $options array with following keys:
282      *      'name', 'shortname', 'description', 'permissions', 'archetype',
283      *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch'
284      */
285     public function force_archetype($archetype, array $options) {
286         $archetypes = get_role_archetypes();
287         if (!isset($archetypes[$archetype])) {
288             throw new coding_exception('Unknown archetype: '.$archetype);
289         }
291         if ($options['shortname']) {
292             $this->role->shortname = '';
293         }
295         if ($options['name']) {
296             $this->role->name = '';
297         }
299         if ($options['description']) {
300             $this->role->description = '';
301         }
303         if ($options['archetype']) {
304             $this->role->archetype = $archetype;
305         }
307         if ($options['contextlevels']) {
308             $this->contextlevels = array();
309             $defaults = get_default_contextlevels($archetype);
310             foreach ($defaults as $cl) {
311                 $this->contextlevels[$cl] = $cl;
312             }
313         }
315         if ($options['allowassign']) {
316             $this->allowassign = get_default_role_archetype_allows('assign', $archetype);
317         }
318         if ($options['allowoverride']) {
319             $this->allowoverride = get_default_role_archetype_allows('override', $archetype);
320         }
321         if ($options['allowswitch']) {
322             $this->allowswitch = get_default_role_archetype_allows('switch', $archetype);
323         }
325         if ($options['permissions']) {
326             $defaultpermissions = get_default_capabilities($archetype);
327             foreach ($this->permissions as $k => $v) {
328                 if (isset($defaultpermissions[$k])) {
329                     $this->permissions[$k] = $defaultpermissions[$k];
330                     continue;
331                 }
332                 $this->permissions[$k] = CAP_INHERIT;
333             }
334         }
335     }
337     /**
338      * Change the role definition to match given preset.
339      *
340      * @param string $xml
341      * @param array $options array with following keys:
342      *      'name', 'shortname', 'description', 'permissions', 'archetype',
343      *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch'
344      */
345     public function force_preset($xml, array $options) {
346         if (!$info = core_role_preset::parse_preset($xml)) {
347             throw new coding_exception('Invalid role preset');
348         }
350         if ($options['shortname']) {
351             if (isset($info['shortname'])) {
352                 $this->role->shortname = $info['shortname'];
353             }
354         }
356         if ($options['name']) {
357             if (isset($info['name'])) {
358                 $this->role->name = $info['name'];
359             }
360         }
362         if ($options['description']) {
363             if (isset($info['description'])) {
364                 $this->role->description = $info['description'];
365             }
366         }
368         if ($options['archetype']) {
369             if (isset($info['archetype'])) {
370                 $this->role->archetype = $info['archetype'];
371             }
372         }
374         if ($options['contextlevels']) {
375             if (isset($info['contextlevels'])) {
376                 $this->contextlevels = $info['contextlevels'];
377             }
378         }
380         foreach (array('assign', 'override', 'switch') as $type) {
381             if ($options['allow'.$type]) {
382                 if (isset($info['allow'.$type])) {
383                     $this->{'allow'.$type} = $info['allow'.$type];
384                 }
385             }
386         }
388         if ($options['permissions']) {
389             foreach ($this->permissions as $k => $v) {
390                 // Note: do not set everything else to CAP_INHERIT here
391                 //       because the xml file might not contain all capabilities.
392                 if (isset($info['permissions'][$k])) {
393                     $this->permissions[$k] = $info['permissions'][$k];
394                 }
395             }
396         }
397     }
399     public function get_role_name() {
400         return $this->role->name;
401     }
403     public function get_role_id() {
404         return $this->role->id;
405     }
407     public function get_archetype() {
408         return $this->role->archetype;
409     }
411     protected function load_parent_permissions() {
412         $this->parentpermissions = get_default_capabilities($this->role->archetype);
413     }
415     public function save_changes() {
416         global $DB;
418         if (!$this->roleid) {
419             // Creating role.
420             $this->role->id = create_role($this->role->name, $this->role->shortname, $this->role->description, $this->role->archetype);
421             $this->roleid = $this->role->id; // Needed to make the parent::save_changes(); call work.
422         } else {
423             // Updating role.
424             $DB->update_record('role', $this->role);
426             // This will ensure the course contacts cache is purged so name changes get updated in
427             // the UI. It would be better to do this only when we know that fields affected are
428             // updated. But thats getting into the weeds of the coursecat cache and role edits
429             // should not be that frequent, so here is the ugly brutal approach.
430             coursecat::role_assignment_changed($this->role->id, context_system::instance());
431         }
433         // Assignable contexts.
434         set_role_contextlevels($this->role->id, $this->contextlevels);
436         // Set allowed roles.
437         $this->save_allow('assign');
438         $this->save_allow('override');
439         $this->save_allow('switch');
441         // Permissions.
442         parent::save_changes();
443     }
445     protected function save_allow($type) {
446         global $DB;
448         $current = array_keys($this->get_allow_roles_list($type));
449         $wanted = $this->{'allow'.$type};
451         $addfunction = 'allow_'.$type;
452         $deltable = 'role_allow_'.$type;
453         $field = 'allow'.$type;
455         foreach ($current as $roleid) {
456             if (!in_array($roleid, $wanted)) {
457                 $DB->delete_records($deltable, array('roleid'=>$this->roleid, $field=>$roleid));
458                 continue;
459             }
460             $key = array_search($roleid, $wanted);
461             unset($wanted[$key]);
462         }
464         foreach ($wanted as $roleid) {
465             if ($roleid == -1) {
466                 $roleid = $this->roleid;
467             }
468             $addfunction($this->roleid, $roleid);
469         }
470     }
472     protected function get_name_field($id) {
473         return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="254" value="' . s($this->role->name) . '" />';
474     }
476     protected function get_shortname_field($id) {
477         return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="254" value="' . s($this->role->shortname) . '" />';
478     }
480     protected function get_description_field($id) {
481         return '<textarea class="form-textarea" id="'. s($id) .'" name="description" rows="10" cols="50">' .
482             htmlspecialchars($this->role->description) .
483             '</textarea>';
484     }
486     protected function get_archetype_field($id) {
487         $options = array();
488         $options[''] = get_string('none');
489         foreach (get_role_archetypes() as $type) {
490             $options[$type] = get_string('archetype'.$type, 'role');
491         }
492         return html_writer::select($options, 'archetype', $this->role->archetype, false);
493     }
495     protected function get_assignable_levels_control() {
496         $output = '';
497         foreach ($this->allcontextlevels as $cl => $clname) {
498             $extraarguments = $this->disabled;
499             if (in_array($cl, $this->contextlevels)) {
500                 $extraarguments .= 'checked="checked" ';
501             }
502             if (!$this->disabled) {
503                 $output .= '<input type="hidden" name="contextlevel' . $cl . '" value="0" />';
504             }
505             $output .= '<input type="checkbox" id="cl' . $cl . '" name="contextlevel' . $cl .
506                 '" value="1" ' . $extraarguments . '/> ';
507             $output .= '<label for="cl' . $cl . '">' . $clname . "</label><br />\n";
508         }
509         return $output;
510     }
512     /**
513      * Returns an array of roles of the allowed type.
514      *
515      * @param string $type Must be one of: assign, switch, or override.
516      * @param int $roleid (null means current role)
517      * @return array
518      */
519     protected function get_allow_roles_list($type, $roleid = null) {
520         global $DB;
522         if ($type !== 'assign' and $type !== 'switch' and $type !== 'override') {
523             debugging('Invalid role allowed type specified', DEBUG_DEVELOPER);
524             return array();
525         }
527         if ($roleid === null) {
528             $roleid = $this->roleid;
529         }
531         if (empty($roleid)) {
532             return array();
533         }
535         $sql = "SELECT r.*
536                   FROM {role} r
537                   JOIN {role_allow_{$type}} a ON a.allow{$type} = r.id
538                  WHERE a.roleid = :roleid
539               ORDER BY r.sortorder ASC";
540         return $DB->get_records_sql($sql, array('roleid'=>$roleid));
541     }
543     /**
544      * Returns an array of roles with the allowed type.
545      *
546      * @param string $type Must be one of: assign, switch, or override.
547      * @return array Am array of role names with the allowed type
548      */
549     protected function get_allow_role_control($type) {
550         if ($type !== 'assign' and $type !== 'switch' and $type !== 'override') {
551             debugging('Invalid role allowed type specified', DEBUG_DEVELOPER);
552             return '';
553         }
555         $property = 'allow'.$type;
556         $selected = $this->$property;
558         $options = array();
559         foreach (role_get_names(null, ROLENAME_ALIAS) as $role) {
560             $options[$role->id] = $role->localname;
561         }
562         if ($this->roleid == 0) {
563             $options[-1] = get_string('thisnewrole', 'core_role');
564         }
565         return html_writer::select($options, 'allow'.$type.'[]', $selected, false, array('multiple'=>'multiple', 'size'=>10));
566     }
568     /**
569      * Returns information about the risks associated with a role.
570      *
571      * @return string
572      */
573     protected function get_role_risks_info() {
574         return '';
575     }
577     protected function print_field($name, $caption, $field) {
578         global $OUTPUT;
579         // Attempt to generate HTML like formslib.
580         echo '<div class="fitem">';
581         echo '<div class="fitemtitle">';
582         if ($name) {
583             echo '<label for="' . $name . '">';
584         }
585         echo $caption;
586         if ($name) {
587             echo "</label>\n";
588         }
589         echo '</div>';
590         if (isset($this->errors[$name])) {
591             $extraclass = ' error';
592         } else {
593             $extraclass = '';
594         }
595         echo '<div class="felement' . $extraclass . '">';
596         echo $field;
597         if (isset($this->errors[$name])) {
598             echo $OUTPUT->error_text($this->errors[$name]);
599         }
600         echo '</div>';
601         echo '</div>';
602     }
604     protected function print_show_hide_advanced_button() {
605         echo '<p class="definenotice">' . get_string('highlightedcellsshowdefault', 'core_role') . ' </p>';
606         echo '<div class="advancedbutton">';
607         echo '<input type="submit" name="toggleadvanced" value="' . get_string('hideadvanced', 'form') . '" />';
608         echo '</div>';
609     }
611     public function display() {
612         global $OUTPUT;
613         // Extra fields at the top of the page.
614         echo '<div class="topfields clearfix">';
615         $this->print_field('shortname', get_string('roleshortname', 'core_role').'&nbsp;'.$OUTPUT->help_icon('roleshortname', 'core_role'), $this->get_shortname_field('shortname'));
616         $this->print_field('name', get_string('customrolename', 'core_role').'&nbsp;'.$OUTPUT->help_icon('customrolename', 'core_role'), $this->get_name_field('name'));
617         $this->print_field('edit-description', get_string('customroledescription', 'core_role').'&nbsp;'.$OUTPUT->help_icon('customroledescription', 'core_role'),
618             $this->get_description_field('description'));
619         $this->print_field('menuarchetype', get_string('archetype', 'core_role').'&nbsp;'.$OUTPUT->help_icon('archetype', 'core_role'), $this->get_archetype_field('archetype'));
620         $this->print_field('', get_string('maybeassignedin', 'core_role'), $this->get_assignable_levels_control());
621         $this->print_field('menuallowassign', get_string('allowassign', 'core_role'), $this->get_allow_role_control('assign'));
622         $this->print_field('menuallowoverride', get_string('allowoverride', 'core_role'), $this->get_allow_role_control('override'));
623         $this->print_field('menuallowswitch', get_string('allowswitch', 'core_role'), $this->get_allow_role_control('switch'));
624         if ($risks = $this->get_role_risks_info()) {
625             $this->print_field('', get_string('rolerisks', 'core_role'), $risks);
626         }
627         echo "</div>";
629         $this->print_show_hide_advanced_button();
631         // Now the permissions table.
632         parent::display();
633     }
635     protected function add_permission_cells($capability) {
636         // One cell for each possible permission.
637         $content = '';
638         foreach ($this->displaypermissions as $perm => $permname) {
639             $strperm = $this->strperms[$permname];
640             $extraclass = '';
641             if ($perm == $this->parentpermissions[$capability->name]) {
642                 $extraclass = ' capdefault';
643             }
644             $checked = '';
645             if ($this->permissions[$capability->name] == $perm) {
646                 $checked = 'checked="checked" ';
647             }
648             $content .= '<td class="' . $permname . $extraclass . '">';
649             $content .= '<label><input type="radio" name="' . $capability->name .
650                 '" value="' . $perm . '" ' . $checked . '/> ';
651             $content .= '<span class="note">' . $strperm . '</span>';
652             $content .= '</label></td>';
653         }
654         return $content;
655     }