MDL-50851 core_tag: introduce tag collections
[moodle.git] / lib / upgradelib.php
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
18 /**
19  * Various upgrade/install related functions and classes.
20  *
21  * @package    core
22  * @subpackage upgrade
23  * @copyright  1999 onwards Martin Dougiamas (http://dougiamas.com)
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 defined('MOODLE_INTERNAL') || die();
29 /** UPGRADE_LOG_NORMAL = 0 */
30 define('UPGRADE_LOG_NORMAL', 0);
31 /** UPGRADE_LOG_NOTICE = 1 */
32 define('UPGRADE_LOG_NOTICE', 1);
33 /** UPGRADE_LOG_ERROR = 2 */
34 define('UPGRADE_LOG_ERROR',  2);
36 /**
37  * Exception indicating unknown error during upgrade.
38  *
39  * @package    core
40  * @subpackage upgrade
41  * @copyright  2009 Petr Skoda {@link http://skodak.org}
42  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43  */
44 class upgrade_exception extends moodle_exception {
45     function __construct($plugin, $version, $debuginfo=NULL) {
46         global $CFG;
47         $a = (object)array('plugin'=>$plugin, 'version'=>$version);
48         parent::__construct('upgradeerror', 'admin', "$CFG->wwwroot/$CFG->admin/index.php", $a, $debuginfo);
49     }
50 }
52 /**
53  * Exception indicating downgrade error during upgrade.
54  *
55  * @package    core
56  * @subpackage upgrade
57  * @copyright  2009 Petr Skoda {@link http://skodak.org}
58  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
59  */
60 class downgrade_exception extends moodle_exception {
61     function __construct($plugin, $oldversion, $newversion) {
62         global $CFG;
63         $plugin = is_null($plugin) ? 'moodle' : $plugin;
64         $a = (object)array('plugin'=>$plugin, 'oldversion'=>$oldversion, 'newversion'=>$newversion);
65         parent::__construct('cannotdowngrade', 'debug', "$CFG->wwwroot/$CFG->admin/index.php", $a);
66     }
67 }
69 /**
70  * @package    core
71  * @subpackage upgrade
72  * @copyright  2009 Petr Skoda {@link http://skodak.org}
73  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
74  */
75 class upgrade_requires_exception extends moodle_exception {
76     function __construct($plugin, $pluginversion, $currentmoodle, $requiremoodle) {
77         global $CFG;
78         $a = new stdClass();
79         $a->pluginname     = $plugin;
80         $a->pluginversion  = $pluginversion;
81         $a->currentmoodle  = $currentmoodle;
82         $a->requiremoodle  = $requiremoodle;
83         parent::__construct('pluginrequirementsnotmet', 'error', "$CFG->wwwroot/$CFG->admin/index.php", $a);
84     }
85 }
87 /**
88  * @package    core
89  * @subpackage upgrade
90  * @copyright  2009 Petr Skoda {@link http://skodak.org}
91  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
92  */
93 class plugin_defective_exception extends moodle_exception {
94     function __construct($plugin, $details) {
95         global $CFG;
96         parent::__construct('detectedbrokenplugin', 'error', "$CFG->wwwroot/$CFG->admin/index.php", $plugin, $details);
97     }
98 }
100 /**
101  * Misplaced plugin exception.
102  *
103  * Note: this should be used only from the upgrade/admin code.
104  *
105  * @package    core
106  * @subpackage upgrade
107  * @copyright  2009 Petr Skoda {@link http://skodak.org}
108  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
109  */
110 class plugin_misplaced_exception extends moodle_exception {
111     /**
112      * Constructor.
113      * @param string $component the component from version.php
114      * @param string $expected expected directory, null means calculate
115      * @param string $current plugin directory path
116      */
117     public function __construct($component, $expected, $current) {
118         global $CFG;
119         if (empty($expected)) {
120             list($type, $plugin) = core_component::normalize_component($component);
121             $plugintypes = core_component::get_plugin_types();
122             if (isset($plugintypes[$type])) {
123                 $expected = $plugintypes[$type] . '/' . $plugin;
124             }
125         }
126         if (strpos($expected, '$CFG->dirroot') !== 0) {
127             $expected = str_replace($CFG->dirroot, '$CFG->dirroot', $expected);
128         }
129         if (strpos($current, '$CFG->dirroot') !== 0) {
130             $current = str_replace($CFG->dirroot, '$CFG->dirroot', $current);
131         }
132         $a = new stdClass();
133         $a->component = $component;
134         $a->expected  = $expected;
135         $a->current   = $current;
136         parent::__construct('detectedmisplacedplugin', 'core_plugin', "$CFG->wwwroot/$CFG->admin/index.php", $a);
137     }
140 /**
141  * Sets maximum expected time needed for upgrade task.
142  * Please always make sure that upgrade will not run longer!
143  *
144  * The script may be automatically aborted if upgrade times out.
145  *
146  * @category upgrade
147  * @param int $max_execution_time in seconds (can not be less than 60 s)
148  */
149 function upgrade_set_timeout($max_execution_time=300) {
150     global $CFG;
152     if (!isset($CFG->upgraderunning) or $CFG->upgraderunning < time()) {
153         $upgraderunning = get_config(null, 'upgraderunning');
154     } else {
155         $upgraderunning = $CFG->upgraderunning;
156     }
158     if (!$upgraderunning) {
159         if (CLI_SCRIPT) {
160             // never stop CLI upgrades
161             $upgraderunning = 0;
162         } else {
163             // web upgrade not running or aborted
164             print_error('upgradetimedout', 'admin', "$CFG->wwwroot/$CFG->admin/");
165         }
166     }
168     if ($max_execution_time < 60) {
169         // protection against 0 here
170         $max_execution_time = 60;
171     }
173     $expected_end = time() + $max_execution_time;
175     if ($expected_end < $upgraderunning + 10 and $expected_end > $upgraderunning - 10) {
176         // no need to store new end, it is nearly the same ;-)
177         return;
178     }
180     if (CLI_SCRIPT) {
181         // there is no point in timing out of CLI scripts, admins can stop them if necessary
182         core_php_time_limit::raise();
183     } else {
184         core_php_time_limit::raise($max_execution_time);
185     }
186     set_config('upgraderunning', $expected_end); // keep upgrade locked until this time
189 /**
190  * Upgrade savepoint, marks end of each upgrade block.
191  * It stores new main version, resets upgrade timeout
192  * and abort upgrade if user cancels page loading.
193  *
194  * Please do not make large upgrade blocks with lots of operations,
195  * for example when adding tables keep only one table operation per block.
196  *
197  * @category upgrade
198  * @param bool $result false if upgrade step failed, true if completed
199  * @param string or float $version main version
200  * @param bool $allowabort allow user to abort script execution here
201  * @return void
202  */
203 function upgrade_main_savepoint($result, $version, $allowabort=true) {
204     global $CFG;
206     //sanity check to avoid confusion with upgrade_mod_savepoint usage.
207     if (!is_bool($allowabort)) {
208         $errormessage = 'Parameter type mismatch. Are you mixing up upgrade_main_savepoint() and upgrade_mod_savepoint()?';
209         throw new coding_exception($errormessage);
210     }
212     if (!$result) {
213         throw new upgrade_exception(null, $version);
214     }
216     if ($CFG->version >= $version) {
217         // something really wrong is going on in main upgrade script
218         throw new downgrade_exception(null, $CFG->version, $version);
219     }
221     set_config('version', $version);
222     upgrade_log(UPGRADE_LOG_NORMAL, null, 'Upgrade savepoint reached');
224     // reset upgrade timeout to default
225     upgrade_set_timeout();
227     // this is a safe place to stop upgrades if user aborts page loading
228     if ($allowabort and connection_aborted()) {
229         die;
230     }
233 /**
234  * Module upgrade savepoint, marks end of module upgrade blocks
235  * It stores module version, resets upgrade timeout
236  * and abort upgrade if user cancels page loading.
237  *
238  * @category upgrade
239  * @param bool $result false if upgrade step failed, true if completed
240  * @param string or float $version main version
241  * @param string $modname name of module
242  * @param bool $allowabort allow user to abort script execution here
243  * @return void
244  */
245 function upgrade_mod_savepoint($result, $version, $modname, $allowabort=true) {
246     global $DB;
248     $component = 'mod_'.$modname;
250     if (!$result) {
251         throw new upgrade_exception($component, $version);
252     }
254     $dbversion = $DB->get_field('config_plugins', 'value', array('plugin'=>$component, 'name'=>'version'));
256     if (!$module = $DB->get_record('modules', array('name'=>$modname))) {
257         print_error('modulenotexist', 'debug', '', $modname);
258     }
260     if ($dbversion >= $version) {
261         // something really wrong is going on in upgrade script
262         throw new downgrade_exception($component, $dbversion, $version);
263     }
264     set_config('version', $version, $component);
266     upgrade_log(UPGRADE_LOG_NORMAL, $component, 'Upgrade savepoint reached');
268     // reset upgrade timeout to default
269     upgrade_set_timeout();
271     // this is a safe place to stop upgrades if user aborts page loading
272     if ($allowabort and connection_aborted()) {
273         die;
274     }
277 /**
278  * Blocks upgrade savepoint, marks end of blocks upgrade blocks
279  * It stores block version, resets upgrade timeout
280  * and abort upgrade if user cancels page loading.
281  *
282  * @category upgrade
283  * @param bool $result false if upgrade step failed, true if completed
284  * @param string or float $version main version
285  * @param string $blockname name of block
286  * @param bool $allowabort allow user to abort script execution here
287  * @return void
288  */
289 function upgrade_block_savepoint($result, $version, $blockname, $allowabort=true) {
290     global $DB;
292     $component = 'block_'.$blockname;
294     if (!$result) {
295         throw new upgrade_exception($component, $version);
296     }
298     $dbversion = $DB->get_field('config_plugins', 'value', array('plugin'=>$component, 'name'=>'version'));
300     if (!$block = $DB->get_record('block', array('name'=>$blockname))) {
301         print_error('blocknotexist', 'debug', '', $blockname);
302     }
304     if ($dbversion >= $version) {
305         // something really wrong is going on in upgrade script
306         throw new downgrade_exception($component, $dbversion, $version);
307     }
308     set_config('version', $version, $component);
310     upgrade_log(UPGRADE_LOG_NORMAL, $component, 'Upgrade savepoint reached');
312     // reset upgrade timeout to default
313     upgrade_set_timeout();
315     // this is a safe place to stop upgrades if user aborts page loading
316     if ($allowabort and connection_aborted()) {
317         die;
318     }
321 /**
322  * Plugins upgrade savepoint, marks end of blocks upgrade blocks
323  * It stores plugin version, resets upgrade timeout
324  * and abort upgrade if user cancels page loading.
325  *
326  * @category upgrade
327  * @param bool $result false if upgrade step failed, true if completed
328  * @param string or float $version main version
329  * @param string $type name of plugin
330  * @param string $dir location of plugin
331  * @param bool $allowabort allow user to abort script execution here
332  * @return void
333  */
334 function upgrade_plugin_savepoint($result, $version, $type, $plugin, $allowabort=true) {
335     global $DB;
337     $component = $type.'_'.$plugin;
339     if (!$result) {
340         throw new upgrade_exception($component, $version);
341     }
343     $dbversion = $DB->get_field('config_plugins', 'value', array('plugin'=>$component, 'name'=>'version'));
345     if ($dbversion >= $version) {
346         // Something really wrong is going on in the upgrade script
347         throw new downgrade_exception($component, $dbversion, $version);
348     }
349     set_config('version', $version, $component);
350     upgrade_log(UPGRADE_LOG_NORMAL, $component, 'Upgrade savepoint reached');
352     // Reset upgrade timeout to default
353     upgrade_set_timeout();
355     // This is a safe place to stop upgrades if user aborts page loading
356     if ($allowabort and connection_aborted()) {
357         die;
358     }
361 /**
362  * Detect if there are leftovers in PHP source files.
363  *
364  * During main version upgrades administrators MUST move away
365  * old PHP source files and start from scratch (or better
366  * use git).
367  *
368  * @return bool true means borked upgrade, false means previous PHP files were properly removed
369  */
370 function upgrade_stale_php_files_present() {
371     global $CFG;
373     $someexamplesofremovedfiles = array(
374         // Removed in 3.0.
375         '/mod/lti/grade.php',
376         '/tag/coursetagslib.php',
377         // Removed in 2.9.
378         '/lib/timezone.txt',
379         // Removed in 2.8.
380         '/course/delete_category_form.php',
381         // Removed in 2.7.
382         '/admin/tool/qeupgradehelper/version.php',
383         // Removed in 2.6.
384         '/admin/block.php',
385         '/admin/oacleanup.php',
386         // Removed in 2.5.
387         '/backup/lib.php',
388         '/backup/bb/README.txt',
389         '/lib/excel/test.php',
390         // Removed in 2.4.
391         '/admin/tool/unittest/simpletestlib.php',
392         // Removed in 2.3.
393         '/lib/minify/builder/',
394         // Removed in 2.2.
395         '/lib/yui/3.4.1pr1/',
396         // Removed in 2.2.
397         '/search/cron_php5.php',
398         '/course/report/log/indexlive.php',
399         '/admin/report/backups/index.php',
400         '/admin/generator.php',
401         // Removed in 2.1.
402         '/lib/yui/2.8.0r4/',
403         // Removed in 2.0.
404         '/blocks/admin/block_admin.php',
405         '/blocks/admin_tree/block_admin_tree.php',
406     );
408     foreach ($someexamplesofremovedfiles as $file) {
409         if (file_exists($CFG->dirroot.$file)) {
410             return true;
411         }
412     }
414     return false;
417 /**
418  * Upgrade plugins
419  * @param string $type The type of plugins that should be updated (e.g. 'enrol', 'qtype')
420  * return void
421  */
422 function upgrade_plugins($type, $startcallback, $endcallback, $verbose) {
423     global $CFG, $DB;
425 /// special cases
426     if ($type === 'mod') {
427         return upgrade_plugins_modules($startcallback, $endcallback, $verbose);
428     } else if ($type === 'block') {
429         return upgrade_plugins_blocks($startcallback, $endcallback, $verbose);
430     }
432     $plugs = core_component::get_plugin_list($type);
434     foreach ($plugs as $plug=>$fullplug) {
435         // Reset time so that it works when installing a large number of plugins
436         core_php_time_limit::raise(600);
437         $component = clean_param($type.'_'.$plug, PARAM_COMPONENT); // standardised plugin name
439         // check plugin dir is valid name
440         if (empty($component)) {
441             throw new plugin_defective_exception($type.'_'.$plug, 'Invalid plugin directory name.');
442         }
444         if (!is_readable($fullplug.'/version.php')) {
445             continue;
446         }
448         $plugin = new stdClass();
449         $plugin->version = null;
450         $module = $plugin; // Prevent some notices when plugin placed in wrong directory.
451         require($fullplug.'/version.php');  // defines $plugin with version etc
452         unset($module);
454         if (empty($plugin->version)) {
455             throw new plugin_defective_exception($component, 'Missing $plugin->version number in version.php.');
456         }
458         if (empty($plugin->component)) {
459             throw new plugin_defective_exception($component, 'Missing $plugin->component declaration in version.php.');
460         }
462         if ($plugin->component !== $component) {
463             throw new plugin_misplaced_exception($plugin->component, null, $fullplug);
464         }
466         $plugin->name     = $plug;
467         $plugin->fullname = $component;
469         if (!empty($plugin->requires)) {
470             if ($plugin->requires > $CFG->version) {
471                 throw new upgrade_requires_exception($component, $plugin->version, $CFG->version, $plugin->requires);
472             } else if ($plugin->requires < 2010000000) {
473                 throw new plugin_defective_exception($component, 'Plugin is not compatible with Moodle 2.x or later.');
474             }
475         }
477         // try to recover from interrupted install.php if needed
478         if (file_exists($fullplug.'/db/install.php')) {
479             if (get_config($plugin->fullname, 'installrunning')) {
480                 require_once($fullplug.'/db/install.php');
481                 $recover_install_function = 'xmldb_'.$plugin->fullname.'_install_recovery';
482                 if (function_exists($recover_install_function)) {
483                     $startcallback($component, true, $verbose);
484                     $recover_install_function();
485                     unset_config('installrunning', $plugin->fullname);
486                     update_capabilities($component);
487                     log_update_descriptions($component);
488                     external_update_descriptions($component);
489                     events_update_definition($component);
490                     \core\task\manager::reset_scheduled_tasks_for_component($component);
491                     message_update_providers($component);
492                     \core\message\inbound\manager::update_handlers_for_component($component);
493                     if ($type === 'message') {
494                         message_update_processors($plug);
495                     }
496                     upgrade_plugin_mnet_functions($component);
497                     core_tag_area::reset_definitions_for_component($component);
498                     $endcallback($component, true, $verbose);
499                 }
500             }
501         }
503         $installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
504         if (empty($installedversion)) { // new installation
505             $startcallback($component, true, $verbose);
507         /// Install tables if defined
508             if (file_exists($fullplug.'/db/install.xml')) {
509                 $DB->get_manager()->install_from_xmldb_file($fullplug.'/db/install.xml');
510             }
512         /// store version
513             upgrade_plugin_savepoint(true, $plugin->version, $type, $plug, false);
515         /// execute post install file
516             if (file_exists($fullplug.'/db/install.php')) {
517                 require_once($fullplug.'/db/install.php');
518                 set_config('installrunning', 1, $plugin->fullname);
519                 $post_install_function = 'xmldb_'.$plugin->fullname.'_install';
520                 $post_install_function();
521                 unset_config('installrunning', $plugin->fullname);
522             }
524         /// Install various components
525             update_capabilities($component);
526             log_update_descriptions($component);
527             external_update_descriptions($component);
528             events_update_definition($component);
529             \core\task\manager::reset_scheduled_tasks_for_component($component);
530             message_update_providers($component);
531             \core\message\inbound\manager::update_handlers_for_component($component);
532             if ($type === 'message') {
533                 message_update_processors($plug);
534             }
535             upgrade_plugin_mnet_functions($component);
536             core_tag_area::reset_definitions_for_component($component);
537             $endcallback($component, true, $verbose);
539         } else if ($installedversion < $plugin->version) { // upgrade
540         /// Run the upgrade function for the plugin.
541             $startcallback($component, false, $verbose);
543             if (is_readable($fullplug.'/db/upgrade.php')) {
544                 require_once($fullplug.'/db/upgrade.php');  // defines upgrading function
546                 $newupgrade_function = 'xmldb_'.$plugin->fullname.'_upgrade';
547                 $result = $newupgrade_function($installedversion);
548             } else {
549                 $result = true;
550             }
552             $installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
553             if ($installedversion < $plugin->version) {
554                 // store version if not already there
555                 upgrade_plugin_savepoint($result, $plugin->version, $type, $plug, false);
556             }
558         /// Upgrade various components
559             update_capabilities($component);
560             log_update_descriptions($component);
561             external_update_descriptions($component);
562             events_update_definition($component);
563             \core\task\manager::reset_scheduled_tasks_for_component($component);
564             message_update_providers($component);
565             \core\message\inbound\manager::update_handlers_for_component($component);
566             if ($type === 'message') {
567                 // Ugly hack!
568                 message_update_processors($plug);
569             }
570             upgrade_plugin_mnet_functions($component);
571             core_tag_area::reset_definitions_for_component($component);
572             $endcallback($component, false, $verbose);
574         } else if ($installedversion > $plugin->version) {
575             throw new downgrade_exception($component, $installedversion, $plugin->version);
576         }
577     }
580 /**
581  * Find and check all modules and load them up or upgrade them if necessary
582  *
583  * @global object
584  * @global object
585  */
586 function upgrade_plugins_modules($startcallback, $endcallback, $verbose) {
587     global $CFG, $DB;
589     $mods = core_component::get_plugin_list('mod');
591     foreach ($mods as $mod=>$fullmod) {
593         if ($mod === 'NEWMODULE') {   // Someone has unzipped the template, ignore it
594             continue;
595         }
597         $component = clean_param('mod_'.$mod, PARAM_COMPONENT);
599         // check module dir is valid name
600         if (empty($component)) {
601             throw new plugin_defective_exception('mod_'.$mod, 'Invalid plugin directory name.');
602         }
604         if (!is_readable($fullmod.'/version.php')) {
605             throw new plugin_defective_exception($component, 'Missing version.php');
606         }
608         $module = new stdClass();
609         $plugin = new stdClass();
610         $plugin->version = null;
611         require($fullmod .'/version.php');  // Defines $plugin with version etc.
613         // Check if the legacy $module syntax is still used.
614         if (!is_object($module) or (count((array)$module) > 0)) {
615             throw new plugin_defective_exception($component, 'Unsupported $module syntax detected in version.php');
616         }
618         // Prepare the record for the {modules} table.
619         $module = clone($plugin);
620         unset($module->version);
621         unset($module->component);
622         unset($module->dependencies);
623         unset($module->release);
625         if (empty($plugin->version)) {
626             throw new plugin_defective_exception($component, 'Missing $plugin->version number in version.php.');
627         }
629         if (empty($plugin->component)) {
630             throw new plugin_defective_exception($component, 'Missing $plugin->component declaration in version.php.');
631         }
633         if ($plugin->component !== $component) {
634             throw new plugin_misplaced_exception($plugin->component, null, $fullmod);
635         }
637         if (!empty($plugin->requires)) {
638             if ($plugin->requires > $CFG->version) {
639                 throw new upgrade_requires_exception($component, $plugin->version, $CFG->version, $plugin->requires);
640             } else if ($plugin->requires < 2010000000) {
641                 throw new plugin_defective_exception($component, 'Plugin is not compatible with Moodle 2.x or later.');
642             }
643         }
645         if (empty($module->cron)) {
646             $module->cron = 0;
647         }
649         // all modules must have en lang pack
650         if (!is_readable("$fullmod/lang/en/$mod.php")) {
651             throw new plugin_defective_exception($component, 'Missing mandatory en language pack.');
652         }
654         $module->name = $mod;   // The name MUST match the directory
656         $installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
658         if (file_exists($fullmod.'/db/install.php')) {
659             if (get_config($module->name, 'installrunning')) {
660                 require_once($fullmod.'/db/install.php');
661                 $recover_install_function = 'xmldb_'.$module->name.'_install_recovery';
662                 if (function_exists($recover_install_function)) {
663                     $startcallback($component, true, $verbose);
664                     $recover_install_function();
665                     unset_config('installrunning', $module->name);
666                     // Install various components too
667                     update_capabilities($component);
668                     log_update_descriptions($component);
669                     external_update_descriptions($component);
670                     events_update_definition($component);
671                     \core\task\manager::reset_scheduled_tasks_for_component($component);
672                     message_update_providers($component);
673                     \core\message\inbound\manager::update_handlers_for_component($component);
674                     upgrade_plugin_mnet_functions($component);
675                     core_tag_area::reset_definitions_for_component($component);
676                     $endcallback($component, true, $verbose);
677                 }
678             }
679         }
681         if (empty($installedversion)) {
682             $startcallback($component, true, $verbose);
684         /// Execute install.xml (XMLDB) - must be present in all modules
685             $DB->get_manager()->install_from_xmldb_file($fullmod.'/db/install.xml');
687         /// Add record into modules table - may be needed in install.php already
688             $module->id = $DB->insert_record('modules', $module);
689             upgrade_mod_savepoint(true, $plugin->version, $module->name, false);
691         /// Post installation hook - optional
692             if (file_exists("$fullmod/db/install.php")) {
693                 require_once("$fullmod/db/install.php");
694                 // Set installation running flag, we need to recover after exception or error
695                 set_config('installrunning', 1, $module->name);
696                 $post_install_function = 'xmldb_'.$module->name.'_install';
697                 $post_install_function();
698                 unset_config('installrunning', $module->name);
699             }
701         /// Install various components
702             update_capabilities($component);
703             log_update_descriptions($component);
704             external_update_descriptions($component);
705             events_update_definition($component);
706             \core\task\manager::reset_scheduled_tasks_for_component($component);
707             message_update_providers($component);
708             \core\message\inbound\manager::update_handlers_for_component($component);
709             upgrade_plugin_mnet_functions($component);
710             core_tag_area::reset_definitions_for_component($component);
712             $endcallback($component, true, $verbose);
714         } else if ($installedversion < $plugin->version) {
715         /// If versions say that we need to upgrade but no upgrade files are available, notify and continue
716             $startcallback($component, false, $verbose);
718             if (is_readable($fullmod.'/db/upgrade.php')) {
719                 require_once($fullmod.'/db/upgrade.php');  // defines new upgrading function
720                 $newupgrade_function = 'xmldb_'.$module->name.'_upgrade';
721                 $result = $newupgrade_function($installedversion, $module);
722             } else {
723                 $result = true;
724             }
726             $installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
727             $currmodule = $DB->get_record('modules', array('name'=>$module->name));
728             if ($installedversion < $plugin->version) {
729                 // store version if not already there
730                 upgrade_mod_savepoint($result, $plugin->version, $mod, false);
731             }
733             // update cron flag if needed
734             if ($currmodule->cron != $module->cron) {
735                 $DB->set_field('modules', 'cron', $module->cron, array('name' => $module->name));
736             }
738             // Upgrade various components
739             update_capabilities($component);
740             log_update_descriptions($component);
741             external_update_descriptions($component);
742             events_update_definition($component);
743             \core\task\manager::reset_scheduled_tasks_for_component($component);
744             message_update_providers($component);
745             \core\message\inbound\manager::update_handlers_for_component($component);
746             upgrade_plugin_mnet_functions($component);
747             core_tag_area::reset_definitions_for_component($component);
749             $endcallback($component, false, $verbose);
751         } else if ($installedversion > $plugin->version) {
752             throw new downgrade_exception($component, $installedversion, $plugin->version);
753         }
754     }
758 /**
759  * This function finds all available blocks and install them
760  * into blocks table or do all the upgrade process if newer.
761  *
762  * @global object
763  * @global object
764  */
765 function upgrade_plugins_blocks($startcallback, $endcallback, $verbose) {
766     global $CFG, $DB;
768     require_once($CFG->dirroot.'/blocks/moodleblock.class.php');
770     $blocktitles   = array(); // we do not want duplicate titles
772     //Is this a first install
773     $first_install = null;
775     $blocks = core_component::get_plugin_list('block');
777     foreach ($blocks as $blockname=>$fullblock) {
779         if (is_null($first_install)) {
780             $first_install = ($DB->count_records('block_instances') == 0);
781         }
783         if ($blockname === 'NEWBLOCK') {   // Someone has unzipped the template, ignore it
784             continue;
785         }
787         $component = clean_param('block_'.$blockname, PARAM_COMPONENT);
789         // check block dir is valid name
790         if (empty($component)) {
791             throw new plugin_defective_exception('block_'.$blockname, 'Invalid plugin directory name.');
792         }
794         if (!is_readable($fullblock.'/version.php')) {
795             throw new plugin_defective_exception('block/'.$blockname, 'Missing version.php file.');
796         }
797         $plugin = new stdClass();
798         $plugin->version = null;
799         $plugin->cron    = 0;
800         $module = $plugin; // Prevent some notices when module placed in wrong directory.
801         include($fullblock.'/version.php');
802         unset($module);
803         $block = clone($plugin);
804         unset($block->version);
805         unset($block->component);
806         unset($block->dependencies);
807         unset($block->release);
809         if (empty($plugin->version)) {
810             throw new plugin_defective_exception($component, 'Missing block version number in version.php.');
811         }
813         if (empty($plugin->component)) {
814             throw new plugin_defective_exception($component, 'Missing $plugin->component declaration in version.php.');
815         }
817         if ($plugin->component !== $component) {
818             throw new plugin_misplaced_exception($plugin->component, null, $fullblock);
819         }
821         if (!empty($plugin->requires)) {
822             if ($plugin->requires > $CFG->version) {
823                 throw new upgrade_requires_exception($component, $plugin->version, $CFG->version, $plugin->requires);
824             } else if ($plugin->requires < 2010000000) {
825                 throw new plugin_defective_exception($component, 'Plugin is not compatible with Moodle 2.x or later.');
826             }
827         }
829         if (!is_readable($fullblock.'/block_'.$blockname.'.php')) {
830             throw new plugin_defective_exception('block/'.$blockname, 'Missing main block class file.');
831         }
832         include_once($fullblock.'/block_'.$blockname.'.php');
834         $classname = 'block_'.$blockname;
836         if (!class_exists($classname)) {
837             throw new plugin_defective_exception($component, 'Can not load main class.');
838         }
840         $blockobj    = new $classname;   // This is what we'll be testing
841         $blocktitle  = $blockobj->get_title();
843         // OK, it's as we all hoped. For further tests, the object will do them itself.
844         if (!$blockobj->_self_test()) {
845             throw new plugin_defective_exception($component, 'Self test failed.');
846         }
848         $block->name     = $blockname;   // The name MUST match the directory
850         $installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
852         if (file_exists($fullblock.'/db/install.php')) {
853             if (get_config('block_'.$blockname, 'installrunning')) {
854                 require_once($fullblock.'/db/install.php');
855                 $recover_install_function = 'xmldb_block_'.$blockname.'_install_recovery';
856                 if (function_exists($recover_install_function)) {
857                     $startcallback($component, true, $verbose);
858                     $recover_install_function();
859                     unset_config('installrunning', 'block_'.$blockname);
860                     // Install various components
861                     update_capabilities($component);
862                     log_update_descriptions($component);
863                     external_update_descriptions($component);
864                     events_update_definition($component);
865                     \core\task\manager::reset_scheduled_tasks_for_component($component);
866                     message_update_providers($component);
867                     \core\message\inbound\manager::update_handlers_for_component($component);
868                     upgrade_plugin_mnet_functions($component);
869                     core_tag_area::reset_definitions_for_component($component);
870                     $endcallback($component, true, $verbose);
871                 }
872             }
873         }
875         if (empty($installedversion)) { // block not installed yet, so install it
876             $conflictblock = array_search($blocktitle, $blocktitles);
877             if ($conflictblock !== false) {
878                 // Duplicate block titles are not allowed, they confuse people
879                 // AND PHP's associative arrays ;)
880                 throw new plugin_defective_exception($component, get_string('blocknameconflict', 'error', (object)array('name'=>$block->name, 'conflict'=>$conflictblock)));
881             }
882             $startcallback($component, true, $verbose);
884             if (file_exists($fullblock.'/db/install.xml')) {
885                 $DB->get_manager()->install_from_xmldb_file($fullblock.'/db/install.xml');
886             }
887             $block->id = $DB->insert_record('block', $block);
888             upgrade_block_savepoint(true, $plugin->version, $block->name, false);
890             if (file_exists($fullblock.'/db/install.php')) {
891                 require_once($fullblock.'/db/install.php');
892                 // Set installation running flag, we need to recover after exception or error
893                 set_config('installrunning', 1, 'block_'.$blockname);
894                 $post_install_function = 'xmldb_block_'.$blockname.'_install';
895                 $post_install_function();
896                 unset_config('installrunning', 'block_'.$blockname);
897             }
899             $blocktitles[$block->name] = $blocktitle;
901             // Install various components
902             update_capabilities($component);
903             log_update_descriptions($component);
904             external_update_descriptions($component);
905             events_update_definition($component);
906             \core\task\manager::reset_scheduled_tasks_for_component($component);
907             message_update_providers($component);
908             \core\message\inbound\manager::update_handlers_for_component($component);
909             core_tag_area::reset_definitions_for_component($component);
910             upgrade_plugin_mnet_functions($component);
912             $endcallback($component, true, $verbose);
914         } else if ($installedversion < $plugin->version) {
915             $startcallback($component, false, $verbose);
917             if (is_readable($fullblock.'/db/upgrade.php')) {
918                 require_once($fullblock.'/db/upgrade.php');  // defines new upgrading function
919                 $newupgrade_function = 'xmldb_block_'.$blockname.'_upgrade';
920                 $result = $newupgrade_function($installedversion, $block);
921             } else {
922                 $result = true;
923             }
925             $installedversion = $DB->get_field('config_plugins', 'value', array('name'=>'version', 'plugin'=>$component)); // No caching!
926             $currblock = $DB->get_record('block', array('name'=>$block->name));
927             if ($installedversion < $plugin->version) {
928                 // store version if not already there
929                 upgrade_block_savepoint($result, $plugin->version, $block->name, false);
930             }
932             if ($currblock->cron != $block->cron) {
933                 // update cron flag if needed
934                 $DB->set_field('block', 'cron', $block->cron, array('id' => $currblock->id));
935             }
937             // Upgrade various components
938             update_capabilities($component);
939             log_update_descriptions($component);
940             external_update_descriptions($component);
941             events_update_definition($component);
942             \core\task\manager::reset_scheduled_tasks_for_component($component);
943             message_update_providers($component);
944             \core\message\inbound\manager::update_handlers_for_component($component);
945             upgrade_plugin_mnet_functions($component);
946             core_tag_area::reset_definitions_for_component($component);
948             $endcallback($component, false, $verbose);
950         } else if ($installedversion > $plugin->version) {
951             throw new downgrade_exception($component, $installedversion, $plugin->version);
952         }
953     }
956     // Finally, if we are in the first_install of BLOCKS setup frontpage and admin page blocks
957     if ($first_install) {
958         //Iterate over each course - there should be only site course here now
959         if ($courses = $DB->get_records('course')) {
960             foreach ($courses as $course) {
961                 blocks_add_default_course_blocks($course);
962             }
963         }
965         blocks_add_default_system_blocks();
966     }
970 /**
971  * Log_display description function used during install and upgrade.
972  *
973  * @param string $component name of component (moodle, mod_assignment, etc.)
974  * @return void
975  */
976 function log_update_descriptions($component) {
977     global $DB;
979     $defpath = core_component::get_component_directory($component).'/db/log.php';
981     if (!file_exists($defpath)) {
982         $DB->delete_records('log_display', array('component'=>$component));
983         return;
984     }
986     // load new info
987     $logs = array();
988     include($defpath);
989     $newlogs = array();
990     foreach ($logs as $log) {
991         $newlogs[$log['module'].'-'.$log['action']] = $log; // kind of unique name
992     }
993     unset($logs);
994     $logs = $newlogs;
996     $fields = array('module', 'action', 'mtable', 'field');
997     // update all log fist
998     $dblogs = $DB->get_records('log_display', array('component'=>$component));
999     foreach ($dblogs as $dblog) {
1000         $name = $dblog->module.'-'.$dblog->action;
1002         if (empty($logs[$name])) {
1003             $DB->delete_records('log_display', array('id'=>$dblog->id));
1004             continue;
1005         }
1007         $log = $logs[$name];
1008         unset($logs[$name]);
1010         $update = false;
1011         foreach ($fields as $field) {
1012             if ($dblog->$field != $log[$field]) {
1013                 $dblog->$field = $log[$field];
1014                 $update = true;
1015             }
1016         }
1017         if ($update) {
1018             $DB->update_record('log_display', $dblog);
1019         }
1020     }
1021     foreach ($logs as $log) {
1022         $dblog = (object)$log;
1023         $dblog->component = $component;
1024         $DB->insert_record('log_display', $dblog);
1025     }
1028 /**
1029  * Web service discovery function used during install and upgrade.
1030  * @param string $component name of component (moodle, mod_assignment, etc.)
1031  * @return void
1032  */
1033 function external_update_descriptions($component) {
1034     global $DB, $CFG;
1036     $defpath = core_component::get_component_directory($component).'/db/services.php';
1038     if (!file_exists($defpath)) {
1039         require_once($CFG->dirroot.'/lib/externallib.php');
1040         external_delete_descriptions($component);
1041         return;
1042     }
1044     // load new info
1045     $functions = array();
1046     $services = array();
1047     include($defpath);
1049     // update all function fist
1050     $dbfunctions = $DB->get_records('external_functions', array('component'=>$component));
1051     foreach ($dbfunctions as $dbfunction) {
1052         if (empty($functions[$dbfunction->name])) {
1053             $DB->delete_records('external_functions', array('id'=>$dbfunction->id));
1054             // do not delete functions from external_services_functions, beacuse
1055             // we want to notify admins when functions used in custom services disappear
1057             //TODO: this looks wrong, we have to delete it eventually (skodak)
1058             continue;
1059         }
1061         $function = $functions[$dbfunction->name];
1062         unset($functions[$dbfunction->name]);
1063         $function['classpath'] = empty($function['classpath']) ? null : $function['classpath'];
1065         $update = false;
1066         if ($dbfunction->classname != $function['classname']) {
1067             $dbfunction->classname = $function['classname'];
1068             $update = true;
1069         }
1070         if ($dbfunction->methodname != $function['methodname']) {
1071             $dbfunction->methodname = $function['methodname'];
1072             $update = true;
1073         }
1074         if ($dbfunction->classpath != $function['classpath']) {
1075             $dbfunction->classpath = $function['classpath'];
1076             $update = true;
1077         }
1078         $functioncapabilities = array_key_exists('capabilities', $function)?$function['capabilities']:'';
1079         if ($dbfunction->capabilities != $functioncapabilities) {
1080             $dbfunction->capabilities = $functioncapabilities;
1081             $update = true;
1082         }
1083         if ($update) {
1084             $DB->update_record('external_functions', $dbfunction);
1085         }
1086     }
1087     foreach ($functions as $fname => $function) {
1088         $dbfunction = new stdClass();
1089         $dbfunction->name       = $fname;
1090         $dbfunction->classname  = $function['classname'];
1091         $dbfunction->methodname = $function['methodname'];
1092         $dbfunction->classpath  = empty($function['classpath']) ? null : $function['classpath'];
1093         $dbfunction->component  = $component;
1094         $dbfunction->capabilities = array_key_exists('capabilities', $function)?$function['capabilities']:'';
1095         $dbfunction->id = $DB->insert_record('external_functions', $dbfunction);
1096     }
1097     unset($functions);
1099     // now deal with services
1100     $dbservices = $DB->get_records('external_services', array('component'=>$component));
1101     foreach ($dbservices as $dbservice) {
1102         if (empty($services[$dbservice->name])) {
1103             $DB->delete_records('external_tokens', array('externalserviceid'=>$dbservice->id));
1104             $DB->delete_records('external_services_functions', array('externalserviceid'=>$dbservice->id));
1105             $DB->delete_records('external_services_users', array('externalserviceid'=>$dbservice->id));
1106             $DB->delete_records('external_services', array('id'=>$dbservice->id));
1107             continue;
1108         }
1109         $service = $services[$dbservice->name];
1110         unset($services[$dbservice->name]);
1111         $service['enabled'] = empty($service['enabled']) ? 0 : $service['enabled'];
1112         $service['requiredcapability'] = empty($service['requiredcapability']) ? null : $service['requiredcapability'];
1113         $service['restrictedusers'] = !isset($service['restrictedusers']) ? 1 : $service['restrictedusers'];
1114         $service['downloadfiles'] = !isset($service['downloadfiles']) ? 0 : $service['downloadfiles'];
1115         $service['uploadfiles'] = !isset($service['uploadfiles']) ? 0 : $service['uploadfiles'];
1116         $service['shortname'] = !isset($service['shortname']) ? null : $service['shortname'];
1118         $update = false;
1119         if ($dbservice->requiredcapability != $service['requiredcapability']) {
1120             $dbservice->requiredcapability = $service['requiredcapability'];
1121             $update = true;
1122         }
1123         if ($dbservice->restrictedusers != $service['restrictedusers']) {
1124             $dbservice->restrictedusers = $service['restrictedusers'];
1125             $update = true;
1126         }
1127         if ($dbservice->downloadfiles != $service['downloadfiles']) {
1128             $dbservice->downloadfiles = $service['downloadfiles'];
1129             $update = true;
1130         }
1131         if ($dbservice->uploadfiles != $service['uploadfiles']) {
1132             $dbservice->uploadfiles = $service['uploadfiles'];
1133             $update = true;
1134         }
1135         //if shortname is not a PARAM_ALPHANUMEXT, fail (tested here for service update and creation)
1136         if (isset($service['shortname']) and
1137                 (clean_param($service['shortname'], PARAM_ALPHANUMEXT) != $service['shortname'])) {
1138             throw new moodle_exception('installserviceshortnameerror', 'webservice', '', $service['shortname']);
1139         }
1140         if ($dbservice->shortname != $service['shortname']) {
1141             //check that shortname is unique
1142             if (isset($service['shortname'])) { //we currently accepts multiple shortname == null
1143                 $existingservice = $DB->get_record('external_services',
1144                         array('shortname' => $service['shortname']));
1145                 if (!empty($existingservice)) {
1146                     throw new moodle_exception('installexistingserviceshortnameerror', 'webservice', '', $service['shortname']);
1147                 }
1148             }
1149             $dbservice->shortname = $service['shortname'];
1150             $update = true;
1151         }
1152         if ($update) {
1153             $DB->update_record('external_services', $dbservice);
1154         }
1156         $functions = $DB->get_records('external_services_functions', array('externalserviceid'=>$dbservice->id));
1157         foreach ($functions as $function) {
1158             $key = array_search($function->functionname, $service['functions']);
1159             if ($key === false) {
1160                 $DB->delete_records('external_services_functions', array('id'=>$function->id));
1161             } else {
1162                 unset($service['functions'][$key]);
1163             }
1164         }
1165         foreach ($service['functions'] as $fname) {
1166             $newf = new stdClass();
1167             $newf->externalserviceid = $dbservice->id;
1168             $newf->functionname      = $fname;
1169             $DB->insert_record('external_services_functions', $newf);
1170         }
1171         unset($functions);
1172     }
1173     foreach ($services as $name => $service) {
1174         //check that shortname is unique
1175         if (isset($service['shortname'])) { //we currently accepts multiple shortname == null
1176             $existingservice = $DB->get_record('external_services',
1177                     array('shortname' => $service['shortname']));
1178             if (!empty($existingservice)) {
1179                 throw new moodle_exception('installserviceshortnameerror', 'webservice');
1180             }
1181         }
1183         $dbservice = new stdClass();
1184         $dbservice->name               = $name;
1185         $dbservice->enabled            = empty($service['enabled']) ? 0 : $service['enabled'];
1186         $dbservice->requiredcapability = empty($service['requiredcapability']) ? null : $service['requiredcapability'];
1187         $dbservice->restrictedusers    = !isset($service['restrictedusers']) ? 1 : $service['restrictedusers'];
1188         $dbservice->downloadfiles      = !isset($service['downloadfiles']) ? 0 : $service['downloadfiles'];
1189         $dbservice->uploadfiles        = !isset($service['uploadfiles']) ? 0 : $service['uploadfiles'];
1190         $dbservice->shortname          = !isset($service['shortname']) ? null : $service['shortname'];
1191         $dbservice->component          = $component;
1192         $dbservice->timecreated        = time();
1193         $dbservice->id = $DB->insert_record('external_services', $dbservice);
1194         foreach ($service['functions'] as $fname) {
1195             $newf = new stdClass();
1196             $newf->externalserviceid = $dbservice->id;
1197             $newf->functionname      = $fname;
1198             $DB->insert_record('external_services_functions', $newf);
1199         }
1200     }
1203 /**
1204  * upgrade logging functions
1205  */
1206 function upgrade_handle_exception($ex, $plugin = null) {
1207     global $CFG;
1209     // rollback everything, we need to log all upgrade problems
1210     abort_all_db_transactions();
1212     $info = get_exception_info($ex);
1214     // First log upgrade error
1215     upgrade_log(UPGRADE_LOG_ERROR, $plugin, 'Exception: ' . get_class($ex), $info->message, $info->backtrace);
1217     // Always turn on debugging - admins need to know what is going on
1218     set_debugging(DEBUG_DEVELOPER, true);
1220     default_exception_handler($ex, true, $plugin);
1223 /**
1224  * Adds log entry into upgrade_log table
1225  *
1226  * @param int $type UPGRADE_LOG_NORMAL, UPGRADE_LOG_NOTICE or UPGRADE_LOG_ERROR
1227  * @param string $plugin frankenstyle component name
1228  * @param string $info short description text of log entry
1229  * @param string $details long problem description
1230  * @param string $backtrace string used for errors only
1231  * @return void
1232  */
1233 function upgrade_log($type, $plugin, $info, $details=null, $backtrace=null) {
1234     global $DB, $USER, $CFG;
1236     if (empty($plugin)) {
1237         $plugin = 'core';
1238     }
1240     list($plugintype, $pluginname) = core_component::normalize_component($plugin);
1241     $component = is_null($pluginname) ? $plugintype : $plugintype . '_' . $pluginname;
1243     $backtrace = format_backtrace($backtrace, true);
1245     $currentversion = null;
1246     $targetversion  = null;
1248     //first try to find out current version number
1249     if ($plugintype === 'core') {
1250         //main
1251         $currentversion = $CFG->version;
1253         $version = null;
1254         include("$CFG->dirroot/version.php");
1255         $targetversion = $version;
1257     } else {
1258         $pluginversion = get_config($component, 'version');
1259         if (!empty($pluginversion)) {
1260             $currentversion = $pluginversion;
1261         }
1262         $cd = core_component::get_component_directory($component);
1263         if (file_exists("$cd/version.php")) {
1264             $plugin = new stdClass();
1265             $plugin->version = null;
1266             $module = $plugin;
1267             include("$cd/version.php");
1268             $targetversion = $plugin->version;
1269         }
1270     }
1272     $log = new stdClass();
1273     $log->type          = $type;
1274     $log->plugin        = $component;
1275     $log->version       = $currentversion;
1276     $log->targetversion = $targetversion;
1277     $log->info          = $info;
1278     $log->details       = $details;
1279     $log->backtrace     = $backtrace;
1280     $log->userid        = $USER->id;
1281     $log->timemodified  = time();
1282     try {
1283         $DB->insert_record('upgrade_log', $log);
1284     } catch (Exception $ignored) {
1285         // possible during install or 2.0 upgrade
1286     }
1289 /**
1290  * Marks start of upgrade, blocks any other access to site.
1291  * The upgrade is finished at the end of script or after timeout.
1292  *
1293  * @global object
1294  * @global object
1295  * @global object
1296  */
1297 function upgrade_started($preinstall=false) {
1298     global $CFG, $DB, $PAGE, $OUTPUT;
1300     static $started = false;
1302     if ($preinstall) {
1303         ignore_user_abort(true);
1304         upgrade_setup_debug(true);
1306     } else if ($started) {
1307         upgrade_set_timeout(120);
1309     } else {
1310         if (!CLI_SCRIPT and !$PAGE->headerprinted) {
1311             $strupgrade  = get_string('upgradingversion', 'admin');
1312             $PAGE->set_pagelayout('maintenance');
1313             upgrade_init_javascript();
1314             $PAGE->set_title($strupgrade.' - Moodle '.$CFG->target_release);
1315             $PAGE->set_heading($strupgrade);
1316             $PAGE->navbar->add($strupgrade);
1317             $PAGE->set_cacheable(false);
1318             echo $OUTPUT->header();
1319         }
1321         ignore_user_abort(true);
1322         core_shutdown_manager::register_function('upgrade_finished_handler');
1323         upgrade_setup_debug(true);
1324         set_config('upgraderunning', time()+300);
1325         $started = true;
1326     }
1329 /**
1330  * Internal function - executed if upgrade interrupted.
1331  */
1332 function upgrade_finished_handler() {
1333     upgrade_finished();
1336 /**
1337  * Indicates upgrade is finished.
1338  *
1339  * This function may be called repeatedly.
1340  *
1341  * @global object
1342  * @global object
1343  */
1344 function upgrade_finished($continueurl=null) {
1345     global $CFG, $DB, $OUTPUT;
1347     if (!empty($CFG->upgraderunning)) {
1348         unset_config('upgraderunning');
1349         // We have to forcefully purge the caches using the writer here.
1350         // This has to be done after we unset the config var. If someone hits the site while this is set they will
1351         // cause the config values to propogate to the caches.
1352         // Caches are purged after the last step in an upgrade but there is several code routines that exceute between
1353         // then and now that leaving a window for things to fall out of sync.
1354         cache_helper::purge_all(true);
1355         upgrade_setup_debug(false);
1356         ignore_user_abort(false);
1357         if ($continueurl) {
1358             echo $OUTPUT->continue_button($continueurl);
1359             echo $OUTPUT->footer();
1360             die;
1361         }
1362     }
1365 /**
1366  * @global object
1367  * @global object
1368  */
1369 function upgrade_setup_debug($starting) {
1370     global $CFG, $DB;
1372     static $originaldebug = null;
1374     if ($starting) {
1375         if ($originaldebug === null) {
1376             $originaldebug = $DB->get_debug();
1377         }
1378         if (!empty($CFG->upgradeshowsql)) {
1379             $DB->set_debug(true);
1380         }
1381     } else {
1382         $DB->set_debug($originaldebug);
1383     }
1386 function print_upgrade_separator() {
1387     if (!CLI_SCRIPT) {
1388         echo '<hr />';
1389     }
1392 /**
1393  * Default start upgrade callback
1394  * @param string $plugin
1395  * @param bool $installation true if installation, false means upgrade
1396  */
1397 function print_upgrade_part_start($plugin, $installation, $verbose) {
1398     global $OUTPUT;
1399     if (empty($plugin) or $plugin == 'moodle') {
1400         upgrade_started($installation); // does not store upgrade running flag yet
1401         if ($verbose) {
1402             echo $OUTPUT->heading(get_string('coresystem'));
1403         }
1404     } else {
1405         upgrade_started();
1406         if ($verbose) {
1407             echo $OUTPUT->heading($plugin);
1408         }
1409     }
1410     if ($installation) {
1411         if (empty($plugin) or $plugin == 'moodle') {
1412             // no need to log - log table not yet there ;-)
1413         } else {
1414             upgrade_log(UPGRADE_LOG_NORMAL, $plugin, 'Starting plugin installation');
1415         }
1416     } else {
1417         if (empty($plugin) or $plugin == 'moodle') {
1418             upgrade_log(UPGRADE_LOG_NORMAL, $plugin, 'Starting core upgrade');
1419         } else {
1420             upgrade_log(UPGRADE_LOG_NORMAL, $plugin, 'Starting plugin upgrade');
1421         }
1422     }
1425 /**
1426  * Default end upgrade callback
1427  * @param string $plugin
1428  * @param bool $installation true if installation, false means upgrade
1429  */
1430 function print_upgrade_part_end($plugin, $installation, $verbose) {
1431     global $OUTPUT;
1432     upgrade_started();
1433     if ($installation) {
1434         if (empty($plugin) or $plugin == 'moodle') {
1435             upgrade_log(UPGRADE_LOG_NORMAL, $plugin, 'Core installed');
1436         } else {
1437             upgrade_log(UPGRADE_LOG_NORMAL, $plugin, 'Plugin installed');
1438         }
1439     } else {
1440         if (empty($plugin) or $plugin == 'moodle') {
1441             upgrade_log(UPGRADE_LOG_NORMAL, $plugin, 'Core upgraded');
1442         } else {
1443             upgrade_log(UPGRADE_LOG_NORMAL, $plugin, 'Plugin upgraded');
1444         }
1445     }
1446     if ($verbose) {
1447         echo $OUTPUT->notification(get_string('success'), 'notifysuccess');
1448         print_upgrade_separator();
1449     }
1452 /**
1453  * Sets up JS code required for all upgrade scripts.
1454  * @global object
1455  */
1456 function upgrade_init_javascript() {
1457     global $PAGE;
1458     // scroll to the end of each upgrade page so that ppl see either error or continue button,
1459     // no need to scroll continuously any more, it is enough to jump to end once the footer is printed ;-)
1460     $js = "window.scrollTo(0, 5000000);";
1461     $PAGE->requires->js_init_code($js);
1464 /**
1465  * Try to upgrade the given language pack (or current language)
1466  *
1467  * @param string $lang the code of the language to update, defaults to the current language
1468  */
1469 function upgrade_language_pack($lang = null) {
1470     global $CFG;
1472     if (!empty($CFG->skiplangupgrade)) {
1473         return;
1474     }
1476     if (!file_exists("$CFG->dirroot/$CFG->admin/tool/langimport/lib.php")) {
1477         // weird, somebody uninstalled the import utility
1478         return;
1479     }
1481     if (!$lang) {
1482         $lang = current_language();
1483     }
1485     if (!get_string_manager()->translation_exists($lang)) {
1486         return;
1487     }
1489     get_string_manager()->reset_caches();
1491     if ($lang === 'en') {
1492         return;  // Nothing to do
1493     }
1495     upgrade_started(false);
1497     require_once("$CFG->dirroot/$CFG->admin/tool/langimport/lib.php");
1498     tool_langimport_preupgrade_update($lang);
1500     get_string_manager()->reset_caches();
1502     print_upgrade_separator();
1505 /**
1506  * Install core moodle tables and initialize
1507  * @param float $version target version
1508  * @param bool $verbose
1509  * @return void, may throw exception
1510  */
1511 function install_core($version, $verbose) {
1512     global $CFG, $DB;
1514     // We can not call purge_all_caches() yet, make sure the temp and cache dirs exist and are empty.
1515     remove_dir($CFG->cachedir.'', true);
1516     make_cache_directory('', true);
1518     remove_dir($CFG->localcachedir.'', true);
1519     make_localcache_directory('', true);
1521     remove_dir($CFG->tempdir.'', true);
1522     make_temp_directory('', true);
1524     remove_dir($CFG->dataroot.'/muc', true);
1525     make_writable_directory($CFG->dataroot.'/muc', true);
1527     try {
1528         core_php_time_limit::raise(600);
1529         print_upgrade_part_start('moodle', true, $verbose); // does not store upgrade running flag
1531         $DB->get_manager()->install_from_xmldb_file("$CFG->libdir/db/install.xml");
1532         upgrade_started();     // we want the flag to be stored in config table ;-)
1534         // set all core default records and default settings
1535         require_once("$CFG->libdir/db/install.php");
1536         xmldb_main_install(); // installs the capabilities too
1538         // store version
1539         upgrade_main_savepoint(true, $version, false);
1541         // Continue with the installation
1542         log_update_descriptions('moodle');
1543         external_update_descriptions('moodle');
1544         events_update_definition('moodle');
1545         \core\task\manager::reset_scheduled_tasks_for_component('moodle');
1546         message_update_providers('moodle');
1547         \core\message\inbound\manager::update_handlers_for_component('moodle');
1548         core_tag_area::reset_definitions_for_component('moodle');
1550         // Write default settings unconditionally
1551         admin_apply_default_settings(NULL, true);
1553         print_upgrade_part_end(null, true, $verbose);
1555         // Purge all caches. They're disabled but this ensures that we don't have any persistent data just in case something
1556         // during installation didn't use APIs.
1557         cache_helper::purge_all();
1558     } catch (exception $ex) {
1559         upgrade_handle_exception($ex);
1560     } catch (Throwable $ex) {
1561         // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
1562         upgrade_handle_exception($ex);
1563     }
1566 /**
1567  * Upgrade moodle core
1568  * @param float $version target version
1569  * @param bool $verbose
1570  * @return void, may throw exception
1571  */
1572 function upgrade_core($version, $verbose) {
1573     global $CFG, $SITE, $DB, $COURSE;
1575     raise_memory_limit(MEMORY_EXTRA);
1577     require_once($CFG->libdir.'/db/upgrade.php');    // Defines upgrades
1579     try {
1580         // Reset caches before any output.
1581         cache_helper::purge_all(true);
1582         purge_all_caches();
1584         // Upgrade current language pack if we can
1585         upgrade_language_pack();
1587         print_upgrade_part_start('moodle', false, $verbose);
1589         // Pre-upgrade scripts for local hack workarounds.
1590         $preupgradefile = "$CFG->dirroot/local/preupgrade.php";
1591         if (file_exists($preupgradefile)) {
1592             core_php_time_limit::raise();
1593             require($preupgradefile);
1594             // Reset upgrade timeout to default.
1595             upgrade_set_timeout();
1596         }
1598         $result = xmldb_main_upgrade($CFG->version);
1599         if ($version > $CFG->version) {
1600             // store version if not already there
1601             upgrade_main_savepoint($result, $version, false);
1602         }
1604         // In case structure of 'course' table has been changed and we forgot to update $SITE, re-read it from db.
1605         $SITE = $DB->get_record('course', array('id' => $SITE->id));
1606         $COURSE = clone($SITE);
1608         // perform all other component upgrade routines
1609         update_capabilities('moodle');
1610         log_update_descriptions('moodle');
1611         external_update_descriptions('moodle');
1612         events_update_definition('moodle');
1613         \core\task\manager::reset_scheduled_tasks_for_component('moodle');
1614         message_update_providers('moodle');
1615         \core\message\inbound\manager::update_handlers_for_component('moodle');
1616         core_tag_area::reset_definitions_for_component('moodle');
1617         // Update core definitions.
1618         cache_helper::update_definitions(true);
1620         // Purge caches again, just to be sure we arn't holding onto old stuff now.
1621         cache_helper::purge_all(true);
1622         purge_all_caches();
1624         // Clean up contexts - more and more stuff depends on existence of paths and contexts
1625         context_helper::cleanup_instances();
1626         context_helper::create_instances(null, false);
1627         context_helper::build_all_paths(false);
1628         $syscontext = context_system::instance();
1629         $syscontext->mark_dirty();
1631         print_upgrade_part_end('moodle', false, $verbose);
1632     } catch (Exception $ex) {
1633         upgrade_handle_exception($ex);
1634     } catch (Throwable $ex) {
1635         // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
1636         upgrade_handle_exception($ex);
1637     }
1640 /**
1641  * Upgrade/install other parts of moodle
1642  * @param bool $verbose
1643  * @return void, may throw exception
1644  */
1645 function upgrade_noncore($verbose) {
1646     global $CFG;
1648     raise_memory_limit(MEMORY_EXTRA);
1650     // upgrade all plugins types
1651     try {
1652         // Reset caches before any output.
1653         cache_helper::purge_all(true);
1654         purge_all_caches();
1656         $plugintypes = core_component::get_plugin_types();
1657         foreach ($plugintypes as $type=>$location) {
1658             upgrade_plugins($type, 'print_upgrade_part_start', 'print_upgrade_part_end', $verbose);
1659         }
1660         // Update cache definitions. Involves scanning each plugin for any changes.
1661         cache_helper::update_definitions();
1662         // Mark the site as upgraded.
1663         set_config('allversionshash', core_component::get_all_versions_hash());
1665         // Purge caches again, just to be sure we arn't holding onto old stuff now.
1666         cache_helper::purge_all(true);
1667         purge_all_caches();
1669     } catch (Exception $ex) {
1670         upgrade_handle_exception($ex);
1671     } catch (Throwable $ex) {
1672         // Engine errors in PHP7 throw exceptions of type Throwable (this "catch" will be ignored in PHP5).
1673         upgrade_handle_exception($ex);
1674     }
1677 /**
1678  * Checks if the main tables have been installed yet or not.
1679  *
1680  * Note: we can not use caches here because they might be stale,
1681  *       use with care!
1682  *
1683  * @return bool
1684  */
1685 function core_tables_exist() {
1686     global $DB;
1688     if (!$tables = $DB->get_tables(false) ) {    // No tables yet at all.
1689         return false;
1691     } else {                                 // Check for missing main tables
1692         $mtables = array('config', 'course', 'groupings'); // some tables used in 1.9 and 2.0, preferable something from the start and end of install.xml
1693         foreach ($mtables as $mtable) {
1694             if (!in_array($mtable, $tables)) {
1695                 return false;
1696             }
1697         }
1698         return true;
1699     }
1702 /**
1703  * upgrades the mnet rpc definitions for the given component.
1704  * this method doesn't return status, an exception will be thrown in the case of an error
1705  *
1706  * @param string $component the plugin to upgrade, eg auth_mnet
1707  */
1708 function upgrade_plugin_mnet_functions($component) {
1709     global $DB, $CFG;
1711     list($type, $plugin) = core_component::normalize_component($component);
1712     $path = core_component::get_plugin_directory($type, $plugin);
1714     $publishes = array();
1715     $subscribes = array();
1716     if (file_exists($path . '/db/mnet.php')) {
1717         require_once($path . '/db/mnet.php'); // $publishes comes from this file
1718     }
1719     if (empty($publishes)) {
1720         $publishes = array(); // still need this to be able to disable stuff later
1721     }
1722     if (empty($subscribes)) {
1723         $subscribes = array(); // still need this to be able to disable stuff later
1724     }
1726     static $servicecache = array();
1728     // rekey an array based on the rpc method for easy lookups later
1729     $publishmethodservices = array();
1730     $subscribemethodservices = array();
1731     foreach($publishes as $servicename => $service) {
1732         if (is_array($service['methods'])) {
1733             foreach($service['methods'] as $methodname) {
1734                 $service['servicename'] = $servicename;
1735                 $publishmethodservices[$methodname][] = $service;
1736             }
1737         }
1738     }
1740     // Disable functions that don't exist (any more) in the source
1741     // Should these be deleted? What about their permissions records?
1742     foreach ($DB->get_records('mnet_rpc', array('pluginname'=>$plugin, 'plugintype'=>$type), 'functionname ASC ') as $rpc) {
1743         if (!array_key_exists($rpc->functionname, $publishmethodservices) && $rpc->enabled) {
1744             $DB->set_field('mnet_rpc', 'enabled', 0, array('id' => $rpc->id));
1745         } else if (array_key_exists($rpc->functionname, $publishmethodservices) && !$rpc->enabled) {
1746             $DB->set_field('mnet_rpc', 'enabled', 1, array('id' => $rpc->id));
1747         }
1748     }
1750     // reflect all the services we're publishing and save them
1751     require_once($CFG->dirroot . '/lib/zend/Zend/Server/Reflection.php');
1752     static $cachedclasses = array(); // to store reflection information in
1753     foreach ($publishes as $service => $data) {
1754         $f = $data['filename'];
1755         $c = $data['classname'];
1756         foreach ($data['methods'] as $method) {
1757             $dataobject = new stdClass();
1758             $dataobject->plugintype  = $type;
1759             $dataobject->pluginname  = $plugin;
1760             $dataobject->enabled     = 1;
1761             $dataobject->classname   = $c;
1762             $dataobject->filename    = $f;
1764             if (is_string($method)) {
1765                 $dataobject->functionname = $method;
1767             } else if (is_array($method)) { // wants to override file or class
1768                 $dataobject->functionname = $method['method'];
1769                 $dataobject->classname     = $method['classname'];
1770                 $dataobject->filename      = $method['filename'];
1771             }
1772             $dataobject->xmlrpcpath = $type.'/'.$plugin.'/'.$dataobject->filename.'/'.$method;
1773             $dataobject->static = false;
1775             require_once($path . '/' . $dataobject->filename);
1776             $functionreflect = null; // slightly different ways to get this depending on whether it's a class method or a function
1777             if (!empty($dataobject->classname)) {
1778                 if (!class_exists($dataobject->classname)) {
1779                     throw new moodle_exception('installnosuchmethod', 'mnet', '', (object)array('method' => $dataobject->functionname, 'class' => $dataobject->classname));
1780                 }
1781                 $key = $dataobject->filename . '|' . $dataobject->classname;
1782                 if (!array_key_exists($key, $cachedclasses)) { // look to see if we've already got a reflection object
1783                     try {
1784                         $cachedclasses[$key] = Zend_Server_Reflection::reflectClass($dataobject->classname);
1785                     } catch (Zend_Server_Reflection_Exception $e) { // catch these and rethrow them to something more helpful
1786                         throw new moodle_exception('installreflectionclasserror', 'mnet', '', (object)array('method' => $dataobject->functionname, 'class' => $dataobject->classname, 'error' => $e->getMessage()));
1787                     }
1788                 }
1789                 $r =& $cachedclasses[$key];
1790                 if (!$r->hasMethod($dataobject->functionname)) {
1791                     throw new moodle_exception('installnosuchmethod', 'mnet', '', (object)array('method' => $dataobject->functionname, 'class' => $dataobject->classname));
1792                 }
1793                 // stupid workaround for zend not having a getMethod($name) function
1794                 $ms = $r->getMethods();
1795                 foreach ($ms as $m) {
1796                     if ($m->getName() == $dataobject->functionname) {
1797                         $functionreflect = $m;
1798                         break;
1799                     }
1800                 }
1801                 $dataobject->static = (int)$functionreflect->isStatic();
1802             } else {
1803                 if (!function_exists($dataobject->functionname)) {
1804                     throw new moodle_exception('installnosuchfunction', 'mnet', '', (object)array('method' => $dataobject->functionname, 'file' => $dataobject->filename));
1805                 }
1806                 try {
1807                     $functionreflect = Zend_Server_Reflection::reflectFunction($dataobject->functionname);
1808                 } catch (Zend_Server_Reflection_Exception $e) { // catch these and rethrow them to something more helpful
1809                     throw new moodle_exception('installreflectionfunctionerror', 'mnet', '', (object)array('method' => $dataobject->functionname, '' => $dataobject->filename, 'error' => $e->getMessage()));
1810                 }
1811             }
1812             $dataobject->profile =  serialize(admin_mnet_method_profile($functionreflect));
1813             $dataobject->help = $functionreflect->getDescription();
1815             if ($record_exists = $DB->get_record('mnet_rpc', array('xmlrpcpath'=>$dataobject->xmlrpcpath))) {
1816                 $dataobject->id      = $record_exists->id;
1817                 $dataobject->enabled = $record_exists->enabled;
1818                 $DB->update_record('mnet_rpc', $dataobject);
1819             } else {
1820                 $dataobject->id = $DB->insert_record('mnet_rpc', $dataobject, true);
1821             }
1823             // TODO this API versioning must be reworked, here the recently processed method
1824             // sets the service API which may not be correct
1825             foreach ($publishmethodservices[$dataobject->functionname] as $service) {
1826                 if ($serviceobj = $DB->get_record('mnet_service', array('name'=>$service['servicename']))) {
1827                     $serviceobj->apiversion = $service['apiversion'];
1828                     $DB->update_record('mnet_service', $serviceobj);
1829                 } else {
1830                     $serviceobj = new stdClass();
1831                     $serviceobj->name        = $service['servicename'];
1832                     $serviceobj->description = empty($service['description']) ? '' : $service['description'];
1833                     $serviceobj->apiversion  = $service['apiversion'];
1834                     $serviceobj->offer       = 1;
1835                     $serviceobj->id          = $DB->insert_record('mnet_service', $serviceobj);
1836                 }
1837                 $servicecache[$service['servicename']] = $serviceobj;
1838                 if (!$DB->record_exists('mnet_service2rpc', array('rpcid'=>$dataobject->id, 'serviceid'=>$serviceobj->id))) {
1839                     $obj = new stdClass();
1840                     $obj->rpcid = $dataobject->id;
1841                     $obj->serviceid = $serviceobj->id;
1842                     $DB->insert_record('mnet_service2rpc', $obj, true);
1843                 }
1844             }
1845         }
1846     }
1847     // finished with methods we publish, now do subscribable methods
1848     foreach($subscribes as $service => $methods) {
1849         if (!array_key_exists($service, $servicecache)) {
1850             if (!$serviceobj = $DB->get_record('mnet_service', array('name' =>  $service))) {
1851                 debugging("TODO: skipping unknown service $service - somebody needs to fix MDL-21993");
1852                 continue;
1853             }
1854             $servicecache[$service] = $serviceobj;
1855         } else {
1856             $serviceobj = $servicecache[$service];
1857         }
1858         foreach ($methods as $method => $xmlrpcpath) {
1859             if (!$rpcid = $DB->get_field('mnet_remote_rpc', 'id', array('xmlrpcpath'=>$xmlrpcpath))) {
1860                 $remoterpc = (object)array(
1861                     'functionname' => $method,
1862                     'xmlrpcpath' => $xmlrpcpath,
1863                     'plugintype' => $type,
1864                     'pluginname' => $plugin,
1865                     'enabled'    => 1,
1866                 );
1867                 $rpcid = $remoterpc->id = $DB->insert_record('mnet_remote_rpc', $remoterpc, true);
1868             }
1869             if (!$DB->record_exists('mnet_remote_service2rpc', array('rpcid'=>$rpcid, 'serviceid'=>$serviceobj->id))) {
1870                 $obj = new stdClass();
1871                 $obj->rpcid = $rpcid;
1872                 $obj->serviceid = $serviceobj->id;
1873                 $DB->insert_record('mnet_remote_service2rpc', $obj, true);
1874             }
1875             $subscribemethodservices[$method][] = $service;
1876         }
1877     }
1879     foreach ($DB->get_records('mnet_remote_rpc', array('pluginname'=>$plugin, 'plugintype'=>$type), 'functionname ASC ') as $rpc) {
1880         if (!array_key_exists($rpc->functionname, $subscribemethodservices) && $rpc->enabled) {
1881             $DB->set_field('mnet_remote_rpc', 'enabled', 0, array('id' => $rpc->id));
1882         } else if (array_key_exists($rpc->functionname, $subscribemethodservices) && !$rpc->enabled) {
1883             $DB->set_field('mnet_remote_rpc', 'enabled', 1, array('id' => $rpc->id));
1884         }
1885     }
1887     return true;
1890 /**
1891  * Given some sort of Zend Reflection function/method object, return a profile array, ready to be serialized and stored
1892  *
1893  * @param Zend_Server_Reflection_Function_Abstract $function can be any subclass of this object type
1894  *
1895  * @return array
1896  */
1897 function admin_mnet_method_profile(Zend_Server_Reflection_Function_Abstract $function) {
1898     $protos = $function->getPrototypes();
1899     $proto = array_pop($protos);
1900     $ret = $proto->getReturnValue();
1901     $profile = array(
1902         'parameters' =>  array(),
1903         'return'     =>  array(
1904             'type'        => $ret->getType(),
1905             'description' => $ret->getDescription(),
1906         ),
1907     );
1908     foreach ($proto->getParameters() as $p) {
1909         $profile['parameters'][] = array(
1910             'name' => $p->getName(),
1911             'type' => $p->getType(),
1912             'description' => $p->getDescription(),
1913         );
1914     }
1915     return $profile;
1919 /**
1920  * This function finds duplicate records (based on combinations of fields that should be unique)
1921  * and then progamatically generated a "most correct" version of the data, update and removing
1922  * records as appropriate
1923  *
1924  * Thanks to Dan Marsden for help
1925  *
1926  * @param   string  $table      Table name
1927  * @param   array   $uniques    Array of field names that should be unique
1928  * @param   array   $fieldstocheck  Array of fields to generate "correct" data from (optional)
1929  * @return  void
1930  */
1931 function upgrade_course_completion_remove_duplicates($table, $uniques, $fieldstocheck = array()) {
1932     global $DB;
1934     // Find duplicates
1935     $sql_cols = implode(', ', $uniques);
1937     $sql = "SELECT {$sql_cols} FROM {{$table}} GROUP BY {$sql_cols} HAVING (count(id) > 1)";
1938     $duplicates = $DB->get_recordset_sql($sql, array());
1940     // Loop through duplicates
1941     foreach ($duplicates as $duplicate) {
1942         $pointer = 0;
1944         // Generate SQL for finding records with these duplicate uniques
1945         $sql_select = implode(' = ? AND ', $uniques).' = ?'; // builds "fieldname = ? AND fieldname = ?"
1946         $uniq_values = array();
1947         foreach ($uniques as $u) {
1948             $uniq_values[] = $duplicate->$u;
1949         }
1951         $sql_order = implode(' DESC, ', $uniques).' DESC'; // builds "fieldname DESC, fieldname DESC"
1953         // Get records with these duplicate uniques
1954         $records = $DB->get_records_select(
1955             $table,
1956             $sql_select,
1957             $uniq_values,
1958             $sql_order
1959         );
1961         // Loop through and build a "correct" record, deleting the others
1962         $needsupdate = false;
1963         $origrecord = null;
1964         foreach ($records as $record) {
1965             $pointer++;
1966             if ($pointer === 1) { // keep 1st record but delete all others.
1967                 $origrecord = $record;
1968             } else {
1969                 // If we have fields to check, update original record
1970                 if ($fieldstocheck) {
1971                     // we need to keep the "oldest" of all these fields as the valid completion record.
1972                     // but we want to ignore null values
1973                     foreach ($fieldstocheck as $f) {
1974                         if ($record->$f && (($origrecord->$f > $record->$f) || !$origrecord->$f)) {
1975                             $origrecord->$f = $record->$f;
1976                             $needsupdate = true;
1977                         }
1978                     }
1979                 }
1980                 $DB->delete_records($table, array('id' => $record->id));
1981             }
1982         }
1983         if ($needsupdate || isset($origrecord->reaggregate)) {
1984             // If this table has a reaggregate field, update to force recheck on next cron run
1985             if (isset($origrecord->reaggregate)) {
1986                 $origrecord->reaggregate = time();
1987             }
1988             $DB->update_record($table, $origrecord);
1989         }
1990     }
1993 /**
1994  * Find questions missing an existing category and associate them with
1995  * a category which purpose is to gather them.
1996  *
1997  * @return void
1998  */
1999 function upgrade_save_orphaned_questions() {
2000     global $DB;
2002     // Looking for orphaned questions
2003     $orphans = $DB->record_exists_select('question',
2004             'NOT EXISTS (SELECT 1 FROM {question_categories} WHERE {question_categories}.id = {question}.category)');
2005     if (!$orphans) {
2006         return;
2007     }
2009     // Generate a unique stamp for the orphaned questions category, easier to identify it later on
2010     $uniquestamp = "unknownhost+120719170400+orphan";
2011     $systemcontext = context_system::instance();
2013     // Create the orphaned category at system level
2014     $cat = $DB->get_record('question_categories', array('stamp' => $uniquestamp,
2015             'contextid' => $systemcontext->id));
2016     if (!$cat) {
2017         $cat = new stdClass();
2018         $cat->parent = 0;
2019         $cat->contextid = $systemcontext->id;
2020         $cat->name = get_string('orphanedquestionscategory', 'question');
2021         $cat->info = get_string('orphanedquestionscategoryinfo', 'question');
2022         $cat->sortorder = 999;
2023         $cat->stamp = $uniquestamp;
2024         $cat->id = $DB->insert_record("question_categories", $cat);
2025     }
2027     // Set a category to those orphans
2028     $params = array('catid' => $cat->id);
2029     $DB->execute('UPDATE {question} SET category = :catid WHERE NOT EXISTS
2030             (SELECT 1 FROM {question_categories} WHERE {question_categories}.id = {question}.category)', $params);
2033 /**
2034  * Rename old backup files to current backup files.
2035  *
2036  * When added the setting 'backup_shortname' (MDL-28657) the backup file names did not contain the id of the course.
2037  * Further we fixed that behaviour by forcing the id to be always present in the file name (MDL-33812).
2038  * This function will explore the backup directory and attempt to rename the previously created files to include
2039  * the id in the name. Doing this will put them back in the process of deleting the excess backups for each course.
2040  *
2041  * This function manually recreates the file name, instead of using
2042  * {@link backup_plan_dbops::get_default_backup_filename()}, use it carefully if you're using it outside of the
2043  * usual upgrade process.
2044  *
2045  * @see backup_cron_automated_helper::remove_excess_backups()
2046  * @link http://tracker.moodle.org/browse/MDL-35116
2047  * @return void
2048  * @since Moodle 2.4
2049  */
2050 function upgrade_rename_old_backup_files_using_shortname() {
2051     global $CFG;
2052     $dir = get_config('backup', 'backup_auto_destination');
2053     $useshortname = get_config('backup', 'backup_shortname');
2054     if (empty($dir) || !is_dir($dir) || !is_writable($dir)) {
2055         return;
2056     }
2058     require_once($CFG->dirroot.'/backup/util/includes/backup_includes.php');
2059     $backupword = str_replace(' ', '_', core_text::strtolower(get_string('backupfilename')));
2060     $backupword = trim(clean_filename($backupword), '_');
2061     $filename = $backupword . '-' . backup::FORMAT_MOODLE . '-' . backup::TYPE_1COURSE . '-';
2062     $regex = '#^'.preg_quote($filename, '#').'.*\.mbz$#';
2063     $thirtyapril = strtotime('30 April 2012 00:00');
2065     // Reading the directory.
2066     if (!$files = scandir($dir)) {
2067         return;
2068     }
2069     foreach ($files as $file) {
2070         // Skip directories and files which do not start with the common prefix.
2071         // This avoids working on files which are not related to this issue.
2072         if (!is_file($dir . '/' . $file) || !preg_match($regex, $file)) {
2073             continue;
2074         }
2076         // Extract the information from the XML file.
2077         try {
2078             $bcinfo = backup_general_helper::get_backup_information_from_mbz($dir . '/' . $file);
2079         } catch (backup_helper_exception $e) {
2080             // Some error while retrieving the backup informations, skipping...
2081             continue;
2082         }
2084         // Make sure this a course backup.
2085         if ($bcinfo->format !== backup::FORMAT_MOODLE || $bcinfo->type !== backup::TYPE_1COURSE) {
2086             continue;
2087         }
2089         // Skip the backups created before the short name option was initially introduced (MDL-28657).
2090         // This was integrated on the 2nd of May 2012. Let's play safe with timezone and use the 30th of April.
2091         if ($bcinfo->backup_date < $thirtyapril) {
2092             continue;
2093         }
2095         // Let's check if the file name contains the ID where it is supposed to be, if it is the case then
2096         // we will skip the file. Of course it could happen that the course ID is identical to the course short name
2097         // even though really unlikely, but then renaming this file is not necessary. If the ID is not found in the
2098         // file name then it was probably the short name which was used.
2099         $idfilename = $filename . $bcinfo->original_course_id . '-';
2100         $idregex = '#^'.preg_quote($idfilename, '#').'.*\.mbz$#';
2101         if (preg_match($idregex, $file)) {
2102             continue;
2103         }
2105         // Generating the file name manually. We do not use backup_plan_dbops::get_default_backup_filename() because
2106         // it will query the database to get some course information, and the course could not exist any more.
2107         $newname = $filename . $bcinfo->original_course_id . '-';
2108         if ($useshortname) {
2109             $shortname = str_replace(' ', '_', $bcinfo->original_course_shortname);
2110             $shortname = core_text::strtolower(trim(clean_filename($shortname), '_'));
2111             $newname .= $shortname . '-';
2112         }
2114         $backupdateformat = str_replace(' ', '_', get_string('backupnameformat', 'langconfig'));
2115         $date = userdate($bcinfo->backup_date, $backupdateformat, 99, false);
2116         $date = core_text::strtolower(trim(clean_filename($date), '_'));
2117         $newname .= $date;
2119         if (isset($bcinfo->root_settings['users']) && !$bcinfo->root_settings['users']) {
2120             $newname .= '-nu';
2121         } else if (isset($bcinfo->root_settings['anonymize']) && $bcinfo->root_settings['anonymize']) {
2122             $newname .= '-an';
2123         }
2124         $newname .= '.mbz';
2126         // Final check before attempting the renaming.
2127         if ($newname == $file || file_exists($dir . '/' . $newname)) {
2128             continue;
2129         }
2130         @rename($dir . '/' . $file, $dir . '/' . $newname);
2131     }
2134 /**
2135  * Detect duplicate grade item sortorders and resort the
2136  * items to remove them.
2137  */
2138 function upgrade_grade_item_fix_sortorder() {
2139     global $DB;
2141     // The simple way to fix these sortorder duplicates would be simply to resort each
2142     // affected course. But in order to reduce the impact of this upgrade step we're trying
2143     // to do it more efficiently by doing a series of update statements rather than updating
2144     // every single grade item in affected courses.
2146     $sql = "SELECT DISTINCT g1.courseid
2147               FROM {grade_items} g1
2148               JOIN {grade_items} g2 ON g1.courseid = g2.courseid
2149              WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id
2150              ORDER BY g1.courseid ASC";
2151     foreach ($DB->get_fieldset_sql($sql) as $courseid) {
2152         $transaction = $DB->start_delegated_transaction();
2153         $items = $DB->get_records('grade_items', array('courseid' => $courseid), '', 'id, sortorder, sortorder AS oldsort');
2155         // Get all duplicates in course order, highest sort order, and higest id first so that we can make space at the
2156         // bottom higher end of the sort orders and work down by id.
2157         $sql = "SELECT DISTINCT g1.id, g1.sortorder
2158                 FROM {grade_items} g1
2159                 JOIN {grade_items} g2 ON g1.courseid = g2.courseid
2160                 WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
2161                 ORDER BY g1.sortorder DESC, g1.id DESC";
2163         // This is the O(N*N) like the database version we're replacing, but at least the constants are a billion times smaller...
2164         foreach ($DB->get_records_sql($sql, array('courseid' => $courseid)) as $duplicate) {
2165             foreach ($items as $item) {
2166                 if ($item->sortorder > $duplicate->sortorder || ($item->sortorder == $duplicate->sortorder && $item->id > $duplicate->id)) {
2167                     $item->sortorder += 1;
2168                 }
2169             }
2170         }
2171         foreach ($items as $item) {
2172             if ($item->sortorder != $item->oldsort) {
2173                 $DB->update_record('grade_items', array('id' => $item->id, 'sortorder' => $item->sortorder));
2174             }
2175         }
2177         $transaction->allow_commit();
2178     }
2181 /**
2182  * Detect file areas with missing root directory records and add them.
2183  */
2184 function upgrade_fix_missing_root_folders() {
2185     global $DB, $USER;
2187     $transaction = $DB->start_delegated_transaction();
2189     $sql = "SELECT contextid, component, filearea, itemid
2190               FROM {files}
2191              WHERE (component <> 'user' OR filearea <> 'draft')
2192           GROUP BY contextid, component, filearea, itemid
2193             HAVING MAX(CASE WHEN filename = '.' AND filepath = '/' THEN 1 ELSE 0 END) = 0";
2195     $rs = $DB->get_recordset_sql($sql);
2196     $defaults = array('filepath' => '/',
2197         'filename' => '.',
2198         'userid' => 0, // Don't rely on any particular user for these system records.
2199         'filesize' => 0,
2200         'timecreated' => time(),
2201         'timemodified' => time(),
2202         'contenthash' => sha1(''));
2203     foreach ($rs as $r) {
2204         $pathhash = sha1("/$r->contextid/$r->component/$r->filearea/$r->itemid/.");
2205         $DB->insert_record('files', (array)$r + $defaults +
2206             array('pathnamehash' => $pathhash));
2207     }
2208     $rs->close();
2209     $transaction->allow_commit();
2212 /**
2213  * Detect draft file areas with missing root directory records and add them.
2214  */
2215 function upgrade_fix_missing_root_folders_draft() {
2216     global $DB;
2218     $transaction = $DB->start_delegated_transaction();
2220     $sql = "SELECT contextid, itemid, MAX(timecreated) AS timecreated, MAX(timemodified) AS timemodified
2221               FROM {files}
2222              WHERE (component = 'user' AND filearea = 'draft')
2223           GROUP BY contextid, itemid
2224             HAVING MAX(CASE WHEN filename = '.' AND filepath = '/' THEN 1 ELSE 0 END) = 0";
2226     $rs = $DB->get_recordset_sql($sql);
2227     $defaults = array('component' => 'user',
2228         'filearea' => 'draft',
2229         'filepath' => '/',
2230         'filename' => '.',
2231         'userid' => 0, // Don't rely on any particular user for these system records.
2232         'filesize' => 0,
2233         'contenthash' => sha1(''));
2234     foreach ($rs as $r) {
2235         $r->pathnamehash = sha1("/$r->contextid/user/draft/$r->itemid/.");
2236         $DB->insert_record('files', (array)$r + $defaults);
2237     }
2238     $rs->close();
2239     $transaction->allow_commit();
2242 /**
2243  * This function verifies that the database is not using an unsupported storage engine.
2244  *
2245  * @param environment_results $result object to update, if relevant
2246  * @return environment_results|null updated results object, or null if the storage engine is supported
2247  */
2248 function check_database_storage_engine(environment_results $result) {
2249     global $DB;
2251     // Check if MySQL is the DB family (this will also be the same for MariaDB).
2252     if ($DB->get_dbfamily() == 'mysql') {
2253         // Get the database engine we will either be using to install the tables, or what we are currently using.
2254         $engine = $DB->get_dbengine();
2255         // Check if MyISAM is the storage engine that will be used, if so, do not proceed and display an error.
2256         if ($engine == 'MyISAM') {
2257             $result->setInfo('unsupported_db_storage_engine');
2258             $result->setStatus(false);
2259             return $result;
2260         }
2261     }
2263     return null;
2266 /**
2267  * Method used to check the usage of slasharguments config and display a warning message.
2268  *
2269  * @param environment_results $result object to update, if relevant.
2270  * @return environment_results|null updated results or null if slasharguments is disabled.
2271  */
2272 function check_slasharguments(environment_results $result){
2273     global $CFG;
2275     if (!during_initial_install() && empty($CFG->slasharguments)) {
2276         $result->setInfo('slasharguments');
2277         $result->setStatus(false);
2278         return $result;
2279     }
2281     return null;
2284 /**
2285  * This function verifies if the database has tables using innoDB Antelope row format.
2286  *
2287  * @param environment_results $result
2288  * @return environment_results|null updated results object, or null if no Antelope table has been found.
2289  */
2290 function check_database_tables_row_format(environment_results $result) {
2291     global $DB;
2293     if ($DB->get_dbfamily() == 'mysql') {
2294         $generator = $DB->get_manager()->generator;
2296         foreach ($DB->get_tables(false) as $table) {
2297             $columns = $DB->get_columns($table, false);
2298             $size = $generator->guess_antelope_row_size($columns);
2299             $format = $DB->get_row_format($table);
2301             if ($size <= $generator::ANTELOPE_MAX_ROW_SIZE) {
2302                 continue;
2303             }
2305             if ($format === 'Compact' or $format === 'Redundant') {
2306                 $result->setInfo('unsupported_db_table_row_format');
2307                 $result->setStatus(false);
2308                 return $result;
2309             }
2310         }
2311     }
2313     return null;
2316 /**
2317  * Upgrade the minmaxgrade setting.
2318  *
2319  * This step should only be run for sites running 2.8 or later. Sites using 2.7 will be fine
2320  * using the new default system setting $CFG->grade_minmaxtouse.
2321  *
2322  * @return void
2323  */
2324 function upgrade_minmaxgrade() {
2325     global $CFG, $DB;
2327     // 2 is a copy of GRADE_MIN_MAX_FROM_GRADE_GRADE.
2328     $settingvalue = 2;
2330     // Set the course setting when:
2331     // - The system setting does not exist yet.
2332     // - The system seeting is not set to what we'd set the course setting.
2333     $setcoursesetting = !isset($CFG->grade_minmaxtouse) || $CFG->grade_minmaxtouse != $settingvalue;
2335     // Identify the courses that have inconsistencies grade_item vs grade_grade.
2336     $sql = "SELECT DISTINCT(gi.courseid)
2337               FROM {grade_grades} gg
2338               JOIN {grade_items} gi
2339                 ON gg.itemid = gi.id
2340              WHERE gi.itemtype NOT IN (?, ?)
2341                AND (gg.rawgrademax != gi.grademax OR gg.rawgrademin != gi.grademin)";
2343     $rs = $DB->get_recordset_sql($sql, array('course', 'category'));
2344     foreach ($rs as $record) {
2345         // Flag the course to show a notice in the gradebook.
2346         set_config('show_min_max_grades_changed_' . $record->courseid, 1);
2348         // Set the appropriate course setting so that grades displayed are not changed.
2349         $configname = 'minmaxtouse';
2350         if ($setcoursesetting &&
2351                 !$DB->record_exists('grade_settings', array('courseid' => $record->courseid, 'name' => $configname))) {
2352             // Do not set the setting when the course already defines it.
2353             $data = new stdClass();
2354             $data->courseid = $record->courseid;
2355             $data->name     = $configname;
2356             $data->value    = $settingvalue;
2357             $DB->insert_record('grade_settings', $data);
2358         }
2360         // Mark the grades to be regraded.
2361         $DB->set_field('grade_items', 'needsupdate', 1, array('courseid' => $record->courseid));
2362     }
2363     $rs->close();
2367 /**
2368  * Assert the upgrade key is provided, if it is defined.
2369  *
2370  * The upgrade key can be defined in the main config.php as $CFG->upgradekey. If
2371  * it is defined there, then its value must be provided every time the site is
2372  * being upgraded, regardless the administrator is logged in or not.
2373  *
2374  * This is supposed to be used at certain places in /admin/index.php only.
2375  *
2376  * @param string|null $upgradekeyhash the SHA-1 of the value provided by the user
2377  */
2378 function check_upgrade_key($upgradekeyhash) {
2379     global $CFG, $PAGE;
2381     if (isset($CFG->config_php_settings['upgradekey'])) {
2382         if ($upgradekeyhash === null or $upgradekeyhash !== sha1($CFG->config_php_settings['upgradekey'])) {
2383             if (!$PAGE->headerprinted) {
2384                 $output = $PAGE->get_renderer('core', 'admin');
2385                 echo $output->upgradekey_form_page(new moodle_url('/admin/index.php', array('cache' => 0)));
2386                 die();
2387             } else {
2388                 // This should not happen.
2389                 die('Upgrade locked');
2390             }
2391         }
2392     }
2395 /**
2396  * Helper procedure/macro for installing remote plugins at admin/index.php
2397  *
2398  * Does not return, always redirects or exits.
2399  *
2400  * @param array $installable list of \core\update\remote_info
2401  * @param bool $confirmed false: display the validation screen, true: proceed installation
2402  * @param string $heading validation screen heading
2403  * @param moodle_url|string|null $continue URL to proceed with installation at the validation screen
2404  * @param moodle_url|string|null $return URL to go back on cancelling at the validation screen
2405  */
2406 function upgrade_install_plugins(array $installable, $confirmed, $heading='', $continue=null, $return=null) {
2407     global $CFG, $PAGE;
2409     if (empty($return)) {
2410         $return = $PAGE->url;
2411     }
2413     if (!empty($CFG->disableupdateautodeploy)) {
2414         redirect($return);
2415     }
2417     if (empty($installable)) {
2418         redirect($return);
2419     }
2421     $pluginman = core_plugin_manager::instance();
2423     if ($confirmed) {
2424         // Installation confirmed at the validation results page.
2425         if (!$pluginman->install_plugins($installable, true, true)) {
2426             throw new moodle_exception('install_plugins_failed', 'core_plugin', $return);
2427         }
2429         // Always redirect to admin/index.php to perform the database upgrade.
2430         // Do not throw away the existing $PAGE->url parameters such as
2431         // confirmupgrade or confirmrelease if $PAGE->url is a superset of the
2432         // URL we must go to.
2433         $mustgoto = new moodle_url('/admin/index.php', array('cache' => 0, 'confirmplugincheck' => 0));
2434         if ($mustgoto->compare($PAGE->url, URL_MATCH_PARAMS)) {
2435             redirect($PAGE->url);
2436         } else {
2437             redirect($mustgoto);
2438         }
2440     } else {
2441         $output = $PAGE->get_renderer('core', 'admin');
2442         echo $output->header();
2443         if ($heading) {
2444             echo $output->heading($heading, 3);
2445         }
2446         echo html_writer::start_tag('pre', array('class' => 'plugin-install-console'));
2447         $validated = $pluginman->install_plugins($installable, false, false);
2448         echo html_writer::end_tag('pre');
2449         if ($validated) {
2450             echo $output->plugins_management_confirm_buttons($continue, $return);
2451         } else {
2452             echo $output->plugins_management_confirm_buttons(null, $return);
2453         }
2454         echo $output->footer();
2455         die();
2456     }