3 // This file is part of GNU social - https://www.gnu.org/software/social
5 // GNU social is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU Affero 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.
10 // GNU social 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 Affero General Public License for more details.
15 // You should have received a copy of the GNU Affero General Public License
16 // along with GNU social. If not, see <http://www.gnu.org/licenses/>.
19 * Upgrade database schema and data to latest software and check DB integrity
20 * Usage: php upgrade.php [options]
23 * @author Bhuvan Krishna <bhuvan@swecha.net>
24 * @author Evan Prodromou <evan@status.net>
25 * @author Mikael Nordfeldth <mmn@hethane.se>
26 * @copyright 2010-2019 Free Software Foundation, Inc http://www.fsf.org
27 * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
30 define('INSTALLDIR', dirname(__DIR__));
31 define('PUBLICDIR', INSTALLDIR . DIRECTORY_SEPARATOR . 'public');
33 $shortoptions = 'dfx::';
34 $longoptions = ['debug', 'files', 'extensions='];
36 $helptext = <<<END_OF_UPGRADE_HELP
37 php upgrade.php [options]
38 Upgrade database schema and data to latest software
42 require_once INSTALLDIR.'/scripts/commandline.inc';
45 if (!defined('DEBUG')) {
46 define('DEBUG', (bool)have_option('d', 'debug'));
51 // "files" option enables possibly disk/resource intensive operations
52 // that aren't really _required_ for the upgrade
53 $iterate_files = (bool)have_option('f', 'files');
55 if (Event::handle('StartUpgrade')) {
56 fixupConversationURIs();
59 updateSchemaPlugins();
61 // These replace old "fixup_*" scripts
63 fixupNoticeConversation();
67 printfnq("Running file iterations:\n");
68 printfnq("* "); fixupFileGeometry();
69 printfnq("* "); deleteLocalFileThumbnailsWithoutFilename();
70 printfnq("* "); deleteMissingLocalFileThumbnails();
71 printfnq("* "); fixupFileThumbnailUrlhash();
72 printfnq("* "); setFilehashOnLocalFiles();
75 printfnq("Skipping intensive/long-running file iteration functions (enable with -f, should be done at least once!)\n");
82 initSubscriptionURI();
87 migrateProfilePrefs();
89 Event::handle('EndUpgrade');
96 require INSTALLDIR . '/db/core.php';
100 function updateSchemaCore()
102 printfnq("Upgrading core schema...");
104 $schema = Schema::get();
105 $schemaUpdater = new SchemaUpdater($schema);
106 foreach (tableDefs() as $table => $def) {
107 $schemaUpdater->register($table, $def);
109 $schemaUpdater->checkSchema();
114 function updateSchemaPlugins()
116 printfnq("Upgrading plugin schema...");
118 Event::handle('BeforePluginCheckSchema');
119 Event::handle('CheckSchema');
124 function fixupNoticeConversation()
126 printfnq("Ensuring all notices have a conversation ID...");
128 $notice = new Notice();
129 $notice->whereAdd('conversation is null');
130 $notice->whereAdd('conversation = 0', 'OR');
131 $notice->orderBy('id'); // try to get originals before replies
134 while ($notice->fetch()) {
138 $orig = clone($notice);
140 if (!empty($notice->reply_to)) {
141 $reply = Notice::getKV('id', $notice->reply_to);
143 if ($reply instanceof Notice && !empty($reply->conversation)) {
144 $notice->conversation = $reply->conversation;
150 if (empty($notice->conversation)) {
151 $child = new Notice();
152 $child->reply_to = $notice->getID();
154 if ($child->find(true) && !empty($child->conversation)) {
155 $notice->conversation = $child->conversation;
160 // if _still_ empty we just create our own conversation
161 if (empty($notice->conversation)) {
162 $notice->conversation = $notice->getID();
165 $result = $notice->update($orig);
168 } catch (Exception $e) {
169 print("Error setting conversation: " . $e->getMessage());
176 function fixupGroupURI()
178 printfnq("Ensuring all groups have an URI...");
180 $group = new User_group();
181 $group->whereAdd('uri IS NULL');
183 if ($group->find()) {
184 while ($group->fetch()) {
185 $orig = User_group::getKV('id', $group->id);
186 $group->uri = $group->getUri();
187 $group->update($orig);
194 function initConversation()
196 if (common_config('fix', 'upgrade_initConversation') <= 1) {
197 printfnq(sprintf("Skipping %s, fixed by previous upgrade.\n", __METHOD__));
201 printfnq("Ensuring all conversations have a row in conversation table...");
203 $notice = new Notice();
204 $notice->selectAdd();
205 $notice->selectAdd('DISTINCT conversation');
206 $notice->joinAdd(['conversation', 'conversation:id'], 'LEFT'); // LEFT to get the null values for conversation.id
207 $notice->whereAdd('conversation.id IS NULL');
209 if ($notice->find()) {
210 printfnq(" fixing {$notice->N} missing conversation entries...");
213 while ($notice->fetch()) {
214 $id = $notice->conversation;
216 $uri = common_local_url('conversation', ['id' => $id]);
218 // @fixme db_dataobject won't save our value for an autoincrement
219 // so we're bypassing the insert wrappers
220 $conv = new Conversation();
221 $sql = "INSERT INTO conversation (id,uri,created) VALUES (%d,'%s','%s')";
226 $conv->escape(common_sql_now())
231 // This is something we should only have to do once unless introducing new, bad code.
233 printfnq(sprintf('Storing in config that we have done %s', __METHOD__));
235 common_config_set('fix', 'upgrade_initConversation', 1);
240 function fixupConversationURIs()
242 printfnq("Ensuring all conversations have a URI...");
244 $conv = new Conversation();
245 $conv->whereAdd('uri IS NULL');
249 while ($conv->fetch()) {
250 $uri = common_local_url('conversation', ['id' => $conv->id]);
252 'UPDATE conversation SET uri="%1$s" WHERE id="%2$d";',
257 if (($conv->N-++$rounds) % 500 == 0) {
258 printfnq(sprintf(' %d items left...', $conv->N-$rounds));
266 function initGroupProfileId()
268 printfnq("Ensuring all User_group entries have a Profile and profile_id...");
270 $group = new User_group();
271 $group->whereAdd('NOT EXISTS (SELECT id FROM profile WHERE id = user_group.profile_id)');
274 while ($group->fetch()) {
276 // We must create a new, incrementally assigned profile_id
277 $profile = new Profile();
278 $profile->nickname = $group->nickname;
279 $profile->fullname = $group->fullname;
280 $profile->profileurl = $group->mainpage;
281 $profile->homepage = $group->homepage;
282 $profile->bio = $group->description;
283 $profile->location = $group->location;
284 $profile->created = $group->created;
285 $profile->modified = $group->modified;
287 $profile->query('BEGIN');
288 $id = $profile->insert();
290 $profile->query('ROLLBACK');
291 throw new Exception('Profile insertion failed, profileurl: '.$profile->profileurl);
293 $group->query("UPDATE user_group SET profile_id={$id} WHERE id={$group->id}");
294 $profile->query('COMMIT');
297 } catch (Exception $e) {
298 printfv("Error initializing Profile for group {$group->nickname}:" . $e->getMessage());
305 function initLocalGroup()
307 printfnq("Ensuring all local user groups have a local_group...");
309 $group = new User_group();
310 $group->whereAdd('NOT EXISTS (select group_id from local_group where group_id = user_group.id)');
313 while ($group->fetch()) {
315 // Hack to check for local groups
316 if ($group->getUri() == common_local_url('groupbyid', ['id' => $group->id])) {
317 $lg = new Local_group();
319 $lg->group_id = $group->id;
320 $lg->nickname = $group->nickname;
321 $lg->created = $group->created; // XXX: common_sql_now() ?
322 $lg->modified = $group->modified;
326 } catch (Exception $e) {
327 printfv("Error initializing local group for {$group->nickname}:" . $e->getMessage());
334 function initNoticeReshare()
336 if (common_config('fix', 'upgrade_initNoticeReshare') <= 1) {
337 printfnq(sprintf("Skipping %s, fixed by previous upgrade.\n", __METHOD__));
341 printfnq("Ensuring all reshares have the correct verb and object-type...");
343 $notice = new Notice();
344 $notice->whereAdd('repeat_of is not null');
345 $notice->whereAdd('(verb != "'.ActivityVerb::SHARE.'" OR object_type != "'.ActivityObject::ACTIVITY.'")');
347 if ($notice->find()) {
348 while ($notice->fetch()) {
350 $orig = Notice::getKV('id', $notice->id);
351 $notice->verb = ActivityVerb::SHARE;
352 $notice->object_type = ActivityObject::ACTIVITY;
353 $notice->update($orig);
354 } catch (Exception $e) {
355 printfv("Error updating verb and object_type for {$notice->id}:" . $e->getMessage());
360 // This is something we should only have to do once unless introducing new, bad code.
362 printfnq(sprintf('Storing in config that we have done %s', __METHOD__));
364 common_config_set('fix', 'upgrade_initNoticeReshare', 1);
369 function initSubscriptionURI()
371 printfnq("Ensuring all subscriptions have a URI...");
373 $sub = new Subscription();
374 $sub->whereAdd('uri IS NULL');
377 while ($sub->fetch()) {
381 'UPDATE subscription '.
383 'WHERE subscriber = %d '.
384 'AND subscribed = %d',
385 $sub->escape(Subscription::newUri($sub->getSubscriber(), $sub->getSubscribed(), $sub->created)),
389 } catch (Exception $e) {
390 common_log(LOG_ERR, "Error updated subscription URI: " . $e->getMessage());
398 function initGroupMemberURI()
400 printfnq("Ensuring all group memberships have a URI...");
402 $mem = new Group_member();
403 $mem->whereAdd('uri IS NULL');
406 while ($mem->fetch()) {
410 'UPDATE group_member '.
412 'WHERE profile_id = %d ' .
414 Group_member::newUri(Profile::getByID($mem->profile_id), User_group::getByID($mem->group_id), $mem->created),
418 } catch (Exception $e) {
419 common_log(LOG_ERR, "Error updated membership URI: " . $e->getMessage());
427 function initProfileLists()
429 printfnq("Ensuring all profile tags have a corresponding list...");
431 $ptag = new Profile_tag();
433 $ptag->selectAdd('tagger, tag, COUNT(*) AS tagged_count');
434 $ptag->whereAdd('NOT EXISTS (SELECT tagger, tagged FROM profile_list '.
435 'WHERE profile_tag.tagger = profile_list.tagger '.
436 'AND profile_tag.tag = profile_list.tag)');
437 $ptag->groupBy('tagger, tag');
438 $ptag->orderBy('tagger, tag');
441 while ($ptag->fetch()) {
442 $plist = new Profile_list();
444 $plist->tagger = $ptag->tagger;
445 $plist->tag = $ptag->tag;
447 $plist->created = common_sql_now();
448 $plist->modified = $plist->created;
449 $plist->mainpage = common_local_url(
451 ['tagger' => $plist->getTagger()->nickname,
452 'tag' => $plist->tag]
456 $plist->tagged_count = $ptag->tagged_count;
457 $plist->subscriber_count = 0;
461 $orig = clone($plist);
462 // After insert since it uses auto-generated ID
463 $plist->uri = common_local_url(
466 'tagger_id' => $plist->tagger]
469 $plist->update($orig);
477 * Added as we now store interpretd width and height in File table.
479 function fixupFileGeometry()
481 printfnq("Ensuring width and height is set for supported local File objects...");
484 $file->whereAdd('filename IS NOT NULL'); // local files
485 $file->whereAdd('width IS NULL OR width = 0');
488 while ($file->fetch()) {
490 printfnq(sprintf('Found file without width: %s\n', _ve($file->getFilename())));
493 // Set file geometrical properties if available
495 $image = ImageFile::fromFileObject($file);
496 } catch (ServerException $e) {
497 // We couldn't make out an image from the file.
499 printfnq(sprintf('Could not make an image out of the file.\n'));
503 $orig = clone($file);
504 $file->width = $image->width;
505 $file->height = $image->height;
507 printfnq(sprintf('Setting image file and with to %sx%s.\n', $file->width, $file->height));
509 $file->update($orig);
511 // FIXME: Do this more automagically inside ImageFile or so.
512 if ($image->getPath() != $file->getPath()) {
514 printfnq(sprintf('Deleting the temporarily stored ImageFile.\n'));
526 * File_thumbnail objects for local Files store their own filenames in the database.
528 function deleteLocalFileThumbnailsWithoutFilename()
530 printfnq("Removing all local File_thumbnail entries without filename property...");
533 $file->whereAdd('filename IS NOT NULL'); // local files
536 // Looping through local File entries
537 while ($file->fetch()) {
538 $thumbs = new File_thumbnail();
539 $thumbs->file_id = $file->id;
540 $thumbs->whereAdd('filename IS NULL OR filename = ""');
541 // Checking if there were any File_thumbnail entries without filename
542 if (!$thumbs->find()) {
545 // deleting incomplete entry to allow regeneration
546 while ($thumbs->fetch()) {
556 * Delete File_thumbnail entries where the referenced file does not exist.
558 function deleteMissingLocalFileThumbnails()
560 printfnq("Removing all local File_thumbnail entries without existing files...");
562 $thumbs = new File_thumbnail();
563 $thumbs->whereAdd('filename IS NOT NULL AND filename != ""');
564 // Checking if there were any File_thumbnail entries without filename
565 if ($thumbs->find()) {
566 while ($thumbs->fetch()) {
569 } catch (FileNotFoundException $e) {
579 * Files are now stored with their hash, so let's generate for previously uploaded files.
581 function setFilehashOnLocalFiles()
583 printfnq('Ensuring all local files have the filehash field set...');
586 $file->whereAdd('filename IS NOT NULL AND filename != ""'); // local files
587 $file->whereAdd('filehash IS NULL', 'AND'); // without filehash value
590 while ($file->fetch()) {
592 $orig = clone($file);
593 $file->filehash = hash_file(File::FILEHASH_ALG, $file->getPath());
594 $file->update($orig);
595 } catch (FileNotFoundException $e) {
596 echo "\n WARNING: file ID {$file->id} does not exist on path '{$e->path}'. If there is no file system error, run: php scripts/clean_file_table.php";
604 function fixupFileThumbnailUrlhash()
606 printfnq("Setting urlhash for File_thumbnail entries: ");
608 $thumb = new File_thumbnail();
609 $thumb->query('UPDATE '.$thumb->escapedTableName().' SET urlhash=SHA2(url, 256) WHERE'.
610 ' url IS NOT NULL AND'. // find all entries with a url value
611 ' url != "" AND'. // precaution against non-null empty strings
612 ' urlhash IS NULL'); // but don't touch those we've already calculated
617 function migrateProfilePrefs()
619 printfnq("Finding and possibly migrating Profile_prefs entries: ");
621 $prefs = []; // ['qvitter' => ['cover_photo'=>'profile_banner_url', ...], ...]
622 Event::handle('GetProfilePrefsMigrations', [&$prefs]);
624 foreach ($prefs as $namespace=>$mods) {
625 echo "$namespace... ";
626 assert(is_array($mods));
627 $p = new Profile_prefs();
628 $p->namespace = $namespace;
629 // find all entries in all modified topics given in this namespace
630 $p->whereAddIn('topic', array_keys($mods), $p->columnType('topic'));
632 while ($p->fetch()) {
633 // for each entry, update 'topic' to the new key value
635 $p->topic = $mods[$p->topic];
636 $p->updateWithKeys($orig);