4 * StatusNet - a distributed open-source microblogging tool
5 * Copyright (C) 2008-2011 StatusNet, Inc.
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
23 $shortoptions = 'dfx::';
24 $longoptions = array('debug', 'files', 'extensions=');
26 $helptext = <<<END_OF_UPGRADE_HELP
27 php upgrade.php [options]
28 Upgrade database schema and data to latest software
32 require_once INSTALLDIR.'/scripts/commandline.inc';
35 if (!defined('DEBUG')) {
36 define('DEBUG', (bool)have_option('d', 'debug'));
41 // "files" option enables possibly disk/resource intensive operations
42 // that aren't really _required_ for the upgrade
43 $iterate_files = (bool)have_option('f', 'files');
45 if (Event::handle('StartUpgrade')) {
46 fixupConversationURIs();
49 updateSchemaPlugins();
51 // These replace old "fixup_*" scripts
53 fixupNoticeConversation();
57 printfnq("Running file iterations:\n");
58 printfnq("* "); fixupFileGeometry();
59 printfnq("* "); deleteLocalFileThumbnailsWithoutFilename();
60 printfnq("* "); deleteMissingLocalFileThumbnails();
61 printfnq("* "); fixupFileThumbnailUrlhash();
62 printfnq("* "); setFilehashOnLocalFiles();
65 printfnq("Skipping intensive/long-running file iteration functions (enable with -f, should be done at least once!)\n");
72 initSubscriptionURI();
77 migrateProfilePrefs();
79 Event::handle('EndUpgrade');
86 require INSTALLDIR.'/db/core.php';
90 function updateSchemaCore()
92 printfnq("Upgrading core schema...");
94 $schema = Schema::get();
95 $schemaUpdater = new SchemaUpdater($schema);
96 foreach (tableDefs() as $table => $def) {
97 $schemaUpdater->register($table, $def);
99 $schemaUpdater->checkSchema();
104 function updateSchemaPlugins()
106 printfnq("Upgrading plugin schema...");
108 Event::handle('BeforePluginCheckSchema');
109 Event::handle('CheckSchema');
114 function fixupNoticeConversation()
116 printfnq("Ensuring all notices have a conversation ID...");
118 $notice = new Notice();
119 $notice->whereAdd('conversation is null');
120 $notice->whereAdd('conversation = 0', 'OR');
121 $notice->orderBy('id'); // try to get originals before replies
124 while ($notice->fetch()) {
128 $orig = clone($notice);
130 if (!empty($notice->reply_to)) {
131 $reply = Notice::getKV('id', $notice->reply_to);
133 if ($reply instanceof Notice && !empty($reply->conversation)) {
134 $notice->conversation = $reply->conversation;
140 if (empty($notice->conversation)) {
141 $child = new Notice();
142 $child->reply_to = $notice->getID();
144 if ($child->find(true) && !empty($child->conversation)) {
145 $notice->conversation = $child->conversation;
150 // if _still_ empty we just create our own conversation
151 if (empty($notice->conversation)) {
152 $notice->conversation = $notice->getID();
155 $result = $notice->update($orig);
158 } catch (Exception $e) {
159 print("Error setting conversation: " . $e->getMessage());
166 function fixupGroupURI()
168 printfnq("Ensuring all groups have an URI...");
170 $group = new User_group();
171 $group->whereAdd('uri IS NULL');
173 if ($group->find()) {
174 while ($group->fetch()) {
175 $orig = User_group::getKV('id', $group->id);
176 $group->uri = $group->getUri();
177 $group->update($orig);
184 function initConversation()
186 if (common_config('fix', 'upgrade_initConversation') <= 1) {
187 printfnq(sprintf("Skipping %s, fixed by previous upgrade.\n", __METHOD__));
191 printfnq("Ensuring all conversations have a row in conversation table...");
193 $notice = new Notice();
194 $notice->selectAdd();
195 $notice->selectAdd('DISTINCT conversation');
196 $notice->joinAdd(['conversation', 'conversation:id'], 'LEFT'); // LEFT to get the null values for conversation.id
197 $notice->whereAdd('conversation.id IS NULL');
199 if ($notice->find()) {
200 printfnq(" fixing {$notice->N} missing conversation entries...");
203 while ($notice->fetch()) {
205 $id = $notice->conversation;
207 $uri = common_local_url('conversation', array('id' => $id));
209 // @fixme db_dataobject won't save our value for an autoincrement
210 // so we're bypassing the insert wrappers
211 $conv = new Conversation();
212 $sql = "insert into conversation (id,uri,created) values(%d,'%s','%s')";
216 $conv->escape(common_sql_now()));
220 // This is something we should only have to do once unless introducing new, bad code.
221 if (DEBUG) printfnq(sprintf('Storing in config that we have done %s', __METHOD__));
222 common_config_set('fix', 'upgrade_initConversation', 1);
227 function fixupConversationURIs()
229 printfnq("Ensuring all conversations have a URI...");
231 $conv = new Conversation();
232 $conv->whereAdd('uri IS NULL');
236 while ($conv->fetch()) {
237 $uri = common_local_url('conversation', array('id' => $conv->id));
238 $sql = sprintf('UPDATE conversation SET uri="%1$s" WHERE id="%2$d";',
239 $conv->escape($uri), $conv->id);
241 if (($conv->N-++$rounds) % 500 == 0) {
242 printfnq(sprintf(' %d items left...', $conv->N-$rounds));
250 function initGroupProfileId()
252 printfnq("Ensuring all User_group entries have a Profile and profile_id...");
254 $group = new User_group();
255 $group->whereAdd('NOT EXISTS (SELECT id FROM profile WHERE id = user_group.profile_id)');
258 while ($group->fetch()) {
260 // We must create a new, incrementally assigned profile_id
261 $profile = new Profile();
262 $profile->nickname = $group->nickname;
263 $profile->fullname = $group->fullname;
264 $profile->profileurl = $group->mainpage;
265 $profile->homepage = $group->homepage;
266 $profile->bio = $group->description;
267 $profile->location = $group->location;
268 $profile->created = $group->created;
269 $profile->modified = $group->modified;
271 $profile->query('BEGIN');
272 $id = $profile->insert();
274 $profile->query('ROLLBACK');
275 throw new Exception('Profile insertion failed, profileurl: '.$profile->profileurl);
277 $group->query("UPDATE user_group SET profile_id={$id} WHERE id={$group->id}");
278 $profile->query('COMMIT');
281 } catch (Exception $e) {
282 printfv("Error initializing Profile for group {$group->nickname}:" . $e->getMessage());
289 function initLocalGroup()
291 printfnq("Ensuring all local user groups have a local_group...");
293 $group = new User_group();
294 $group->whereAdd('NOT EXISTS (select group_id from local_group where group_id = user_group.id)');
297 while ($group->fetch()) {
299 // Hack to check for local groups
300 if ($group->getUri() == common_local_url('groupbyid', array('id' => $group->id))) {
301 $lg = new Local_group();
303 $lg->group_id = $group->id;
304 $lg->nickname = $group->nickname;
305 $lg->created = $group->created; // XXX: common_sql_now() ?
306 $lg->modified = $group->modified;
310 } catch (Exception $e) {
311 printfv("Error initializing local group for {$group->nickname}:" . $e->getMessage());
318 function initNoticeReshare()
320 if (common_config('fix', 'upgrade_initNoticeReshare') <= 1) {
321 printfnq(sprintf("Skipping %s, fixed by previous upgrade.\n", __METHOD__));
325 printfnq("Ensuring all reshares have the correct verb and object-type...");
327 $notice = new Notice();
328 $notice->whereAdd('repeat_of is not null');
329 $notice->whereAdd('(verb != "'.ActivityVerb::SHARE.'" OR object_type != "'.ActivityObject::ACTIVITY.'")');
331 if ($notice->find()) {
332 while ($notice->fetch()) {
334 $orig = Notice::getKV('id', $notice->id);
335 $notice->verb = ActivityVerb::SHARE;
336 $notice->object_type = ActivityObject::ACTIVITY;
337 $notice->update($orig);
338 } catch (Exception $e) {
339 printfv("Error updating verb and object_type for {$notice->id}:" . $e->getMessage());
344 // This is something we should only have to do once unless introducing new, bad code.
345 if (DEBUG) printfnq(sprintf('Storing in config that we have done %s', __METHOD__));
346 common_config_set('fix', 'upgrade_initNoticeReshare', 1);
351 function initSubscriptionURI()
353 printfnq("Ensuring all subscriptions have a URI...");
355 $sub = new Subscription();
356 $sub->whereAdd('uri IS NULL');
359 while ($sub->fetch()) {
362 $sub->query(sprintf('update subscription '.
364 'where subscriber = %d '.
365 'and subscribed = %d',
366 $sub->escape(Subscription::newUri($sub->getSubscriber(), $sub->getSubscribed(), $sub->created)),
369 } catch (Exception $e) {
370 common_log(LOG_ERR, "Error updated subscription URI: " . $e->getMessage());
378 function initGroupMemberURI()
380 printfnq("Ensuring all group memberships have a URI...");
382 $mem = new Group_member();
383 $mem->whereAdd('uri IS NULL');
386 while ($mem->fetch()) {
389 $mem->query(sprintf('update group_member set uri = "%s" '.
390 'where profile_id = %d ' .
391 'and group_id = %d ',
392 Group_member::newUri(Profile::getByID($mem->profile_id), User_group::getByID($mem->group_id), $mem->created),
395 } catch (Exception $e) {
396 common_log(LOG_ERR, "Error updated membership URI: " . $e->getMessage());
404 function initProfileLists()
406 printfnq("Ensuring all profile tags have a corresponding list...");
408 $ptag = new Profile_tag();
410 $ptag->selectAdd('tagger, tag, count(*) as tagged_count');
411 $ptag->whereAdd('NOT EXISTS (SELECT tagger, tagged from profile_list '.
412 'where profile_tag.tagger = profile_list.tagger '.
413 'and profile_tag.tag = profile_list.tag)');
414 $ptag->groupBy('tagger, tag');
415 $ptag->orderBy('tagger, tag');
418 while ($ptag->fetch()) {
419 $plist = new Profile_list();
421 $plist->tagger = $ptag->tagger;
422 $plist->tag = $ptag->tag;
424 $plist->created = common_sql_now();
425 $plist->modified = $plist->created;
426 $plist->mainpage = common_local_url('showprofiletag',
427 array('tagger' => $plist->getTagger()->nickname,
428 'tag' => $plist->tag));;
430 $plist->tagged_count = $ptag->tagged_count;
431 $plist->subscriber_count = 0;
435 $orig = clone($plist);
436 // After insert since it uses auto-generated ID
437 $plist->uri = common_local_url('profiletagbyid',
438 array('id' => $plist->id, 'tagger_id' => $plist->tagger));
440 $plist->update($orig);
448 * Added as we now store interpretd width and height in File table.
450 function fixupFileGeometry()
452 printfnq("Ensuring width and height is set for supported local File objects...");
455 $file->whereAdd('filename IS NOT NULL'); // local files
456 $file->whereAdd('width IS NULL OR width = 0');
459 while ($file->fetch()) {
460 if (DEBUG) printfnq(sprintf('Found file without width: %s\n', _ve($file->getFilename())));
462 // Set file geometrical properties if available
464 $image = ImageFile::fromFileObject($file);
465 } catch (ServerException $e) {
466 // We couldn't make out an image from the file.
467 if (DEBUG) printfnq(sprintf('Could not make an image out of the file.\n'));
470 $orig = clone($file);
471 $file->width = $image->width;
472 $file->height = $image->height;
473 if (DEBUG) printfnq(sprintf('Setting image file and with to %sx%s.\n', $file->width, $file->height));
474 $file->update($orig);
476 // FIXME: Do this more automagically inside ImageFile or so.
477 if ($image->getPath() != $file->getPath()) {
478 if (DEBUG) printfnq(sprintf('Deleting the temporarily stored ImageFile.\n'));
489 * File_thumbnail objects for local Files store their own filenames in the database.
491 function deleteLocalFileThumbnailsWithoutFilename()
493 printfnq("Removing all local File_thumbnail entries without filename property...");
496 $file->whereAdd('filename IS NOT NULL'); // local files
499 // Looping through local File entries
500 while ($file->fetch()) {
501 $thumbs = new File_thumbnail();
502 $thumbs->file_id = $file->id;
503 $thumbs->whereAdd('filename IS NULL OR filename = ""');
504 // Checking if there were any File_thumbnail entries without filename
505 if (!$thumbs->find()) {
508 // deleting incomplete entry to allow regeneration
509 while ($thumbs->fetch()) {
519 * Delete File_thumbnail entries where the referenced file does not exist.
521 function deleteMissingLocalFileThumbnails()
523 printfnq("Removing all local File_thumbnail entries without existing files...");
525 $thumbs = new File_thumbnail();
526 $thumbs->whereAdd('filename IS NOT NULL AND filename != ""');
527 // Checking if there were any File_thumbnail entries without filename
528 if ($thumbs->find()) {
529 while ($thumbs->fetch()) {
532 } catch (FileNotFoundException $e) {
542 * Files are now stored with their hash, so let's generate for previously uploaded files.
544 function setFilehashOnLocalFiles()
546 printfnq('Ensuring all local files have the filehash field set...');
549 $file->whereAdd('filename IS NOT NULL AND filename != ""'); // local files
550 $file->whereAdd('filehash IS NULL', 'AND'); // without filehash value
553 while ($file->fetch()) {
555 $orig = clone($file);
556 $file->filehash = hash_file(File::FILEHASH_ALG, $file->getPath());
557 $file->update($orig);
558 } catch (FileNotFoundException $e) {
559 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";
567 function fixupFileThumbnailUrlhash()
569 printfnq("Setting urlhash for File_thumbnail entries: ");
571 $thumb = new File_thumbnail();
572 $thumb->query('UPDATE '.$thumb->escapedTableName().' SET urlhash=SHA2(url, 256) WHERE'.
573 ' url IS NOT NULL AND'. // find all entries with a url value
574 ' url != "" AND'. // precaution against non-null empty strings
575 ' urlhash IS NULL'); // but don't touch those we've already calculated
580 function migrateProfilePrefs()
582 printfnq("Finding and possibly migrating Profile_prefs entries: ");
584 $prefs = array(); // ['qvitter' => ['cover_photo'=>'profile_banner_url', ...], ...]
585 Event::handle('GetProfilePrefsMigrations', array(&$prefs));
587 foreach($prefs as $namespace=>$mods) {
588 echo "$namespace... ";
589 assert(is_array($mods));
590 $p = new Profile_prefs();
591 $p->namespace = $namespace;
592 // find all entries in all modified topics given in this namespace
593 $p->whereAddIn('topic', array_keys($mods), $p->columnType('topic'));
595 while ($p->fetch()) {
596 // for each entry, update 'topic' to the new key value
598 $p->topic = $mods[$p->topic];
599 $p->updateWithKeys($orig);