diff --git a/admin/tool/task/classes/edit_scheduled_task_form.php b/admin/tool/task/classes/edit_scheduled_task_form.php new file mode 100644 index 00000000000..43ff051db3c --- /dev/null +++ b/admin/tool/task/classes/edit_scheduled_task_form.php @@ -0,0 +1,90 @@ +. + +/** + * Form for scheduled tasks admin pages. + * + * @package tool_task + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir.'/formslib.php'); + +/** + * Edit scheduled task form. + * + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_task_edit_scheduled_task_form extends moodleform { + public function definition() { + global $CFG; + + $mform = $this->_form; + $task = $this->_customdata; + + $never = get_string('never'); + $none = get_string('none'); + $lastrun = $task->get_last_run_time() ? userdate($task->get_last_run_time()) : $never; + $nextrun = $task->get_next_run_time() ? userdate($task->get_next_run_time()) : $none; + $mform->addElement('static', 'lastrun', get_string('lastruntime', 'tool_task'), $lastrun); + $mform->addElement('static', 'nextrun', get_string('nextruntime', 'tool_task'), $nextrun); + + $mform->addElement('text', 'minute', get_string('taskscheduleminute', 'tool_task')); + $mform->setType('minute', PARAM_RAW); + $mform->addHelpButton('minute', 'taskscheduleminute', 'tool_task'); + $mform->setDefault('minute', $task->get_minute()); + + $mform->addElement('text', 'hour', get_string('taskschedulehour', 'tool_task')); + $mform->setType('hour', PARAM_RAW); + $mform->addHelpButton('hour', 'taskschedulehour', 'tool_task'); + $mform->setDefault('hour', $task->get_hour()); + + $mform->addElement('text', 'day', get_string('taskscheduleday', 'tool_task')); + $mform->setType('day', PARAM_RAW); + $mform->addHelpButton('day', 'taskscheduleday', 'tool_task'); + $mform->setDefault('day', $task->get_day()); + + $mform->addElement('text', 'month', get_string('taskschedulemonth', 'tool_task')); + $mform->setType('month', PARAM_RAW); + $mform->addHelpButton('month', 'taskschedulemonth', 'tool_task'); + $mform->setDefault('month', $task->get_month()); + + $mform->addElement('text', 'dayofweek', get_string('taskscheduledayofweek', 'tool_task')); + $mform->setType('dayofweek', PARAM_RAW); + $mform->addHelpButton('dayofweek', 'taskscheduledayofweek', 'tool_task'); + $mform->setDefault('dayofweek', $task->get_day_of_week()); + + $mform->addElement('advcheckbox', 'resettodefaults', get_string('resettasktodefaults', 'tool_task')); + $mform->addHelpButton('resettodefaults', 'resettasktodefaults', 'tool_task'); + + $mform->disabledIf('minute', 'resettodefaults', 'checked'); + $mform->disabledIf('hour', 'resettodefaults', 'checked'); + $mform->disabledIf('day', 'resettodefaults', 'checked'); + $mform->disabledIf('dayofweek', 'resettodefaults', 'checked'); + $mform->disabledIf('month', 'resettodefaults', 'checked'); + + $mform->addElement('hidden', 'task', get_class($task)); + $mform->setType('task', PARAM_RAW); + $mform->addElement('hidden', 'action', 'edit'); + $mform->setType('action', PARAM_ALPHANUMEXT); + $this->add_action_buttons(true, get_string('savechanges')); + } +} + diff --git a/admin/tool/task/lang/en/tool_task.php b/admin/tool/task/lang/en/tool_task.php new file mode 100644 index 00000000000..8810afb7354 --- /dev/null +++ b/admin/tool/task/lang/en/tool_task.php @@ -0,0 +1,44 @@ +. + +/** + * Strings for component 'tool_task', language 'en' + * + * @package tool_task + * @copyright 2014 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['blocking'] = 'Blocking'; +$string['default'] = 'Default'; +$string['edittaskschedule'] = 'Edit task schedule: {$a}'; +$string['faildelay'] = 'Fail delay'; +$string['lastruntime'] = 'Last run'; +$string['nextruntime'] = 'Next run'; +$string['pluginname'] = 'Scheduled task configuration'; +$string['resettasktodefaults'] = 'Reset task schedule to defaults'; +$string['resettasktodefaults_help'] = 'This will discard any local changes and revert the schedule for this task to it\'s original settings'; +$string['scheduledtasks'] = 'Scheduled tasks'; +$string['taskscheduleday'] = 'Day'; +$string['taskscheduleday_help'] = 'Day of month field for task schedule. The field uses the same format as unix cron. Some examples are:
'; +$string['taskscheduledayofweek'] = 'Day of week'; +$string['taskscheduledayofweek_help'] = 'Day of week field for task schedule. The field uses the same format as unix cron. Some examples are:
'; +$string['taskschedulehour'] = 'Hour'; +$string['taskschedulehour_help'] = 'Hour field for task schedule. The field uses the same format as unix cron. Some examples are:
'; +$string['taskscheduleminute'] = 'Minute'; +$string['taskscheduleminute_help'] = 'Minute field for task schedule. The field uses the same format as unix cron. Some examples are:
'; +$string['taskschedulemonth'] = 'Month'; +$string['taskschedulemonth_help'] = 'Month field for task schedule. The field uses the same format as unix cron. Some examples are:
'; diff --git a/admin/tool/task/renderer.php b/admin/tool/task/renderer.php new file mode 100644 index 00000000000..bc47388411d --- /dev/null +++ b/admin/tool/task/renderer.php @@ -0,0 +1,87 @@ +. + +/** + * Output rendering for the plugin. + * + * @package tool_task + * @copyright 2014 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Implements the plugin renderer + * + * @copyright 2014 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class tool_task_renderer extends plugin_renderer_base { + /** + * This function will render one beautiful table with all the scheduled tasks. + * + * @param array(scheduled_task) - list of all scheduled tasks. + * @return string HTML to output. + */ + public function scheduled_tasks_table($tasks) { + $table = new html_table(); + $table->head = array(get_string('name'), + get_string('edit'), + get_string('lastruntime', 'tool_task'), + get_string('nextruntime', 'tool_task'), + get_string('taskscheduleminute', 'tool_task'), + get_string('taskschedulehour', 'tool_task'), + get_string('taskscheduleday', 'tool_task'), + get_string('taskscheduledayofweek', 'tool_task'), + get_string('taskschedulemonth', 'tool_task'), + get_string('faildelay', 'tool_task'), + get_string('default', 'tool_task')); + $table->attributes['class'] = 'admintable generaltable'; + $data = array(); + $yes = get_string('yes'); + $no = get_string('no'); + $never = get_string('never'); + $now = get_string('now'); + foreach ($tasks as $task) { + $customised = $task->is_customised() ? $no : $yes; + $lastrun = $task->get_last_run_time() ? userdate($task->get_last_run_time()) : $never; + $nextrun = $task->get_next_run_time() ? userdate($task->get_next_run_time()) : $now; + $configureurl = new moodle_url('/admin/tool/task/scheduledtasks.php', array('action'=>'edit', 'task' => get_class($task))); + $editlink = $this->action_icon($configureurl, new pix_icon('t/edit', get_string('edittaskschedule', 'tool_task', $task->get_name()))); + + $namecell = new html_table_cell($task->get_name()); + $namecell->header = true; + + $row = new html_table_row(array( $namecell, + new html_table_cell($editlink), + new html_table_cell($lastrun), + new html_table_cell($nextrun), + new html_table_cell($task->get_minute()), + new html_table_cell($task->get_hour()), + new html_table_cell($task->get_day()), + new html_table_cell($task->get_day_of_week()), + new html_table_cell($task->get_month()), + new html_table_cell($task->get_fail_delay()), + new html_table_cell($customised))); + + $data[] = $row; + } + $table->data = $data; + return html_writer::table($table); + } +} diff --git a/admin/tool/task/scheduledtasks.php b/admin/tool/task/scheduledtasks.php new file mode 100644 index 00000000000..27796151d63 --- /dev/null +++ b/admin/tool/task/scheduledtasks.php @@ -0,0 +1,120 @@ +. + +/** + * Scheduled task admin pages. + * + * @package tool_task + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(dirname(__FILE__) . '/../../../config.php'); +require_once($CFG->libdir.'/adminlib.php'); +require_once($CFG->libdir.'/tablelib.php'); + +$PAGE->set_url('/admin/tool/task/scheduledtasks.php'); +$PAGE->set_context(context_system::instance()); +$PAGE->set_pagelayout('admin'); +$strheading = get_string('scheduledtasks', 'tool_task'); +$PAGE->set_title($strheading); +$PAGE->set_heading($strheading); + +require_login(); + +require_capability('moodle/site:config', context_system::instance()); + +$renderer = $PAGE->get_renderer('tool_task'); + +$action = optional_param('action', '', PARAM_ALPHAEXT); +$taskname = optional_param('task', '', PARAM_RAW); +$task = null; +$mform = null; + +if ($taskname) { + $task = \core\task\manager::get_scheduled_task($taskname); + if (!$task) { + print_error('invaliddata'); + } +} + +if ($action == 'edit') { + $PAGE->navbar->add(get_string('edittaskschedule', 'tool_task', $task->get_name())); +} + +if ($task) { + $mform = new tool_task_edit_scheduled_task_form(null, $task); +} + +if ($mform && $mform->is_cancelled()) { + redirect(new moodle_url('/admin/tool/task/scheduledtasks.php')); +} else if ($action == 'edit') { + + if ($data = $mform->get_data()) { + + + if ($data->resettodefaults) { + $defaulttask = \core\task\manager::get_default_scheduled_task($taskname); + $task->set_minute($defaulttask->get_minute()); + $task->set_hour($defaulttask->get_hour()); + $task->set_month($defaulttask->get_month()); + $task->set_day_of_week($defaulttask->get_day_of_week()); + $task->set_day($defaulttask->get_day()); + $task->set_customised(false); + } else { + $task->set_minute($data->minute); + $task->set_hour($data->hour); + $task->set_month($data->month); + $task->set_day_of_week($data->dayofweek); + $task->set_day($data->day); + $task->set_customised(true); + } + + try { + \core\task\manager::configure_scheduled_task($task); + $url = $PAGE->url; + $url->params(array('success'=>get_string('changessaved'))); + redirect($url); + } catch (Exception $e) { + $url = $PAGE->url; + $url->params(array('error'=>$e->getMessage())); + redirect($url); + } + } else { + echo $OUTPUT->header(); + echo $OUTPUT->heading(get_string('edittaskschedule', 'tool_task', $task->get_name())); + $mform->display(); + echo $OUTPUT->footer(); + } + +} else { + echo $OUTPUT->header(); + $error = optional_param('error', '', PARAM_RAW); + if ($error) { + echo $OUTPUT->notification($error, 'notifyerror'); + } + $success = optional_param('success', '', PARAM_RAW); + if ($success) { + echo $OUTPUT->notification($success, 'notifysuccess'); + } + $tasks = core\task\manager::get_all_scheduled_tasks(); + echo $renderer->scheduled_tasks_table($tasks); + echo $OUTPUT->footer(); +} + + + + diff --git a/admin/tool/task/settings.php b/admin/tool/task/settings.php new file mode 100644 index 00000000000..ac75859b5fc --- /dev/null +++ b/admin/tool/task/settings.php @@ -0,0 +1,29 @@ +. + +/** + * Scheduled tasks. + * + * @package tool_task + * @copyright 2014 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die; + +if ($hassiteconfig) { + $ADMIN->add('server', new admin_externalpage('scheduledtasks', new lang_string('scheduledtasks','tool_task'), "$CFG->wwwroot/$CFG->admin/tool/task/scheduledtasks.php")); +} diff --git a/admin/tool/task/version.php b/admin/tool/task/version.php new file mode 100644 index 00000000000..8cb4801178f --- /dev/null +++ b/admin/tool/task/version.php @@ -0,0 +1,30 @@ +. + +/** + * Plugin version info + * + * @package tool_task + * @copyright 2014 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2014021900; // The current plugin version (Date: YYYYMMDDXX) +$plugin->requires = 2013110500; // Requires this Moodle version +$plugin->component = 'tool_task'; // Full name of the plugin (used for diagnostics) + diff --git a/lang/en/admin.php b/lang/en/admin.php index 902ecc1564b..2db067d8918 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -994,6 +994,37 @@ $string['tablesnosave'] = 'Changes in tables above are saved automatically.'; $string['tabselectedtofront'] = 'On tables with tabs, should the row with the currently selected tab be placed at the front'; $string['tabselectedtofronttext'] = 'Bring selected tab row to front'; $string['testsiteupgradewarning'] = 'You are currently using the {$a} test site, to upgrade it properly use the command line interface tool'; +$string['taskautomatedbackup'] = 'Automated backups'; +$string['taskbackupcleanup'] = 'Clean backup tables and logs'; +$string['taskbadgescron'] = 'Award badges'; +$string['taskblogcron'] = 'Sync external blogs'; +$string['taskcachecleanup'] = 'Remove expired cache entries'; +$string['taskcachecron'] = 'Background processing for caches'; +$string['taskcalendarcron'] = 'Send calendar notifications'; +$string['taskcheckforupdates'] = 'Check for updates'; +$string['taskcompletioncron'] = 'Calculate completion data'; +$string['taskcontextcleanup'] = 'Cleanup contexts'; +$string['taskcreatecontexts'] = 'Create missing contexts'; +$string['taskdeletecachetext'] = 'Delete old text cache records'; +$string['taskdeleteincompleteusers'] = 'Delete incomplete users'; +$string['taskdeletelogs'] = 'Delete logs'; +$string['taskdeleteunconfirmedusers'] = 'Delete unconfirmed users'; +$string['taskeventscron'] = 'Background processing for events'; +$string['taskfiletrashcleanup'] = 'Cleanup files in trash'; +$string['taskgradecron'] = 'Background processing for gradebook'; +$string['tasklegacycron'] = 'Legacy cron processing for plugins'; +$string['taskmessagingcleanup'] = 'Background processing for messaging'; +$string['taskpasswordresetcleanup'] = 'Cleanup password reset attempts'; +$string['taskplagiarismcron'] = 'Background processing for plagiarism plugins'; +$string['taskportfoliocron'] = 'Background processing for portfolio plugins'; +$string['taskquestioncron'] = 'Background processing for question engine'; +$string['taskregistrationcron'] = 'Site registration'; +$string['tasksendfailedloginnotifications'] = 'Send failed login notifications'; +$string['tasksendnewuserpasswords'] = 'Send new user passwords'; +$string['tasksessioncleanup'] = 'Cleanup old sessions'; +$string['taskstatscron'] = 'Background processing for statistics'; +$string['tasktagcron'] = 'Background processing for tags'; +$string['tasktempfilecleanup'] = 'Delete stale temp files'; $string['themedesignermode'] = 'Theme designer mode'; $string['themelist'] = 'Theme list'; $string['themenoselected'] = 'No theme selected'; diff --git a/lang/en/moodle.php b/lang/en/moodle.php index f049d0223dc..9d8b80af3ce 100644 --- a/lang/en/moodle.php +++ b/lang/en/moodle.php @@ -1025,6 +1025,7 @@ $string['local'] = 'Local'; $string['localplugins'] = 'Local plugins'; $string['localpluginsmanage'] = 'Manage local plugins'; $string['location'] = 'Location'; +$string['locktimeout'] = 'The operation timed out while waiting for a lock.'; $string['log_excel_date_format'] = 'yyyy mmmm d h:mm'; $string['loggedinas'] = 'You are logged in as {$a}'; $string['loggedinasguest'] = 'You are currently using guest access'; diff --git a/lib/adminlib.php b/lib/adminlib.php index 7215c9f3cae..9b53590c186 100644 --- a/lib/adminlib.php +++ b/lib/adminlib.php @@ -195,6 +195,9 @@ function uninstall_plugin($type, $name) { // delete calendar events $DB->delete_records('event', array('modulename' => $pluginname)); + // Delete scheduled tasks. + $DB->delete_records('scheduled_task', array('component' => $pluginname)); + // delete all the logs $DB->delete_records('log', array('module' => $pluginname)); diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 7ae4380e5ff..167c5444396 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -1118,7 +1118,7 @@ class core_plugin_manager { 'assignmentupgrade', 'behat', 'capability', 'customlang', 'dbtransfer', 'generator', 'health', 'innodb', 'installaddon', 'langimport', 'multilangupgrade', 'phpunit', 'profiling', - 'qeupgradehelper', 'replace', 'spamcleaner', 'timezoneimport', + 'qeupgradehelper', 'replace', 'spamcleaner', 'task', 'timezoneimport', 'unittest', 'uploadcourse', 'uploaduser', 'unsuproles', 'xmldb' ), diff --git a/lib/classes/task/adhoc_task.php b/lib/classes/task/adhoc_task.php new file mode 100644 index 00000000000..7fc3fa65d2a --- /dev/null +++ b/lib/classes/task/adhoc_task.php @@ -0,0 +1,75 @@ +. + +/** + * Adhoc task abstract class. + * + * All background tasks should extend this class. + * + * @package core + * @category task + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\task; + +/** + * Abstract class defining an adhoc task. + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class adhoc_task extends task_base { + + /** @var string $customdata - Custom data required for when this task is executed. */ + private $customdata = ''; + + /** @var integer|null $id - Adhoc tasks each have their own database record id. */ + private $id = null; + + /** + * Setter for $id. + * @param int|null $id + */ + public function set_id($id) { + $this->id = $id; + } + + /** + * Getter for $id. + * @return int|null $id + */ + public function get_id() { + return $this->id; + } + + /** + * Setter for $customdata. + * @param object $customdata (anything that can be handled by json_encode) + */ + public function set_custom_data($customdata) { + $this->customdata = json_encode($customdata); + } + + /** + * Getter for $customdata. + * @return object (anything that can be handled by json_decode). + */ + public function get_custom_data() { + return json_decode($this->customdata); + } + +} diff --git a/lib/classes/task/automated_backup_task.php b/lib/classes/task/automated_backup_task.php new file mode 100644 index 00000000000..6bbeba7a6e5 --- /dev/null +++ b/lib/classes/task/automated_backup_task.php @@ -0,0 +1,53 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the backup cron. + */ +class automated_backup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskautomatedbackup', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + // Run automated backups if required - these may take a long time to execute. + require_once($CFG->dirroot.'/backup/util/includes/backup_includes.php'); + require_once($CFG->dirroot.'/backup/util/helper/backup_cron_helper.class.php'); + \backup_cron_automated_helper::run_automated_backup(); + } + +} diff --git a/lib/classes/task/backup_cleanup_task.php b/lib/classes/task/backup_cleanup_task.php new file mode 100644 index 00000000000..1df8fbb29b6 --- /dev/null +++ b/lib/classes/task/backup_cleanup_task.php @@ -0,0 +1,67 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to delete old backup records. + */ +class backup_cleanup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskbackupcleanup', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $DB; + + $timenow = time(); + + // Delete old backup_controllers and logs. + $loglifetime = get_config('backup', 'loglifetime'); + if (!empty($loglifetime)) { // Value in days. + $loglifetime = $timenow - ($loglifetime * 3600 * 24); + // Delete child records from backup_logs. + $DB->execute("DELETE FROM {backup_logs} + WHERE EXISTS ( + SELECT 'x' + FROM {backup_controllers} bc + WHERE bc.backupid = {backup_logs}.backupid + AND bc.timecreated < ?)", array($loglifetime)); + // Delete records from backup_controllers. + $DB->execute("DELETE FROM {backup_controllers} + WHERE timecreated < ?", array($loglifetime)); + } + + } + +} diff --git a/lib/classes/task/badges_cron_task.php b/lib/classes/task/badges_cron_task.php new file mode 100644 index 00000000000..d5aaee0bc4d --- /dev/null +++ b/lib/classes/task/badges_cron_task.php @@ -0,0 +1,51 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the badges cron. + */ +class badges_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskbadgescron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + // Run badges review cron. + require_once($CFG->dirroot . '/badges/cron.php'); + badge_cron(); + } + +} diff --git a/lib/classes/task/blog_cron_task.php b/lib/classes/task/blog_cron_task.php new file mode 100644 index 00000000000..94df72dba05 --- /dev/null +++ b/lib/classes/task/blog_cron_task.php @@ -0,0 +1,67 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the blog cron. + */ +class blog_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskblogcron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG, $DB; + + $timenow = time(); + // Run external blog cron if needed. + if (!empty($CFG->enableblogs) && $CFG->useexternalblogs) { + require_once($CFG->dirroot . '/blog/lib.php'); + $sql = "timefetched < ? OR timefetched = 0"; + $externalblogs = $DB->get_records_select('blog_external', $sql, array($timenow - $CFG->externalblogcrontime)); + + foreach ($externalblogs as $eb) { + blog_sync_external_entries($eb); + } + } + // Run blog associations cleanup. + if (!empty($CFG->enableblogs) && $CFG->useblogassociations) { + require_once($CFG->dirroot . '/blog/lib.php'); + // Delete entries whose contextids no longer exists. + $DB->delete_records_select('blog_association', 'contextid NOT IN (SELECT id FROM {context})'); + } + + } + +} diff --git a/lib/classes/task/cache_cleanup_task.php b/lib/classes/task/cache_cleanup_task.php new file mode 100644 index 00000000000..e824867d379 --- /dev/null +++ b/lib/classes/task/cache_cleanup_task.php @@ -0,0 +1,50 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to delete old cache records. + */ +class cache_cleanup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskcachecleanup', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + // Remove expired cache flags. + gc_cache_flags(); + + } + +} diff --git a/lib/classes/task/cache_cron_task.php b/lib/classes/task/cache_cron_task.php new file mode 100644 index 00000000000..623d9b179ab --- /dev/null +++ b/lib/classes/task/cache_cron_task.php @@ -0,0 +1,48 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the cache cron. + */ +class cache_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskcachecron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + \cache_helper::cron(); + } + +} diff --git a/lib/classes/task/calendar_cron_task.php b/lib/classes/task/calendar_cron_task.php new file mode 100644 index 00000000000..54516f61a30 --- /dev/null +++ b/lib/classes/task/calendar_cron_task.php @@ -0,0 +1,52 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the calendar cron. + */ +class calendar_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskcalendarcron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + // Run calendar cron. + require_once("{$CFG->dirroot}/calendar/lib.php"); + calendar_cron(); + } + +} diff --git a/lib/classes/task/check_for_updates_task.php b/lib/classes/task/check_for_updates_task.php new file mode 100644 index 00000000000..e06e21a13e7 --- /dev/null +++ b/lib/classes/task/check_for_updates_task.php @@ -0,0 +1,54 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the registration cron. + */ +class check_for_updates_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskcheckforupdates', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + // If enabled, fetch information about available updates and eventually notify site admins. + if (empty($CFG->disableupdatenotifications)) { + $updateschecker = \core\update\checker::instance(); + $updateschecker->cron(); + } + + } + +} diff --git a/lib/classes/task/completion_cron_task.php b/lib/classes/task/completion_cron_task.php new file mode 100644 index 00000000000..b34860a8014 --- /dev/null +++ b/lib/classes/task/completion_cron_task.php @@ -0,0 +1,54 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the completion cron. + */ +class completion_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskcompletioncron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + if ($CFG->enablecompletion) { + // Completion cron. + require_once($CFG->dirroot.'/completion/cron.php'); + completion_cron(); + } + } + +} diff --git a/lib/classes/task/context_cleanup_task.php b/lib/classes/task/context_cleanup_task.php new file mode 100644 index 00000000000..35239b0f1c0 --- /dev/null +++ b/lib/classes/task/context_cleanup_task.php @@ -0,0 +1,54 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core\task; + +/** + * Simple task to delete old context records. + */ +class context_cleanup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskcontextcleanup', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + // Context maintenance stuff. + \context_helper::cleanup_instances(); + mtrace(' Cleaned up context instances'); + \context_helper::build_all_paths(false); + // If you suspect that the context paths are somehow corrupt + // replace the line below with: context_helper::build_all_paths(true). + } + +} diff --git a/lib/classes/task/create_contexts_task.php b/lib/classes/task/create_contexts_task.php new file mode 100644 index 00000000000..38d6779d6c9 --- /dev/null +++ b/lib/classes/task/create_contexts_task.php @@ -0,0 +1,50 @@ +. + +/** + * Scheduled task class. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to create missing contexts at all levels. + */ +class create_contexts_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskcreatecontexts', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + // Make sure all context instances are properly created - they may be required in auth, enrol, etc. + \context_helper::create_instances(); + mtrace(' Created missing context instances'); + } + +} diff --git a/lib/classes/task/delete_incomplete_users_task.php b/lib/classes/task/delete_incomplete_users_task.php new file mode 100644 index 00000000000..0fb84b822c0 --- /dev/null +++ b/lib/classes/task/delete_incomplete_users_task.php @@ -0,0 +1,69 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to delete user accounts for users who have not completed their profile in time. + */ +class delete_incomplete_users_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskdeleteincompleteusers', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG, $DB; + + $timenow = time(); + + // Delete users who haven't completed profile within required period. + if (!empty($CFG->deleteincompleteusers)) { + $cuttime = $timenow - ($CFG->deleteincompleteusers * 3600); + $rs = $DB->get_recordset_sql ("SELECT * + FROM {user} + WHERE confirmed = 1 AND lastaccess > 0 + AND lastaccess < ? AND deleted = 0 + AND (lastname = '' OR firstname = '' OR email = '')", + array($cuttime)); + foreach ($rs as $user) { + if (isguestuser($user) or is_siteadmin($user)) { + continue; + } + delete_user($user); + mtrace(" Deleted not fully setup user $user->username ($user->id)"); + } + $rs->close(); + } + } + +} diff --git a/lib/classes/task/delete_logs_task.php b/lib/classes/task/delete_logs_task.php new file mode 100644 index 00000000000..d25104eace6 --- /dev/null +++ b/lib/classes/task/delete_logs_task.php @@ -0,0 +1,58 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to delete old log records. + */ +class delete_logs_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskdeletelogs', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG, $DB; + + $timenow = time(); + + // Delete old logs to save space. + // Value in days. + if (!empty($CFG->loglifetime)) { + $loglifetime = $timenow - ($CFG->loglifetime * 3600 * 24); + $DB->delete_records_select("log", "time < ?", array($loglifetime)); + } + + } + +} diff --git a/lib/classes/task/delete_unconfirmed_users_task.php b/lib/classes/task/delete_unconfirmed_users_task.php new file mode 100644 index 00000000000..22c5076568e --- /dev/null +++ b/lib/classes/task/delete_unconfirmed_users_task.php @@ -0,0 +1,65 @@ +. + +/** + * Scheduled task abstract class. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to delete user accounts for users who have not confirmed in time. + */ +class delete_unconfirmed_users_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskdeleteunconfirmedusers', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG, $DB; + + $timenow = time(); + + // Delete users who haven't confirmed within required period. + if (!empty($CFG->deleteunconfirmed)) { + $cuttime = $timenow - ($CFG->deleteunconfirmed * 3600); + $rs = $DB->get_recordset_sql ("SELECT * + FROM {user} + WHERE confirmed = 0 AND firstaccess > 0 + AND firstaccess < ?", array($cuttime)); + foreach ($rs as $user) { + delete_user($user); // We MUST delete user properly first. + $DB->delete_records('user', array('id' => $user->id)); // This is a bloody hack, but it might work. + mtrace(" Deleted unconfirmed user for ".fullname($user, true)." ($user->id)"); + } + $rs->close(); + } + } + +} diff --git a/lib/classes/task/events_cron_task.php b/lib/classes/task/events_cron_task.php new file mode 100644 index 00000000000..07f8baef914 --- /dev/null +++ b/lib/classes/task/events_cron_task.php @@ -0,0 +1,48 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the events cron. + */ +class events_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskeventscron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + events_cron(); + } + +} diff --git a/lib/classes/task/file_temp_cleanup_task.php b/lib/classes/task/file_temp_cleanup_task.php new file mode 100644 index 00000000000..5867ce0edcf --- /dev/null +++ b/lib/classes/task/file_temp_cleanup_task.php @@ -0,0 +1,77 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to delete temp files older than 1 week. + */ +class file_temp_cleanup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('tasktempfilecleanup', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + $tmpdir = $CFG->tempdir; + // Default to last weeks time. + $time = strtotime('-1 week'); + + $dir = new \RecursiveDirectoryIterator($tmpdir); + // Show all child nodes prior to their parent. + $iter = new \RecursiveIteratorIterator($dir, \RecursiveIteratorIterator::CHILD_FIRST); + + for ($iter->rewind(); $iter->valid(); $iter->next()) { + $node = $iter->getRealPath(); + if (!is_readable($node)) { + continue; + } + // Check if file or directory is older than the given time. + if ($iter->getMTime() < $time) { + if ($iter->isDir() && !$iter->isDot()) { + if (@rmdir($node) === false) { + mtrace("Failed removing directory '$node'."); + } + } + if ($iter->isFile()) { + if (@unlink($node) === false) { + mtrace("Failed removing file '$node'."); + } + } + } + } + + } + +} diff --git a/lib/classes/task/file_trash_cleanup_task.php b/lib/classes/task/file_trash_cleanup_task.php new file mode 100644 index 00000000000..bc817428d70 --- /dev/null +++ b/lib/classes/task/file_trash_cleanup_task.php @@ -0,0 +1,51 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the file trash cleanup cron. + */ +class file_trash_cleanup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskfiletrashcleanup', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + + // Cleanup file trash - not very important. + $fs = get_file_storage(); + $fs->cron(); + } + +} diff --git a/lib/classes/task/grade_cron_task.php b/lib/classes/task/grade_cron_task.php new file mode 100644 index 00000000000..063a8ef1cdf --- /dev/null +++ b/lib/classes/task/grade_cron_task.php @@ -0,0 +1,51 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the grade cron. + */ +class grade_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskgradecron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + require_once($CFG->libdir.'/gradelib.php'); + grade_cron(); + } + +} diff --git a/lib/classes/task/legacy_plugin_cron_task.php b/lib/classes/task/legacy_plugin_cron_task.php new file mode 100644 index 00000000000..b2d08733715 --- /dev/null +++ b/lib/classes/task/legacy_plugin_cron_task.php @@ -0,0 +1,168 @@ +. + +/** + * Scheduled task class. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run cron for all plugins. + * Note - this is only for plugins using the legacy cron method, + * plugins can also now just add their own scheduled tasks which is the preferred method. + */ +class legacy_plugin_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('tasklegacycron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG, $DB; + + $timenow = time(); + // Run the auth cron, if any before enrolments + // because it might add users that will be needed in enrol plugins. + $auths = get_enabled_auth_plugins(); + mtrace("Running auth crons if required..."); + foreach ($auths as $auth) { + $authplugin = get_auth_plugin($auth); + if (method_exists($authplugin, 'cron')) { + mtrace("Running cron for auth/$auth..."); + $authplugin->cron(); + if (!empty($authplugin->log)) { + mtrace($authplugin->log); + } + } + unset($authplugin); + } + + // It is very important to run enrol early + // because other plugins depend on correct enrolment info. + mtrace("Running enrol crons if required..."); + $enrols = enrol_get_plugins(true); + foreach ($enrols as $ename => $enrol) { + // Do this for all plugins, disabled plugins might want to cleanup stuff such as roles. + if (!$enrol->is_cron_required()) { + continue; + } + mtrace("Running cron for enrol_$ename..."); + $enrol->cron(); + $enrol->set_config('lastcron', time()); + } + + // Run all cron jobs for each module. + mtrace("Starting activity modules"); + get_mailer('buffer'); + if ($mods = $DB->get_records_select("modules", "cron > 0 AND ((? - lastcron) > cron) AND visible = 1", array($timenow))) { + foreach ($mods as $mod) { + $libfile = "$CFG->dirroot/mod/$mod->name/lib.php"; + if (file_exists($libfile)) { + include_once($libfile); + $cronfunction = $mod->name."_cron"; + if (function_exists($cronfunction)) { + mtrace("Processing module function $cronfunction ...\n", ''); + $predbqueries = null; + $predbqueries = $DB->perf_get_queries(); + $pretime = microtime(1); + if ($cronfunction()) { + $DB->set_field("modules", "lastcron", $timenow, array("id" => $mod->id)); + } + if (isset($predbqueries)) { + mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries"); + mtrace("... used " . (microtime(1) - $pretime) . " seconds"); + } + // Reset possible changes by modules to time_limit. MDL-11597. + \core_php_time_limit::raise(); + mtrace("done."); + } + } + } + } + get_mailer('close'); + mtrace("Finished activity modules"); + + mtrace("Starting blocks"); + if ($blocks = $DB->get_records_select("block", "cron > 0 AND ((? - lastcron) > cron) AND visible = 1", array($timenow))) { + // We will need the base class. + require_once($CFG->dirroot.'/blocks/moodleblock.class.php'); + foreach ($blocks as $block) { + $blockfile = $CFG->dirroot.'/blocks/'.$block->name.'/block_'.$block->name.'.php'; + if (file_exists($blockfile)) { + require_once($blockfile); + $classname = '\\block_'.$block->name; + $blockobj = new $classname; + if (method_exists($blockobj, 'cron')) { + mtrace("Processing cron function for ".$block->name.'....', ''); + if ($blockobj->cron()) { + $DB->set_field('block', 'lastcron', $timenow, array('id' => $block->id)); + } + // Reset possible changes by blocks to time_limit. MDL-11597. + \core_php_time_limit::raise(); + mtrace('done.'); + } + } + + } + } + mtrace('Finished blocks'); + + mtrace('Starting admin reports'); + cron_execute_plugin_type('report'); + mtrace('Finished admin reports'); + + mtrace('Starting course reports'); + cron_execute_plugin_type('coursereport'); + mtrace('Finished course reports'); + + // Run gradebook import/export/report cron. + mtrace('Starting gradebook plugins'); + cron_execute_plugin_type('gradeimport'); + cron_execute_plugin_type('gradeexport'); + cron_execute_plugin_type('gradereport'); + mtrace('Finished gradebook plugins'); + + // All other plugins. + cron_execute_plugin_type('message', 'message plugins'); + cron_execute_plugin_type('filter', 'filters'); + cron_execute_plugin_type('editor', 'editors'); + cron_execute_plugin_type('format', 'course formats'); + cron_execute_plugin_type('profilefield', 'profile fields'); + cron_execute_plugin_type('webservice', 'webservices'); + cron_execute_plugin_type('repository', 'repository plugins'); + cron_execute_plugin_type('qbehaviour', 'question behaviours'); + cron_execute_plugin_type('qformat', 'question import/export formats'); + cron_execute_plugin_type('qtype', 'question types'); + cron_execute_plugin_type('plagiarism', 'plagiarism plugins'); + cron_execute_plugin_type('theme', 'themes'); + cron_execute_plugin_type('tool', 'admin tools'); + cron_execute_plugin_type('local', 'local plugins'); + } + +} diff --git a/lib/classes/task/manager.php b/lib/classes/task/manager.php new file mode 100644 index 00000000000..16726e86264 --- /dev/null +++ b/lib/classes/task/manager.php @@ -0,0 +1,621 @@ +. + +/** + * Scheduled and adhoc task management. + * + * @package core + * @category task + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +define('CORE_TASK_TASKS_FILENAME', 'db/tasks.php'); +/** + * Collection of task related methods. + * + * Some locking rules for this class: + * All changes to scheduled tasks must be protected with both - the global cron lock and the lock + * for the specific scheduled task (in that order). Locks must be released in the reverse order. + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class manager { + + /** + * Given a component name, will load the list of tasks in the db/tasks.php file for that component. + * + * @param string $componentname - The name of the component to fetch the tasks for. + * @return array(core\task\scheduled_task) - List of scheduled tasks for this component. + */ + public static function load_default_scheduled_tasks_for_component($componentname) { + $dir = \core_component::get_component_directory($componentname); + + if (!$dir) { + return array(); + } + + $file = $dir . '/' . CORE_TASK_TASKS_FILENAME; + if (!file_exists($file)) { + return array(); + } + + $tasks = null; + require_once($file); + + if (!isset($tasks)) { + return array(); + } + + $scheduledtasks = array(); + + foreach ($tasks as $task) { + $record = (object) $task; + $scheduledtask = self::scheduled_task_from_record($record); + $scheduledtask->set_component($componentname); + $scheduledtasks[] = $scheduledtask; + } + + return $scheduledtasks; + } + + /** + * Update the database to contain a list of scheduled task for a component. + * The list of scheduled tasks is taken from @load_scheduled_tasks_for_component. + * Will throw exceptions for any errors. + * + * @param string $componentname - The frankenstyle component name. + */ + public static function reset_scheduled_tasks_for_component($componentname) { + global $DB; + $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); + + if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) { + throw new \moodle_exception('locktimeout'); + } + $tasks = self::load_default_scheduled_tasks_for_component($componentname); + + $tasklocks = array(); + foreach ($tasks as $task) { + $classname = get_class($task); + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + + if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) { + // Could not get all the locks required - release all locks and fail. + foreach ($tasklocks as $tasklock) { + $tasklock->release(); + } + $cronlock->release(); + throw new \moodle_exception('locktimeout'); + } + $tasklocks[] = $lock; + } + + // Got a lock on cron and all the tasks for this component, time to reset the config. + $DB->delete_records('task_scheduled', array('component' => $componentname)); + foreach ($tasks as $task) { + $record = self::record_from_scheduled_task($task); + $DB->insert_record('task_scheduled', $record); + } + + // Release the locks. + foreach ($tasklocks as $tasklock) { + $tasklock->release(); + } + + $cronlock->release(); + } + + /** + * Queue an adhoc task to run in the background. + * + * @param \core\task\adhoc_task $task - The new adhoc task information to store. + * @return boolean - True if the config was saved. + */ + public static function queue_adhoc_task(adhoc_task $task) { + global $DB; + + $record = self::record_from_adhoc_task($task); + // Schedule it immediately. + $record->nextruntime = time() - 1; + $result = $DB->insert_record('task_adhoc', $record); + + return $result; + } + + /** + * Change the default configuration for a scheduled task. + * The list of scheduled tasks is taken from {@link load_scheduled_tasks_for_component}. + * + * @param \core\task\scheduled_task $task - The new scheduled task information to store. + * @return boolean - True if the config was saved. + */ + public static function configure_scheduled_task(scheduled_task $task) { + global $DB; + $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); + + if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10, 60)) { + throw new \moodle_exception('locktimeout'); + } + + $classname = get_class($task); + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + if (!$lock = $cronlockfactory->get_lock($classname, 10, 60)) { + $cronlock->release(); + throw new \moodle_exception('locktimeout'); + } + + $original = $DB->get_record('task_scheduled', array('classname'=>$classname), 'id', MUST_EXIST); + + $record = self::record_from_scheduled_task($task); + $record->id = $original->id; + $record->nextruntime = $task->get_next_scheduled_time(); + $result = $DB->update_record('task_scheduled', $record); + + $lock->release(); + $cronlock->release(); + return $result; + } + + /** + * Utility method to create a DB record from a scheduled task. + * + * @param \core\task\scheduled_task $task + * @return stdClass + */ + public static function record_from_scheduled_task($task) { + $record = new \stdClass(); + $record->classname = get_class($task); + if (strpos($record->classname, '\\') !== 0) { + $record->classname = '\\' . $record->classname; + } + $record->component = $task->get_component(); + $record->blocking = $task->is_blocking(); + $record->customised = $task->is_customised(); + $record->lastruntime = $task->get_last_run_time(); + $record->nextruntime = $task->get_next_run_time(); + $record->faildelay = $task->get_fail_delay(); + $record->hour = $task->get_hour(); + $record->minute = $task->get_minute(); + $record->day = $task->get_day(); + $record->dayofweek = $task->get_day_of_week(); + $record->month = $task->get_month(); + + return $record; + } + + /** + * Utility method to create a DB record from an adhoc task. + * + * @param \core\task\adhoc_task $task + * @return stdClass + */ + public static function record_from_adhoc_task($task) { + $record = new \stdClass(); + $record->classname = get_class($task); + if (strpos($record->classname, '\\') !== 0) { + $record->classname = '\\' . $record->classname; + } + $record->id = $task->get_id(); + $record->component = $task->get_component(); + $record->blocking = $task->is_blocking(); + $record->nextruntime = $task->get_next_run_time(); + $record->faildelay = $task->get_fail_delay(); + $record->customdata = $task->get_custom_data(); + + return $record; + } + + /** + * Utility method to create an adhoc task from a DB record. + * + * @param stdClass $record + * @return \core\task\adhoc_task + */ + public static function adhoc_task_from_record($record) { + $classname = $record->classname; + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + if (!class_exists($classname)) { + return false; + } + $task = new $classname; + if (isset($record->nextruntime)) { + $task->set_next_run_time($record->nextruntime); + } + if (isset($record->id)) { + $task->set_id($record->id); + } + if (isset($record->component)) { + $task->set_component($record->component); + } + $task->set_blocking(!empty($record->blocking)); + if (isset($record->faildelay)) { + $task->set_fail_delay($record->faildelay); + } + if (isset($record->customdata)) { + $task->set_custom_data($record->customdata); + } + + return $task; + } + + /** + * Utility method to create a task from a DB record. + * + * @param stdClass $record + * @return \core\task\scheduled_task + */ + public static function scheduled_task_from_record($record) { + $classname = $record->classname; + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + if (!class_exists($classname)) { + return false; + } + $task = new $classname; + if (isset($record->lastruntime)) { + $task->set_last_run_time($record->lastruntime); + } + if (isset($record->nextruntime)) { + $task->set_next_run_time($record->nextruntime); + } + if (isset($record->customised)) { + $task->set_customised($record->customised); + } + if (isset($record->component)) { + $task->set_component($record->component); + } + $task->set_blocking(!empty($record->blocking)); + if (isset($record->minute)) { + $task->set_minute($record->minute); + } + if (isset($record->hour)) { + $task->set_hour($record->hour); + } + if (isset($record->day)) { + $task->set_day($record->day); + } + if (isset($record->month)) { + $task->set_month($record->month); + } + if (isset($record->dayofweek)) { + $task->set_day_of_week($record->dayofweek); + } + if (isset($record->faildelay)) { + $task->set_fail_delay($record->faildelay); + } + + return $task; + } + + /** + * Given a component name, will load the list of tasks from the scheduled_tasks table for that component. + * Do not execute tasks loaded from this function - they have not been locked. + * @param string $componentname - The name of the component to load the tasks for. + * @return array(core\task\scheduled_task) + */ + public static function load_scheduled_tasks_for_component($componentname) { + global $DB; + + $tasks = array(); + // We are just reading - so no locks required. + $records = $DB->get_records('task_scheduled', array('componentname' => $componentname), 'classname', '*', IGNORE_MISSING); + foreach ($records as $record) { + $task = self::scheduled_task_from_record($record); + $tasks[] = $task; + } + + return $tasks; + } + + /** + * This function load the scheduled task details for a given classname. + * + * @return core\task\scheduled_task or false + */ + public static function get_scheduled_task($classname) { + global $DB; + + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + // We are just reading - so no locks required. + $record = $DB->get_record('task_scheduled', array('classname'=>$classname), '*', IGNORE_MISSING); + if (!$record) { + return false; + } + return self::scheduled_task_from_record($record); + } + + /** + * This function load the default scheduled task details for a given classname. + * + * @return core\task\scheduled_task or false + */ + public static function get_default_scheduled_task($classname) { + $task = self::get_scheduled_task($classname); + + $componenttasks = self::load_default_scheduled_tasks_for_component($task->get_component()); + + foreach ($componenttasks as $componenttask) { + if (get_class($componenttask) == get_class($task)) { + return $componenttask; + } + } + + return false; + } + + /** + * This function will return a list of all the scheduled tasks that exist in the database. + * + * @return array(core\task\scheduled_task) or null + */ + public static function get_all_scheduled_tasks() { + global $DB; + + $records = $DB->get_records('task_scheduled', null, 'component, classname', '*', IGNORE_MISSING); + $tasks = array(); + + foreach ($records as $record) { + $task = self::scheduled_task_from_record($record); + $tasks[] = $task; + } + + return $tasks; + } + + /** + * This function will dispatch the next adhoc task in the queue. The task will be handed out + * with an open lock - possibly on the entire cron process. Make sure you call either + * {@link adhoc_task_failed} or {@link adhoc_task_complete} to release the lock and reschedule the task. + * + * @return core\task\adhoc_task or null + */ + public static function get_next_adhoc_task($timestart) { + global $DB; + $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); + + if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) { + throw new \moodle_exception('locktimeout'); + } + + $where = '(nextruntime IS NULL OR nextruntime < :timestart1)'; + $params = array('timestart1' => $timestart); + $records = $DB->get_records_select('task_adhoc', $where, $params); + + foreach ($records as $record) { + + if ($lock = $cronlockfactory->get_lock('adhoc_' . $record->id, 10)) { + $classname = '\\' . $record->classname; + $task = self::adhoc_task_from_record($record); + + $task->set_lock($lock); + if (!$task->is_blocking()) { + $cronlock->release(); + } else { + $task->set_cron_lock($cronlock); + } + return $task; + } + } + + // No tasks. + $cronlock->release(); + return null; + } + + /** + * This function will dispatch the next scheduled task in the queue. The task will be handed out + * with an open lock - possibly on the entire cron process. Make sure you call either + * {@link scheduled_task_failed} or {@link scheduled_task_complete} to release the lock and reschedule the task. + * + * @param int $timestart - The start of the cron process - do not repeat any tasks that have been run more recently than this. + * @return core\task\scheduled_task or null + */ + public static function get_next_scheduled_task($timestart) { + global $DB; + $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron'); + + if (!$cronlock = $cronlockfactory->get_lock('core_cron', 10)) { + throw new \moodle_exception('locktimeout'); + } + + $where = '(lastruntime IS NULL OR lastruntime < :timestart1) AND (nextruntime IS NULL OR nextruntime < :timestart2)'; + $params = array('timestart1' => $timestart, 'timestart2' => $timestart); + $records = $DB->get_records_select('task_scheduled', $where, $params); + + foreach ($records as $record) { + + if ($lock = $cronlockfactory->get_lock(($record->classname), 10)) { + $classname = '\\' . $record->classname; + $task = self::scheduled_task_from_record($record); + + $task->set_lock($lock); + if (!$task->is_blocking()) { + $cronlock->release(); + } else { + $task->set_cron_lock($cronlock); + } + return $task; + } + } + + // No tasks. + $cronlock->release(); + return null; + } + + /** + * This function indicates that an adhoc task was not completed succesfully and should be retried. + * + * @param core\task\adhoc_task $task + */ + public static function adhoc_task_failed(adhoc_task $task) { + global $DB; + $delay = $task->get_fail_delay(); + + // Reschedule task with exponential fall off for failing tasks. + if (empty($delay)) { + $delay = 60; + } else { + $delay *= 2; + } + + // Max of 24 hour delay. + if ($delay > 86400) { + $delay = 86400; + } + + $classname = get_class($task); + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + + $task->set_next_run_time(time() + $delay); + $task->set_fail_delay($delay); + $record = self::record_from_adhoc_task($task); + $DB->update_record('task_adhoc', $record); + + if ($task->is_blocking()) { + $task->get_cron_lock()->release(); + } + $task->get_lock()->release(); + } + + /** + * This function indicates that an adhoc task was completed succesfully. + * + * @param core\task\adhoc_task $task + */ + public static function adhoc_task_complete(adhoc_task $task) { + global $DB; + + // Delete the adhoc task record - it is finished. + $DB->delete_records('task_adhoc', array('id' => $task->get_id())); + + // Reschedule and then release the locks. + if ($task->is_blocking()) { + $task->get_cron_lock()->release(); + } + $task->get_lock()->release(); + } + + /** + * This function indicates that a scheduled task was not completed succesfully and should be retried. + * + * @param core\task\scheduled_task $task + */ + public static function scheduled_task_failed(scheduled_task $task) { + global $DB; + + $delay = $task->get_fail_delay(); + + // Reschedule task with exponential fall off for failing tasks. + if (empty($delay)) { + $delay = 60; + } else { + $delay *= 2; + } + + // Max of 24 hour delay. + if ($delay > 86400) { + $delay = 86400; + } + + $classname = get_class($task); + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + + $record = $DB->get_record('task_scheduled', array('classname' => $classname)); + $record->nextruntime = time() + $delay; + $record->faildelay = $delay; + $DB->update_record('task_scheduled', $record); + + if ($task->is_blocking()) { + $task->get_cron_lock()->release(); + } + $task->get_lock()->release(); + } + + /** + * This function indicates that a scheduled task was completed succesfully and should be rescheduled. + * + * @param core\task\scheduled_task $task + */ + public static function scheduled_task_complete(scheduled_task $task) { + global $DB; + + $classname = get_class($task); + if (strpos($classname, '\\') !== 0) { + $classname = '\\' . $classname; + } + $record = $DB->get_record('task_scheduled', array('classname' => $classname)); + if ($record) { + $record->lastruntime = time(); + $record->faildelay = 0; + $record->nextruntime = $task->get_next_scheduled_time(); + + $DB->update_record('task_scheduled', $record); + } + + // Reschedule and then release the locks. + if ($task->is_blocking()) { + $task->get_cron_lock()->release(); + } + $task->get_lock()->release(); + } + + /** + * This function is used to indicate that any long running cron processes should exit at the + * next opportunity and restart. This is because something (e.g. DB changes) has changed and + * the static caches may be stale. + */ + public static function clear_static_caches() { + global $DB; + // Do not use get/set config here because the caches cannot be relied on. + $record = $DB->get_record('config', array('name'=>'scheduledtaskreset')); + if ($record) { + $record->value = time(); + $DB->update_record('config', $record); + } else { + $record = new \stdClass(); + $record->name = 'scheduledtaskreset'; + $record->value = time(); + $DB->insert_record('config', $record); + } + } + + /** + * Return true if the static caches have been cleared since $starttime. + * @param int $starttime The time this process started. + * @return boolean True if static caches need reseting. + */ + public static function static_caches_cleared_since($starttime) { + global $DB; + $record = $DB->get_record('config', array('name'=>'scheduledtaskreset')); + return $record && intval($record->value) > $starttime; + } +} diff --git a/lib/classes/task/messaging_cleanup_task.php b/lib/classes/task/messaging_cleanup_task.php new file mode 100644 index 00000000000..4debce36c0d --- /dev/null +++ b/lib/classes/task/messaging_cleanup_task.php @@ -0,0 +1,58 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to delete old messaging records. + */ +class messaging_cleanup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskmessagingcleanup', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG, $DB; + + $timenow = time(); + + // Cleanup messaging. + if (!empty($CFG->messagingdeletereadnotificationsdelay)) { + $notificationdeletetime = $timenow - $CFG->messagingdeletereadnotificationsdelay; + $params = array('notificationdeletetime' => $notificationdeletetime); + $DB->delete_records_select('message_read', 'notification=1 AND timeread<:notificationdeletetime', $params); + } + + } + +} diff --git a/lib/classes/task/password_reset_cleanup_task.php b/lib/classes/task/password_reset_cleanup_task.php new file mode 100644 index 00000000000..21e7a58ace9 --- /dev/null +++ b/lib/classes/task/password_reset_cleanup_task.php @@ -0,0 +1,58 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to delete old password reset records. + */ +class password_reset_cleanup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskpasswordresetcleanup', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $DB, $CFG; + + // Cleanup user password reset records. + // Delete any reset request records which are expired by more than a day. + // (We keep recently expired requests around so we can give a different error msg to users who + // are trying to user a recently expired reset attempt). + $pwresettime = isset($CFG->pwresettime) ? $CFG->pwresettime : 1800; + $earliestvalid = time() - $pwresettime - DAYSECS; + $DB->delete_records_select('user_password_resets', "timerequested < ?", array($earliestvalid)); + mtrace(' Cleaned up old password reset records'); + + } + +} diff --git a/lib/classes/task/plagiarism_cron_task.php b/lib/classes/task/plagiarism_cron_task.php new file mode 100644 index 00000000000..b5010a891d1 --- /dev/null +++ b/lib/classes/task/plagiarism_cron_task.php @@ -0,0 +1,51 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the plagiarism cron. + */ +class plagiarism_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskplagiarismcron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + require_once($CFG->libdir.'/plagiarismlib.php'); + plagiarism_cron(); + } + +} diff --git a/lib/classes/task/portfolio_cron_task.php b/lib/classes/task/portfolio_cron_task.php new file mode 100644 index 00000000000..b44d28d51ee --- /dev/null +++ b/lib/classes/task/portfolio_cron_task.php @@ -0,0 +1,53 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the portfolio cron. + */ +class portfolio_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskportfoliocron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + if ($CFG->enableportfolios) { + require_once($CFG->libdir . '/portfoliolib.php'); + portfolio_cron(); + } + } + +} diff --git a/lib/classes/task/question_cron_task.php b/lib/classes/task/question_cron_task.php new file mode 100644 index 00000000000..d7d9cc5063b --- /dev/null +++ b/lib/classes/task/question_cron_task.php @@ -0,0 +1,53 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the question cron. + */ +class question_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskquestioncron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + // Run question bank clean-up. + require_once($CFG->libdir . '/questionlib.php'); + \question_bank::cron(); + + } + +} diff --git a/lib/classes/task/registration_cron_task.php b/lib/classes/task/registration_cron_task.php new file mode 100644 index 00000000000..b8ed0099741 --- /dev/null +++ b/lib/classes/task/registration_cron_task.php @@ -0,0 +1,52 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the registration cron. + */ +class registration_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskregistrationcron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php'); + $registrationmanager = new \registration_manager(); + $registrationmanager->cron(); + } + +} diff --git a/lib/classes/task/scheduled_task.php b/lib/classes/task/scheduled_task.php new file mode 100644 index 00000000000..8812c4255a1 --- /dev/null +++ b/lib/classes/task/scheduled_task.php @@ -0,0 +1,355 @@ +. + +/** + * Scheduled task abstract class. + * + * @package core + * @category task + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Abstract class defining a scheduled task. + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class scheduled_task extends task_base { + + /** @var string $hour - Pattern to work out the valid hours */ + private $hour = '*'; + + /** @var string $minute - Pattern to work out the valid minutes */ + private $minute = '*'; + + /** @var string $day - Pattern to work out the valid days */ + private $day = '*'; + + /** @var string $month - Pattern to work out the valid months */ + private $month = '*'; + + /** @var string $dayofweek - Pattern to work out the valid dayofweek */ + private $dayofweek = '*'; + + /** @var int $lastruntime - When this task was last run */ + private $lastruntime = 0; + + /** @var boolean $customised - Has this task been changed from it's default schedule? */ + private $customised = false; + + /** + * Get the last run time for this scheduled task. + * @return int + */ + public function get_last_run_time() { + return $this->lastruntime; + } + + /** + * Set the last run time for this scheduled task. + * @return int + */ + public function set_last_run_time($lastruntime) { + $this->lastruntime = $lastruntime; + } + + /** + * Has this task been changed from it's default config? + * @return bool + */ + public function is_customised() { + return $this->customised; + } + + /** + * Has this task been changed from it's default config? + * @param bool + */ + public function set_customised($customised) { + $this->customised = $customised; + } + + /** + * Setter for $minute. + * @param string $minute + */ + public function set_minute($minute) { + $this->minute = $minute; + } + + /** + * Getter for $minute. + * @return string + */ + public function get_minute() { + return $this->minute; + } + + /** + * Setter for $hour. + * @param string $hour + */ + public function set_hour($hour) { + $this->hour = $hour; + } + + /** + * Getter for $hour. + * @return string + */ + public function get_hour() { + return $this->hour; + } + + /** + * Setter for $month. + * @param string $month + */ + public function set_month($month) { + $this->month = $month; + } + + /** + * Getter for $month. + * @return string + */ + public function get_month() { + return $this->month; + } + + /** + * Setter for $day. + * @param string $day + */ + public function set_day($day) { + $this->day = $day; + } + + /** + * Getter for $day. + * @return string + */ + public function get_day() { + return $this->day; + } + + /** + * Setter for $dayofweek. + * @param string $dayofweek + */ + public function set_day_of_week($dayofweek) { + $this->dayofweek = $dayofweek; + } + + /** + * Getter for $dayofweek. + * @return string + */ + public function get_day_of_week() { + return $this->dayofweek; + } + + /** + * Take a cron field definition and return an array of valid numbers with the range min-max. + * + * @param string $field - The field definition. + * @param int $min - The minimum allowable value. + * @param int $max - The maximum allowable value. + * @return array(int) + */ + public function eval_cron_field($field, $min, $max) { + // Cleanse the input. + $field = trim($field); + + // Format for a field is: + // := (/)(,) + // := int + // := || + // := * + // := int-int + // End of format BNF. + + // This function is complicated but is covered by unit tests. + $range = array(); + + $matches = array(); + preg_match_all('@[0-9]+|\*|,|/|-@', $field, $matches); + + $last = 0; + $inrange = false; + $instep = false; + + foreach ($matches[0] as $match) { + if ($match == '*') { + array_push($range, range($min, $max)); + } else if ($match == '/') { + $instep = true; + } else if ($match == '-') { + $inrange = true; + } else if (is_numeric($match)) { + if ($instep) { + $i = 0; + for ($i = 0; $i < count($range[count($range) - 1]); $i++) { + if (($i) % $match != 0) { + $range[count($range) - 1][$i] = -1; + } + } + $inrange = false; + } else if ($inrange) { + if (count($range)) { + $range[count($range) - 1] = range($last, $match); + } + $inrange = false; + } else { + if ($match >= $min && $match <= $max) { + array_push($range, $match); + } + $last = $match; + } + } + } + + // Flatten the result. + $result = array(); + foreach ($range as $r) { + if (is_array($r)) { + foreach ($r as $rr) { + if ($rr >= $min && $rr <= $max) { + $result[$rr] = 1; + } + } + } else if (is_numeric($r)) { + if ($r >= $min && $r <= $max) { + $result[$r] = 1; + } + } + } + $result = array_keys($result); + sort($result, SORT_NUMERIC); + return $result; + } + + /** + * Assuming $list is an ordered list of items, this function returns the item + * in the list that is greater than or equal to the current value (or 0). If + * no value is greater than or equal, this will return the first valid item in the list. + * If list is empty, this function will return 0. + * + * @param int $current The current value + * @param array(int) $list The list of valid items. + * @return int $next. + */ + private function next_in_list($current, $list) { + foreach ($list as $l) { + if ($l >= $current) { + return $l; + } + } + if (count($list)) { + return $list[0]; + } + + return 0; + } + + /** + * Calculate when this task should next be run based on the schedule. + * @return int $nextruntime. + */ + public function get_next_scheduled_time() { + $validminutes = $this->eval_cron_field($this->minute, 0, 59); + $validhours = $this->eval_cron_field($this->hour, 0, 23); + + $daysinmonth = date("t"); + $validdays = $this->eval_cron_field($this->day, 0, $daysinmonth); + $validdaysofweek = $this->eval_cron_field($this->dayofweek, 0, 7); + $validmonths = $this->eval_cron_field($this->month, 1, 12); + $nextvalidyear = date('Y'); + + $currentminute = date("i") + 1; + $currenthour = date("H"); + $currentday = date("j"); + $currentmonth = date("n"); + $currentdayofweek = date("w"); + + $nextvalidminute = $this->next_in_list($currentminute, $validminutes); + if ($nextvalidminute < $currentminute) { + $currenthour += 1; + } + $nextvalidhour = $this->next_in_list($currenthour, $validhours); + if ($nextvalidhour < $currenthour) { + $currentdayofweek += 1; + $currentday += 1; + } + $nextvaliddayofmonth = $this->next_in_list($currentday, $validdays); + $nextvaliddayofweek = $this->next_in_list($currentdayofweek, $validdaysofweek); + $daysincrementbymonth = $nextvaliddayofmonth - $currentday; + if ($nextvaliddayofmonth < $currentday) { + $daysincrementbymonth += $daysinmonth; + } + + $daysincrementbyweek = $nextvaliddayofweek - $currentdayofweek; + if ($nextvaliddayofweek < $currentdayofweek) { + $daysincrementbyweek += 7; + } + + // Special handling for dayofmonth vs dayofweek: + // if either field is * - use the other field + // otherwise - choose the soonest (see man 5 cron). + if ($this->dayofweek == '*') { + $daysincrement = $daysincrementbymonth; + } else if ($this->dayofmonth == '*') { + $daysincrement = $daysincrementbyweek; + } else { + // Take the smaller increment of days by month or week. + $daysincrement = $daysincrementbymonth; + if ($daysincrementbyweek < $daysincrementbymonth) { + $daysincrement = $daysincrementbyweek; + } + } + + $nextvaliddayofmonth = $currentday + $daysincrement; + if ($nextvaliddayofmonth > $daysinmonth) { + $currentmonth += 1; + $nextvaliddayofmonth -= $daysinmonth; + } + + $nextvalidmonth = $this->next_in_list($currentmonth, $validmonths); + if ($nextvalidmonth < $currentmonth) { + $nextvalidyear += 1; + } + + // Work out the next valid time. + $nexttime = mktime($nextvalidhour, + $nextvalidminute, + 0, + $nextvalidmonth, + $nextvaliddayofmonth, + $nextvalidyear); + + return $nexttime; + } + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public abstract function get_name(); + +} diff --git a/lib/classes/task/send_failed_login_notifications_task.php b/lib/classes/task/send_failed_login_notifications_task.php new file mode 100644 index 00000000000..1c71b6fdf1b --- /dev/null +++ b/lib/classes/task/send_failed_login_notifications_task.php @@ -0,0 +1,159 @@ +. + +/** + * Scheduled task class. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to send notifications about failed login attempts. + */ +class send_failed_login_notifications_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('tasksendfailedloginnotifications', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG, $DB, $OUTPUT; + + if (empty($CFG->notifyloginfailures)) { + return; + } + + $recip = get_users_from_config($CFG->notifyloginfailures, 'moodle/site:config'); + + if (empty($CFG->lastnotifyfailure)) { + $CFG->lastnotifyfailure = 0; + } + + // If it has been less than an hour, or if there are no recipients, don't execute. + if (((time() - HOURSECS) < $CFG->lastnotifyfailure) || !is_array($recip) || count($recip) <= 0) { + return; + } + + // We need to deal with the threshold stuff first. + if (empty($CFG->notifyloginthreshold)) { + $CFG->notifyloginthreshold = 10; // Default to something sensible. + } + + // Get all the IPs with more than notifyloginthreshold failures since lastnotifyfailure + // and insert them into the cache_flags temp table. + $sql = "SELECT ip, COUNT(*) + FROM {log} + WHERE module = 'login' AND action = 'error' + AND time > ? + GROUP BY ip + HAVING COUNT(*) >= ?"; + $params = array($CFG->lastnotifyfailure, $CFG->notifyloginthreshold); + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $iprec) { + if (!empty($iprec->ip)) { + set_cache_flag('login_failure_by_ip', $iprec->ip, '1', 0); + } + } + $rs->close(); + + // Get all the INFOs with more than notifyloginthreshold failures since lastnotifyfailure + // and insert them into the cache_flags temp table. + $sql = "SELECT info, count(*) + FROM {log} + WHERE module = 'login' AND action = 'error' + AND time > ? + GROUP BY info + HAVING count(*) >= ?"; + $params = array($CFG->lastnotifyfailure, $CFG->notifyloginthreshold); + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $inforec) { + if (!empty($inforec->info)) { + set_cache_flag('login_failure_by_info', $inforec->info, '1', 0); + } + } + $rs->close(); + + // Now, select all the login error logged records belonging to the ips and infos + // since lastnotifyfailure, that we have stored in the cache_flags table. + $sql = "SELECT * FROM ( + SELECT l.*, u.firstname, u.lastname + FROM {log} l + JOIN {cache_flags} cf ON l.ip = cf.name + LEFT JOIN {user} u ON l.userid = u.id + WHERE l.module = 'login' AND l.action = 'error' + AND l.time > ? + AND cf.flagtype = 'login_failure_by_ip' + UNION ALL + SELECT l.*, u.firstname, u.lastname + FROM {log} l + JOIN {cache_flags} cf ON l.info = cf.name + LEFT JOIN {user} u ON l.userid = u.id + WHERE l.module = 'login' AND l.action = 'error' + AND l.time > ? + AND cf.flagtype = 'login_failure_by_info') t + ORDER BY t.time DESC"; + $params = array($CFG->lastnotifyfailure, $CFG->lastnotifyfailure); + + // Init some variables. + $count = 0; + $messages = ''; + // Iterate over the logs recordset. + $rs = $DB->get_recordset_sql($sql, $params); + foreach ($rs as $log) { + $log->time = userdate($log->time); + $messages .= get_string('notifyloginfailuresmessage', '', $log) . "\n"; + $count++; + } + $rs->close(); + + // If we have something useful to report. + if ($count > 0) { + $site = get_site(); + $subject = get_string('notifyloginfailuressubject', '', format_string($site->fullname)); + // Calculate the complete body of notification (start + messages + end). + $body = get_string('notifyloginfailuresmessagestart', '', $CFG->wwwroot) . + (($CFG->lastnotifyfailure != 0) ? '('.userdate($CFG->lastnotifyfailure).')' : '')."\n\n" . + $messages . + "\n\n" . get_string('notifyloginfailuresmessageend', '', $CFG->wwwroot) . "\n\n"; + + // For each destination, send mail. + mtrace('Emailing admins about '. $count .' failed login attempts'); + foreach ($recip as $admin) { + // Emailing the admins directly rather than putting these through the messaging system. + email_to_user($admin, \core_user::get_support_user(), $subject, $body); + } + } + + // Update lastnotifyfailure with current time. + set_config('lastnotifyfailure', time()); + + // Finally, delete all the temp records we have created in cache_flags. + $DB->delete_records_select('cache_flags', "flagtype IN ('login_failure_by_ip', 'login_failure_by_info')"); + + } +} diff --git a/lib/classes/task/send_new_user_passwords_task.php b/lib/classes/task/send_new_user_passwords_task.php new file mode 100644 index 00000000000..f0926fc3290 --- /dev/null +++ b/lib/classes/task/send_new_user_passwords_task.php @@ -0,0 +1,77 @@ +. + +/** + * Scheduled task class. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to create accounts and send password emails for new users. + */ +class send_new_user_passwords_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('tasksendnewuserpasswords', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $DB; + + // Generate new password emails for users - ppl expect these generated asap. + if ($DB->count_records('user_preferences', array('name' => 'create_password', 'value' => '1'))) { + mtrace('Creating passwords for new users...'); + $usernamefields = get_all_user_name_fields(true, 'u'); + $newusers = $DB->get_recordset_sql("SELECT u.id as id, u.email, + $usernamefields, u.username, u.lang, + p.id as prefid + FROM {user} u + JOIN {user_preferences} p ON u.id=p.userid + WHERE p.name='create_password' AND p.value='1' AND + u.email !='' AND u.suspended = 0 AND + u.auth != 'nologin' AND u.deleted = 0"); + + // Note: we can not send emails to suspended accounts. + foreach ($newusers as $newuser) { + // Use a low cost factor when generating bcrypt hash otherwise + // hashing would be slow when emailing lots of users. Hashes + // will be automatically updated to a higher cost factor the first + // time the user logs in. + if (setnew_password_and_mail($newuser, true)) { + unset_user_preference('create_password', $newuser); + set_user_preference('auth_forcepasswordchange', 1, $newuser); + } else { + trigger_error("Could not create and mail new user password!"); + } + } + $newusers->close(); + } + } + +} diff --git a/lib/classes/task/session_cleanup_task.php b/lib/classes/task/session_cleanup_task.php new file mode 100644 index 00000000000..14337d38ded --- /dev/null +++ b/lib/classes/task/session_cleanup_task.php @@ -0,0 +1,57 @@ +. + +/** + * Scheduled task abstract class. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to cleanup user sessions from a scheduled task. + */ +class session_cleanup_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('tasksessioncleanup', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $DB; + + $timenow = time(); + + \core\session\manager::gc(); + + // Cleanup old session linked tokens. + // Deletes the session linked tokens that are over a day old. + $DB->delete_records_select('external_tokens', 'lastaccess < :onedayago AND tokentype = :tokentype', + array('onedayago' => $timenow - DAYSECS, 'tokentype' => EXTERNAL_TOKEN_EMBEDDED)); + } + +} diff --git a/lib/classes/task/stats_cron_task.php b/lib/classes/task/stats_cron_task.php new file mode 100644 index 00000000000..5ce0791ba79 --- /dev/null +++ b/lib/classes/task/stats_cron_task.php @@ -0,0 +1,71 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the stats cron. + */ +class stats_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('taskstatscron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + $timenow = time(); + // Run stats as at the end because they are known to take very long time on large sites. + if (!empty($CFG->enablestats) and empty($CFG->disablestatsprocessing)) { + require_once($CFG->dirroot.'/lib/statslib.php'); + // Check we're not before our runtime. + $timetocheck = stats_get_base_daily() + $CFG->statsruntimestarthour * 60 * 60 + $CFG->statsruntimestartminute * 60; + + if ($timenow > $timetocheck) { + // Process configured number of days as max (defaulting to 31). + $maxdays = empty($CFG->statsruntimedays) ? 31 : abs($CFG->statsruntimedays); + if (stats_cron_daily($maxdays)) { + if (stats_cron_weekly()) { + if (stats_cron_monthly()) { + stats_clean_old(); + } + } + } + \core_php_time_limit::raise(); + } else { + mtrace('Next stats run after:'. userdate($timetocheck)); + } + } + } + +} diff --git a/lib/classes/task/tag_cron_task.php b/lib/classes/task/tag_cron_task.php new file mode 100644 index 00000000000..cdedf29f4a8 --- /dev/null +++ b/lib/classes/task/tag_cron_task.php @@ -0,0 +1,53 @@ +. + +/** + * A scheduled task. + * + * @package core + * @copyright 2013 onwards Martin Dougiamas http://dougiamas.com + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Simple task to run the tag cron. + */ +class tag_cron_task extends scheduled_task { + + /** + * Get a descriptive name for this task (shown to admins). + * + * @return string + */ + public function get_name() { + return get_string('tasktagcron', 'admin'); + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public function execute() { + global $CFG; + + if (!empty($CFG->usetags)) { + require_once($CFG->dirroot.'/tag/lib.php'); + tag_cron(); + } + } + +} diff --git a/lib/classes/task/task_base.php b/lib/classes/task/task_base.php new file mode 100644 index 00000000000..b10107ee80e --- /dev/null +++ b/lib/classes/task/task_base.php @@ -0,0 +1,154 @@ +. + +/** + * Abstract class for common properties of scheduled_task and adhoc_task. + * + * @package core + * @category task + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace core\task; + +/** + * Abstract class for common properties of scheduled_task and adhoc_task. + * + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class task_base { + + /** @var \core\lock\lock $lock - The lock controlling this task. */ + private $lock = null; + + /** @var \core\lock\lock $cronlock - The lock controlling the entire cron process. */ + private $cronlock = null; + + /** @var $string $component - The component this task belongs to. */ + private $component = ''; + + /** @var bool $blocking - Does this task block the entire cron process. */ + private $blocking = false; + + /** @var int $faildelay - Exponentially increasing fail delay */ + private $faildelay = 0; + + /** @var int $nextruntime - When this task is due to run next */ + private $nextruntime = 0; + + /** + * Set the current lock for this task. + * @param \core\lock\lock $lock + */ + public function set_lock(\core\lock\lock $lock) { + $this->lock = $lock; + } + + /** + * Set the current lock for the entire cron process. + * @param \core\lock\lock $lock + */ + public function set_cron_lock(\core\lock\lock $lock) { + $this->cronlock = $lock; + } + + /** + * Get the current lock for this task. + * @return \core\lock\lock + */ + public function get_lock() { + return $this->lock; + } + + /** + * Get the last run time for this task. + * @return int + */ + public function get_next_run_time() { + return $this->nextruntime; + } + + /** + * Set the next run time for this task. + * @return int + */ + public function set_next_run_time($nextruntime) { + $this->nextruntime = $nextruntime; + } + + /** + * Get the current lock for the entire cron. + * @return \core\lock\lock + */ + public function get_cron_lock() { + return $this->cronlock; + } + + /** + * Setter for $blocking. + * @param bool $blocking + */ + public function set_blocking($blocking) { + $this->blocking = $blocking; + } + + /** + * Getter for $blocking. + * @return bool + */ + public function is_blocking() { + return $this->blocking; + } + + /** + * Setter for $component. + * @param string $component + */ + public function set_component($component) { + $this->component = $component; + } + + /** + * Getter for $component. + * @return string + */ + public function get_component() { + return $this->component; + } + + /** + * Setter for $faildelay. + * @param int $faildelay + */ + public function set_fail_delay($faildelay) { + $this->faildelay = $faildelay; + } + + /** + * Getter for $faildelay. + * @return int + */ + public function get_fail_delay() { + return $this->faildelay; + } + + /** + * Do the job. + * Throw exceptions on errors (the job will be retried). + */ + public abstract function execute(); +} diff --git a/lib/cronlib.php b/lib/cronlib.php index b3537bb925b..4312ba6900c 100644 --- a/lib/cronlib.php +++ b/lib/cronlib.php @@ -40,7 +40,6 @@ function cron_run() { } require_once($CFG->libdir.'/adminlib.php'); - require_once($CFG->libdir.'/gradelib.php'); if (!empty($CFG->showcronsql)) { $DB->set_debug(true); @@ -62,443 +61,68 @@ function cron_run() { $timenow = time(); mtrace("Server Time: ".date('r', $timenow)."\n\n"); - - // Run cleanup core cron jobs, but not every time since they aren't too important. - // These don't have a timer to reduce load, so we'll use a random number - // to randomly choose the percentage of times we should run these jobs. - $random100 = rand(0,100); - if ($random100 < 20) { // Approximately 20% of the time. - mtrace("Running clean-up tasks..."); + // Run all scheduled tasks. + while (!\core\task\manager::static_caches_cleared_since($timenow) && + $task = \core\task\manager::get_next_scheduled_task($timenow)) { + mtrace("Execute scheduled task: " . $task->get_name()); cron_trace_time_and_memory(); - - // Delete users who haven't confirmed within required period - if (!empty($CFG->deleteunconfirmed)) { - $cuttime = $timenow - ($CFG->deleteunconfirmed * 3600); - $rs = $DB->get_recordset_sql ("SELECT * - FROM {user} - WHERE confirmed = 0 AND firstaccess > 0 - AND firstaccess < ?", array($cuttime)); - foreach ($rs as $user) { - delete_user($user); // we MUST delete user properly first - $DB->delete_records('user', array('id'=>$user->id)); // this is a bloody hack, but it might work - mtrace(" Deleted unconfirmed user for ".fullname($user, true)." ($user->id)"); + $predbqueries = null; + $predbqueries = $DB->perf_get_queries(); + $pretime = microtime(1); + try { + $task->execute(); + if (isset($predbqueries)) { + mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries"); + mtrace("... used " . (microtime(1) - $pretime) . " seconds"); } - $rs->close(); - } - - - // Delete users who haven't completed profile within required period - if (!empty($CFG->deleteincompleteusers)) { - $cuttime = $timenow - ($CFG->deleteincompleteusers * 3600); - $rs = $DB->get_recordset_sql ("SELECT * - FROM {user} - WHERE confirmed = 1 AND lastaccess > 0 - AND lastaccess < ? AND deleted = 0 - AND (lastname = '' OR firstname = '' OR email = '')", - array($cuttime)); - foreach ($rs as $user) { - if (isguestuser($user) or is_siteadmin($user)) { - continue; - } - delete_user($user); - mtrace(" Deleted not fully setup user $user->username ($user->id)"); + mtrace("Scheduled task complete: " . $task->get_name()); + \core\task\manager::scheduled_task_complete($task); + } catch (Exception $e) { + if ($DB && $DB->is_transaction_started()) { + error_log('Database transaction aborted automatically in ' . get_class($task)); + $DB->force_transaction_rollback(); } - $rs->close(); + if (isset($predbqueries)) { + mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries"); + mtrace("... used " . (microtime(1) - $pretime) . " seconds"); + } + mtrace("Scheduled task failed: " . $task->get_name() . "," . $e->getMessage()); + \core\task\manager::scheduled_task_failed($task); } - - - // Delete old logs to save space (this might need a timer to slow it down...) - if (!empty($CFG->loglifetime)) { // value in days - $loglifetime = $timenow - ($CFG->loglifetime * 3600 * 24); - $DB->delete_records_select("log", "time < ?", array($loglifetime)); - mtrace(" Deleted old log records"); - } - - - // Delete old backup_controllers and logs. - $loglifetime = get_config('backup', 'loglifetime'); - if (!empty($loglifetime)) { // Value in days. - $loglifetime = $timenow - ($loglifetime * 3600 * 24); - // Delete child records from backup_logs. - $DB->execute("DELETE FROM {backup_logs} - WHERE EXISTS ( - SELECT 'x' - FROM {backup_controllers} bc - WHERE bc.backupid = {backup_logs}.backupid - AND bc.timecreated < ?)", array($loglifetime)); - // Delete records from backup_controllers. - $DB->execute("DELETE FROM {backup_controllers} - WHERE timecreated < ?", array($loglifetime)); - mtrace(" Deleted old backup records"); - } - - - if (!empty($CFG->usetags)) { - require_once($CFG->dirroot.'/tag/lib.php'); - tag_cron(); - mtrace(' Executed tag cron'); - } - - - // Context maintenance stuff - context_helper::cleanup_instances(); - mtrace(' Cleaned up context instances'); - context_helper::build_all_paths(false); - // If you suspect that the context paths are somehow corrupt - // replace the line below with: context_helper::build_all_paths(true); - mtrace(' Built context paths'); - - - // Remove expired cache flags - gc_cache_flags(); - mtrace(' Cleaned cache flags'); - - - // Cleanup messaging - if (!empty($CFG->messagingdeletereadnotificationsdelay)) { - $notificationdeletetime = time() - $CFG->messagingdeletereadnotificationsdelay; - $DB->delete_records_select('message_read', 'notification=1 AND timeread<:notificationdeletetime', array('notificationdeletetime'=>$notificationdeletetime)); - mtrace(' Cleaned up read notifications'); - } - - mtrace(' Deleting temporary files...'); - cron_delete_from_temp(); - - // Cleanup user password reset records - // Delete any reset request records which are expired by more than a day. - // (We keep recently expired requests around so we can give a different error msg to users who - // are trying to user a recently expired reset attempt). - $pwresettime = isset($CFG->pwresettime) ? $CFG->pwresettime : 1800; - $earliestvalid = time() - $pwresettime - DAYSECS; - $DB->delete_records_select('user_password_resets', "timerequested < ?", array($earliestvalid)); - mtrace(' Cleaned up old password reset records'); - - mtrace("...finished clean-up tasks"); - - } // End of occasional clean-up tasks - - - // Send login failures notification - brute force protection in moodle is weak, - // we should at least send notices early in each cron execution - if (notify_login_failures()) { - mtrace(' Notified login failures'); + unset($task); } - - // Make sure all context instances are properly created - they may be required in auth, enrol, etc. - context_helper::create_instances(); - mtrace(' Created missing context instances'); - - - // Session gc. - mtrace("Running session gc tasks..."); - \core\session\manager::gc(); - mtrace("...finished stale session cleanup"); - - - // Run the auth cron, if any before enrolments - // because it might add users that will be needed in enrol plugins - $auths = get_enabled_auth_plugins(); - mtrace("Running auth crons if required..."); - cron_trace_time_and_memory(); - foreach ($auths as $auth) { - $authplugin = get_auth_plugin($auth); - if (method_exists($authplugin, 'cron')) { - mtrace("Running cron for auth/$auth..."); - $authplugin->cron(); - if (!empty($authplugin->log)) { - mtrace($authplugin->log); - } - } - unset($authplugin); - } - // Generate new password emails for users - ppl expect these generated asap - if ($DB->count_records('user_preferences', array('name'=>'create_password', 'value'=>'1'))) { - mtrace('Creating passwords for new users...'); - $usernamefields = get_all_user_name_fields(true, 'u'); - $newusers = $DB->get_recordset_sql("SELECT u.id as id, u.email, - $usernamefields, u.username, u.lang, - p.id as prefid - FROM {user} u - JOIN {user_preferences} p ON u.id=p.userid - WHERE p.name='create_password' AND p.value='1' AND u.email !='' AND u.suspended = 0 AND u.auth != 'nologin' AND u.deleted = 0"); - - // note: we can not send emails to suspended accounts - foreach ($newusers as $newuser) { - // Use a low cost factor when generating bcrypt hash otherwise - // hashing would be slow when emailing lots of users. Hashes - // will be automatically updated to a higher cost factor the first - // time the user logs in. - if (setnew_password_and_mail($newuser, true)) { - unset_user_preference('create_password', $newuser); - set_user_preference('auth_forcepasswordchange', 1, $newuser); - } else { - trigger_error("Could not create and mail new user password!"); - } - } - $newusers->close(); - } - - - // It is very important to run enrol early - // because other plugins depend on correct enrolment info. - mtrace("Running enrol crons if required..."); - $enrols = enrol_get_plugins(true); - foreach($enrols as $ename=>$enrol) { - // do this for all plugins, disabled plugins might want to cleanup stuff such as roles - if (!$enrol->is_cron_required()) { - continue; - } - mtrace("Running cron for enrol_$ename..."); + // Run all adhoc tasks. + while (!\core\task\manager::static_caches_cleared_since($timenow) && + $task = \core\task\manager::get_next_adhoc_task($timenow)) { + mtrace("Execute adhoc task: " . get_class($task)); cron_trace_time_and_memory(); - $enrol->cron(); - $enrol->set_config('lastcron', time()); - } - - - // Run all cron jobs for each module - mtrace("Starting activity modules"); - get_mailer('buffer'); - if ($mods = $DB->get_records_select("modules", "cron > 0 AND ((? - lastcron) > cron) AND visible = 1", array($timenow))) { - foreach ($mods as $mod) { - $libfile = "$CFG->dirroot/mod/$mod->name/lib.php"; - if (file_exists($libfile)) { - include_once($libfile); - $cron_function = $mod->name."_cron"; - if (function_exists($cron_function)) { - mtrace("Processing module function $cron_function ...", ''); - cron_trace_time_and_memory(); - $pre_dbqueries = null; - $pre_dbqueries = $DB->perf_get_queries(); - $pre_time = microtime(1); - if ($cron_function()) { - $DB->set_field("modules", "lastcron", $timenow, array("id"=>$mod->id)); - } - if (isset($pre_dbqueries)) { - mtrace("... used " . ($DB->perf_get_queries() - $pre_dbqueries) . " dbqueries"); - mtrace("... used " . (microtime(1) - $pre_time) . " seconds"); - } - // Reset possible changes by modules to time_limit. MDL-11597 - core_php_time_limit::raise(); - mtrace("done."); - } + $predbqueries = null; + $predbqueries = $DB->perf_get_queries(); + $pretime = microtime(1); + try { + $task->execute(); + if (isset($predbqueries)) { + mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries"); + mtrace("... used " . (microtime(1) - $pretime) . " seconds"); } - } - } - get_mailer('close'); - mtrace("Finished activity modules"); - - - mtrace("Starting blocks"); - if ($blocks = $DB->get_records_select("block", "cron > 0 AND ((? - lastcron) > cron) AND visible = 1", array($timenow))) { - // We will need the base class. - require_once($CFG->dirroot.'/blocks/moodleblock.class.php'); - foreach ($blocks as $block) { - $blockfile = $CFG->dirroot.'/blocks/'.$block->name.'/block_'.$block->name.'.php'; - if (file_exists($blockfile)) { - require_once($blockfile); - $classname = 'block_'.$block->name; - $blockobj = new $classname; - if (method_exists($blockobj,'cron')) { - mtrace("Processing cron function for ".$block->name.'....',''); - cron_trace_time_and_memory(); - if ($blockobj->cron()) { - $DB->set_field('block', 'lastcron', $timenow, array('id'=>$block->id)); - } - // Reset possible changes by blocks to time_limit. MDL-11597 - core_php_time_limit::raise(); - mtrace('done.'); - } + mtrace("Adhoc task complete: " . get_class($task)); + \core\task\manager::adhoc_task_complete($task); + } catch (Exception $e) { + if ($DB && $DB->is_transaction_started()) { + error_log('Database transaction aborted automatically in ' . get_class($task)); + $DB->force_transaction_rollback(); } - - } - } - mtrace('Finished blocks'); - - - mtrace('Starting admin reports'); - cron_execute_plugin_type('report'); - mtrace('Finished admin reports'); - - - mtrace('Starting main gradebook job...'); - cron_trace_time_and_memory(); - grade_cron(); - mtrace('done.'); - - - mtrace('Starting processing the event queue...'); - cron_trace_time_and_memory(); - events_cron(); - mtrace('done.'); - - - if ($CFG->enablecompletion) { - // Completion cron - mtrace('Starting the completion cron...'); - cron_trace_time_and_memory(); - require_once($CFG->dirroot.'/completion/cron.php'); - completion_cron(); - mtrace('done'); - } - - //now do plagiarism checks - require_once($CFG->libdir.'/plagiarismlib.php'); - plagiarism_cron(); - - - mtrace('Starting course reports'); - cron_execute_plugin_type('coursereport'); - mtrace('Finished course reports'); - - - // run gradebook import/export/report cron - mtrace('Starting gradebook plugins'); - cron_execute_plugin_type('gradeimport'); - cron_execute_plugin_type('gradeexport'); - cron_execute_plugin_type('gradereport'); - mtrace('Finished gradebook plugins'); - - // run calendar cron - require_once "{$CFG->dirroot}/calendar/lib.php"; - calendar_cron(); - - // Run external blog cron if needed - if (!empty($CFG->enableblogs) && $CFG->useexternalblogs) { - require_once($CFG->dirroot . '/blog/lib.php'); - mtrace("Fetching external blog entries...", ''); - cron_trace_time_and_memory(); - $sql = "timefetched < ? OR timefetched = 0"; - $externalblogs = $DB->get_records_select('blog_external', $sql, array(time() - $CFG->externalblogcrontime)); - - foreach ($externalblogs as $eb) { - blog_sync_external_entries($eb); - } - mtrace('done.'); - } - // Run blog associations cleanup - if (!empty($CFG->enableblogs) && $CFG->useblogassociations) { - require_once($CFG->dirroot . '/blog/lib.php'); - // delete entries whose contextids no longer exists - mtrace("Deleting blog associations linked to non-existent contexts...", ''); - cron_trace_time_and_memory(); - $DB->delete_records_select('blog_association', 'contextid NOT IN (SELECT id FROM {context})'); - mtrace('done.'); - } - - - // Run question bank clean-up. - mtrace("Starting the question bank cron...", ''); - cron_trace_time_and_memory(); - require_once($CFG->libdir . '/questionlib.php'); - question_bank::cron(); - mtrace('done.'); - - - //Run registration updated cron - mtrace(get_string('siteupdatesstart', 'hub')); - cron_trace_time_and_memory(); - require_once($CFG->dirroot . '/' . $CFG->admin . '/registration/lib.php'); - $registrationmanager = new registration_manager(); - $registrationmanager->cron(); - mtrace(get_string('siteupdatesend', 'hub')); - - // If enabled, fetch information about available updates and eventually notify site admins - if (empty($CFG->disableupdatenotifications)) { - $updateschecker = \core\update\checker::instance(); - $updateschecker->cron(); - } - - //cleanup old session linked tokens - //deletes the session linked tokens that are over a day old. - mtrace("Deleting session linked tokens more than one day old...", ''); - cron_trace_time_and_memory(); - $DB->delete_records_select('external_tokens', 'lastaccess < :onedayago AND tokentype = :tokentype', - array('onedayago' => time() - DAYSECS, 'tokentype' => EXTERNAL_TOKEN_EMBEDDED)); - mtrace('done.'); - - - // all other plugins - cron_execute_plugin_type('message', 'message plugins'); - cron_execute_plugin_type('filter', 'filters'); - cron_execute_plugin_type('editor', 'editors'); - cron_execute_plugin_type('format', 'course formats'); - cron_execute_plugin_type('profilefield', 'profile fields'); - cron_execute_plugin_type('webservice', 'webservices'); - cron_execute_plugin_type('repository', 'repository plugins'); - cron_execute_plugin_type('qbehaviour', 'question behaviours'); - cron_execute_plugin_type('qformat', 'question import/export formats'); - cron_execute_plugin_type('qtype', 'question types'); - cron_execute_plugin_type('plagiarism', 'plagiarism plugins'); - cron_execute_plugin_type('theme', 'themes'); - cron_execute_plugin_type('tool', 'admin tools'); - - - // and finally run any local cronjobs, if any - if ($locals = core_component::get_plugin_list('local')) { - mtrace('Processing customized cron scripts ...', ''); - // new cron functions in lib.php first - cron_execute_plugin_type('local'); - // legacy cron files are executed directly - foreach ($locals as $local => $localdir) { - if (file_exists("$localdir/cron.php")) { - include("$localdir/cron.php"); + if (isset($predbqueries)) { + mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries"); + mtrace("... used " . (microtime(1) - $pretime) . " seconds"); } + mtrace("Adhoc task failed: " . get_class($task) . "," . $e->getMessage()); + \core\task\manager::adhoc_task_failed($task); } - mtrace('done.'); + unset($task); } - mtrace('Running cache cron routines'); - cache_helper::cron(); - mtrace('done.'); - - // Portfolio cron - this needs to be close to the end because it may take a long time. - if ($CFG->enableportfolios) { - mtrace('Starting the portfolio cron...'); - cron_trace_time_and_memory(); - require_once($CFG->libdir . '/portfoliolib.php'); - portfolio_cron(); - mtrace('done'); - } - - // Run automated backups if required - these may take a long time to execute - require_once($CFG->dirroot.'/backup/util/includes/backup_includes.php'); - require_once($CFG->dirroot.'/backup/util/helper/backup_cron_helper.class.php'); - backup_cron_automated_helper::run_automated_backup(); - - - // Run stats as at the end because they are known to take very long time on large sites - if (!empty($CFG->enablestats) and empty($CFG->disablestatsprocessing)) { - require_once($CFG->dirroot.'/lib/statslib.php'); - // check we're not before our runtime - $timetocheck = stats_get_base_daily() + $CFG->statsruntimestarthour*60*60 + $CFG->statsruntimestartminute*60; - - if (time() > $timetocheck) { - // process configured number of days as max (defaulting to 31) - $maxdays = empty($CFG->statsruntimedays) ? 31 : abs($CFG->statsruntimedays); - if (stats_cron_daily($maxdays)) { - if (stats_cron_weekly()) { - if (stats_cron_monthly()) { - stats_clean_old(); - } - } - } - core_php_time_limit::raise(); - } else { - mtrace('Next stats run after:'. userdate($timetocheck)); - } - } - - // Run badges review cron. - mtrace("Starting badges cron..."); - require_once($CFG->dirroot . '/badges/cron.php'); - badge_cron(); - mtrace('done.'); - - // cleanup file trash - not very important - $fs = get_file_storage(); - $fs->cron(); - mtrace("Cron script completed correctly"); gc_collect_cycles(); @@ -507,6 +131,16 @@ function cron_run() { mtrace("Execution took ".$difftime." seconds"); } +/** + * Output some standard information during cron runs. Specifically current time + * and memory usage. This method also does gc_collect_cycles() (before displaying + * memory usage) to try to help PHP manage memory better. + */ +function cron_trace_time_and_memory() { + gc_collect_cycles(); + mtrace('... started ' . date('H:i:s') . '. Current memory use ' . display_size(memory_get_usage()) . '.'); +} + /** * Executes cron functions for a specific type of plugin. * @@ -633,184 +267,3 @@ function cron_bc_hack_plugin_functions($plugintype, $plugins) { return $plugins; } - -/** - * Output some standard information during cron runs. Specifically current time - * and memory usage. This method also does gc_collect_cycles() (before displaying - * memory usage) to try to help PHP manage memory better. - */ -function cron_trace_time_and_memory() { - gc_collect_cycles(); - mtrace('... started ' . date('H:i:s') . '. Current memory use ' . display_size(memory_get_usage()) . '.'); -} - -/** - * Notify admin users or admin user of any failed logins (since last notification). - * - * Note that this function must be only executed from the cron script - * It uses the cache_flags system to store temporary records, deleting them - * by name before finishing - * - * @return bool True if executed, false if not - */ -function notify_login_failures() { - global $CFG, $DB, $OUTPUT; - - if (empty($CFG->notifyloginfailures)) { - return false; - } - - $recip = get_users_from_config($CFG->notifyloginfailures, 'moodle/site:config'); - - if (empty($CFG->lastnotifyfailure)) { - $CFG->lastnotifyfailure=0; - } - - // If it has been less than an hour, or if there are no recipients, don't execute. - if (((time() - HOURSECS) < $CFG->lastnotifyfailure) || !is_array($recip) || count($recip) <= 0) { - return false; - } - - // we need to deal with the threshold stuff first. - if (empty($CFG->notifyloginthreshold)) { - $CFG->notifyloginthreshold = 10; // default to something sensible. - } - - // Get all the IPs with more than notifyloginthreshold failures since lastnotifyfailure - // and insert them into the cache_flags temp table - $sql = "SELECT ip, COUNT(*) - FROM {log} - WHERE module = 'login' AND action = 'error' - AND time > ? - GROUP BY ip - HAVING COUNT(*) >= ?"; - $params = array($CFG->lastnotifyfailure, $CFG->notifyloginthreshold); - $rs = $DB->get_recordset_sql($sql, $params); - foreach ($rs as $iprec) { - if (!empty($iprec->ip)) { - set_cache_flag('login_failure_by_ip', $iprec->ip, '1', 0); - } - } - $rs->close(); - - // Get all the INFOs with more than notifyloginthreshold failures since lastnotifyfailure - // and insert them into the cache_flags temp table - $sql = "SELECT info, count(*) - FROM {log} - WHERE module = 'login' AND action = 'error' - AND time > ? - GROUP BY info - HAVING count(*) >= ?"; - $params = array($CFG->lastnotifyfailure, $CFG->notifyloginthreshold); - $rs = $DB->get_recordset_sql($sql, $params); - foreach ($rs as $inforec) { - if (!empty($inforec->info)) { - set_cache_flag('login_failure_by_info', $inforec->info, '1', 0); - } - } - $rs->close(); - - // Now, select all the login error logged records belonging to the ips and infos - // since lastnotifyfailure, that we have stored in the cache_flags table - $sql = "SELECT * FROM ( - SELECT l.*, u.firstname, u.lastname - FROM {log} l - JOIN {cache_flags} cf ON l.ip = cf.name - LEFT JOIN {user} u ON l.userid = u.id - WHERE l.module = 'login' AND l.action = 'error' - AND l.time > ? - AND cf.flagtype = 'login_failure_by_ip' - UNION ALL - SELECT l.*, u.firstname, u.lastname - FROM {log} l - JOIN {cache_flags} cf ON l.info = cf.name - LEFT JOIN {user} u ON l.userid = u.id - WHERE l.module = 'login' AND l.action = 'error' - AND l.time > ? - AND cf.flagtype = 'login_failure_by_info') t - ORDER BY t.time DESC"; - $params = array($CFG->lastnotifyfailure, $CFG->lastnotifyfailure); - - // Init some variables - $count = 0; - $messages = ''; - // Iterate over the logs recordset - $rs = $DB->get_recordset_sql($sql, $params); - foreach ($rs as $log) { - $log->time = userdate($log->time); - $messages .= get_string('notifyloginfailuresmessage','',$log)."\n"; - $count++; - } - $rs->close(); - - // If we have something useful to report. - if ($count > 0) { - $site = get_site(); - $subject = get_string('notifyloginfailuressubject', '', format_string($site->fullname)); - // Calculate the complete body of notification (start + messages + end) - $body = get_string('notifyloginfailuresmessagestart', '', $CFG->wwwroot) . - (($CFG->lastnotifyfailure != 0) ? '('.userdate($CFG->lastnotifyfailure).')' : '')."\n\n" . - $messages . - "\n\n".get_string('notifyloginfailuresmessageend','',$CFG->wwwroot)."\n\n"; - - // For each destination, send mail - mtrace('Emailing admins about '. $count .' failed login attempts'); - foreach ($recip as $admin) { - //emailing the admins directly rather than putting these through the messaging system - email_to_user($admin, core_user::get_support_user(), $subject, $body); - } - } - - // Update lastnotifyfailure with current time - set_config('lastnotifyfailure', time()); - - // Finally, delete all the temp records we have created in cache_flags - $DB->delete_records_select('cache_flags', "flagtype IN ('login_failure_by_ip', 'login_failure_by_info')"); - - return true; -} - -/** - * Delete files and directories older than one week from directory provided by $CFG->tempdir. - * - * @throws Exception Failed reading/accessing file or directory - * @return bool True on successful file and directory deletion; otherwise, false on failure - */ -function cron_delete_from_temp() { - global $CFG; - - $tmpdir = $CFG->tempdir; - // Default to last weeks time. - $time = strtotime('-1 week'); - - try { - $dir = new RecursiveDirectoryIterator($tmpdir); - // Show all child nodes prior to their parent. - $iter = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::CHILD_FIRST); - - for ($iter->rewind(); $iter->valid(); $iter->next()) { - $node = $iter->getRealPath(); - if (!is_readable($node)) { - continue; - } - // Check if file or directory is older than the given time. - if ($iter->getMTime() < $time) { - if ($iter->isDir() && !$iter->isDot()) { - if (@rmdir($node) === false) { - mtrace("Failed removing directory '$node'."); - } - } - if ($iter->isFile()) { - if (@unlink($node) === false) { - mtrace("Failed removing file '$node'."); - } - } - } - } - } catch (Exception $e) { - mtrace('Failed reading/accessing file or directory.'); - return false; - } - - return true; -} diff --git a/lib/db/install.xml b/lib/db/install.xml old mode 100644 new mode 100755 index 9ad4f242c90..c8eef76325e --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -3043,5 +3043,45 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
diff --git a/lib/db/tasks.php b/lib/db/tasks.php new file mode 100644 index 00000000000..028a8e2354b --- /dev/null +++ b/lib/db/tasks.php @@ -0,0 +1,305 @@ +. + +/** + * Definition of core scheduled tasks. + * + * The handlers defined on this file are processed and registered into + * the Moodle DB after any install or upgrade operation. All plugins + * support this. + * + * @package core + * @category task + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/* List of handlers */ + +$tasks = array( + array( + 'classname' => 'core\task\session_cleanup_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\delete_unconfirmed_users_task', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\delete_incomplete_users_task', + 'blocking' => 0, + 'minute' => '5', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\delete_logs_task', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '2', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\backup_cleanup_task', + 'blocking' => 0, + 'minute' => '10', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\tag_cron_task', + 'blocking' => 0, + 'minute' => '20', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\context_cleanup_task', + 'blocking' => 0, + 'minute' => '25', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\cache_cleanup_task', + 'blocking' => 0, + 'minute' => '30', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\messaging_cleanup_task', + 'blocking' => 0, + 'minute' => '35', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\send_new_user_passwords_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\send_failed_login_notifications_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\create_contexts_task', + 'blocking' => 1, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\legacy_plugin_cron_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\grade_cron_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\events_cron_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\completion_cron_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\portfolio_cron_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\plagiarism_cron_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\calendar_cron_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\blog_cron_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\question_cron_task', + 'blocking' => 0, + 'minute' => '*', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\registration_cron_task', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '3', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\check_for_updates_task', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '*/2', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\cache_cron_task', + 'blocking' => 0, + 'minute' => '50', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\automated_backup_task', + 'blocking' => 0, + 'minute' => '50', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\badges_cron_task', + 'blocking' => 0, + 'minute' => '*/5', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\file_temp_cleanup_task', + 'blocking' => 0, + 'minute' => '55', + 'hour' => '*/6', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\file_trash_cleanup_task', + 'blocking' => 0, + 'minute' => '55', + 'hour' => '*/6', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\stats_cron_task', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '*', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'core\task\password_reset_cleanup_task', + 'blocking' => 0, + 'minute' => '0', + 'hour' => '*/6', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ) +); diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 3a487cdf96c..0a73cc22287 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2286,7 +2286,6 @@ function xmldb_main_upgrade($oldversion) { // Main savepoint reached. upgrade_main_savepoint(true, 2013070800.01); } - if ($oldversion < 2013071500.01) { // The enrol_authorize plugin has been removed, if there are no records // and no plugin files then remove the plugin data. @@ -3069,5 +3068,61 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2014021900.03); } + if ($oldversion < 2014022600.00) { + $table = new xmldb_table('task_scheduled'); + + // Adding fields to table task_scheduled. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('component', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('classname', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('lastruntime', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('nextruntime', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('blocking', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('minute', XMLDB_TYPE_CHAR, '25', null, XMLDB_NOTNULL, null, null); + $table->add_field('hour', XMLDB_TYPE_CHAR, '25', null, XMLDB_NOTNULL, null, null); + $table->add_field('day', XMLDB_TYPE_CHAR, '25', null, XMLDB_NOTNULL, null, null); + $table->add_field('month', XMLDB_TYPE_CHAR, '25', null, XMLDB_NOTNULL, null, null); + $table->add_field('dayofweek', XMLDB_TYPE_CHAR, '25', null, XMLDB_NOTNULL, null, null); + $table->add_field('faildelay', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('customised', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table task_scheduled. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table task_scheduled. + $table->add_index('classname_uniq', XMLDB_INDEX_UNIQUE, array('classname')); + + // Conditionally launch create table for task_scheduled. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define table task_adhoc to be created. + $table = new xmldb_table('task_adhoc'); + + // Adding fields to table task_adhoc. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('component', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('classname', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null); + $table->add_field('nextruntime', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('faildelay', XMLDB_TYPE_INTEGER, '10', null, null, null, null); + $table->add_field('customdata', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('blocking', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0'); + + // Adding keys to table task_adhoc. + $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id')); + + // Adding indexes to table task_adhoc. + $table->add_index('nextruntime_idx', XMLDB_INDEX_NOTUNIQUE, array('nextruntime')); + + // Conditionally launch create table for task_adhoc. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Main savepoint reached. + upgrade_main_savepoint(true, 2014022600.00); + } + return true; } diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 5bc9b636d38..e6670bd8de5 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -1610,6 +1610,7 @@ function purge_all_caches() { remove_dir($CFG->localcachedir, true); set_config('localcachedirpurged', time()); make_localcache_directory('', true); + \core\task\manager::clear_static_caches(); } /** diff --git a/lib/setuplib.php b/lib/setuplib.php index 25f456ba283..6bb48407fa7 100644 --- a/lib/setuplib.php +++ b/lib/setuplib.php @@ -1261,7 +1261,7 @@ function disable_output_buffering() { */ function redirect_if_major_upgrade_required() { global $CFG; - $lastmajordbchanges = 2014012400.00; + $lastmajordbchanges = 2014022600.00; if (empty($CFG->version) or (float)$CFG->version < $lastmajordbchanges or during_initial_install() or !empty($CFG->adminsetuppending)) { try { diff --git a/lib/tests/adhoc_task_test.php b/lib/tests/adhoc_task_test.php new file mode 100644 index 00000000000..ebe592f207e --- /dev/null +++ b/lib/tests/adhoc_task_test.php @@ -0,0 +1,76 @@ +. + +/** + * This file contains the unittests for the css optimiser in csslib.php + * + * @package core + * @category phpunit + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Test class for adhoc tasks. + * + * @package core + * @category task + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class adhoc_task_testcase extends advanced_testcase { + + public function test_get_next_adhoc_task() { + global $DB; + + $this->resetAfterTest(true); + // Create an adhoc task. + $task = new testable_adhoc_task(); + + // Queue it. + $task = \core\task\manager::queue_adhoc_task($task); + + $now = time(); + // Get it from the scheduler. + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertNotNull($task); + $task->execute(); + + \core\task\manager::adhoc_task_failed($task); + // Should not get any task. + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertNull($task); + + // Should get the adhoc task (retry after delay). + $task = \core\task\manager::get_next_adhoc_task($now + 120); + $this->assertNotNull($task); + $task->execute(); + + \core\task\manager::adhoc_task_complete($task); + + // Should not get any task. + $task = \core\task\manager::get_next_adhoc_task($now); + $this->assertNull($task); + } +} + +class testable_adhoc_task extends \core\task\adhoc_task { + public function execute() { + } +} diff --git a/lib/tests/cronlib_test.php b/lib/tests/cronlib_test.php index 5426884d79c..c9eb572dee3 100644 --- a/lib/tests/cronlib_test.php +++ b/lib/tests/cronlib_test.php @@ -137,7 +137,8 @@ class cronlib_testcase extends basic_testcase { touch($tmpdir.$data->path, $data->time); } - cron_delete_from_temp(); + $task = new \core\task\file_temp_cleanup_task(); + $task->execute(); $dir = new RecursiveDirectoryIterator($tmpdir); $iter = new RecursiveIteratorIterator($dir, RecursiveIteratorIterator::CHILD_FIRST); diff --git a/lib/tests/scheduled_task_test.php b/lib/tests/scheduled_task_test.php new file mode 100644 index 00000000000..600988162bc --- /dev/null +++ b/lib/tests/scheduled_task_test.php @@ -0,0 +1,156 @@ +. + +/** + * This file contains the unittests for the css optimiser in csslib.php + * + * @package core + * @category phpunit + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + + +/** + * Test class for scheduled task. + * + * @package core + * @category task + * @copyright 2013 Damyon Wiese + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class scheduled_task_testcase extends advanced_testcase { + + /** + * Test the cron scheduling method + */ + public function test_eval_cron_field() { + $testclass = new testable_scheduled_task(); + + $this->assertEquals(20, count($testclass->eval_cron_field('*/3', 0, 59))); + $this->assertEquals(31, count($testclass->eval_cron_field('1,*/2', 0, 59))); + $this->assertEquals(15, count($testclass->eval_cron_field('1-10,5-15', 0, 59))); + $this->assertEquals(13, count($testclass->eval_cron_field('1-10,5-15/2', 0, 59))); + $this->assertEquals(3, count($testclass->eval_cron_field('1,2,3,1,2,3', 0, 59))); + $this->assertEquals(1, count($testclass->eval_cron_field('-1,10,80', 0, 59))); + } + + public function test_get_next_scheduled_time() { + // Test job run at 1 am. + $testclass = new testable_scheduled_task(); + + // All fields default to '*'. + $testclass->set_hour('1'); + $testclass->set_minute('0'); + // Next valid time should be 1am of the next day. + $nexttime = $testclass->get_next_scheduled_time(); + + $oneam = mktime(1, 0, 0); + // Make it 1 am tomorrow if the time is after 1am. + if ($oneam < time()) { + $oneam += 86400; + } + + $this->assertEquals($oneam, $nexttime, 'Next scheduled time is 1am.'); + + // Now test for job run every 10 minutes. + $testclass = new testable_scheduled_task(); + + // All fields default to '*'. + $testclass->set_minute('*/10'); + // Next valid time should be next 10 minute boundary. + $nexttime = $testclass->get_next_scheduled_time(); + + $minutes = ((intval(date('i') / 10))+1) * 10; + $nexttenminutes = mktime(date('H'), $minutes, 0); + + $this->assertEquals($nexttenminutes, $nexttime, 'Next scheduled time is in 10 minutes.'); + } + + public function test_get_next_scheduled_task() { + global $DB; + + $this->resetAfterTest(true); + // Delete all existing scheduled tasks. + $DB->delete_records('task_scheduled'); + // Add a scheduled task. + + // A task that runs once per hour. + $record = new stdClass(); + $record->blocking = true; + $record->minute = '0'; + $record->hour = '0'; + $record->dayofweek = '*'; + $record->day = '*'; + $record->month = '*'; + $record->component = 'test_scheduled_task'; + $record->classname = '\\testable_scheduled_task'; + + $DB->insert_record('task_scheduled', $record); + // And another one to test failures. + $record->classname = '\\testable_scheduled_task2'; + $DB->insert_record('task_scheduled', $record); + $now = time(); + + // Should get handed the first task. + $task = \core\task\manager::get_next_scheduled_task($now); + $this->assertNotNull($task); + $task->execute(); + + \core\task\manager::scheduled_task_complete($task); + // Should get handed the second task. + $task = \core\task\manager::get_next_scheduled_task($now); + $this->assertNotNull($task); + $task->execute(); + + \core\task\manager::scheduled_task_failed($task); + // Should not get any task. + $task = \core\task\manager::get_next_scheduled_task($now); + $this->assertNull($task); + + // Should get the second task (retry after delay). + $task = \core\task\manager::get_next_scheduled_task($now + 120); + $this->assertNotNull($task); + $task->execute(); + + \core\task\manager::scheduled_task_complete($task); + + // Should not get any task. + $task = \core\task\manager::get_next_scheduled_task($now); + $this->assertNull($task); + } +} + +class testable_scheduled_task extends \core\task\scheduled_task { + public function get_name() { + return "Test task"; + } + + public function execute() { + } +} + +class testable_scheduled_task2 extends \core\task\scheduled_task { + public function get_name() { + return "Test task 2"; + } + + public function execute() { + } +} + diff --git a/lib/upgradelib.php b/lib/upgradelib.php index d323d743050..c87f9482db9 100644 --- a/lib/upgradelib.php +++ b/lib/upgradelib.php @@ -458,6 +458,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { log_update_descriptions($component); external_update_descriptions($component); events_update_definition($component); + \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); if ($type === 'message') { message_update_processors($plug); @@ -494,6 +495,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { log_update_descriptions($component); external_update_descriptions($component); events_update_definition($component); + \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); if ($type === 'message') { message_update_processors($plug); @@ -525,6 +527,7 @@ function upgrade_plugins($type, $startcallback, $endcallback, $verbose) { log_update_descriptions($component); external_update_descriptions($component); events_update_definition($component); + \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); if ($type === 'message') { // Ugly hack! @@ -626,6 +629,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) { log_update_descriptions($component); external_update_descriptions($component); events_update_definition($component); + \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); upgrade_plugin_mnet_functions($component); $endcallback($component, true, $verbose); @@ -658,6 +662,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) { log_update_descriptions($component); external_update_descriptions($component); events_update_definition($component); + \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); upgrade_plugin_mnet_functions($component); @@ -692,6 +697,7 @@ function upgrade_plugins_modules($startcallback, $endcallback, $verbose) { log_update_descriptions($component); external_update_descriptions($component); events_update_definition($component); + \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); upgrade_plugin_mnet_functions($component); @@ -812,6 +818,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) { log_update_descriptions($component); external_update_descriptions($component); events_update_definition($component); + \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); upgrade_plugin_mnet_functions($component); $endcallback($component, true, $verbose); @@ -850,6 +857,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) { log_update_descriptions($component); external_update_descriptions($component); events_update_definition($component); + \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); upgrade_plugin_mnet_functions($component); @@ -883,6 +891,7 @@ function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) { log_update_descriptions($component); external_update_descriptions($component); events_update_definition($component); + \core\task\manager::reset_scheduled_tasks_for_component($component); message_update_providers($component); upgrade_plugin_mnet_functions($component); @@ -1483,6 +1492,7 @@ function install_core($version, $verbose) { log_update_descriptions('moodle'); external_update_descriptions('moodle'); events_update_definition('moodle'); + \core\task\manager::reset_scheduled_tasks_for_component('moodle'); message_update_providers('moodle'); // Write default settings unconditionally @@ -1545,6 +1555,7 @@ function upgrade_core($version, $verbose) { log_update_descriptions('moodle'); external_update_descriptions('moodle'); events_update_definition('moodle'); + \core\task\manager::reset_scheduled_tasks_for_component('moodle'); message_update_providers('moodle'); // Update core definitions. cache_helper::update_definitions(true); diff --git a/version.php b/version.php index 18c19d9f66e..cb809828078 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2014022500.02; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2014022600.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.