MDL-21579 "Implement session token for embedded application" Oops. Error in sql...
[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.');
246     if ($CFG->enablecompletion) {
247         // Completion cron
248         mtrace('Starting the completion cron...');
249         require_once($CFG->libdir . '/completion/cron.php');
250         completion_cron();
251         mtrace('done');
252     }
255     if ($CFG->enableportfolios) {
256         // Portfolio cron
257         mtrace('Starting the portfolio cron...');
258         require_once($CFG->libdir . '/portfoliolib.php');
259         portfolio_cron();
260         mtrace('done');
261     }
263 /// Run all core cron jobs, but not every time since they aren't too important.
264 /// These don't have a timer to reduce load, so we'll use a random number
265 /// to randomly choose the percentage of times we should run these jobs.
267     srand ((double) microtime() * 10000000);
268     $random100 = rand(0,100);
270     if ($random100 < 20) {     // Approximately 20% of the time.
271         mtrace("Running clean-up tasks...");
273         /// Unenrol users who haven't logged in for $CFG->longtimenosee
275         if ($CFG->longtimenosee) { // value in days
276             $cuttime = $timenow - ($CFG->longtimenosee * 3600 * 24);
277             $rs = $DB->get_recordset_sql ("SELECT id, userid, courseid
278                                              FROM {user_lastaccess}
279                                             WHERE courseid != ".SITEID."
280                                                   AND timeaccess < ?", array($cuttime));
281             foreach ($rs as $assign) {
282                 if ($context = get_context_instance(CONTEXT_COURSE, $assign->courseid)) {
283                     if (role_unassign(0, $assign->userid, 0, $context->id)) {
284                         mtrace("removing user $assign->userid from course $assign->courseid as they have not accessed the course for over $CFG->longtimenosee days");
285                     }
286                 }
287             }
288             $rs->close();
289         /// Execute the same query again, looking for remaining records and deleting them
290         /// if the user hasn't moodle/course:participate in the CONTEXT_COURSE context (orphan records)
291             $rs = $DB->get_recordset_sql ("SELECT id, userid, courseid
292                                              FROM {user_lastaccess}
293                                             WHERE courseid != ".SITEID."
294                                                   AND timeaccess < ?", array($cuttime));
295             foreach ($rs as $assign) {
296                 if ($context = get_context_instance(CONTEXT_COURSE, $assign->courseid)) {
297                     if (!is_enrolled($context, $assign->userid) and !is_viewing($context, $assign->userid)) {
298                         $DB->delete_records('user_lastaccess', array('userid'=>$assign->userid, 'courseid'=>$assign->courseid));
299                         mtrace("Deleted orphan user_lastaccess for user $assign->userid from course $assign->courseid");
300                     }
301                 }
302             }
303             $rs->close();
304         }
305         flush();
308         /// Delete users who haven't confirmed within required period
310         if (!empty($CFG->deleteunconfirmed)) {
311             $cuttime = $timenow - ($CFG->deleteunconfirmed * 3600);
312             $rs = $DB->get_recordset_sql ("SELECT id, firstname, lastname
313                                              FROM {user}
314                                             WHERE confirmed = 0 AND firstaccess > 0
315                                                   AND firstaccess < ?", array($cuttime));
316             foreach ($rs as $user) {
317                 if ($DB->delete_records('user', array('id'=>$user->id))) {
318                     mtrace("Deleted unconfirmed user for ".fullname($user, true)." ($user->id)");
319                 }
320             }
321             $rs->close();
322         }
323         flush();
326         /// Delete users who haven't completed profile within required period
328         if (!empty($CFG->deleteincompleteusers)) {
329             $cuttime = $timenow - ($CFG->deleteincompleteusers * 3600);
330             $rs = $DB->get_recordset_sql ("SELECT id, username
331                                              FROM {user}
332                                             WHERE confirmed = 1 AND lastaccess > 0
333                                                   AND lastaccess < ? AND deleted = 0
334                                                   AND (lastname = '' OR firstname = '' OR email = '')",
335                                           array($cuttime));
336             foreach ($rs as $user) {
337                 if (delete_user($user)) {
338                     mtrace("Deleted not fully setup user $user->username ($user->id)");
339                 }
340             }
341             $rs->close();
342         }
343         flush();
346         /// Delete old logs to save space (this might need a timer to slow it down...)
348         if (!empty($CFG->loglifetime)) {  // value in days
349             $loglifetime = $timenow - ($CFG->loglifetime * 3600 * 24);
350             if ($DB->delete_records_select("log", "time < ?", array($loglifetime))) {
351                 mtrace("Deleted old log records");
352             }
353         }
354         flush();
357         /// Delete old cached texts
359         if (!empty($CFG->cachetext)) {   // Defined in config.php
360             $cachelifetime = time() - $CFG->cachetext - 60;  // Add an extra minute to allow for really heavy sites
361             if ($DB->delete_records_select('cache_text', "timemodified < ?", array($cachelifetime))) {
362                 mtrace("Deleted old cache_text records");
363             }
364         }
365         flush();
367         if (!empty($CFG->notifyloginfailures)) {
368             notify_login_failures();
369             mtrace('Notified login failured');
370         }
371         flush();
373         sync_metacourses();
374         mtrace('Synchronised metacourses');
376         //
377         // generate new password emails for users
378         //
379         mtrace('checking for create_password');
380         if ($DB->count_records('user_preferences', array('name'=>'create_password', 'value'=>'1'))) {
381             mtrace('creating passwords for new users');
382             $newusers = $DB->get_records_sql("SELECT u.id as id, u.email, u.firstname,
383                                                      u.lastname, u.username,
384                                                      p.id as prefid
385                                                 FROM {user} u
386                                                 JOIN {user_preferences} p ON u.id=p.userid
387                                                WHERE p.name='create_password' AND p.value='1' AND u.email !='' ");
389             foreach ($newusers as $newuserid => $newuser) {
390                 $newuser->emailstop = 0; // send email regardless
391                 // email user
392                 if (setnew_password_and_mail($newuser)) {
393                     // remove user pref
394                     $DB->delete_records('user_preferences', array('id'=>$newuser->prefid));
395                 } else {
396                     trigger_error("Could not create and mail new user password!");
397                 }
398             }
399         }
401         if (!empty($CFG->usetags)) {
402             require_once($CFG->dirroot.'/tag/lib.php');
403             tag_cron();
404             mtrace ('Executed tag cron');
405         }
407         // Accesslib stuff
408         cleanup_contexts();
409         mtrace ('Cleaned up contexts');
410         gc_cache_flags();
411         mtrace ('Cleaned cache flags');
412         // If you suspect that the context paths are somehow corrupt
413         // replace the line below with: build_context_path(true);
414         build_context_path();
415         mtrace ('Built context paths');
417         mtrace("Finished clean-up tasks...");
419     } // End of occasional clean-up tasks
421     // Disabled until implemented. MDL-21432, MDL-22184
422     if (1 == 2 && empty($CFG->disablescheduledbackups)) {   // Defined in config.php
423         //Execute backup's cron
424         //Perhaps a long time and memory could help in large sites
425         @set_time_limit(0);
426         @raise_memory_limit("192M");
427         if (function_exists('apache_child_terminate')) {
428             // if we are running from Apache, give httpd a hint that
429             // it can recycle the process after it's done. Apache's
430             // memory management is truly awful but we can help it.
431             @apache_child_terminate();
432         }
433         if (file_exists("$CFG->dirroot/backup/backup_scheduled.php") and
434             file_exists("$CFG->dirroot/backup/backuplib.php") and
435             file_exists("$CFG->dirroot/backup/lib.php") and
436             file_exists("$CFG->libdir/blocklib.php")) {
437             include_once("$CFG->dirroot/backup/backup_scheduled.php");
438             include_once("$CFG->dirroot/backup/backuplib.php");
439             include_once("$CFG->dirroot/backup/lib.php");
440             mtrace("Running backups if required...");
442             if (! schedule_backup_cron()) {
443                 mtrace("ERROR: Something went wrong while performing backup tasks!!!");
444             } else {
445                 mtrace("Backup tasks finished.");
446             }
447         }
448     }
450 /// Run the auth cron, if any
451 /// before enrolments because it might add users that will be needed in enrol plugins
452     $auths = get_enabled_auth_plugins();
454     mtrace("Running auth crons if required...");
455     foreach ($auths as $auth) {
456         $authplugin = get_auth_plugin($auth);
457         if (method_exists($authplugin, 'cron')) {
458             mtrace("Running cron for auth/$auth...");
459             $authplugin->cron();
460             if (!empty($authplugin->log)) {
461                 mtrace($authplugin->log);
462             }
463         }
464         unset($authplugin);
465     }
467 /// Run the enrolment cron, if any
468     if (!($plugins = explode(',', $CFG->enrol_plugins_enabled))) {
469         $plugins = array($CFG->enrol);
470     }
471     require_once($CFG->dirroot .'/enrol/enrol.class.php');
472     foreach ($plugins as $p) {
473         $enrol = enrolment_factory::factory($p);
474         if (method_exists($enrol, 'cron')) {
475             $enrol->cron();
476         }
477         if (!empty($enrol->log)) {
478             mtrace($enrol->log);
479         }
480         unset($enrol);
481     }
483     if (!empty($CFG->enablestats) and empty($CFG->disablestatsprocessing)) {
484         require_once($CFG->dirroot.'/lib/statslib.php');
485         // check we're not before our runtime
486         $timetocheck = stats_get_base_daily() + $CFG->statsruntimestarthour*60*60 + $CFG->statsruntimestartminute*60;
488         if (time() > $timetocheck) {
489             // process configured number of days as max (defaulting to 31)
490             $maxdays = empty($CFG->statsruntimedays) ? 31 : abs($CFG->statsruntimedays);
491             if (stats_cron_daily($maxdays)) {
492                 if (stats_cron_weekly()) {
493                     if (stats_cron_monthly()) {
494                         stats_clean_old();
495                     }
496                 }
497             }
498             @set_time_limit(0);
499         } else {
500             mtrace('Next stats run after:'. userdate($timetocheck));
501         }
502     }
504     // run gradebook import/export/report cron
505     if ($gradeimports = get_plugin_list('gradeimport')) {
506         foreach ($gradeimports as $gradeimport => $plugindir) {
507             if (file_exists($plugindir.'/lib.php')) {
508                 require_once($plugindir.'/lib.php');
509                 $cron_function = 'grade_import_'.$gradeimport.'_cron';
510                 if (function_exists($cron_function)) {
511                     mtrace("Processing gradebook import function $cron_function ...", '');
512                     $cron_function();
513                 }
514             }
515         }
516     }
518     if ($gradeexports = get_plugin_list('gradeexport')) {
519         foreach ($gradeexports as $gradeexport => $plugindir) {
520             if (file_exists($plugindir.'/lib.php')) {
521                 require_once($plugindir.'/lib.php');
522                 $cron_function = 'grade_export_'.$gradeexport.'_cron';
523                 if (function_exists($cron_function)) {
524                     mtrace("Processing gradebook export function $cron_function ...", '');
525                     $cron_function();
526                 }
527             }
528         }
529     }
531     if ($gradereports = get_plugin_list('gradereport')) {
532         foreach ($gradereports as $gradereport => $plugindir) {
533             if (file_exists($plugindir.'/lib.php')) {
534                 require_once($plugindir.'/lib.php');
535                 $cron_function = 'grade_report_'.$gradereport.'_cron';
536                 if (function_exists($cron_function)) {
537                     mtrace("Processing gradebook report function $cron_function ...", '');
538                     $cron_function();
539                 }
540             }
541         }
542     }
544     // Run external blog cron if needed
545     if ($CFG->useexternalblogs) {
546         require_once($CFG->dirroot . '/blog/lib.php');
547         mtrace("Fetching external blog entries...", '');
548         $sql = "timefetched < ? OR timefetched = 0";
549         $externalblogs = $DB->get_records_select('blog_external', $sql, array(mktime() - $CFG->externalblogcrontime));
551         foreach ($externalblogs as $eb) {
552             blog_sync_external_entries($eb);
553         }
554     }
556     // Run blog associations cleanup
557     if ($CFG->useblogassociations) {
558         require_once($CFG->dirroot . '/blog/lib.php');
559         // delete entries whose contextids no longer exists
560         mtrace("Deleting blog associations linked to non-existent contexts...", '');
561         $DB->delete_records_select('blog_association', 'contextid NOT IN (SELECT id FROM {context})');
562     }
564     // cleanup file trash
565     $fs = get_file_storage();
566     $fs->cron();
568     //cleanup old session linked tokens
569     //deletes the session linked tokens that are over a day old.
570     mtrace("Deleting session linked tokens more than one day old...", '');
571     $DB->delete_records_select('external_tokens', 'lastaccess < :onedayago AND tokentype = :tokentype', 
572                     array('onedayago' => time() - DAYSECS, 'tokentype' => EXTERNAL_TOKEN_EMBEDDED));
573     mtrace('done.');
574     
575     // run any customized cronjobs, if any
576     if ($locals = get_plugin_list('local')) {
577         mtrace('Processing customized cron scripts ...', '');
578         foreach ($locals as $local => $localdir) {
579             if (file_exists("$localdir/cron.php")) {
580                 include("$localdir/cron.php");
581             }
582         }
583         mtrace('done.');
584     }
587     mtrace("Cron script completed correctly");
589     $difftime = microtime_diff($starttime, microtime());
590     mtrace("Execution took ".$difftime." seconds");
592 /// finish the IE hack
593     if (check_browser_version('MSIE')) {
594         echo "</xmp>";
595     }