MDL-52370 role: purge coursecontacts when role is edited
[moodle.git] / admin / roles / classes / define_role_table_advanced.php
CommitLineData
bc7b53fb
PS
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/**
bb061951 18 * Advanced role definition form.
bc7b53fb
PS
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 */
24
25defined('MOODLE_INTERNAL') || die();
26
27
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 */
33class core_role_define_role_table_advanced extends core_role_capability_table_with_risks {
bb061951 34 /** @var stdClass Used to store other information (besides permissions) about the role we are creating/editing. */
bc7b53fb 35 protected $role;
bb061951 36 /** @var array Used to store errors found when validating the data. */
bc7b53fb
PS
37 protected $errors;
38 protected $contextlevels;
39 protected $allcontextlevels;
40 protected $disabled = '';
41
42 protected $allowassign;
43 protected $allowoverride;
44 protected $allowswitch;
45
46 public function __construct($context, $roleid) {
47 $this->roleid = $roleid;
48 parent::__construct($context, 'defineroletable', $roleid);
49 $this->displaypermissions = $this->allpermissions;
5089cca0 50 $this->strperms[$this->allpermissions[CAP_INHERIT]] = get_string('notset', 'core_role');
bc7b53fb 51
0aa535bf
PS
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 }
bc7b53fb
PS
57 }
58
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'));
75
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 }
89
90 public function read_submitted_permissions() {
91 global $DB;
92 $this->errors = array();
93
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;
2f1e464a
PS
99 $this->role->shortname = core_text::specialtoascii($this->role->shortname);
100 $this->role->shortname = core_text::strtolower(clean_param($this->role->shortname, PARAM_ALPHANUMEXT));
bc7b53fb 101 if (empty($this->role->shortname)) {
5089cca0 102 $this->errors['shortname'] = get_string('errorbadroleshortname', 'core_role');
bc7b53fb
PS
103 }
104 }
105 if ($DB->record_exists_select('role', 'shortname = ? and id <> ?', array($this->role->shortname, $this->roleid))) {
5089cca0 106 $this->errors['shortname'] = get_string('errorexistsroleshortname', 'core_role');
bc7b53fb
PS
107 }
108
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)) {
5089cca0 116 $this->errors['name'] = get_string('errorbadrolename', 'core_role');
bc7b53fb
PS
117 }
118 }
119 if ($this->role->name !== '' and $DB->record_exists_select('role', 'name = ? and id <> ?', array($this->role->name, $this->roleid))) {
5089cca0 120 $this->errors['name'] = get_string('errorexistsrolename', 'core_role');
bc7b53fb
PS
121 }
122
123 // Description.
124 $description = optional_param('description', null, PARAM_RAW);
125 if (!is_null($description)) {
126 $this->role->description = $description;
127 }
128
129 // Legacy type.
130 $archetype = optional_param('archetype', null, PARAM_RAW);
131 if (isset($archetype)) {
132 $archetypes = get_role_archetypes();
bb061951 133 if (isset($archetypes[$archetype])) {
bc7b53fb
PS
134 $this->role->archetype = $archetype;
135 } else {
136 $this->role->archetype = '';
137 }
138 }
139
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 }
151
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 }
165
166 // Now read the permissions for each capability.
167 parent::read_submitted_permissions();
168 }
169
170 public function is_submission_valid() {
171 return empty($this->errors);
172 }
173
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;
185
186 if ($roleid == 0) {
187 // This means reset to nothing == remove everything.
188
189 if ($options['shortname']) {
190 $this->role->shortname = '';
191 }
192
193 if ($options['name']) {
194 $this->role->name = '';
195 }
196
197 if ($options['description']) {
198 $this->role->description = '';
199 }
200
201 if ($options['archetype']) {
202 $this->role->archetype = '';
203 }
204
205 if ($options['contextlevels']) {
206 $this->contextlevels = array();
207 }
208
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 }
218
219 if ($options['permissions']) {
220 foreach ($this->capabilities as $capid => $cap) {
221 $this->permissions[$cap->name] = CAP_INHERIT;
222 }
223 }
224
225 return;
226 }
227
228 $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
229
230 if ($options['shortname']) {
231 $this->role->shortname = $role->shortname;
232 }
233
234 if ($options['name']) {
235 $this->role->name = $role->name;
236 }
237
238 if ($options['description']) {
239 $this->role->description = $role->description;
240 }
241
242 if ($options['archetype']) {
243 $this->role->archetype = $role->archetype;
244 }
245
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 }
253
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 }
263
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');
268
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 }
276
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 }
290
291 if ($options['shortname']) {
292 $this->role->shortname = '';
293 }
294
295 if ($options['name']) {
296 $this->role->name = '';
297 }
298
299 if ($options['description']) {
300 $this->role->description = '';
301 }
302
303 if ($options['archetype']) {
304 $this->role->archetype = $archetype;
305 }
306
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 }
314
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 }
324
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 }
336
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 }
349
350 if ($options['shortname']) {
351 if (isset($info['shortname'])) {
352 $this->role->shortname = $info['shortname'];
353 }
354 }
355
356 if ($options['name']) {
357 if (isset($info['name'])) {
358 $this->role->name = $info['name'];
359 }
360 }
361
362 if ($options['description']) {
363 if (isset($info['description'])) {
364 $this->role->description = $info['description'];
365 }
366 }
367
368 if ($options['archetype']) {
369 if (isset($info['archetype'])) {
370 $this->role->archetype = $info['archetype'];
371 }
372 }
373
374 if ($options['contextlevels']) {
375 if (isset($info['contextlevels'])) {
376 $this->contextlevels = $info['contextlevels'];
377 }
378 }
379
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 }
387
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 }
398
399 public function get_role_name() {
400 return $this->role->name;
401 }
402
403 public function get_role_id() {
404 return $this->role->id;
405 }
406
407 public function get_archetype() {
408 return $this->role->archetype;
409 }
410
411 protected function load_parent_permissions() {
412 $this->parentpermissions = get_default_capabilities($this->role->archetype);
413 }
414
415 public function save_changes() {
416 global $DB;
417
418 if (!$this->roleid) {
bb061951 419 // Creating role.
bc7b53fb
PS
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 {
bb061951 423 // Updating role.
bc7b53fb 424 $DB->update_record('role', $this->role);
8f2c99d6
DP
425
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());
bc7b53fb
PS
431 }
432
433 // Assignable contexts.
434 set_role_contextlevels($this->role->id, $this->contextlevels);
435
436 // Set allowed roles.
437 $this->save_allow('assign');
438 $this->save_allow('override');
439 $this->save_allow('switch');
440
441 // Permissions.
442 parent::save_changes();
443 }
444
445 protected function save_allow($type) {
446 global $DB;
447
448 $current = array_keys($this->get_allow_roles_list($type));
449 $wanted = $this->{'allow'.$type};
450
451 $addfunction = 'allow_'.$type;
452 $deltable = 'role_allow_'.$type;
453 $field = 'allow'.$type;
454
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 }
463
464 foreach ($wanted as $roleid) {
465 if ($roleid == -1) {
466 $roleid = $this->roleid;
467 }
468 $addfunction($this->roleid, $roleid);
469 }
470 }
471
472 protected function get_name_field($id) {
473 return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="254" value="' . s($this->role->name) . '" />';
474 }
475
476 protected function get_shortname_field($id) {
477 return '<input type="text" id="' . $id . '" name="' . $id . '" maxlength="254" value="' . s($this->role->shortname) . '" />';
478 }
479
480 protected function get_description_field($id) {
2bf60e22
MG
481 return '<textarea class="form-textarea" id="'. s($id) .'" name="description" rows="10" cols="50">' .
482 htmlspecialchars($this->role->description) .
483 '</textarea>';
bc7b53fb
PS
484 }
485
486 protected function get_archetype_field($id) {
487 $options = array();
488 $options[''] = get_string('none');
bb061951 489 foreach (get_role_archetypes() as $type) {
bc7b53fb
PS
490 $options[$type] = get_string('archetype'.$type, 'role');
491 }
492 return html_writer::select($options, 'archetype', $this->role->archetype, false);
493 }
494
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 }
511
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;
521
522 if ($type !== 'assign' and $type !== 'switch' and $type !== 'override') {
523 debugging('Invalid role allowed type specified', DEBUG_DEVELOPER);
524 return array();
525 }
526
527 if ($roleid === null) {
528 $roleid = $this->roleid;
529 }
530
531 if (empty($roleid)) {
532 return array();
533 }
534
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 }
542
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 }
554
555 $property = 'allow'.$type;
556 $selected = $this->$property;
557
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 }
567
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 }
576
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 }
603
604 protected function print_show_hide_advanced_button() {
5089cca0 605 echo '<p class="definenotice">' . get_string('highlightedcellsshowdefault', 'core_role') . ' </p>';
bc7b53fb
PS
606 echo '<div class="advancedbutton">';
607 echo '<input type="submit" name="toggleadvanced" value="' . get_string('hideadvanced', 'form') . '" />';
608 echo '</div>';
609 }
610
611 public function display() {
612 global $OUTPUT;
613 // Extra fields at the top of the page.
614 echo '<div class="topfields clearfix">';
5089cca0
PS
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'),
bb061951 618 $this->get_description_field('description'));
5089cca0
PS
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'));
bc7b53fb 624 if ($risks = $this->get_role_risks_info()) {
5089cca0 625 $this->print_field('', get_string('rolerisks', 'core_role'), $risks);
bc7b53fb
PS
626 }
627 echo "</div>";
628
629 $this->print_show_hide_advanced_button();
630
631 // Now the permissions table.
632 parent::display();
633 }
634
635 protected function add_permission_cells($capability) {
bb061951 636 // One cell for each possible permission.
2ff1f051 637 $content = '';
bc7b53fb
PS
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 }
2ff1f051
MG
648 $content .= '<td class="' . $permname . $extraclass . '">';
649 $content .= '<label><input type="radio" name="' . $capability->name .
bc7b53fb 650 '" value="' . $perm . '" ' . $checked . '/> ';
2ff1f051
MG
651 $content .= '<span class="note">' . $strperm . '</span>';
652 $content .= '</label></td>';
bc7b53fb 653 }
2ff1f051 654 return $content;
bc7b53fb
PS
655 }
656}