Merge remote-tracking branch 'upstream/master' into social-master
[quix0rs-gnu-social.git] / scripts / upgrade.php
1 #!/usr/bin/env php
2 <?php
3 /*
4  * StatusNet - a distributed open-source microblogging tool
5  * Copyright (C) 2008-2011 StatusNet, Inc.
6  *
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.
11  *
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.
16  *
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/>.
19  */
20
21 define('INSTALLDIR', realpath(dirname(__FILE__) . '/..'));
22
23 $shortoptions = 'x::';
24 $longoptions = array('extensions=');
25
26 $helptext = <<<END_OF_UPGRADE_HELP
27 php upgrade.php [options]
28 Upgrade database schema and data to latest software
29
30 END_OF_UPGRADE_HELP;
31
32 require_once INSTALLDIR.'/scripts/commandline.inc.php';
33
34 function main()
35 {
36     if (Event::handle('StartUpgrade')) {
37         fixupConversationURIs();
38
39         updateSchemaCore();
40         updateSchemaPlugins();
41
42         // These replace old "fixup_*" scripts
43
44         fixupNoticeRendered();
45         fixupNoticeConversation();
46         initConversation();
47         fixupGroupURI();
48         fixupFileGeometry();
49         deleteLocalFileThumbnailsWithoutFilename();
50         deleteMissingLocalFileThumbnails();
51         setFilehashOnLocalFiles();
52
53         initGroupProfileId();
54         initLocalGroup();
55         initNoticeReshare();
56     
57         initSubscriptionURI();
58         initGroupMemberURI();
59
60         initProfileLists();
61
62         Event::handle('EndUpgrade');
63     }
64 }
65
66 function tableDefs()
67 {
68         $schema = array();
69         require INSTALLDIR.'/db/core.php';
70         return $schema;
71 }
72
73 function updateSchemaCore()
74 {
75     printfnq("Upgrading core schema...");
76
77     $schema = Schema::get();
78     $schemaUpdater = new SchemaUpdater($schema);
79     foreach (tableDefs() as $table => $def) {
80         $schemaUpdater->register($table, $def);
81     }
82     $schemaUpdater->checkSchema();
83
84     printfnq("DONE.\n");
85 }
86
87 function updateSchemaPlugins()
88 {
89     printfnq("Upgrading plugin schema...");
90
91     Event::handle('BeforePluginCheckSchema');
92     Event::handle('CheckSchema');
93
94     printfnq("DONE.\n");
95 }
96
97 function fixupNoticeRendered()
98 {
99     printfnq("Ensuring all notices have rendered HTML...");
100
101     $notice = new Notice();
102
103     $notice->whereAdd('rendered IS NULL');
104     $notice->find();
105
106     while ($notice->fetch()) {
107         $original = clone($notice);
108         $notice->rendered = common_render_content($notice->content,
109                                                   $notice->getProfile(),
110                                                   $notice->hasParent() ? $notice->getParent() : null);
111         $notice->update($original);
112     }
113
114     printfnq("DONE.\n");
115 }
116
117 function fixupNoticeConversation()
118 {
119     printfnq("Ensuring all notices have a conversation ID...");
120
121     $notice = new Notice();
122     $notice->whereAdd('conversation is null');
123     $notice->orderBy('id'); // try to get originals before replies
124     $notice->find();
125
126     while ($notice->fetch()) {
127         try {
128             $cid = null;
129     
130             $orig = clone($notice);
131     
132             if (empty($notice->reply_to)) {
133                 $notice->conversation = $notice->id;
134             } else {
135                 $reply = Notice::getKV('id', $notice->reply_to);
136
137                 if (empty($reply)) {
138                     $notice->conversation = $notice->id;
139                 } else if (empty($reply->conversation)) {
140                     $notice->conversation = $notice->id;
141                 } else {
142                     $notice->conversation = $reply->conversation;
143                 }
144         
145                 unset($reply);
146                 $reply = null;
147             }
148
149             $result = $notice->update($orig);
150
151             $orig = null;
152             unset($orig);
153         } catch (Exception $e) {
154             printv("Error setting conversation: " . $e->getMessage());
155         }
156     }
157
158     printfnq("DONE.\n");
159 }
160
161 function fixupGroupURI()
162 {
163     printfnq("Ensuring all groups have an URI...");
164
165     $group = new User_group();
166     $group->whereAdd('uri IS NULL');
167
168     if ($group->find()) {
169         while ($group->fetch()) {
170             $orig = User_group::getKV('id', $group->id);
171             $group->uri = $group->getUri();
172             $group->update($orig);
173         }
174     }
175
176     printfnq("DONE.\n");
177 }
178
179 function initConversation()
180 {
181     printfnq("Ensuring all conversations have a row in conversation table...");
182
183     $notice = new Notice();
184     $notice->query('select distinct notice.conversation from notice '.
185                    'where notice.conversation is not null '.
186                    'and not exists (select conversation.id from conversation where id = notice.conversation)');
187
188     while ($notice->fetch()) {
189
190         $id = $notice->conversation;
191
192         $uri = common_local_url('conversation', array('id' => $id));
193
194         // @fixme db_dataobject won't save our value for an autoincrement
195         // so we're bypassing the insert wrappers
196         $conv = new Conversation();
197         $sql = "insert into conversation (id,uri,created) values(%d,'%s','%s')";
198         $sql = sprintf($sql,
199                        $id,
200                        $conv->escape($uri),
201                        $conv->escape(common_sql_now()));
202         $conv->query($sql);
203     }
204
205     printfnq("DONE.\n");
206 }
207
208 function fixupConversationURIs()
209 {
210     printfnq("Ensuring all conversations have a URI...");
211
212     $conv = new Conversation();
213     $conv->whereAdd('uri IS NULL');
214
215     if ($conv->find()) {
216         $rounds = 0;
217         while ($conv->fetch()) {
218             $uri = common_local_url('conversation', array('id' => $conv->id));
219             $sql = sprintf('UPDATE conversation SET uri="%1$s" WHERE id="%2$d";',
220                             $conv->escape($uri), $conv->id);
221             $conv->query($sql);
222             if (($conv->N-++$rounds) % 500 == 0) {
223                 printfnq(sprintf(' %d items left...', $conv->N-$rounds));
224             }
225         }
226     }
227
228     printfnq("DONE.\n");
229 }
230
231 function initGroupProfileId()
232 {
233     printfnq("Ensuring all User_group entries have a Profile and profile_id...");
234
235     $group = new User_group();
236     $group->whereAdd('NOT EXISTS (SELECT id FROM profile WHERE id = user_group.profile_id)');
237     $group->find();
238
239     while ($group->fetch()) {
240         try {
241             // We must create a new, incrementally assigned profile_id
242             $profile = new Profile();
243             $profile->nickname   = $group->nickname;
244             $profile->fullname   = $group->fullname;
245             $profile->profileurl = $group->mainpage;
246             $profile->homepage   = $group->homepage;
247             $profile->bio        = $group->description;
248             $profile->location   = $group->location;
249             $profile->created    = $group->created;
250             $profile->modified   = $group->modified;
251
252             $profile->query('BEGIN');
253             $id = $profile->insert();
254             if (empty($id)) {
255                 $profile->query('ROLLBACK');
256                 throw new Exception('Profile insertion failed, profileurl: '.$profile->profileurl);
257             }
258             $group->query("UPDATE user_group SET profile_id={$id} WHERE id={$group->id}");
259             $profile->query('COMMIT');
260
261             $profile->free();
262         } catch (Exception $e) {
263             printfv("Error initializing Profile for group {$group->nickname}:" . $e->getMessage());
264         }
265     }
266
267     printfnq("DONE.\n");
268 }
269
270 function initLocalGroup()
271 {
272     printfnq("Ensuring all local user groups have a local_group...");
273
274     $group = new User_group();
275     $group->whereAdd('NOT EXISTS (select group_id from local_group where group_id = user_group.id)');
276     $group->find();
277
278     while ($group->fetch()) {
279         try {
280             // Hack to check for local groups
281             if ($group->getUri() == common_local_url('groupbyid', array('id' => $group->id))) {
282                 $lg = new Local_group();
283
284                 $lg->group_id = $group->id;
285                 $lg->nickname = $group->nickname;
286                 $lg->created  = $group->created; // XXX: common_sql_now() ?
287                 $lg->modified = $group->modified;
288
289                 $lg->insert();
290             }
291         } catch (Exception $e) {
292             printfv("Error initializing local group for {$group->nickname}:" . $e->getMessage());
293         }
294     }
295
296     printfnq("DONE.\n");
297 }
298
299 function initNoticeReshare()
300 {
301     printfnq("Ensuring all reshares have the correct verb and object-type...");
302     
303     $notice = new Notice();
304     $notice->whereAdd('repeat_of is not null');
305     $notice->whereAdd('(verb != "'.ActivityVerb::SHARE.'" OR object_type != "'.ActivityObject::ACTIVITY.'")');
306
307     if ($notice->find()) {
308         while ($notice->fetch()) {
309             try {
310                 $orig = Notice::getKV('id', $notice->id);
311                 $notice->verb = ActivityVerb::SHARE;
312                 $notice->object_type = ActivityObject::ACTIVITY;
313                 $notice->update($orig);
314             } catch (Exception $e) {
315                 printfv("Error updating verb and object_type for {$notice->id}:" . $e->getMessage());
316             }
317         }
318     }
319
320     printfnq("DONE.\n");
321 }
322
323 function initSubscriptionURI()
324 {
325     printfnq("Ensuring all subscriptions have a URI...");
326
327     $sub = new Subscription();
328     $sub->whereAdd('uri IS NULL');
329
330     if ($sub->find()) {
331         while ($sub->fetch()) {
332             try {
333                 $sub->decache();
334                 $sub->query(sprintf('update subscription '.
335                                     'set uri = "%s" '.
336                                     'where subscriber = %d '.
337                                     'and subscribed = %d',
338                                     $sub->escape(Subscription::newUri($sub->getSubscriber(), $sub->getSubscribed(), $sub->created)),
339                                     $sub->subscriber,
340                                     $sub->subscribed));
341             } catch (Exception $e) {
342                 common_log(LOG_ERR, "Error updated subscription URI: " . $e->getMessage());
343             }
344         }
345     }
346
347     printfnq("DONE.\n");
348 }
349
350 function initGroupMemberURI()
351 {
352     printfnq("Ensuring all group memberships have a URI...");
353
354     $mem = new Group_member();
355     $mem->whereAdd('uri IS NULL');
356
357     if ($mem->find()) {
358         while ($mem->fetch()) {
359             try {
360                 $mem->decache();
361                 $mem->query(sprintf('update group_member set uri = "%s" '.
362                                     'where profile_id = %d ' . 
363                                     'and group_id = %d ',
364                                     Group_member::newUri(Profile::getByID($mem->profile_id), User_group::getByID($mem->group_id), $mem->created),
365                                     $mem->profile_id,
366                                     $mem->group_id));
367             } catch (Exception $e) {
368                 common_log(LOG_ERR, "Error updated membership URI: " . $e->getMessage());  
369           }
370         }
371     }
372
373     printfnq("DONE.\n");
374 }
375
376 function initProfileLists()
377 {
378     printfnq("Ensuring all profile tags have a corresponding list...");
379
380     $ptag = new Profile_tag();
381     $ptag->selectAdd();
382     $ptag->selectAdd('tagger, tag, count(*) as tagged_count');
383     $ptag->whereAdd('NOT EXISTS (SELECT tagger, tagged from profile_list '.
384                     'where profile_tag.tagger = profile_list.tagger '.
385                     'and profile_tag.tag = profile_list.tag)');
386     $ptag->groupBy('tagger, tag');
387     $ptag->orderBy('tagger, tag');
388
389     if ($ptag->find()) {
390         while ($ptag->fetch()) {
391             $plist = new Profile_list();
392
393             $plist->tagger   = $ptag->tagger;
394             $plist->tag      = $ptag->tag;
395             $plist->private  = 0;
396             $plist->created  = common_sql_now();
397             $plist->modified = $plist->created;
398             $plist->mainpage = common_local_url('showprofiletag',
399                                                 array('tagger' => $plist->getTagger()->nickname,
400                                                       'tag'    => $plist->tag));;
401
402             $plist->tagged_count     = $ptag->tagged_count;
403             $plist->subscriber_count = 0;
404
405             $plist->insert();
406
407             $orig = clone($plist);
408             // After insert since it uses auto-generated ID
409             $plist->uri      = common_local_url('profiletagbyid',
410                                         array('id' => $plist->id, 'tagger_id' => $plist->tagger));
411
412             $plist->update($orig);
413         }
414     }
415
416     printfnq("DONE.\n");
417 }
418
419 /*
420  * Added as we now store interpretd width and height in File table.
421  */
422 function fixupFileGeometry()
423 {
424     printfnq("Ensuring width and height is set for supported local File objects...");
425
426     $file = new File();
427     $file->whereAdd('filename IS NOT NULL');    // local files
428     $file->whereAdd('width IS NULL OR width = 0');
429
430     if ($file->find()) {
431         while ($file->fetch()) {
432             // Set file geometrical properties if available
433             try {
434                 $image = ImageFile::fromFileObject($file);
435             } catch (ServerException $e) {
436                 // We couldn't make out an image from the file.
437                 continue;
438             }
439             $orig = clone($file);
440             $file->width = $image->width;
441             $file->height = $image->height;
442             $file->update($orig);
443
444             // FIXME: Do this more automagically inside ImageFile or so.
445             if ($image->getPath() != $file->getPath()) {
446                 $image->unlink();
447             }
448             unset($image);
449         }
450     }
451
452     printfnq("DONE.\n");
453 }
454
455 /*
456  * File_thumbnail objects for local Files store their own filenames in the database.
457  */
458 function deleteLocalFileThumbnailsWithoutFilename()
459 {
460     printfnq("Removing all local File_thumbnail entries without filename property...");
461
462     $file = new File();
463     $file->whereAdd('filename IS NOT NULL');    // local files
464
465     if ($file->find()) {
466         // Looping through local File entries
467         while ($file->fetch()) {
468             $thumbs = new File_thumbnail();
469             $thumbs->file_id = $file->id;
470             $thumbs->whereAdd('filename IS NULL');
471             // Checking if there were any File_thumbnail entries without filename
472             if (!$thumbs->find()) {
473                 continue;
474             }
475             // deleting incomplete entry to allow regeneration
476             while ($thumbs->fetch()) {
477                 $thumbs->delete();
478             }
479         }
480     }
481
482     printfnq("DONE.\n");
483 }
484
485 /*
486  * Delete File_thumbnail entries where the referenced file does not exist.
487  */
488 function deleteMissingLocalFileThumbnails()
489 {
490     printfnq("Removing all local File_thumbnail entries without existing files...");
491
492     $thumbs = new File_thumbnail();
493     $thumbs->whereAdd('filename IS NOT NULL');  // only fill in names where they're missing
494     // Checking if there were any File_thumbnail entries without filename
495     if ($thumbs->find()) {
496         while ($thumbs->fetch()) {
497             try {
498                 $thumbs->getPath();
499             } catch (FileNotFoundException $e) {
500                 $thumbs->delete();
501             }
502         }
503     }
504
505     printfnq("DONE.\n");
506 }
507
508 /*
509  * Files are now stored with their hash, so let's generate for previously uploaded files.
510  */
511 function setFilehashOnLocalFiles()
512 {
513     printfnq('Ensuring all local files have the filehash field set...');
514
515     $file = new File();
516     $file->whereAdd('filename IS NOT NULL');        // local files
517     $file->whereAdd('filehash IS NULL', 'AND');     // without filehash value
518
519     if ($file->find()) {
520         while ($file->fetch()) {
521             try {
522                 $orig = clone($file);
523                 $file->filehash = hash_file(File::FILEHASH_ALG, $file->getPath());
524                 $file->update($orig);
525             } catch (FileNotFoundException $e) {
526                 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";
527             }
528         }
529     }
530
531     printfnq("DONE.\n");
532 }
533
534 main();