Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
}