Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * Scheduled task class. 19 * 20 * @package core 21 * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 namespace core\task; 25 26 /** 27 * Simple task to send notifications about failed login attempts. 28 */ 29 class send_failed_login_notifications_task extends scheduled_task { 30 31 /** The maximum time period to look back (30 days = 30 * 24 * 3600) */ 32 const NOTIFY_MAXIMUM_TIME = 2592000; 33 34 /** 35 * Get a descriptive name for this task (shown to admins). 36 * 37 * @return string 38 */ 39 public function get_name() { 40 return get_string('tasksendfailedloginnotifications', 'admin'); 41 } 42 43 /** 44 * Do the job. 45 * Throw exceptions on errors (the job will be retried). 46 */ 47 public function execute() { 48 global $CFG, $DB; 49 50 if (empty($CFG->notifyloginfailures)) { 51 return; 52 } 53 54 $recip = get_users_from_config($CFG->notifyloginfailures, 'moodle/site:config'); 55 56 // Do not look back more than 1 month to avoid crashes due to huge number of records. 57 $maximumlastnotifytime = time() - self::NOTIFY_MAXIMUM_TIME; 58 if (empty($CFG->lastnotifyfailure) || ($CFG->lastnotifyfailure < $maximumlastnotifytime)) { 59 $CFG->lastnotifyfailure = $maximumlastnotifytime; 60 } 61 62 // If it has been less than an hour, or if there are no recipients, don't execute. 63 if (((time() - HOURSECS) < $CFG->lastnotifyfailure) || !is_array($recip) || count($recip) <= 0) { 64 return; 65 } 66 67 // We need to deal with the threshold stuff first. 68 if (empty($CFG->notifyloginthreshold)) { 69 $CFG->notifyloginthreshold = 10; // Default to something sensible. 70 } 71 72 // Get all the IPs with more than notifyloginthreshold failures since lastnotifyfailure 73 // and insert them into the cache_flags temp table. 74 $logmang = get_log_manager(); 75 /** @var \core\log\sql_internal_table_reader[] $readers */ 76 $readers = $logmang->get_readers('\core\log\sql_internal_table_reader'); 77 $reader = reset($readers); 78 $readername = key($readers); 79 if (empty($reader) || empty($readername)) { 80 // No readers, no processing. 81 return true; 82 } 83 $logtable = $reader->get_internal_log_table_name(); 84 85 $sql = "SELECT ip, COUNT(*) 86 FROM {" . $logtable . "} 87 WHERE eventname = ? 88 AND timecreated > ? 89 GROUP BY ip 90 HAVING COUNT(*) >= ?"; 91 $params = array('\core\event\user_login_failed', $CFG->lastnotifyfailure, $CFG->notifyloginthreshold); 92 $rs = $DB->get_recordset_sql($sql, $params); 93 foreach ($rs as $iprec) { 94 if (!empty($iprec->ip)) { 95 set_cache_flag('login_failure_by_ip', $iprec->ip, '1', 0); 96 } 97 } 98 $rs->close(); 99 100 // Get all the INFOs with more than notifyloginthreshold failures since lastnotifyfailure 101 // and insert them into the cache_flags temp table. 102 $sql = "SELECT userid, count(*) 103 FROM {" . $logtable . "} 104 WHERE eventname = ? 105 AND timecreated > ? 106 GROUP BY userid 107 HAVING count(*) >= ?"; 108 $params = array('\core\event\user_login_failed', $CFG->lastnotifyfailure, $CFG->notifyloginthreshold); 109 $rs = $DB->get_recordset_sql($sql, $params); 110 foreach ($rs as $inforec) { 111 if (!empty($inforec->info)) { 112 set_cache_flag('login_failure_by_id', $inforec->userid, '1', 0); 113 } 114 } 115 $rs->close(); 116 117 // Now, select all the login error logged records belonging to the ips and infos 118 // since lastnotifyfailure, that we have stored in the cache_flags table. 119 $userfieldsapi = \core_user\fields::for_name(); 120 $namefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; 121 $sql = "SELECT * FROM ( 122 SELECT l.*, u.username, $namefields 123 FROM {" . $logtable . "} l 124 JOIN {cache_flags} cf ON l.ip = cf.name 125 LEFT JOIN {user} u ON l.userid = u.id 126 WHERE l.eventname = ? 127 AND l.timecreated > ? 128 AND cf.flagtype = 'login_failure_by_ip' 129 UNION ALL 130 SELECT l.*, u.username, $namefields 131 FROM {" . $logtable . "} l 132 JOIN {cache_flags} cf ON l.userid = " . $DB->sql_cast_char2int('cf.name') . " 133 LEFT JOIN {user} u ON l.userid = u.id 134 WHERE l.eventname = ? 135 AND l.timecreated > ? 136 AND cf.flagtype = 'login_failure_by_info') t 137 ORDER BY t.timecreated DESC"; 138 $params = array('\core\event\user_login_failed', $CFG->lastnotifyfailure, '\core\event\user_login_failed', $CFG->lastnotifyfailure); 139 140 // Init some variables. 141 $count = 0; 142 $messages = ''; 143 // Iterate over the logs recordset. 144 $rs = $DB->get_recordset_sql($sql, $params); 145 foreach ($rs as $log) { 146 $a = new \stdClass(); 147 $a->time = userdate($log->timecreated); 148 if (empty($log->username)) { 149 // Entries with no valid username. We get attempted username from the event's other field. 150 $other = \tool_log\local\privacy\helper::decode_other($log->other); 151 $a->info = empty($other['username']) ? '' : $other['username']; 152 $a->name = get_string('unknownuser'); 153 } else { 154 $a->info = $log->username; 155 $a->name = fullname($log); 156 } 157 $a->ip = $log->ip; 158 $messages .= get_string('notifyloginfailuresmessage', '', $a)."\n"; 159 $count++; 160 } 161 $rs->close(); 162 163 // If we have something useful to report. 164 if ($count > 0) { 165 $site = get_site(); 166 $subject = get_string('notifyloginfailuressubject', '', format_string($site->fullname)); 167 // Calculate the complete body of notification (start + messages + end). 168 $params = array('id' => 0, 'modid' => 'site_errors', 'chooselog' => '1', 'logreader' => $readername); 169 $url = new \moodle_url('/report/log/index.php', $params); 170 $body = get_string('notifyloginfailuresmessagestart', '', $CFG->wwwroot) . 171 (($CFG->lastnotifyfailure != 0) ? '('.userdate($CFG->lastnotifyfailure).')' : '')."\n\n" . 172 $messages . 173 "\n\n".get_string('notifyloginfailuresmessageend', '', $url->out(false).' ')."\n\n"; 174 175 // For each destination, send mail. 176 mtrace('Emailing admins about '. $count .' failed login attempts'); 177 foreach ($recip as $admin) { 178 // Emailing the admins directly rather than putting these through the messaging system. 179 email_to_user($admin, \core_user::get_noreply_user(), $subject, $body); 180 } 181 } 182 183 // Update lastnotifyfailure with current time. 184 set_config('lastnotifyfailure', time()); 185 186 // Finally, delete all the temp records we have created in cache_flags. 187 $DB->delete_records_select('cache_flags', "flagtype IN ('login_failure_by_ip', 'login_failure_by_info')"); 188 189 } 190 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body