MDL-62274 Theme boost: bootstrap classes define roles form
[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;
45     protected $allowview;
47     public function __construct($context, $roleid) {
48         $this->roleid = $roleid;
49         parent::__construct($context, 'defineroletable', $roleid);
50         $this->displaypermissions = $this->allpermissions;
51         $this->strperms[$this->allpermissions[CAP_INHERIT]] = get_string('notset', 'core_role');
53         $this->allcontextlevels = array();
54         $levels = context_helper::get_all_levels();
55         foreach ($levels as $level => $classname) {
56             $this->allcontextlevels[$level] = context_helper::get_level_name($level);
57         }
58     }
60     protected function load_current_permissions() {
61         global $DB;
62         if ($this->roleid) {
63             if (!$this->role = $DB->get_record('role', array('id' => $this->roleid))) {
64                 throw new moodle_exception('invalidroleid');
65             }
66             $contextlevels = get_role_contextlevels($this->roleid);
67             // Put the contextlevels in the array keys, as well as the values.
68             if (!empty($contextlevels)) {
69                 $this->contextlevels = array_combine($contextlevels, $contextlevels);
70             } else {
71                 $this->contextlevels = array();
72             }
73             $this->allowassign = array_keys($this->get_allow_roles_list('assign'));
74             $this->allowoverride = array_keys($this->get_allow_roles_list('override'));
75             $this->allowswitch = array_keys($this->get_allow_roles_list('switch'));
76             $this->allowview = array_keys($this->get_allow_roles_list('view'));
78         } else {
79             $this->role = new stdClass;
80             $this->role->name = '';
81             $this->role->shortname = '';
82             $this->role->description = '';
83             $this->role->archetype = '';
84             $this->contextlevels = array();
85             $this->allowassign = array();
86             $this->allowoverride = array();
87             $this->allowswitch = array();
88             $this->allowview = array();
89         }
90         parent::load_current_permissions();
91     }
93     public function read_submitted_permissions() {
94         global $DB;
95         $this->errors = array();
97         // Role short name. We clean this in a special way. We want to end up
98         // with only lowercase safe ASCII characters.
99         $shortname = optional_param('shortname', null, PARAM_RAW);
100         if (!is_null($shortname)) {
101             $this->role->shortname = $shortname;
102             $this->role->shortname = core_text::specialtoascii($this->role->shortname);
103             $this->role->shortname = core_text::strtolower(clean_param($this->role->shortname, PARAM_ALPHANUMEXT));
104             if (empty($this->role->shortname)) {
105                 $this->errors['shortname'] = get_string('errorbadroleshortname', 'core_role');
106             } else if (core_text::strlen($this->role->shortname) > 100) { // Check if it exceeds the max of 100 characters.
107                 $this->errors['shortname'] = get_string('errorroleshortnametoolong', 'core_role');
108             }
109         }
110         if ($DB->record_exists_select('role', 'shortname = ? and id <> ?', array($this->role->shortname, $this->roleid))) {
111             $this->errors['shortname'] = get_string('errorexistsroleshortname', 'core_role');
112         }
114         // Role name.
115         $name = optional_param('name', null, PARAM_TEXT);
116         if (!is_null($name)) {
117             $this->role->name = $name;
118             // Hack: short names of standard roles are equal to archetypes, empty name means localised via lang packs.
119             $archetypes = get_role_archetypes();
120             if (!isset($archetypes[$shortname]) and html_is_blank($this->role->name)) {
121                 $this->errors['name'] = get_string('errorbadrolename', 'core_role');
122             }
123         }
124         if ($this->role->name !== '' and $DB->record_exists_select('role', 'name = ? and id <> ?', array($this->role->name, $this->roleid))) {
125             $this->errors['name'] = get_string('errorexistsrolename', 'core_role');
126         }
128         // Description.
129         $description = optional_param('description', null, PARAM_RAW);
130         if (!is_null($description)) {
131             $this->role->description = $description;
132         }
134         // Legacy type.
135         $archetype = optional_param('archetype', null, PARAM_RAW);
136         if (isset($archetype)) {
137             $archetypes = get_role_archetypes();
138             if (isset($archetypes[$archetype])) {
139                 $this->role->archetype = $archetype;
140             } else {
141                 $this->role->archetype = '';
142             }
143         }
145         // Assignable context levels.
146         foreach ($this->allcontextlevels as $cl => $notused) {
147             $assignable = optional_param('contextlevel' . $cl, null, PARAM_BOOL);
148             if (!is_null($assignable)) {
149                 if ($assignable) {
150                     $this->contextlevels[$cl] = $cl;
151                 } else {
152                     unset($this->contextlevels[$cl]);
153                 }
154             }
155         }
157         // Allowed roles.
158         $allow = optional_param_array('allowassign', null, PARAM_INT);
159         if (!is_null($allow)) {
160             $this->allowassign = $allow;
161         }
162         $allow = optional_param_array('allowoverride', null, PARAM_INT);
163         if (!is_null($allow)) {
164             $this->allowoverride = $allow;
165         }
166         $allow = optional_param_array('allowswitch', null, PARAM_INT);
167         if (!is_null($allow)) {
168             $this->allowswitch = $allow;
169         }
170         $allow = optional_param_array('allowview', null, PARAM_INT);
171         if (!is_null($allow)) {
172             $this->allowview = $allow;
173         }
175         // Now read the permissions for each capability.
176         parent::read_submitted_permissions();
177     }
179     public function is_submission_valid() {
180         return empty($this->errors);
181     }
183     /**
184      * Call this after the table has been initialised,
185      * this resets everything to that role.
186      *
187      * @param int $roleid role id or 0 for no role
188      * @param array $options array with following keys:
189      *      'name', 'shortname', 'description', 'permissions', 'archetype',
190      *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch',
191      *      'allowview'
192      */
193     public function force_duplicate($roleid, array $options) {
194         global $DB;
196         if ($roleid == 0) {
197             // This means reset to nothing == remove everything.
199             if ($options['shortname']) {
200                 $this->role->shortname = '';
201             }
203             if ($options['name']) {
204                 $this->role->name = '';
205             }
207             if ($options['description']) {
208                 $this->role->description = '';
209             }
211             if ($options['archetype']) {
212                 $this->role->archetype = '';
213             }
215             if ($options['contextlevels']) {
216                 $this->contextlevels = array();
217             }
219             if ($options['allowassign']) {
220                 $this->allowassign = array();
221             }
222             if ($options['allowoverride']) {
223                 $this->allowoverride = array();
224             }
225             if ($options['allowswitch']) {
226                 $this->allowswitch = array();
227             }
228             if ($options['allowview']) {
229                 $this->allowview = array();
230             }
232             if ($options['permissions']) {
233                 foreach ($this->capabilities as $capid => $cap) {
234                     $this->permissions[$cap->name] = CAP_INHERIT;
235                 }
236             }
238             return;
239         }
241         $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
243         if ($options['shortname']) {
244             $this->role->shortname = $role->shortname;
245         }
247         if ($options['name']) {
248             $this->role->name = $role->name;
249         }
251         if ($options['description']) {
252             $this->role->description = $role->description;
253         }
255         if ($options['archetype']) {
256             $this->role->archetype = $role->archetype;
257         }
259         if ($options['contextlevels']) {
260             $this->contextlevels = array();
261             $levels = get_role_contextlevels($roleid);
262             foreach ($levels as $cl) {
263                 $this->contextlevels[$cl] = $cl;
264             }
265         }
267         if ($options['allowassign']) {
268             $this->allowassign = array_keys($this->get_allow_roles_list('assign', $roleid));
269         }
270         if ($options['allowoverride']) {
271             $this->allowoverride = array_keys($this->get_allow_roles_list('override', $roleid));
272         }
273         if ($options['allowswitch']) {
274             $this->allowswitch = array_keys($this->get_allow_roles_list('switch', $roleid));
275         }
276         if ($options['allowview']) {
277             $this->allowview = array_keys($this->get_allow_roles_list('view', $roleid));
278         }
280         if ($options['permissions']) {
281             $this->permissions = $DB->get_records_menu('role_capabilities',
282                 array('roleid' => $roleid, 'contextid' => context_system::instance()->id),
283                 '', 'capability,permission');
285             foreach ($this->capabilities as $capid => $cap) {
286                 if (!isset($this->permissions[$cap->name])) {
287                     $this->permissions[$cap->name] = CAP_INHERIT;
288                 }
289             }
290         }
291     }
293     /**
294      * Change the role definition to match given archetype.
295      *
296      * @param string $archetype
297      * @param array $options array with following keys:
298      *      'name', 'shortname', 'description', 'permissions', 'archetype',
299      *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch',
300      *      'allowview'
301      */
302     public function force_archetype($archetype, array $options) {
303         $archetypes = get_role_archetypes();
304         if (!isset($archetypes[$archetype])) {
305             throw new coding_exception('Unknown archetype: '.$archetype);
306         }
308         if ($options['shortname']) {
309             $this->role->shortname = '';
310         }
312         if ($options['name']) {
313             $this->role->name = '';
314         }
316         if ($options['description']) {
317             $this->role->description = '';
318         }
320         if ($options['archetype']) {
321             $this->role->archetype = $archetype;
322         }
324         if ($options['contextlevels']) {
325             $this->contextlevels = array();
326             $defaults = get_default_contextlevels($archetype);
327             foreach ($defaults as $cl) {
328                 $this->contextlevels[$cl] = $cl;
329             }
330         }
332         if ($options['allowassign']) {
333             $this->allowassign = get_default_role_archetype_allows('assign', $archetype);
334         }
335         if ($options['allowoverride']) {
336             $this->allowoverride = get_default_role_archetype_allows('override', $archetype);
337         }
338         if ($options['allowswitch']) {
339             $this->allowswitch = get_default_role_archetype_allows('switch', $archetype);
340         }
341         if ($options['allowview']) {
342             $this->allowview = get_default_role_archetype_allows('view', $archetype);
343         }
345         if ($options['permissions']) {
346             $defaultpermissions = get_default_capabilities($archetype);
347             foreach ($this->permissions as $k => $v) {
348                 if (isset($defaultpermissions[$k])) {
349                     $this->permissions[$k] = $defaultpermissions[$k];
350                     continue;
351                 }
352                 $this->permissions[$k] = CAP_INHERIT;
353             }
354         }
355     }
357     /**
358      * Change the role definition to match given preset.
359      *
360      * @param string $xml
361      * @param array $options array with following keys:
362      *      'name', 'shortname', 'description', 'permissions', 'archetype',
363      *      'contextlevels', 'allowassign', 'allowoverride', 'allowswitch',
364      *      'allowview'
365      */
366     public function force_preset($xml, array $options) {
367         if (!$info = core_role_preset::parse_preset($xml)) {
368             throw new coding_exception('Invalid role preset');
369         }
371         if ($options['shortname']) {
372             if (isset($info['shortname'])) {
373                 $this->role->shortname = $info['shortname'];
374             }
375         }
377         if ($options['name']) {
378             if (isset($info['name'])) {
379                 $this->role->name = $info['name'];
380             }
381         }
383         if ($options['description']) {
384             if (isset($info['description'])) {
385                 $this->role->description = $info['description'];
386             }
387         }
389         if ($options['archetype']) {
390             if (isset($info['archetype'])) {
391                 $this->role->archetype = $info['archetype'];
392             }
393         }
395         if ($options['contextlevels']) {
396             if (isset($info['contextlevels'])) {
397                 $this->contextlevels = $info['contextlevels'];
398             }
399         }
401         foreach (array('assign', 'override', 'switch', 'view') as $type) {
402             if ($options['allow'.$type]) {
403                 if (isset($info['allow'.$type])) {
404                     $this->{'allow'.$type} = $info['allow'.$type];
405                 }
406             }
407         }
409         if ($options['permissions']) {
410             foreach ($this->permissions as $k => $v) {
411                 // Note: do not set everything else to CAP_INHERIT here
412                 //       because the xml file might not contain all capabilities.
413                 if (isset($info['permissions'][$k])) {
414                     $this->permissions[$k] = $info['permissions'][$k];
415                 }
416             }
417         }
418     }
420     public function get_role_name() {
421         return $this->role->name;
422     }
424     public function get_role_id() {
425         return $this->role->id;
426     }
428     public function get_archetype() {
429         return $this->role->archetype;
430     }
432     protected function load_parent_permissions() {
433         $this->parentpermissions = get_default_capabilities($this->role->archetype);
434     }
436     public function save_changes() {
437         global $DB, $CFG;
439         if (!$this->roleid) {
440             // Creating role.
441             $this->role->id = create_role($this->role->name, $this->role->shortname, $this->role->description, $this->role->archetype);
442             $this->roleid = $this->role->id; // Needed to make the parent::save_changes(); call work.
443         } else {
444             // Updating role.
445             $DB->update_record('role', $this->role);
447             // This will ensure the course contacts cache is purged so name changes get updated in
448             // the UI. It would be better to do this only when we know that fields affected are
449             // updated. But thats getting into the weeds of the coursecat cache and role edits
450             // should not be that frequent, so here is the ugly brutal approach.
451             require_once($CFG->libdir . '/coursecatlib.php');
452             coursecat::role_assignment_changed($this->role->id, context_system::instance());
453         }
455         // Assignable contexts.
456         set_role_contextlevels($this->role->id, $this->contextlevels);
458         // Set allowed roles.
459         $this->save_allow('assign');
460         $this->save_allow('override');
461         $this->save_allow('switch');
462         $this->save_allow('view');
464         // Permissions.
465         parent::save_changes();
466     }
468     protected function save_allow($type) {
469         global $DB;
471         $current = array_keys($this->get_allow_roles_list($type));
472         $wanted = $this->{'allow'.$type};
474         $addfunction = "core_role_set_{$type}_allowed";
475         $deltable = 'role_allow_'.$type;
476         $field = 'allow'.$type;
478         foreach ($current as $roleid) {
479             if (!in_array($roleid, $wanted)) {
480                 $DB->delete_records($deltable, array('roleid'=>$this->roleid, $field=>$roleid));
481                 continue;
482             }
483             $key = array_search($roleid, $wanted);
484             unset($wanted[$key]);
485         }
487         foreach ($wanted as $roleid) {
488             if ($roleid == -1) {
489                 $roleid = $this->roleid;
490             }
491             $addfunction($this->roleid, $roleid);
492         }
493     }
495     protected function get_name_field($id) {
496         return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="254" value="' . s($this->role->name) . '"' .
497                 ' class="form-control"/>';
498     }
500     protected function get_shortname_field($id) {
501         return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="100" value="' . s($this->role->shortname) . '"' .
502                 ' class="form-control"/>';
503     }
505     protected function get_description_field($id) {
506         return '<textarea class="form-textarea form-control" id="'. s($id) .'" name="description" rows="10" cols="50">' .
507             htmlspecialchars($this->role->description) .
508             '</textarea>';
509     }
511     protected function get_archetype_field($id) {
512         $options = array();
513         $options[''] = get_string('none');
514         foreach (get_role_archetypes() as $type) {
515             $options[$type] = get_string('archetype'.$type, 'role');
516         }
517         return html_writer::select($options, 'archetype', $this->role->archetype, false,
518             array('class' => 'custom-select'));
519     }
521     protected function get_assignable_levels_control() {
522         $output = '';
523         foreach ($this->allcontextlevels as $cl => $clname) {
524             $extraarguments = $this->disabled;
525             if (in_array($cl, $this->contextlevels)) {
526                 $extraarguments .= 'checked="checked" ';
527             }
528             if (!$this->disabled) {
529                 $output .= '<input type="hidden" name="contextlevel' . $cl . '" value="0" />';
530             }
531             $output .= '<div class="form-check justify-content-start w-100">';
532             $output .= '<input class="form-check-input" type="checkbox" id="cl' . $cl . '" name="contextlevel' . $cl .
533                 '" value="1" ' . $extraarguments . '/> ';
534             $output .= '<label class="form-check-label" for="cl' . $cl . '">' . $clname . "</label>\n";
535             $output .= '</div>';
536         }
537         return $output;
538     }
540     /**
541      * Returns an array of roles of the allowed type.
542      *
543      * @param string $type Must be one of: assign, switch, or override.
544      * @param int $roleid (null means current role)
545      * @return array
546      */
547     protected function get_allow_roles_list($type, $roleid = null) {
548         global $DB;
550         if ($type !== 'assign' and $type !== 'switch' and $type !== 'override' and $type !== 'view') {
551             debugging('Invalid role allowed type specified', DEBUG_DEVELOPER);
552             return array();
553         }
555         if ($roleid === null) {
556             $roleid = $this->roleid;
557         }
559         if (empty($roleid)) {
560             return array();
561         }
563         $sql = "SELECT r.*
564                   FROM {role} r
565                   JOIN {role_allow_{$type}} a ON a.allow{$type} = r.id
566                  WHERE a.roleid = :roleid
567               ORDER BY r.sortorder ASC";
568         return $DB->get_records_sql($sql, array('roleid'=>$roleid));
569     }
571     /**
572      * Returns an array of roles with the allowed type.
573      *
574      * @param string $type Must be one of: assign, switch, override or view.
575      * @return array Am array of role names with the allowed type
576      */
577     protected function get_allow_role_control($type) {
578         if ($type !== 'assign' and $type !== 'switch' and $type !== 'override' and $type !== 'view') {
579             debugging('Invalid role allowed type specified', DEBUG_DEVELOPER);
580             return '';
581         }
583         $property = 'allow'.$type;
584         $selected = $this->$property;
586         $options = array();
587         foreach (role_get_names(null, ROLENAME_ALIAS) as $role) {
588             $options[$role->id] = $role->localname;
589         }
590         if ($this->roleid == 0) {
591             $options[-1] = get_string('thisnewrole', 'core_role');
592         }
593         return html_writer::select($options, 'allow'.$type.'[]', $selected, false, array('multiple' => 'multiple',
594             'size' => 10, 'class' => 'form-control'));
595     }
597     /**
598      * Returns information about the risks associated with a role.
599      *
600      * @return string
601      */
602     protected function get_role_risks_info() {
603         return '';
604     }
606     /**
607      * Print labels, fields and help icon on role administration page.
608      *
609      * @param string $name The field name.
610      * @param string $caption The field caption.
611      * @param string $field The field type.
612      * @param null|string $helpicon The help icon content.
613      */
614     protected function print_field($name, $caption, $field, $helpicon = null) {
615         global $OUTPUT;
616         // Attempt to generate HTML like formslib.
617         echo '<div class="fitem row form-group">';
618         echo '<div class="fitemtitle col-md-3">';
619         if ($name) {
620             echo '<label for="' . $name . '">';
621         }
622         echo $caption;
623         if ($name) {
624             echo "</label>\n";
625         }
626         if ($helpicon) {
627             echo '<span class="pull-xs-right text-nowrap">'.$helpicon.'</span>';
628         }
629         echo '</div>';
630         if (isset($this->errors[$name])) {
631             $extraclass = ' error';
632         } else {
633             $extraclass = '';
634         }
635         echo '<div class="felement col-md-9 form-inline' . $extraclass . '">';
636         echo $field;
637         if (isset($this->errors[$name])) {
638             echo $OUTPUT->error_text($this->errors[$name]);
639         }
640         echo '</div>';
641         echo '</div>';
642     }
644     protected function print_show_hide_advanced_button() {
645         echo '<p class="definenotice">' . get_string('highlightedcellsshowdefault', 'core_role') . ' </p>';
646         echo '<div class="advancedbutton">';
647         echo '<input type="submit" class="btn btn-secondary" name="toggleadvanced" value="' .
648             get_string('hideadvanced', 'form') . '" />';
649         echo '</div>';
650     }
652     public function display() {
653         global $OUTPUT;
654         // Extra fields at the top of the page.
655         echo '<div class="topfields clearfix">';
656         $this->print_field('shortname', get_string('roleshortname', 'core_role'),
657             $this->get_shortname_field('shortname'), $OUTPUT->help_icon('roleshortname', 'core_role'));
658         $this->print_field('name', get_string('customrolename', 'core_role'), $this->get_name_field('name'),
659             $OUTPUT->help_icon('customrolename', 'core_role'));
660         $this->print_field('edit-description', get_string('customroledescription', 'core_role'),
661             $this->get_description_field('description'), $OUTPUT->help_icon('customroledescription', 'core_role'));
662         $this->print_field('menuarchetype', get_string('archetype', 'core_role'), $this->get_archetype_field('archetype'),
663             $OUTPUT->help_icon('archetype', 'core_role'));
664         $this->print_field('', get_string('maybeassignedin', 'core_role'), $this->get_assignable_levels_control());
665         $this->print_field('menuallowassign', get_string('allowassign', 'core_role'), $this->get_allow_role_control('assign'));
666         $this->print_field('menuallowoverride', get_string('allowoverride', 'core_role'), $this->get_allow_role_control('override'));
667         $this->print_field('menuallowswitch', get_string('allowswitch', 'core_role'), $this->get_allow_role_control('switch'));
668         $this->print_field('menuallowview', get_string('allowview', 'core_role'), $this->get_allow_role_control('view'));
669         if ($risks = $this->get_role_risks_info()) {
670             $this->print_field('', get_string('rolerisks', 'core_role'), $risks);
671         }
672         echo "</div>";
674         $this->print_show_hide_advanced_button();
676         // Now the permissions table.
677         parent::display();
678     }
680     protected function add_permission_cells($capability) {
681         // One cell for each possible permission.
682         $content = '';
683         foreach ($this->displaypermissions as $perm => $permname) {
684             $strperm = $this->strperms[$permname];
685             $extraclass = '';
686             if ($perm == $this->parentpermissions[$capability->name]) {
687                 $extraclass = ' capdefault';
688             }
689             $checked = '';
690             if ($this->permissions[$capability->name] == $perm) {
691                 $checked = 'checked="checked" ';
692             }
693             $content .= '<td class="' . $permname . $extraclass . '">';
694             $content .= '<label><input type="radio" name="' . $capability->name .
695                 '" value="' . $perm . '" ' . $checked . '/> ';
696             $content .= '<span class="note">' . $strperm . '</span>';
697             $content .= '</label></td>';
698         }
699         return $content;
700     }