Merge branch 'w02_MDL-43529_m27_globals' of https://github.com/skodak/moodle
[moodle.git] / report / security / locallib.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  * Lib functions
19  *
20  * @package    report
21  * @subpackage security
22  * @copyright  2008 petr Skoda
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die;
29 define('REPORT_SECURITY_OK', 'ok');
30 define('REPORT_SECURITY_INFO', 'info');
31 define('REPORT_SECURITY_WARNING', 'warning');
32 define('REPORT_SECURITY_SERIOUS', 'serious');
33 define('REPORT_SECURITY_CRITICAL', 'critical');
35 function report_security_hide_timearning() {
36      global $PAGE;
37      $PAGE->requires->js_init_code("Y.one('#timewarning').addClass('timewarninghidden')");
38 }
40 function report_security_get_issue_list() {
41     return array(
42         'report_security_check_unsecuredataroot',
43         'report_security_check_displayerrors',
44         'report_security_check_noauth',
45         'report_security_check_embed',
46         'report_security_check_mediafilterswf',
47         'report_security_check_openprofiles',
48         'report_security_check_google',
49         'report_security_check_passwordpolicy',
50         'report_security_check_emailchangeconfirmation',
51         'report_security_check_cookiesecure',
52         'report_security_check_configrw',
53         'report_security_check_riskxss',
54         'report_security_check_riskadmin',
55         'report_security_check_riskbackup',
56         'report_security_check_defaultuserrole',
57         'report_security_check_guestrole',
58         'report_security_check_frontpagerole',
60     );
61 }
63 function report_security_doc_link($issue, $name) {
64     global $CFG, $OUTPUT;
66     if (empty($CFG->docroot)) {
67         return $name;
68     }
70     return $OUTPUT->doc_link('report/security/'.$issue, $name);
71 }
73 ///=============================================
74 ///               Issue checks
75 ///=============================================
78 /**
79  * Verifies unsupported noauth setting
80  * @param bool $detailed
81  * @return object result
82  */
83 function report_security_check_noauth($detailed=false) {
84     global $CFG;
86     $result = new stdClass();
87     $result->issue   = 'report_security_check_noauth';
88     $result->name    = get_string('check_noauth_name', 'report_security');
89     $result->info    = null;
90     $result->details = null;
91     $result->status  = null;
92     $result->link    = null;
93     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=manageauths\">".get_string('authsettings', 'admin').'</a>';
95     if (is_enabled_auth('none')) {
96         $result->status = REPORT_SECURITY_CRITICAL;
97         $result->info   = get_string('check_noauth_error', 'report_security');
98     } else {
99         $result->status = REPORT_SECURITY_OK;
100         $result->info   = get_string('check_noauth_ok', 'report_security');
101     }
103     if ($detailed) {
104         $result->details = get_string('check_noauth_details', 'report_security');
105     }
107     return $result;
110 /**
111  * Verifies if password policy set
112  * @param bool $detailed
113  * @return object result
114  */
115 function report_security_check_passwordpolicy($detailed=false) {
116     global $CFG;
118     $result = new stdClass();
119     $result->issue   = 'report_security_check_passwordpolicy';
120     $result->name    = get_string('check_passwordpolicy_name', 'report_security');
121     $result->info    = null;
122     $result->details = null;
123     $result->status  = null;
124     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=sitepolicies\">".get_string('sitepolicies', 'admin').'</a>';
126     if (empty($CFG->passwordpolicy)) {
127         $result->status = REPORT_SECURITY_WARNING;
128         $result->info   = get_string('check_passwordpolicy_error', 'report_security');
129     } else {
130         $result->status = REPORT_SECURITY_OK;
131         $result->info   = get_string('check_passwordpolicy_ok', 'report_security');
132     }
134     if ($detailed) {
135         $result->details = get_string('check_passwordpolicy_details', 'report_security');
136     }
138     return $result;
141 /**
142  * Verifies sloppy embedding - this should have been removed long ago!!
143  * @param bool $detailed
144  * @return object result
145  */
146 function report_security_check_embed($detailed=false) {
147     global $CFG;
149     $result = new stdClass();
150     $result->issue   = 'report_security_check_embed';
151     $result->name    = get_string('check_embed_name', 'report_security');
152     $result->info    = null;
153     $result->details = null;
154     $result->status  = null;
155     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=sitepolicies\">".get_string('sitepolicies', 'admin').'</a>';
157     if (!empty($CFG->allowobjectembed)) {
158         $result->status = REPORT_SECURITY_CRITICAL;
159         $result->info   = get_string('check_embed_error', 'report_security');
160     } else {
161         $result->status = REPORT_SECURITY_OK;
162         $result->info   = get_string('check_embed_ok', 'report_security');
163     }
165     if ($detailed) {
166         $result->details = get_string('check_embed_details', 'report_security');
167     }
169     return $result;
172 /**
173  * Verifies sloppy swf embedding - this should have been removed long ago!!
174  * @param bool $detailed
175  * @return object result
176  */
177 function report_security_check_mediafilterswf($detailed=false) {
178     global $CFG;
180     $result = new stdClass();
181     $result->issue   = 'report_security_check_mediafilterswf';
182     $result->name    = get_string('check_mediafilterswf_name', 'report_security');
183     $result->info    = null;
184     $result->details = null;
185     $result->status  = null;
186     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=filtersettingfiltermediaplugin\">".get_string('filtersettings', 'admin').'</a>';
188     $activefilters = filter_get_globally_enabled();
190     if (array_search('mediaplugin', $activefilters) !== false and !empty($CFG->filter_mediaplugin_enable_swf)) {
191         $result->status = REPORT_SECURITY_CRITICAL;
192         $result->info   = get_string('check_mediafilterswf_error', 'report_security');
193     } else {
194         $result->status = REPORT_SECURITY_OK;
195         $result->info   = get_string('check_mediafilterswf_ok', 'report_security');
196     }
198     if ($detailed) {
199         $result->details = get_string('check_mediafilterswf_details', 'report_security');
200     }
202     return $result;
205 /**
206  * Verifies fatal misconfiguration of dataroot
207  * @param bool $detailed
208  * @return object result
209  */
210 function report_security_check_unsecuredataroot($detailed=false) {
211     global $CFG;
213     $result = new stdClass();
214     $result->issue   = 'report_security_check_unsecuredataroot';
215     $result->name    = get_string('check_unsecuredataroot_name', 'report_security');
216     $result->info    = null;
217     $result->details = null;
218     $result->status  = null;
219     $result->link    = null;
221     $insecuredataroot = is_dataroot_insecure(true);
223     if ($insecuredataroot == INSECURE_DATAROOT_WARNING) {
224         $result->status = REPORT_SECURITY_SERIOUS;
225         $result->info   = get_string('check_unsecuredataroot_warning', 'report_security', $CFG->dataroot);
227     } else if ($insecuredataroot == INSECURE_DATAROOT_ERROR) {
228         $result->status = REPORT_SECURITY_CRITICAL;
229         $result->info   = get_string('check_unsecuredataroot_error', 'report_security', $CFG->dataroot);
231     } else {
232         $result->status = REPORT_SECURITY_OK;
233         $result->info   = get_string('check_unsecuredataroot_ok', 'report_security');
234     }
236     if ($detailed) {
237         $result->details = get_string('check_unsecuredataroot_details', 'report_security');
238     }
240     return $result;
243 /**
244  * Verifies displaying of errors - problem for lib files and 3rd party code
245  * because we can not disable debugging in these scripts (they do not include config.php)
246  * @param bool $detailed
247  * @return object result
248  */
249 function report_security_check_displayerrors($detailed=false) {
250     $result = new stdClass();
251     $result->issue   = 'report_security_check_displayerrors';
252     $result->name    = get_string('check_displayerrors_name', 'report_security');
253     $result->info    = null;
254     $result->details = null;
255     $result->status  = null;
256     $result->link    = null;
258     if (defined('WARN_DISPLAY_ERRORS_ENABLED')) {
259         $result->status = REPORT_SECURITY_WARNING;
260         $result->info   = get_string('check_displayerrors_error', 'report_security');
261     } else {
262         $result->status = REPORT_SECURITY_OK;
263         $result->info   = get_string('check_displayerrors_ok', 'report_security');
264     }
266     if ($detailed) {
267         $result->details = get_string('check_displayerrors_details', 'report_security');
268     }
270     return $result;
273 /**
274  * Verifies open profiles - originally open by default, not anymore because spammer abused it a lot
275  * @param bool $detailed
276  * @return object result
277  */
278 function report_security_check_openprofiles($detailed=false) {
279     global $CFG;
281     $result = new stdClass();
282     $result->issue   = 'report_security_check_openprofiles';
283     $result->name    = get_string('check_openprofiles_name', 'report_security');
284     $result->info    = null;
285     $result->details = null;
286     $result->status  = null;
287     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=sitepolicies\">".get_string('sitepolicies', 'admin').'</a>';
289     if (empty($CFG->forcelogin) and empty($CFG->forceloginforprofiles)) {
290         $result->status = REPORT_SECURITY_WARNING;
291         $result->info   = get_string('check_openprofiles_error', 'report_security');
292     } else {
293         $result->status = REPORT_SECURITY_OK;
294         $result->info   = get_string('check_openprofiles_ok', 'report_security');
295     }
297     if ($detailed) {
298         $result->details = get_string('check_openprofiles_details', 'report_security');
299     }
301     return $result;
304 /**
305  * Verifies google access not combined with disabled guest access
306  * because attackers might gain guest access by modifying browser signature.
307  * @param bool $detailed
308  * @return object result
309  */
310 function report_security_check_google($detailed=false) {
311     global $CFG;
313     $result = new stdClass();
314     $result->issue   = 'report_security_check_google';
315     $result->name    = get_string('check_google_name', 'report_security');
316     $result->info    = null;
317     $result->details = null;
318     $result->status  = null;
319     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=sitepolicies\">".get_string('sitepolicies', 'admin').'</a>';
321     if (empty($CFG->opentogoogle)) {
322         $result->status = REPORT_SECURITY_OK;
323         $result->info   = get_string('check_google_ok', 'report_security');
324     } else if (!empty($CFG->guestloginbutton)) {
325         $result->status = REPORT_SECURITY_INFO;
326         $result->info   = get_string('check_google_info', 'report_security');
327     } else {
328         $result->status = REPORT_SECURITY_SERIOUS;
329         $result->info   = get_string('check_google_error', 'report_security');
330     }
332     if ($detailed) {
333         $result->details = get_string('check_google_details', 'report_security');
334     }
336     return $result;
339 /**
340  * Verifies email confirmation - spammers were changing mails very often
341  * @param bool $detailed
342  * @return object result
343  */
344 function report_security_check_emailchangeconfirmation($detailed=false) {
345     global $CFG;
347     $result = new stdClass();
348     $result->issue   = 'report_security_check_emailchangeconfirmation';
349     $result->name    = get_string('check_emailchangeconfirmation_name', 'report_security');
350     $result->info    = null;
351     $result->details = null;
352     $result->status  = null;
353     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=sitepolicies\">".get_string('sitepolicies', 'admin').'</a>';
355     if (empty($CFG->emailchangeconfirmation)) {
356         if (empty($CFG->allowemailaddresses)) {
357             $result->status = REPORT_SECURITY_WARNING;
358             $result->info   = get_string('check_emailchangeconfirmation_error', 'report_security');
359         } else {
360             $result->status = REPORT_SECURITY_INFO;
361             $result->info   = get_string('check_emailchangeconfirmation_info', 'report_security');
362         }
363     } else {
364         $result->status = REPORT_SECURITY_OK;
365         $result->info   = get_string('check_emailchangeconfirmation_ok', 'report_security');
366     }
368     if ($detailed) {
369         $result->details = get_string('check_emailchangeconfirmation_details', 'report_security');
370     }
372     return $result;
375 /**
376  * Verifies if https enabled only secure cookies allowed,
377  * this prevents redirections and sending of cookies to unsecure port.
378  * @param bool $detailed
379  * @return object result
380  */
381 function report_security_check_cookiesecure($detailed=false) {
382     global $CFG;
384     if (strpos($CFG->wwwroot, 'https://') !== 0) {
385         return null;
386     }
388     $result = new stdClass();
389     $result->issue   = 'report_security_check_cookiesecure';
390     $result->name    = get_string('check_cookiesecure_name', 'report_security');
391     $result->info    = null;
392     $result->details = null;
393     $result->status  = null;
394     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=httpsecurity\">".get_string('httpsecurity', 'admin').'</a>';
396     if (empty($CFG->cookiesecure)) {
397         $result->status = REPORT_SECURITY_SERIOUS;
398         $result->info   = get_string('check_cookiesecure_error', 'report_security');
399     } else {
400         $result->status = REPORT_SECURITY_OK;
401         $result->info   = get_string('check_cookiesecure_ok', 'report_security');
402     }
404     if ($detailed) {
405         $result->details = get_string('check_cookiesecure_details', 'report_security');
406     }
408     return $result;
411 /**
412  * Verifies config.php is not writable anymore after installation,
413  * config files were changed on several outdated server.
414  * @param bool $detailed
415  * @return object result
416  */
417 function report_security_check_configrw($detailed=false) {
418     global $CFG;
420     $result = new stdClass();
421     $result->issue   = 'report_security_check_configrw';
422     $result->name    = get_string('check_configrw_name', 'report_security');
423     $result->info    = null;
424     $result->details = null;
425     $result->status  = null;
426     $result->link    = null;
428     if (is_writable($CFG->dirroot.'/config.php')) {
429         $result->status = REPORT_SECURITY_WARNING;
430         $result->info   = get_string('check_configrw_warning', 'report_security');
431     } else {
432         $result->status = REPORT_SECURITY_OK;
433         $result->info   = get_string('check_configrw_ok', 'report_security');
434     }
436     if ($detailed) {
437         $result->details = get_string('check_configrw_details', 'report_security');
438     }
440     return $result;
444 /**
445  * Lists all users with XSS risk, it would be great to combine this with risk trusts in user table,
446  * unfortunately nobody implemented user trust UI yet :-(
447  * @param bool $detailed
448  * @return object result
449  */
450 function report_security_check_riskxss($detailed=false) {
451     global $DB;
453     $result = new stdClass();
454     $result->issue   = 'report_security_check_riskxss';
455     $result->name    = get_string('check_riskxss_name', 'report_security');
456     $result->info    = null;
457     $result->details = null;
458     $result->status  = REPORT_SECURITY_WARNING;
459     $result->link    = null;
461     $params = array('capallow'=>CAP_ALLOW);
463     $sqlfrom = "FROM (SELECT rcx.*
464                        FROM {role_capabilities} rcx
465                        JOIN {capabilities} cap ON (cap.name = rcx.capability AND ".$DB->sql_bitand('cap.riskbitmask', RISK_XSS)." <> 0)
466                        WHERE rcx.permission = :capallow) rc,
467                      {context} c,
468                      {context} sc,
469                      {role_assignments} ra,
470                      {user} u
471                WHERE c.id = rc.contextid
472                      AND (sc.path = c.path OR sc.path LIKE ".$DB->sql_concat('c.path', "'/%'")." OR c.path LIKE ".$DB->sql_concat('sc.path', "'/%'").")
473                      AND u.id = ra.userid AND u.deleted = 0
474                      AND ra.contextid = sc.id AND ra.roleid = rc.roleid";
476     $count = $DB->count_records_sql("SELECT COUNT(DISTINCT u.id) $sqlfrom", $params);
478     $result->info = get_string('check_riskxss_warning', 'report_security', $count);
480     if ($detailed) {
481         $userfields = user_picture::fields('u');
482         $users = $DB->get_records_sql("SELECT DISTINCT $userfields $sqlfrom", $params);
483         foreach ($users as $uid=>$user) {
484             $users[$uid] = fullname($user);
485         }
486         $users = implode(', ', $users);
487         $result->details = get_string('check_riskxss_details', 'report_security', $users);
488     }
490     return $result;
493 /**
494  * Verifies sanity of default user role.
495  * @param bool $detailed
496  * @return object result
497  */
498 function report_security_check_defaultuserrole($detailed=false) {
499     global $DB, $CFG;
501     $result = new stdClass();
502     $result->issue   = 'report_security_check_defaultuserrole';
503     $result->name    = get_string('check_defaultuserrole_name', 'report_security');
504     $result->info    = null;
505     $result->details = null;
506     $result->status  = null;
507     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=userpolicies\">".get_string('userpolicies', 'admin').'</a>';
509     if (!$default_role = $DB->get_record('role', array('id'=>$CFG->defaultuserroleid))) {
510         $result->status  = REPORT_SECURITY_WARNING;
511         $result->info    = get_string('check_defaultuserrole_notset', 'report_security');
512         $result->details = $result->info;
514         return $result;
515     }
517     // risky caps - usually very dangerous
518     $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$default_role->id);
519     $sql = "SELECT COUNT(DISTINCT rc.contextid)
520               FROM {role_capabilities} rc
521               JOIN {capabilities} cap ON cap.name = rc.capability
522              WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." <> 0
523                    AND rc.permission = :capallow
524                    AND rc.roleid = :roleid";
526     $riskycount = $DB->count_records_sql($sql, $params);
528     // it may have either none or 'user' archetype - nothing else, or else it would break during upgrades badly
529     if ($default_role->archetype === '' or $default_role->archetype === 'user') {
530         $legacyok = true;
531     } else {
532         $legacyok = false;
533     }
535     if ($riskycount or !$legacyok) {
536         $result->status  = REPORT_SECURITY_CRITICAL;
537         $result->info    = get_string('check_defaultuserrole_error', 'report_security', role_get_name($default_role));
539     } else {
540         $result->status  = REPORT_SECURITY_OK;
541         $result->info    = get_string('check_defaultuserrole_ok', 'report_security');
542     }
544     if ($detailed) {
545         $result->details = get_string('check_defaultuserrole_details', 'report_security');
546     }
548     return $result;
551 /**
552  * Verifies sanity of guest role
553  * @param bool $detailed
554  * @return object result
555  */
556 function report_security_check_guestrole($detailed=false) {
557     global $DB, $CFG;
559     $result = new stdClass();
560     $result->issue   = 'report_security_check_guestrole';
561     $result->name    = get_string('check_guestrole_name', 'report_security');
562     $result->info    = null;
563     $result->details = null;
564     $result->status  = null;
565     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=userpolicies\">".get_string('userpolicies', 'admin').'</a>';
567     if (!$guest_role = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
568         $result->status  = REPORT_SECURITY_WARNING;
569         $result->info    = get_string('check_guestrole_notset', 'report_security');
570         $result->details = $result->info;
572         return $result;
573     }
575     // risky caps - usually very dangerous
576     $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$guest_role->id);
577     $sql = "SELECT COUNT(DISTINCT rc.contextid)
578               FROM {role_capabilities} rc
579               JOIN {capabilities} cap ON cap.name = rc.capability
580              WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." <> 0
581                    AND rc.permission = :capallow
582                    AND rc.roleid = :roleid";
584     $riskycount = $DB->count_records_sql($sql, $params);
586     // it may have either no or 'guest' archetype - nothing else, or else it would break during upgrades badly
587     if ($guest_role->archetype === '' or $guest_role->archetype === 'guest') {
588         $legacyok = true;
589     } else {
590         $legacyok = false;
591     }
593     if ($riskycount or !$legacyok) {
594         $result->status  = REPORT_SECURITY_CRITICAL;
595         $result->info    = get_string('check_guestrole_error', 'report_security', format_string($guest_role->name));
597     } else {
598         $result->status  = REPORT_SECURITY_OK;
599         $result->info    = get_string('check_guestrole_ok', 'report_security');
600     }
602     if ($detailed) {
603         $result->details = get_string('check_guestrole_details', 'report_security');
604     }
606     return $result;
609 /**
610  * Verifies sanity of frontpage role
611  * @param bool $detailed
612  * @return object result
613  */
614 function report_security_check_frontpagerole($detailed=false) {
615     global $DB, $CFG;
617     $result = new stdClass();
618     $result->issue   = 'report_security_check_frontpagerole';
619     $result->name    = get_string('check_frontpagerole_name', 'report_security');
620     $result->info    = null;
621     $result->details = null;
622     $result->status  = null;
623     $result->link    = "<a href=\"$CFG->wwwroot/$CFG->admin/settings.php?section=frontpagesettings\">".get_string('frontpagesettings','admin').'</a>';
625     if (!$frontpage_role = $DB->get_record('role', array('id'=>$CFG->defaultfrontpageroleid))) {
626         $result->status  = REPORT_SECURITY_INFO;
627         $result->info    = get_string('check_frontpagerole_notset', 'report_security');
628         $result->details = get_string('check_frontpagerole_details', 'report_security');
630         return $result;
631     }
633     // risky caps - usually very dangerous
634     $params = array('capallow'=>CAP_ALLOW, 'roleid'=>$frontpage_role->id);
635     $sql = "SELECT COUNT(DISTINCT rc.contextid)
636               FROM {role_capabilities} rc
637               JOIN {capabilities} cap ON cap.name = rc.capability
638              WHERE ".$DB->sql_bitand('cap.riskbitmask', (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))." <> 0
639                    AND rc.permission = :capallow
640                    AND rc.roleid = :roleid";
642     $riskycount = $DB->count_records_sql($sql, $params);
644     // there is no legacy role type for frontpage yet - anyway we can not allow teachers or admins there!
645     if ($frontpage_role->archetype === 'teacher' or $frontpage_role->archetype === 'editingteacher'
646       or $frontpage_role->archetype === 'coursecreator' or $frontpage_role->archetype === 'manager') {
647         $legacyok = false;
648     } else {
649         $legacyok = true;
650     }
652     if ($riskycount or !$legacyok) {
653         $result->status  = REPORT_SECURITY_CRITICAL;
654         $result->info    = get_string('check_frontpagerole_error', 'report_security', format_string($frontpage_role->name));
656     } else {
657         $result->status  = REPORT_SECURITY_OK;
658         $result->info    = get_string('check_frontpagerole_ok', 'report_security');
659     }
661     if ($detailed) {
662         $result->details = get_string('check_frontpagerole_details', 'report_security');
663     }
665     return $result;
668 /**
669  * Lists all admins.
670  * @param bool $detailed
671  * @return object result
672  */
673 function report_security_check_riskadmin($detailed=false) {
674     global $DB, $CFG;
676     $result = new stdClass();
677     $result->issue   = 'report_security_check_riskadmin';
678     $result->name    = get_string('check_riskadmin_name', 'report_security');
679     $result->info    = null;
680     $result->details = null;
681     $result->status  = null;
682     $result->link    = null;
684     $userfields = user_picture::fields('u');
685     $sql = "SELECT $userfields
686               FROM {user} u
687              WHERE u.id IN ($CFG->siteadmins)";
689     $admins = $DB->get_records_sql($sql);
690     $admincount = count($admins);
692     if ($detailed) {
693         foreach ($admins as $uid=>$user) {
694             $url = "$CFG->wwwroot/user/view.php?id=$user->id";
695             $admins[$uid] = '<li><a href="'.$url.'">'.fullname($user).' ('.$user->email.')</a></li>';
696         }
697         $admins = '<ul>'.implode('', $admins).'</ul>';
698     }
700     $result->status  = REPORT_SECURITY_OK;
701     $result->info = get_string('check_riskadmin_ok', 'report_security', $admincount);
703     if ($detailed) {
704         $result->details = get_string('check_riskadmin_detailsok', 'report_security', $admins);
705     }
707     return $result;
710 /**
711  * Lists all roles that have the ability to backup user data, as well as users
712  * @param bool $detailed
713  * @return object result
714  */
715 function report_security_check_riskbackup($detailed=false) {
716     global $CFG, $DB;
718     $result = new stdClass();
719     $result->issue   = 'report_security_check_riskbackup';
720     $result->name    = get_string('check_riskbackup_name', 'report_security');
721     $result->info    = null;
722     $result->details = null;
723     $result->status  = null;
724     $result->link    = null;
726     $syscontext = context_system::instance();
728     $params = array('capability'=>'moodle/backup:userinfo', 'permission'=>CAP_ALLOW, 'contextid'=>$syscontext->id);
729     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, r.archetype
730               FROM {role} r
731               JOIN {role_capabilities} rc ON rc.roleid = r.id
732              WHERE rc.capability = :capability
733                AND rc.contextid  = :contextid
734                AND rc.permission = :permission";
735     $systemroles = $DB->get_records_sql($sql, $params);
737     $params = array('capability'=>'moodle/backup:userinfo', 'permission'=>CAP_ALLOW, 'contextid'=>$syscontext->id);
738     $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, r.archetype, rc.contextid
739               FROM {role} r
740               JOIN {role_capabilities} rc ON rc.roleid = r.id
741              WHERE rc.capability = :capability
742                AND rc.contextid <> :contextid
743                AND rc.permission = :permission";
744     $overriddenroles = $DB->get_records_sql($sql, $params);
746     // list of users that are able to backup personal info
747     // note: "sc" is context where is role assigned,
748     //       "c" is context where is role overridden or system context if in role definition
749     $params = array('capability'=>'moodle/backup:userinfo', 'permission'=>CAP_ALLOW, 'context1'=>CONTEXT_COURSE, 'context2'=>CONTEXT_COURSE);
751     $sqluserinfo = "
752         FROM (SELECT rcx.*
753                 FROM {role_capabilities} rcx
754                WHERE rcx.permission = :permission AND rcx.capability = :capability) rc,
755              {context} c,
756              {context} sc,
757              {role_assignments} ra,
758              {user} u
759        WHERE c.id = rc.contextid
760              AND (sc.path = c.path OR sc.path LIKE ".$DB->sql_concat('c.path', "'/%'")." OR c.path LIKE ".$DB->sql_concat('sc.path', "'/%'").")
761              AND u.id = ra.userid AND u.deleted = 0
762              AND ra.contextid = sc.id AND ra.roleid = rc.roleid
763              AND sc.contextlevel <= :context1 AND c.contextlevel <= :context2";
765     $usercount = $DB->count_records_sql("SELECT COUNT('x') FROM (SELECT DISTINCT u.id $sqluserinfo) userinfo", $params);
766     $systemrolecount = empty($systemroles) ? 0 : count($systemroles);
767     $overriddenrolecount = empty($overriddenroles) ? 0 : count($overriddenroles);
769     $result->status  = REPORT_SECURITY_WARNING; // there is always at least one admin
770     $a = (object)array('rolecount'=>$systemrolecount,'overridecount'=>$overriddenrolecount,'usercount'=>$usercount);
771     $result->info = get_string('check_riskbackup_warning', 'report_security', $a);
773     if ($detailed) {
775         $result->details = '';  // Will be added to later
777         // Make a list of roles
778         if ($systemroles) {
779             $links = array();
780             foreach ($systemroles as $role) {
781                 $role->name = role_get_name($role);
782                 $role->url = "$CFG->wwwroot/$CFG->admin/roles/manage.php?action=edit&amp;roleid=$role->id";
783                 $links[] = '<li>'.get_string('check_riskbackup_editrole', 'report_security', $role).'</li>';
784             }
785             $links = '<ul>'.implode($links).'</ul>';
786             $result->details .= get_string('check_riskbackup_details_systemroles', 'report_security', $links);
787         }
789         // Make a list of overrides to roles
790         $rolelinks2 = array();
791         if ($overriddenroles) {
792             $links = array();
793             foreach ($overriddenroles as $role) {
794                 $role->name = $role->localname;
795                 $context = context::instance_by_id($role->contextid);
796                 $role->name = role_get_name($role, $context, ROLENAME_BOTH);
797                 $role->contextname = $context->get_context_name();
798                 $role->url = "$CFG->wwwroot/$CFG->admin/roles/override.php?contextid=$role->contextid&amp;roleid=$role->id";
799                 $links[] = '<li>'.get_string('check_riskbackup_editoverride', 'report_security', $role).'</li>';
800             }
801             $links = '<ul>'.implode('', $links).'</ul>';
802             $result->details .= get_string('check_riskbackup_details_overriddenroles', 'report_security', $links);
803         }
805         // Get a list of affected users as well
806         $users = array();
808         list($sort, $sortparams) = users_order_by_sql('u');
809         $userfields = user_picture::fields('u');
810         $rs = $DB->get_recordset_sql("SELECT DISTINCT $userfields, ra.contextid, ra.roleid
811             $sqluserinfo ORDER BY $sort", array_merge($params, $sortparams));
813         foreach ($rs as $user) {
814             $context = context::instance_by_id($user->contextid);
815             $url = "$CFG->wwwroot/$CFG->admin/roles/assign.php?contextid=$user->contextid&amp;roleid=$user->roleid";
816             $a = (object)array('fullname'=>fullname($user), 'url'=>$url, 'email'=>$user->email,
817                                'contextname'=>$context->get_context_name());
818             $users[] = '<li>'.get_string('check_riskbackup_unassign', 'report_security', $a).'</li>';
819         }
820         if (!empty($users)) {
821             $users = '<ul>'.implode('', $users).'</ul>';
822             $result->details .= get_string('check_riskbackup_details_users', 'report_security', $users);
823         }
824     }
826     return $result;