just a little note for other devs looking for cli info
[moodle.git] / admin / cron.php
1 <?php
3 /// This script looks through all the module directories for cron.php files
4 /// and runs them.  These files can contain cleanup functions, email functions
5 /// or anything that needs to be run on a regular basis.
6 ///
7 /// This file is best run from cron on the host system (ie outside PHP).
8 /// The script can either be invoked via the web server or via a standalone
9 /// version of PHP compiled for CGI.
10 ///
11 /// eg   wget -q -O /dev/null 'http://moodle.somewhere.edu/admin/cron.php'
12 /// or   php /web/moodle/admin/cron.php
13     set_time_limit(0);
14     $starttime = microtime();
16 /// this cron script might be considered to be a CLI script even when accessed over HTTP,
17 /// we do not want HTML in output and there is no real session ;-)
18 /// NOTE: do not use this hack in your cli scripts, it is defined automatically
19 ///       in lib/setup.php, all you need to do is to prevent access via web
20     define('CLI_SCRIPT', true);
22 /// Following hack used to identify cron and other CLI scripts - use CLI_SCRIPT or $FULLME == "/$CFG->admin/cropn.php" instead
23     //define('FULLME', 'cron');
25 /// Do not set moodle cookie because we do not need it here, it is better to emulate session
26     define('NO_MOODLE_COOKIES', true);
28     require_once(dirname(__FILE__) . '/../config.php');
29     require_once($CFG->libdir.'/adminlib.php');
30     require_once($CFG->libdir.'/gradelib.php');
32 /// Extra debugging (set in config.php)
33     if (!empty($CFG->showcronsql)) {
34         $DB->set_debug(true);
35     }
36     if (!empty($CFG->showcrondebugging)) {
37         $CFG->debug = DEBUG_DEVELOPER;
38         $CFG->debugdisplay = true;
39     }
41 /// extra safety
42     session_get_instance()->write_close();
44 /// check if execution allowed
45     if (isset($_SERVER['REMOTE_ADDR'])) { // if the script is accessed via the web.
46         if (!empty($CFG->cronclionly)) {
47             // This script can only be run via the cli.
48             print_error('cronerrorclionly', 'admin');
49             exit;
50         }
51         // This script is being called via the web, so check the password if there is one.
52         if (!empty($CFG->cronremotepassword)) {
53             $pass = optional_param('password', '', PARAM_RAW);
54             if($pass != $CFG->cronremotepassword) {
55                 // wrong password.
56                 print_error('cronerrorpassword', 'admin');
57                 exit;
58             }
59         }
60     }
63 /// emulate normal session
64     cron_setup_user();
66 /// send mime type and encoding
67     if (check_browser_version('MSIE')) {
68         //ugly IE hack to work around downloading instead of viewing
69         @header('Content-Type: text/html; charset=utf-8');
70         echo "<xmp>"; //<pre> is not good enough for us here
71     } else {
72         //send proper plaintext header
73         @header('Content-Type: text/plain; charset=utf-8');
74     }
76 /// no more headers and buffers
77     while(@ob_end_flush());
79 /// increase memory limit (PHP 5.2 does different calculation, we need more memory now)
80     @raise_memory_limit('128M');
82 /// Start output log
84     $timenow  = time();
86     mtrace("Server Time: ".date('r',$timenow)."\n\n");
89 /// Session gc
91     mtrace("Cleaning up stale sessions");
92     session_gc();
94 /// Run all cron jobs for each module
96     mtrace("Starting activity modules");
97     get_mailer('buffer');
98     if ($mods = $DB->get_records_select("modules", "cron > 0 AND ((? - lastcron) > cron) AND visible = 1", array($timenow))) {
99         foreach ($mods as $mod) {
100             $libfile = "$CFG->dirroot/mod/$mod->name/lib.php";
101             if (file_exists($libfile)) {
102                 include_once($libfile);
103                 $cron_function = $mod->name."_cron";
104                 if (function_exists($cron_function)) {
105                     mtrace("Processing module function $cron_function ...", '');
106                     $pre_dbqueries = null;
107                     $pre_dbqueries = $DB->perf_get_queries();
108                     $pre_time      = microtime(1);
109                     if ($cron_function()) {
110                         if (!$DB->set_field("modules", "lastcron", $timenow, array("id"=>$mod->id))) {
111                             mtrace("Error: could not update timestamp for $mod->fullname");
112                         }
113                     }
114                     if (isset($pre_dbqueries)) {
115                         mtrace("... used " . ($DB->perf_get_queries() - $pre_dbqueries) . " dbqueries");
116                         mtrace("... used " . (microtime(1) - $pre_time) . " seconds");
117                     }
118                 /// Reset possible changes by modules to time_limit. MDL-11597
119                     @set_time_limit(0);
120                     mtrace("done.");
121                 }
122             }
123         }
124     }
125     get_mailer('close');
126     mtrace("Finished activity modules");
128     mtrace("Starting blocks");
129     if ($blocks = $DB->get_records_select("block", "cron > 0 AND ((? - lastcron) > cron) AND visible = 1", array($timenow))) {
130         // we will need the base class.
131         require_once($CFG->dirroot.'/blocks/moodleblock.class.php');
132         foreach ($blocks as $block) {
133             $blockfile = $CFG->dirroot.'/blocks/'.$block->name.'/block_'.$block->name.'.php';
134             if (file_exists($blockfile)) {
135                 require_once($blockfile);
136                 $classname = 'block_'.$block->name;
137                 $blockobj = new $classname;
138                 if (method_exists($blockobj,'cron')) {
139                     mtrace("Processing cron function for ".$block->name.'....','');
140                     if ($blockobj->cron()) {
141                         if (!$DB->set_field('block', 'lastcron', $timenow, array('id'=>$block->id))) {
142                             mtrace('Error: could not update timestamp for '.$block->name);
143                         }
144                     }
145                 /// Reset possible changes by blocks to time_limit. MDL-11597
146                     @set_time_limit(0);
147                     mtrace('done.');
148                 }
149             }
151         }
152     }
153     mtrace('Finished blocks');
155     mtrace("Starting quiz reports");
156     if ($reports = $DB->get_records_select('quiz_report', "cron > 0 AND ((? - lastcron) > cron)", array($timenow))) {
157         foreach ($reports as $report) {
158             $cronfile = "$CFG->dirroot/mod/quiz/report/$report->name/cron.php";
159             if (file_exists($cronfile)) {
160                 include_once($cronfile);
161                 $cron_function = 'quiz_report_'.$report->name."_cron";
162                 if (function_exists($cron_function)) {
163                     mtrace("Processing quiz report cron function $cron_function ...", '');
164                     $pre_dbqueries = null;
165                     $pre_dbqueries = $DB->perf_get_queries();
166                     $pre_time      = microtime(1);
167                     if ($cron_function()) {
168                         if (!$DB->set_field('quiz_report', "lastcron", $timenow, array("id"=>$report->id))) {
169                             mtrace("Error: could not update timestamp for $report->name");
170                         }
171                     }
172                     if (isset($pre_dbqueries)) {
173                         mtrace("... used " . ($DB->perf_get_queries() - $pre_dbqueries) . " dbqueries");
174                         mtrace("... used " . (microtime(1) - $pre_time) . " seconds");
175                     }
176                     mtrace("done.");
177                 }
178             }
179         }
180     }
181     mtrace("Finished quiz reports");
183     mtrace('Starting admin reports');
184     // Admin reports do not have a database table that lists them. Instead a
185     // report includes cron.php with function report_reportname_cron() if it wishes
186     // to be cronned. It is up to cron.php to handle e.g. if it only needs to
187     // actually do anything occasionally.
188     $reports = get_plugin_list('report');
189     foreach($reports as $report => $reportdir) {
190         $cronfile = $reportdir.'/cron.php';
191         if (file_exists($cronfile)) {
192             require_once($cronfile);
193             $cronfunction = 'report_'.$report.'_cron';
194             mtrace('Processing cron function for '.$report.'...', '');
195             $pre_dbqueries = null;
196             $pre_dbqueries = $DB->perf_get_queries();
197             $pre_time      = microtime(true);
198             $cronfunction();
199             if (isset($pre_dbqueries)) {
200                 mtrace("... used " . ($DB->perf_get_queries() - $pre_dbqueries) . " dbqueries");
201                 mtrace("... used " . round(microtime(true) - $pre_time, 2) . " seconds");
202             }
203             mtrace('done.');
204         }
205     }
206     mtrace('Finished admin reports');
208     mtrace('Removing expired enrolments ...', '');     // See MDL-8785
209     $timenow = time();
210     $somefound = false;
211     // The preferred way saves memory, datablib
212     // find courses where limited enrolment is enabled
213     $sql = "SELECT ra.roleid, ra.userid, ra.contextid
214               FROM {course} c
215               JOIN {context} cx ON cx.instanceid = c.id
216               JOIN {role_assignments} ra ON ra.contextid = cx.id
217              WHERE cx.contextlevel = '".CONTEXT_COURSE."'
218                    AND ra.timeend > 0
219                    AND ra.timeend < ?
220                    AND c.enrolperiod > 0";
221     if ($rs = $DB->get_recordset_sql($sql, array($timenow))) {
222         foreach ($rs as $oldenrolment) {
223             role_unassign($oldenrolment->roleid, $oldenrolment->userid, 0, $oldenrolment->contextid);
224             $somefound = true;
225         }
226         $rs->close();
227     }
228     if ($somefound) {
229         mtrace('Done');
230     } else {
231         mtrace('none found');
232     }
236     mtrace('Starting main gradebook job ...');
237     grade_cron();
238     mtrace('done.');
241     mtrace('Starting processing the event queue...');
242     events_cron();
243     mtrace('done.');
245     if ($CFG->enableportfolios) {
246         // Portfolio cron
247         mtrace('Starting the portfolio cron...');
248         require_once($CFG->libdir . '/portfoliolib.php');
249         portfolio_cron();
250         mtrace('done');
251     }
253 /// Run all core cron jobs, but not every time since they aren't too important.
254 /// These don't have a timer to reduce load, so we'll use a random number
255 /// to randomly choose the percentage of times we should run these jobs.
257     srand ((double) microtime() * 10000000);
258     $random100 = rand(0,100);
260     if ($random100 < 20) {     // Approximately 20% of the time.
261         mtrace("Running clean-up tasks...");
263         /// Unenrol users who haven't logged in for $CFG->longtimenosee
265         if ($CFG->longtimenosee) { // value in days
266             $cuttime = $timenow - ($CFG->longtimenosee * 3600 * 24);
267             $rs = $DB->get_recordset_sql ("SELECT id, userid, courseid
268                                              FROM {user_lastaccess}
269                                             WHERE courseid != ".SITEID."
270                                                   AND timeaccess < ?", array($cuttime));
271             foreach ($rs as $assign) {
272                 if ($context = get_context_instance(CONTEXT_COURSE, $assign->courseid)) {
273                     if (role_unassign(0, $assign->userid, 0, $context->id)) {
274                         mtrace("removing user $assign->userid from course $assign->courseid as they have not accessed the course for over $CFG->longtimenosee days");
275                     }
276                 }
277             }
278             $rs->close();
279         /// Execute the same query again, looking for remaining records and deleting them
280         /// if the user hasn't moodle/course:participate in the CONTEXT_COURSE context (orphan records)
281             $rs = $DB->get_recordset_sql ("SELECT id, userid, courseid
282                                              FROM {user_lastaccess}
283                                             WHERE courseid != ".SITEID."
284                                                   AND timeaccess < ?", array($cuttime));
285             foreach ($rs as $assign) {
286                 if ($context = get_context_instance(CONTEXT_COURSE, $assign->courseid)) {
287                     if (!is_enrolled($context, $assign->userid) and !is_viewing($context, $assign->userid)) {
288                         $DB->delete_records('user_lastaccess', array('userid'=>$assign->userid, 'courseid'=>$assign->courseid));
289                         mtrace("Deleted orphan user_lastaccess for user $assign->userid from course $assign->courseid");
290                     }
291                 }
292             }
293             $rs->close();
294         }
295         flush();
298         /// Delete users who haven't confirmed within required period
300         if (!empty($CFG->deleteunconfirmed)) {
301             $cuttime = $timenow - ($CFG->deleteunconfirmed * 3600);
302             $rs = $DB->get_recordset_sql ("SELECT id, firstname, lastname
303                                              FROM {user}
304                                             WHERE confirmed = 0 AND firstaccess > 0
305                                                   AND firstaccess < ?", array($cuttime));
306             foreach ($rs as $user) {
307                 if ($DB->delete_records('user', array('id'=>$user->id))) {
308                     mtrace("Deleted unconfirmed user for ".fullname($user, true)." ($user->id)");
309                 }
310             }
311             $rs->close();
312         }
313         flush();
316         /// Delete users who haven't completed profile within required period
318         if (!empty($CFG->deleteincompleteusers)) {
319             $cuttime = $timenow - ($CFG->deleteincompleteusers * 3600);
320             $rs = $DB->get_recordset_sql ("SELECT id, username
321                                              FROM {user}
322                                             WHERE confirmed = 1 AND lastaccess > 0
323                                                   AND lastaccess < ? AND deleted = 0
324                                                   AND (lastname = '' OR firstname = '' OR email = '')",
325                                           array($cuttime));
326             foreach ($rs as $user) {
327                 if (delete_user($user)) {
328                     mtrace("Deleted not fully setup user $user->username ($user->id)");
329                 }
330             }
331             $rs->close();
332         }
333         flush();
336         /// Delete old logs to save space (this might need a timer to slow it down...)
338         if (!empty($CFG->loglifetime)) {  // value in days
339             $loglifetime = $timenow - ($CFG->loglifetime * 3600 * 24);
340             if ($DB->delete_records_select("log", "time < ?", array($loglifetime))) {
341                 mtrace("Deleted old log records");
342             }
343         }
344         flush();
347         /// Delete old cached texts
349         if (!empty($CFG->cachetext)) {   // Defined in config.php
350             $cachelifetime = time() - $CFG->cachetext - 60;  // Add an extra minute to allow for really heavy sites
351             if ($DB->delete_records_select('cache_text', "timemodified < ?", array($cachelifetime))) {
352                 mtrace("Deleted old cache_text records");
353             }
354         }
355         flush();
357         if (!empty($CFG->notifyloginfailures)) {
358             notify_login_failures();
359             mtrace('Notified login failured');
360         }
361         flush();
363         sync_metacourses();
364         mtrace('Synchronised metacourses');
366         //
367         // generate new password emails for users
368         //
369         mtrace('checking for create_password');
370         if ($DB->count_records('user_preferences', array('name'=>'create_password', 'value'=>'1'))) {
371             mtrace('creating passwords for new users');
372             $newusers = $DB->get_records_sql("SELECT u.id as id, u.email, u.firstname,
373                                                      u.lastname, u.username,
374                                                      p.id as prefid
375                                                 FROM {user} u
376                                                 JOIN {user_preferences} p ON u.id=p.userid
377                                                WHERE p.name='create_password' AND p.value='1' AND u.email !='' ");
379             foreach ($newusers as $newuserid => $newuser) {
380                 $newuser->emailstop = 0; // send email regardless
381                 // email user
382                 if (setnew_password_and_mail($newuser)) {
383                     // remove user pref
384                     $DB->delete_records('user_preferences', array('id'=>$newuser->prefid));
385                 } else {
386                     trigger_error("Could not create and mail new user password!");
387                 }
388             }
389         }
391         if (!empty($CFG->usetags)) {
392             require_once($CFG->dirroot.'/tag/lib.php');
393             tag_cron();
394             mtrace ('Executed tag cron');
395         }
397         // Accesslib stuff
398         cleanup_contexts();
399         mtrace ('Cleaned up contexts');
400         gc_cache_flags();
401         mtrace ('Cleaned cache flags');
402         // If you suspect that the context paths are somehow corrupt
403         // replace the line below with: build_context_path(true);
404         build_context_path();
405         mtrace ('Built context paths');
407         mtrace("Finished clean-up tasks...");
409     } // End of occasional clean-up tasks
412     if (empty($CFG->disablescheduledbackups)) {   // Defined in config.php
413         //Execute backup's cron
414         //Perhaps a long time and memory could help in large sites
415         @set_time_limit(0);
416         @raise_memory_limit("192M");
417         if (function_exists('apache_child_terminate')) {
418             // if we are running from Apache, give httpd a hint that
419             // it can recycle the process after it's done. Apache's
420             // memory management is truly awful but we can help it.
421             @apache_child_terminate();
422         }
423         if (file_exists("$CFG->dirroot/backup/backup_scheduled.php") and
424             file_exists("$CFG->dirroot/backup/backuplib.php") and
425             file_exists("$CFG->dirroot/backup/lib.php") and
426             file_exists("$CFG->libdir/blocklib.php")) {
427             include_once("$CFG->dirroot/backup/backup_scheduled.php");
428             include_once("$CFG->dirroot/backup/backuplib.php");
429             include_once("$CFG->dirroot/backup/lib.php");
430             mtrace("Running backups if required...");
432             if (! schedule_backup_cron()) {
433                 mtrace("ERROR: Something went wrong while performing backup tasks!!!");
434             } else {
435                 mtrace("Backup tasks finished.");
436             }
437         }
438     }
440     if (!empty($CFG->enablerssfeeds)) {  //Defined in admin/variables page
441         include_once("$CFG->libdir/rsslib.php");
442         mtrace("Running rssfeeds if required...");
444         if ( ! cron_rss_feeds()) {
445             mtrace("Something went wrong while generating rssfeeds!!!");
446         } else {
447             mtrace("Rssfeeds finished");
448         }
449     }
451 /// Run the auth cron, if any
452 /// before enrolments because it might add users that will be needed in enrol plugins
453     $auths = get_enabled_auth_plugins();
455     mtrace("Running auth crons if required...");
456     foreach ($auths as $auth) {
457         $authplugin = get_auth_plugin($auth);
458         if (method_exists($authplugin, 'cron')) {
459             mtrace("Running cron for auth/$auth...");
460             $authplugin->cron();
461             if (!empty($authplugin->log)) {
462                 mtrace($authplugin->log);
463             }
464         }
465         unset($authplugin);
466     }
468 /// Run the enrolment cron, if any
469     if (!($plugins = explode(',', $CFG->enrol_plugins_enabled))) {
470         $plugins = array($CFG->enrol);
471     }
472     require_once($CFG->dirroot .'/enrol/enrol.class.php');
473     foreach ($plugins as $p) {
474         $enrol = enrolment_factory::factory($p);
475         if (method_exists($enrol, 'cron')) {
476             $enrol->cron();
477         }
478         if (!empty($enrol->log)) {
479             mtrace($enrol->log);
480         }
481         unset($enrol);
482     }
484     if (!empty($CFG->enablestats) and empty($CFG->disablestatsprocessing)) {
485         require_once($CFG->dirroot.'/lib/statslib.php');
486         // check we're not before our runtime
487         $timetocheck = stats_get_base_daily() + $CFG->statsruntimestarthour*60*60 + $CFG->statsruntimestartminute*60;
489         if (time() > $timetocheck) {
490             // process configured number of days as max (defaulting to 31)
491             $maxdays = empty($CFG->statsruntimedays) ? 31 : abs($CFG->statsruntimedays);
492             if (stats_cron_daily($maxdays)) {
493                 if (stats_cron_weekly()) {
494                     if (stats_cron_monthly()) {
495                         stats_clean_old();
496                     }
497                 }
498             }
499             @set_time_limit(0);
500         } else {
501             mtrace('Next stats run after:'. userdate($timetocheck));
502         }
503     }
505     // run gradebook import/export/report cron
506     if ($gradeimports = get_plugin_list('gradeimport')) {
507         foreach ($gradeimports as $gradeimport => $plugindir) {
508             if (file_exists($plugindir.'/lib.php')) {
509                 require_once($plugindir.'/lib.php');
510                 $cron_function = 'grade_import_'.$gradeimport.'_cron';
511                 if (function_exists($cron_function)) {
512                     mtrace("Processing gradebook import function $cron_function ...", '');
513                     $cron_function();
514                 }
515             }
516         }
517     }
519     if ($gradeexports = get_plugin_list('gradeexport')) {
520         foreach ($gradeexports as $gradeexport => $plugindir) {
521             if (file_exists($plugindir.'/lib.php')) {
522                 require_once($plugindir.'/lib.php');
523                 $cron_function = 'grade_export_'.$gradeexport.'_cron';
524                 if (function_exists($cron_function)) {
525                     mtrace("Processing gradebook export function $cron_function ...", '');
526                     $cron_function();
527                 }
528             }
529         }
530     }
532     if ($gradereports = get_plugin_list('gradereport')) {
533         foreach ($gradereports as $gradereport => $plugindir) {
534             if (file_exists($plugindir.'/lib.php')) {
535                 require_once($plugindir.'/lib.php');
536                 $cron_function = 'grade_report_'.$gradereport.'_cron';
537                 if (function_exists($cron_function)) {
538                     mtrace("Processing gradebook report function $cron_function ...", '');
539                     $cron_function();
540                 }
541             }
542         }
543     }
545     // Run external blog cron if needed
546     if ($CFG->useexternalblogs) {
547         require_once($CFG->dirroot . '/blog/lib.php');
548         mtrace("Fetching external blog entries...", '');
549         $sql = "timefetched < ? OR timefetched = 0";
550         $externalblogs = $DB->get_records_select('blog_external', $sql, array(mktime() - $CFG->externalblogcrontime));
552         foreach ($externalblogs as $eb) {
553             blog_sync_external_entries($eb);
554         }
555     }
557     // Run blog associations cleanup
558     if ($CFG->useblogassociations) {
559         require_once($CFG->dirroot . '/blog/lib.php');
560         // delete entries whose contextids no longer exists
561         mtrace("Deleting blog associations linked to non-existent contexts...", '');
562         $DB->delete_records_select('blog_association', 'contextid NOT IN (SELECT id FROM {context})');
563     }
565     // cleanup file trash
566     $fs = get_file_storage();
567     $fs->cron();
569     // run any customized cronjobs, if any
570     if ($locals = get_plugin_list('local')) {
571         mtrace('Processing customized cron scripts ...', '');
572         foreach ($locals as $local => $localdir) {
573             if (file_exists("$localdir/cron.php")) {
574                 include("$localdir/cron.php");
575             }
576         }
577         mtrace('done.');
578     }
581     mtrace("Cron script completed correctly");
583     $difftime = microtime_diff($starttime, microtime());
584     mtrace("Execution took ".$difftime." seconds");
586 /// finish the IE hack
587     if (check_browser_version('MSIE')) {
588         echo "</xmp>";
589     }