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