]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - scripts/upgrade.php
Merge remote-tracking branch 'upstream/master' into nightly
[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->selectAdd();
175     $notice->selectAdd('DISTINCT conversation');
176     $notice->joinAdd(['conversation', 'conversation:id'], 'LEFT');  // LEFT to get the null values for conversation.id
177     $notice->whereAdd('conversation.id IS NULL');
178
179     if ($notice->find()) {
180         printfnq(" fixing {$notice->N} missing conversation entries...");
181     }
182
183     while ($notice->fetch()) {
184
185         $id = $notice->conversation;
186
187         $uri = common_local_url('conversation', array('id' => $id));
188
189         // @fixme db_dataobject won't save our value for an autoincrement
190         // so we're bypassing the insert wrappers
191         $conv = new Conversation();
192         $sql = "insert into conversation (id,uri,created) values(%d,'%s','%s')";
193         $sql = sprintf($sql,
194                        $id,
195                        $conv->escape($uri),
196                        $conv->escape(common_sql_now()));
197         $conv->query($sql);
198     }
199
200     printfnq("DONE.\n");
201 }
202
203 function fixupConversationURIs()
204 {
205     printfnq("Ensuring all conversations have a URI...");
206
207     $conv = new Conversation();
208     $conv->whereAdd('uri IS NULL');
209
210     if ($conv->find()) {
211         $rounds = 0;
212         while ($conv->fetch()) {
213             $uri = common_local_url('conversation', array('id' => $conv->id));
214             $sql = sprintf('UPDATE conversation SET uri="%1$s" WHERE id="%2$d";',
215                             $conv->escape($uri), $conv->id);
216             $conv->query($sql);
217             if (($conv->N-++$rounds) % 500 == 0) {
218                 printfnq(sprintf(' %d items left...', $conv->N-$rounds));
219             }
220         }
221     }
222
223     printfnq("DONE.\n");
224 }
225
226 function initGroupProfileId()
227 {
228     printfnq("Ensuring all User_group entries have a Profile and profile_id...");
229
230     $group = new User_group();
231     $group->whereAdd('NOT EXISTS (SELECT id FROM profile WHERE id = user_group.profile_id)');
232     $group->find();
233
234     while ($group->fetch()) {
235         try {
236             // We must create a new, incrementally assigned profile_id
237             $profile = new Profile();
238             $profile->nickname   = $group->nickname;
239             $profile->fullname   = $group->fullname;
240             $profile->profileurl = $group->mainpage;
241             $profile->homepage   = $group->homepage;
242             $profile->bio        = $group->description;
243             $profile->location   = $group->location;
244             $profile->created    = $group->created;
245             $profile->modified   = $group->modified;
246
247             $profile->query('BEGIN');
248             $id = $profile->insert();
249             if (empty($id)) {
250                 $profile->query('ROLLBACK');
251                 throw new Exception('Profile insertion failed, profileurl: '.$profile->profileurl);
252             }
253             $group->query("UPDATE user_group SET profile_id={$id} WHERE id={$group->id}");
254             $profile->query('COMMIT');
255
256             $profile->free();
257         } catch (Exception $e) {
258             printfv("Error initializing Profile for group {$group->nickname}:" . $e->getMessage());
259         }
260     }
261
262     printfnq("DONE.\n");
263 }
264
265 function initLocalGroup()
266 {
267     printfnq("Ensuring all local user groups have a local_group...");
268
269     $group = new User_group();
270     $group->whereAdd('NOT EXISTS (select group_id from local_group where group_id = user_group.id)');
271     $group->find();
272
273     while ($group->fetch()) {
274         try {
275             // Hack to check for local groups
276             if ($group->getUri() == common_local_url('groupbyid', array('id' => $group->id))) {
277                 $lg = new Local_group();
278
279                 $lg->group_id = $group->id;
280                 $lg->nickname = $group->nickname;
281                 $lg->created  = $group->created; // XXX: common_sql_now() ?
282                 $lg->modified = $group->modified;
283
284                 $lg->insert();
285             }
286         } catch (Exception $e) {
287             printfv("Error initializing local group for {$group->nickname}:" . $e->getMessage());
288         }
289     }
290
291     printfnq("DONE.\n");
292 }
293
294 function initNoticeReshare()
295 {
296     printfnq("Ensuring all reshares have the correct verb and object-type...");
297     
298     $notice = new Notice();
299     $notice->whereAdd('repeat_of is not null');
300     $notice->whereAdd('(verb != "'.ActivityVerb::SHARE.'" OR object_type != "'.ActivityObject::ACTIVITY.'")');
301
302     if ($notice->find()) {
303         while ($notice->fetch()) {
304             try {
305                 $orig = Notice::getKV('id', $notice->id);
306                 $notice->verb = ActivityVerb::SHARE;
307                 $notice->object_type = ActivityObject::ACTIVITY;
308                 $notice->update($orig);
309             } catch (Exception $e) {
310                 printfv("Error updating verb and object_type for {$notice->id}:" . $e->getMessage());
311             }
312         }
313     }
314
315     printfnq("DONE.\n");
316 }
317
318 function initSubscriptionURI()
319 {
320     printfnq("Ensuring all subscriptions have a URI...");
321
322     $sub = new Subscription();
323     $sub->whereAdd('uri IS NULL');
324
325     if ($sub->find()) {
326         while ($sub->fetch()) {
327             try {
328                 $sub->decache();
329                 $sub->query(sprintf('update subscription '.
330                                     'set uri = "%s" '.
331                                     'where subscriber = %d '.
332                                     'and subscribed = %d',
333                                     $sub->escape(Subscription::newUri($sub->getSubscriber(), $sub->getSubscribed(), $sub->created)),
334                                     $sub->subscriber,
335                                     $sub->subscribed));
336             } catch (Exception $e) {
337                 common_log(LOG_ERR, "Error updated subscription URI: " . $e->getMessage());
338             }
339         }
340     }
341
342     printfnq("DONE.\n");
343 }
344
345 function initGroupMemberURI()
346 {
347     printfnq("Ensuring all group memberships have a URI...");
348
349     $mem = new Group_member();
350     $mem->whereAdd('uri IS NULL');
351
352     if ($mem->find()) {
353         while ($mem->fetch()) {
354             try {
355                 $mem->decache();
356                 $mem->query(sprintf('update group_member set uri = "%s" '.
357                                     'where profile_id = %d ' . 
358                                     'and group_id = %d ',
359                                     Group_member::newUri(Profile::getByID($mem->profile_id), User_group::getByID($mem->group_id), $mem->created),
360                                     $mem->profile_id,
361                                     $mem->group_id));
362             } catch (Exception $e) {
363                 common_log(LOG_ERR, "Error updated membership URI: " . $e->getMessage());  
364           }
365         }
366     }
367
368     printfnq("DONE.\n");
369 }
370
371 function initProfileLists()
372 {
373     printfnq("Ensuring all profile tags have a corresponding list...");
374
375     $ptag = new Profile_tag();
376     $ptag->selectAdd();
377     $ptag->selectAdd('tagger, tag, count(*) as tagged_count');
378     $ptag->whereAdd('NOT EXISTS (SELECT tagger, tagged from profile_list '.
379                     'where profile_tag.tagger = profile_list.tagger '.
380                     'and profile_tag.tag = profile_list.tag)');
381     $ptag->groupBy('tagger, tag');
382     $ptag->orderBy('tagger, tag');
383
384     if ($ptag->find()) {
385         while ($ptag->fetch()) {
386             $plist = new Profile_list();
387
388             $plist->tagger   = $ptag->tagger;
389             $plist->tag      = $ptag->tag;
390             $plist->private  = 0;
391             $plist->created  = common_sql_now();
392             $plist->modified = $plist->created;
393             $plist->mainpage = common_local_url('showprofiletag',
394                                                 array('tagger' => $plist->getTagger()->nickname,
395                                                       'tag'    => $plist->tag));;
396
397             $plist->tagged_count     = $ptag->tagged_count;
398             $plist->subscriber_count = 0;
399
400             $plist->insert();
401
402             $orig = clone($plist);
403             // After insert since it uses auto-generated ID
404             $plist->uri      = common_local_url('profiletagbyid',
405                                         array('id' => $plist->id, 'tagger_id' => $plist->tagger));
406
407             $plist->update($orig);
408         }
409     }
410
411     printfnq("DONE.\n");
412 }
413
414 /*
415  * Added as we now store interpretd width and height in File table.
416  */
417 function fixupFileGeometry()
418 {
419     printfnq("Ensuring width and height is set for supported local File objects...");
420
421     $file = new File();
422     $file->whereAdd('filename IS NOT NULL');    // local files
423     $file->whereAdd('width IS NULL OR width = 0');
424
425     if ($file->find()) {
426         while ($file->fetch()) {
427             // Set file geometrical properties if available
428             try {
429                 $image = ImageFile::fromFileObject($file);
430             } catch (ServerException $e) {
431                 // We couldn't make out an image from the file.
432                 continue;
433             }
434             $orig = clone($file);
435             $file->width = $image->width;
436             $file->height = $image->height;
437             $file->update($orig);
438
439             // FIXME: Do this more automagically inside ImageFile or so.
440             if ($image->getPath() != $file->getPath()) {
441                 $image->unlink();
442             }
443             unset($image);
444         }
445     }
446
447     printfnq("DONE.\n");
448 }
449
450 /*
451  * File_thumbnail objects for local Files store their own filenames in the database.
452  */
453 function deleteLocalFileThumbnailsWithoutFilename()
454 {
455     printfnq("Removing all local File_thumbnail entries without filename property...");
456
457     $file = new File();
458     $file->whereAdd('filename IS NOT NULL');    // local files
459
460     if ($file->find()) {
461         // Looping through local File entries
462         while ($file->fetch()) {
463             $thumbs = new File_thumbnail();
464             $thumbs->file_id = $file->id;
465             $thumbs->whereAdd('filename IS NULL OR filename = ""');
466             // Checking if there were any File_thumbnail entries without filename
467             if (!$thumbs->find()) {
468                 continue;
469             }
470             // deleting incomplete entry to allow regeneration
471             while ($thumbs->fetch()) {
472                 $thumbs->delete();
473             }
474         }
475     }
476
477     printfnq("DONE.\n");
478 }
479
480 /*
481  * Delete File_thumbnail entries where the referenced file does not exist.
482  */
483 function deleteMissingLocalFileThumbnails()
484 {
485     printfnq("Removing all local File_thumbnail entries without existing files...");
486
487     $thumbs = new File_thumbnail();
488     $thumbs->whereAdd('filename IS NOT NULL AND filename != ""');
489     // Checking if there were any File_thumbnail entries without filename
490     if ($thumbs->find()) {
491         while ($thumbs->fetch()) {
492             try {
493                 $thumbs->getPath();
494             } catch (FileNotFoundException $e) {
495                 $thumbs->delete();
496             }
497         }
498     }
499
500     printfnq("DONE.\n");
501 }
502
503 /*
504  * Files are now stored with their hash, so let's generate for previously uploaded files.
505  */
506 function setFilehashOnLocalFiles()
507 {
508     printfnq('Ensuring all local files have the filehash field set...');
509
510     $file = new File();
511     $file->whereAdd('filename IS NOT NULL AND filename != ""');        // local files
512     $file->whereAdd('filehash IS NULL', 'AND');     // without filehash value
513
514     if ($file->find()) {
515         while ($file->fetch()) {
516             try {
517                 $orig = clone($file);
518                 $file->filehash = hash_file(File::FILEHASH_ALG, $file->getPath());
519                 $file->update($orig);
520             } catch (FileNotFoundException $e) {
521                 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";
522             }
523         }
524     }
525
526     printfnq("DONE.\n");
527 }
528
529 function fixupFileThumbnailUrlhash()
530 {
531     printfnq("Setting urlhash for File_thumbnail entries: ");
532
533     $thumb = new File_thumbnail();
534     $thumb->query('UPDATE '.$thumb->escapedTableName().' SET urlhash=SHA2(url, 256) WHERE'.
535                     ' url IS NOT NULL AND'. // find all entries with a url value
536                     ' url != "" AND'.       // precaution against non-null empty strings
537                     ' urlhash IS NULL');    // but don't touch those we've already calculated
538
539     printfnq("DONE.\n");
540 }
541
542 function migrateProfilePrefs()
543 {
544     printfnq("Finding and possibly migrating Profile_prefs entries: ");
545
546     $prefs = array();   // ['qvitter' => ['cover_photo'=>'profile_banner_url', ...], ...]
547     Event::handle('GetProfilePrefsMigrations', array(&$prefs));
548
549     foreach($prefs as $namespace=>$mods) {
550         echo "$namespace... ";
551         assert(is_array($mods));
552         $p = new Profile_prefs();
553         $p->namespace = $namespace;
554         // find all entries in all modified topics given in this namespace
555         $p->whereAddIn('topic', array_keys($mods), $p->columnType('topic'));
556         $p->find();
557         while ($p->fetch()) {
558             // for each entry, update 'topic' to the new key value
559             $orig = clone($p);
560             $p->topic = $mods[$p->topic];
561             $p->updateWithKeys($orig);
562         }
563     }
564
565     printfnq("DONE.\n");
566 }
567
568 main();