8029650740b838ba18b7145856d6f03b8d359587
[moodle.git] / lib / db / 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  * Upgrade helper functions
20  *
21  * This file is used for special upgrade functions - for example groups and gradebook.
22  * These functions must use SQL and database related functions only- no other Moodle API,
23  * because it might depend on db structures that are not yet present during upgrade.
24  * (Do not use functions from accesslib.php, grades classes or group functions at all!)
25  *
26  * @package    core
27  * @subpackage admin
28  * @copyright  2007 Petr Skoda (http://skodak.org)
29  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30  */
32 defined('MOODLE_INTERNAL') || die();
34 function upgrade_fix_category_depths() {
35     global $CFG, $DB;
37     // first fix incorrect parents
38     $sql = "SELECT c.id
39               FROM {course_categories} c
40              WHERE c.parent > 0 AND c.parent NOT IN (SELECT pc.id FROM {course_categories} pc)";
41     if ($rs = $DB->get_recordset_sql($sql)) {
42         foreach ($rs as $cat) {
43             $cat->depth  = 1;
44             $cat->path   = '/'.$cat->id;
45             $cat->parent = 0;
46             $DB->update_record('course_categories', $cat);
47         }
48         $rs->close();
49     }
51     // now add path and depth to top level categories
52     $sql = "UPDATE {course_categories}
53                SET depth = 1, path = ".$DB->sql_concat("'/'", "id")."
54              WHERE parent = 0";
55     $DB->execute($sql);
57     // now fix all other levels - slow but works in all supported dbs
58     $parentdepth = 1;
59     while ($DB->record_exists('course_categories', array('depth'=>0))) {
60         $sql = "SELECT c.id, pc.path
61                   FROM {course_categories} c, {course_categories} pc
62                  WHERE c.parent=pc.id AND c.depth=0 AND pc.depth=?";
63         if ($rs = $DB->get_recordset_sql($sql, array($parentdepth))) {
64             foreach ($rs as $cat) {
65                 $cat->depth = $parentdepth+1;
66                 $cat->path  = $cat->path.'/'.$cat->id;
67                 $DB->update_record('course_categories', $cat);
68             }
69             $rs->close();
70         }
71         $parentdepth++;
72         if ($parentdepth > 100) {
73             //something must have gone wrong - nobody can have more than 100 levels of categories, right?
74             debugging('Unknown error fixing category depths');
75             break;
76         }
77     }
78 }
80 /**
81  * Moves all course files except the moddata to new file storage
82  *
83  * Unfortunately this function uses core file related functions - it might be necessary to tweak it if something changes there :-(
84  */
85 function upgrade_migrate_files_courses() {
86     global $DB, $CFG;
87     require_once($CFG->libdir.'/filelib.php');
89     $count = $DB->count_records('course');
90     $pbar = new progress_bar('migratecoursefiles', 500, true);
92     $rs = $DB->get_recordset('course');
93     $i = 0;
94     foreach ($rs as $course) {
95         $i++;
96         upgrade_set_timeout(60*5); // set up timeout, may also abort execution
97         $context = get_context_instance(CONTEXT_COURSE, $course->id);
98         upgrade_migrate_files_course($context, '/', true);
99         $pbar->update($i, $count, "Migrated course files - course $i/$count.");
100     }
101     $rs->close();
103     return true;
106 /**
107  * Internal function - do not use directly
108  */
109 function upgrade_migrate_user_icons() {
110     global $CFG, $OUTPUT, $DB;
112     $fs = get_file_storage();
114     $icon = array('component'=>'user', 'filearea'=>'icon', 'itemid'=>0, 'filepath'=>'/');
116     $count = $DB->count_records('user', array('picture'=>1, 'deleted'=>0));
117     $pbar = new progress_bar('migratecoursefiles', 500, true);
119     $rs = $DB->get_recordset('user', array('picture'=>1, 'deleted'=>0), 'id ASC', 'id, picture');
120     $i = 0;
121     foreach ($rs as $user) {
122         $i++;
123         upgrade_set_timeout(60); /// Give upgrade at least 60 more seconds
124         $pbar->update($i, $count, "Migrated user icons $i/$count.");
126         $context = get_context_instance(CONTEXT_USER, $user->id);
128         if ($fs->file_exists($context->id, 'user', 'icon', 0, '/', 'f1.jpg')) {
129             // already converted!
130             continue;
131         }
133         $level1 = floor($user->id / 1000) * 1000;
134         $userdir = "$CFG->dataroot/user/$level1/$user->id";
135         if (!file_exists("$userdir/f1.jpg") or !file_exists("$userdir/f2.jpg")) {
136             $userdir = "$CFG->dataroot/users/$user->id";
137             if (!file_exists("$userdir/f1.jpg") or !file_exists("$userdir/f2.jpg")) {
138                 // no image found, sorry
139                 $user->picture = 0;
140                 $DB->update_record('user', $user);
141                 continue;
142             }
143         }
145         $icon['contextid'] = $context->id;
146         $icon['filename']  = 'f1.jpg';
147         $fs->create_file_from_pathname($icon, "$userdir/f1.jpg");
148         $icon['filename']  = 'f2.jpg';
149         $fs->create_file_from_pathname($icon, "$userdir/f2.jpg");
150     }
151     $rs->close();
153     // purge all old user image dirs
154     remove_dir("$CFG->dataroot/user");
155     remove_dir("$CFG->dataroot/users");
158 /**
159  * Internal function - do not use directly
160  */
161 function upgrade_migrate_group_icons() {
162     global $CFG, $OUTPUT, $DB;
164     $fs = get_file_storage();
166     $icon = array('component'=>'group', 'filearea'=>'icon', 'filepath'=>'/');
168     $count = $DB->count_records('groups', array('picture'=>1));
169     $pbar = new progress_bar('migrategroupfiles', 500, true);
171     $rs = $DB->get_recordset('groups', array('picture'=>1), 'courseid ASC', 'id, picture, courseid');
172     $i = 0;
173     foreach ($rs as $group) {
174         $i++;
175         upgrade_set_timeout(60); /// Give upgrade at least 60 more seconds
176         $pbar->update($i, $count, "Migrated group icons  $i/$count.");
178         $context = get_context_instance(CONTEXT_COURSE, $group->courseid);
180         if ($fs->file_exists($context->id, 'group', 'icon', $group->id, '/', 'f1.jpg')) {
181             // already converted!
182             continue;
183         }
185         $groupdir = "$CFG->dataroot/groups/$group->id";
186         if (!file_exists("$groupdir/f1.jpg") or !file_exists("$groupdir/f2.jpg")) {
187             // no image found, sorry
188             $group->picture = 0;
189             $DB->update_record('groups', $group);
190             continue;
191         }
193         $icon['contextid'] = $context->id;
194         $icon['itemid']    = $group->id;
195         $icon['filename']  = 'f1.jpg';
196         $fs->create_file_from_pathname($icon, "$groupdir/f1.jpg");
197         $icon['filename']  = 'f2.jpg';
198         $fs->create_file_from_pathname($icon, "$groupdir/f2.jpg");
199     }
200     $rs->close();
202     // purge all old group image dirs
203     remove_dir("$CFG->dataroot/groups");
206 /**
207  * Internal function - do not use directly
208  */
209 function upgrade_migrate_files_course($context, $path, $delete) {
210     global $CFG, $OUTPUT;
212     $fullpathname = $CFG->dataroot.'/'.$context->instanceid.$path;
213     if (!file_exists($fullpathname)) {
214         return;
215     }
216     $items = new DirectoryIterator($fullpathname);
217     $fs = get_file_storage();
219     foreach ($items as $item) {
220         if ($item->isDot()) {
221             continue;
222         }
224         if ($item->isLink()) {
225             // do not delete symbolic links or its children
226             $delete_this = false;
227         } else {
228             $delete_this = $delete;
229         }
231         if (strpos($path, '/backupdata/') === 0) {
232             $component = 'backup';
233             $filearea  = 'course';
234             $filepath  = substr($path, strlen('/backupdata'));
235         } else {
236             $component = 'course';
237             $filearea  = 'legacy';
238             $filepath  = $path;
239         }
241         if ($item->isFile()) {
242             if (!$item->isReadable()) {
243                 echo $OUTPUT->notification(" File not readable, skipping: ".$fullpathname.$item->getFilename());
244                 continue;
245             }
247             $filepath = clean_param($filepath, PARAM_PATH);
248             $filename = clean_param($item->getFilename(), PARAM_FILE);
250             if ($filename === '') {
251                 //unsupported chars, sorry
252                 continue;
253             }
255             if (!$fs->file_exists($context->id, $component, $filearea, '0', $filepath, $filename)) {
256                 $file_record = array('contextid'=>$context->id, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>0, 'filepath'=>$filepath, 'filename'=>$filename,
257                                      'timecreated'=>$item->getCTime(), 'timemodified'=>$item->getMTime());
258                 if ($fs->create_file_from_pathname($file_record, $fullpathname.$item->getFilename())) {
259                     if ($delete_this) {
260                         @unlink($fullpathname.$item->getFilename());
261                     }
262                 }
263             }
265         } else {
266             if ($path == '/' and $item->getFilename() == 'moddata') {
267                 continue; // modules are responsible
268             }
270             $dirname = clean_param($item->getFilename(), PARAM_PATH);
271             if ($dirname === '') {
272                 //unsupported chars, sorry
273                 continue;
274             }
275             $filepath = ($filepath.$dirname.'/');
276             if ($filepath !== '/backupdata/') {
277                 $fs->create_directory($context->id, $component, $filearea, 0, $filepath);
278             }
280             //migrate recursively all subdirectories
281             upgrade_migrate_files_course($context, $path.$item->getFilename().'/', $delete_this);
282             if ($delete_this) {
283                 // delete dir if empty
284                 @rmdir($fullpathname.$item->getFilename());
285             }
286         }
287     }
288     unset($items); //release file handles
291 /**
292  * Moves all block attachments
293  *
294  * Unfortunately this function uses core file related functions - it might be necessary to tweak it if something changes there :-(
295  */
296 function upgrade_migrate_files_blog() {
297     global $DB, $CFG, $OUTPUT;
299     $fs = get_file_storage();
301     $count = $DB->count_records_select('post', "module='blog' AND attachment IS NOT NULL AND attachment <> '1'");
303     if ($rs = $DB->get_recordset_select('post', "module='blog' AND attachment IS NOT NULL AND attachment <> '1'")) {
305         upgrade_set_timeout(60*20); // set up timeout, may also abort execution
307         $pbar = new progress_bar('migrateblogfiles', 500, true);
309         $i = 0;
310         foreach ($rs as $entry) {
311             $i++;
312             $pathname = "$CFG->dataroot/blog/attachments/$entry->id/$entry->attachment";
313             if (!file_exists($pathname)) {
314                 $entry->attachment = NULL;
315                 $DB->update_record('post', $entry);
316                 continue;
317             }
319             $filename = clean_param($entry->attachment, PARAM_FILE);
320             if ($filename === '') {
321                 // weird file name, ignore it
322                 $entry->attachment = NULL;
323                 $DB->update_record('post', $entry);
324                 continue;
325             }
327             if (!is_readable($pathname)) {
328                 echo $OUTPUT->notification(" File not readable, skipping: ".$pathname);
329                 continue;
330             }
332             if (!$fs->file_exists(SYSCONTEXTID, 'blog', 'attachment', $entry->id, '/', $filename)) {
333                 $file_record = array('contextid'=>SYSCONTEXTID, 'component'=>'blog', 'filearea'=>'attachment', 'itemid'=>$entry->id, 'filepath'=>'/', 'filename'=>$filename,
334                                      'timecreated'=>filectime($pathname), 'timemodified'=>filemtime($pathname), 'userid'=>$entry->userid);
335                 $fs->create_file_from_pathname($file_record, $pathname);
336             }
337             @unlink($pathname);
338             @rmdir("$CFG->dataroot/blog/attachments/$entry->id/");
340             $entry->attachment = 1; // file name not needed there anymore
341             $DB->update_record('post', $entry);
342             $pbar->update($i, $count, "Migrated blog attachments - $i/$count.");
343         }
344         $rs->close();
345     }
347     @rmdir("$CFG->dataroot/blog/attachments/");
348     @rmdir("$CFG->dataroot/blog/");
351 /**
352  * This function will fix the status of the localhost/all records in the mnet_host table
353  * checking they exist and adding them if missing + redefine CFG->mnet_localhost_id  and
354  * CFG->mnet_all_hosts_id if needed + update all the users having non-existent mnethostid
355  * to correct CFG->mnet_localhost_id
356  *
357  * Implemented because, at some point, specially in old installations upgraded along
358  * multiple versions, sometimes the stuff above has ended being inconsistent, causing
359  * problems here and there (noticeably in backup/restore). MDL-16879
360  */
361 function upgrade_fix_incorrect_mnethostids() {
363     global $CFG, $DB;
365 /// Get current $CFG/mnet_host records
366     $old_mnet_localhost_id = !empty($CFG->mnet_localhost_id) ? $CFG->mnet_localhost_id : 0;
367     $old_mnet_all_hosts_id = !empty($CFG->mnet_all_hosts_id) ? $CFG->mnet_all_hosts_id : 0;
369     $current_mnet_localhost_host = $DB->get_record('mnet_host', array('wwwroot' => $CFG->wwwroot)); /// By wwwroot
370     $current_mnet_all_hosts_host = $DB->get_record_select('mnet_host', $DB->sql_isempty('mnet_host', 'wwwroot', false, false)); /// By empty wwwroot
372     if (!$moodleapplicationid = $DB->get_field('mnet_application', 'id', array('name' => 'moodle'))) {
373         $m = (object)array(
374             'name'              => 'moodle',
375             'display_name'      => 'Moodle',
376             'xmlrpc_server_url' => '/mnet/xmlrpc/server.php',
377             'sso_land_url'      => '/auth/mnet/land.php',
378             'sso_jump_url'      => '/auth/mnet/jump.php',
379         );
380         $moodleapplicationid = $DB->insert_record('mnet_application', $m);
381     }
383 /// Create localhost_host if necessary (pretty improbable but better to be 100% in the safe side)
384 /// Code stolen from mnet_environment->init
385     if (!$current_mnet_localhost_host) {
386         $current_mnet_localhost_host                     = new stdClass();
387         $current_mnet_localhost_host->wwwroot            = $CFG->wwwroot;
388         $current_mnet_localhost_host->ip_address         = '';
389         $current_mnet_localhost_host->public_key         = '';
390         $current_mnet_localhost_host->public_key_expires = 0;
391         $current_mnet_localhost_host->last_connect_time  = 0;
392         $current_mnet_localhost_host->last_log_id        = 0;
393         $current_mnet_localhost_host->deleted            = 0;
394         $current_mnet_localhost_host->name               = '';
395         $current_mnet_localhost_host->applicationid      = $moodleapplicationid;
396     /// Get the ip of the server
397         if (empty($_SERVER['SERVER_ADDR'])) {
398         /// SERVER_ADDR is only returned by Apache-like webservers
399             $count = preg_match("@^(?:http[s]?://)?([A-Z0-9\-\.]+).*@i", $current_mnet_localhost_host->wwwroot, $matches);
400             $my_hostname = $count > 0 ? $matches[1] : false;
401             $my_ip       = gethostbyname($my_hostname);  // Returns unmodified hostname on failure. DOH!
402             if ($my_ip == $my_hostname) {
403                 $current_mnet_localhost_host->ip_address = 'UNKNOWN';
404             } else {
405                 $current_mnet_localhost_host->ip_address = $my_ip;
406             }
407         } else {
408             $current_mnet_localhost_host->ip_address = $_SERVER['SERVER_ADDR'];
409         }
410         $current_mnet_localhost_host->id = $DB->insert_record('mnet_host', $current_mnet_localhost_host, true);
411     }
413 /// Create all_hosts_host if necessary (pretty improbable but better to be 100% in the safe side)
414 /// Code stolen from mnet_environment->init
415     if (!$current_mnet_all_hosts_host) {
416         $current_mnet_all_hosts_host                     = new stdClass();
417         $current_mnet_all_hosts_host->wwwroot            = '';
418         $current_mnet_all_hosts_host->ip_address         = '';
419         $current_mnet_all_hosts_host->public_key         = '';
420         $current_mnet_all_hosts_host->public_key_expires = 0;
421         $current_mnet_all_hosts_host->last_connect_time  = 0;
422         $current_mnet_all_hosts_host->last_log_id        = 0;
423         $current_mnet_all_hosts_host->deleted            = 0;
424         $current_mnet_all_hosts_host->name               = 'All Hosts';
425         $current_mnet_all_hosts_host->applicationid      = $moodleapplicationid;
426         $current_mnet_all_hosts_host->id                 = $DB->insert_record('mnet_host', $current_mnet_all_hosts_host, true);
427     }
429 /// Compare old_mnet_localhost_id and current_mnet_localhost_host
431     if ($old_mnet_localhost_id != $current_mnet_localhost_host->id) { /// Different = problems
432     /// Update $CFG->mnet_localhost_id to correct value
433         set_config('mnet_localhost_id', $current_mnet_localhost_host->id);
435     /// Delete $old_mnet_localhost_id if exists (users will be assigned to new one below)
436         $DB->delete_records('mnet_host', array('id' => $old_mnet_localhost_id));
437     }
439 /// Compare old_mnet_all_hosts_id and current_mnet_all_hosts_host
441     if ($old_mnet_all_hosts_id != $current_mnet_all_hosts_host->id) { /// Different = problems
442     /// Update $CFG->mnet_localhost_id to correct value
443         set_config('mnet_all_hosts_id', $current_mnet_all_hosts_host->id);
445     /// Delete $old_mnet_all_hosts_id if exists
446         $DB->delete_records('mnet_host', array('id' => $old_mnet_all_hosts_id));
447     }
449 /// Finally, update all the incorrect user->mnethostid to the correct CFG->mnet_localhost_id, preventing UIX dupes
450     $hosts = $DB->get_records_menu('mnet_host', null, '', 'id, id AS id2');
451     list($in_sql, $in_params) = $DB->get_in_or_equal($hosts, SQL_PARAMS_QM, null, false);
453     $sql = "SELECT id
454             FROM {user} u1
455             WHERE u1.mnethostid $in_sql
456               AND NOT EXISTS (
457                   SELECT 'x'
458                     FROM {user} u2
459                    WHERE u2.username = u1.username
460                      AND u2.mnethostid = ?)";
462     $params = array_merge($in_params, array($current_mnet_localhost_host->id));
464     if ($rs = $DB->get_recordset_sql($sql, $params)) {
465         foreach ($rs as $rec) {
466             $DB->set_field('user', 'mnethostid', $current_mnet_localhost_host->id, array('id' => $rec->id));
467             upgrade_set_timeout(60); /// Give upgrade at least 60 more seconds
468         }
469         $rs->close();
470     }
472     // fix up any host records that have incorrect ids
473     $DB->set_field_select('mnet_host', 'applicationid', $moodleapplicationid, 'id = ? or id = ?', array($current_mnet_localhost_host->id, $current_mnet_all_hosts_host->id));
477 /**
478  * This function is used as part of the great navigation upgrade of 20090828
479  * It is used to clean up contexts that are unique to a blocks that are about
480  * to be removed.
481  *
482  *
483  * Look at {@link blocklib.php::blocks_delete_instance()} the function from
484  * which I based this code. It is important to mention one very important fact
485  * before doing this I checked that the blocks did not override the
486  * {@link block_base::instance_delete()} method. Should this function ever
487  * be repeated check this again
488  *
489  * @link lib/db/upgrade.php
490  *
491  * @since navigation upgrade 20090828
492  * @param array $contextidarray An array of block instance context ids
493  * @return void
494  */
495 function upgrade_cleanup_unwanted_block_contexts($contextidarray) {
496     global $DB;
498     if (!is_array($contextidarray) || count($contextidarray)===0) {
499         // Ummmm no instances?
500         return;
501     }
503     $contextidstring = join(',', $contextidarray);
505     $blockcontexts = $DB->get_recordset_select('context', 'contextlevel = '.CONTEXT_BLOCK.' AND id IN ('.$contextidstring.')', array(), '', 'id, contextlevel');
506     $blockcontextids = array();
507     foreach ($blockcontexts as $blockcontext) {
508         $blockcontextids[] = $blockcontext->id;
509     }
511     if (count($blockcontextids)===0) {
512         // None of the instances have unique contexts
513         return;
514     }
516     $blockcontextidsstring = join(',', $blockcontextids);
518     $DB->delete_records_select('role_assignments', 'contextid IN ('.$blockcontextidsstring.')');
519     $DB->delete_records_select('role_capabilities', 'contextid IN ('.$blockcontextidsstring.')');
520     $DB->delete_records_select('role_names', 'contextid IN ('.$blockcontextidsstring.')');
521     $DB->delete_records_select('context', 'id IN ('.$blockcontextidsstring.')');