]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - scripts/upgrade.php
Merge branch 'master' of git.gnu.io:gnu/gnu-social into mmn_fixes
[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 = 'dfx::';
24 $longoptions = array('debug', 'files', '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
35 if (!defined('DEBUG')) {
36     define('DEBUG', (bool)have_option('d', 'debug'));
37 }
38
39 function main()
40 {
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');
44
45     if (Event::handle('StartUpgrade')) {
46         fixupConversationURIs();
47
48         updateSchemaCore();
49         updateSchemaPlugins();
50
51         // These replace old "fixup_*" scripts
52
53         fixupNoticeConversation();
54         initConversation();
55         fixupGroupURI();
56         if ($iterate_files) {
57             printfnq("Running file iterations:\n");
58             printfnq("* "); fixupFileGeometry();
59             printfnq("* "); deleteLocalFileThumbnailsWithoutFilename();
60             printfnq("* "); deleteMissingLocalFileThumbnails();
61             printfnq("* "); fixupFileThumbnailUrlhash();
62             printfnq("* "); setFilehashOnLocalFiles();
63             printfnq("DONE.\n");
64         } else {
65             printfnq("Skipping intensive/long-running file iteration functions (enable with -f, should be done at least once!)\n");
66         }
67
68         initGroupProfileId();
69         initLocalGroup();
70         initNoticeReshare();
71     
72         initSubscriptionURI();
73         initGroupMemberURI();
74
75         initProfileLists();
76
77         migrateProfilePrefs();
78
79         Event::handle('EndUpgrade');
80     }
81 }
82
83 function tableDefs()
84 {
85         $schema = array();
86         require INSTALLDIR.'/db/core.php';
87         return $schema;
88 }
89
90 function updateSchemaCore()
91 {
92     printfnq("Upgrading core schema...");
93
94     $schema = Schema::get();
95     $schemaUpdater = new SchemaUpdater($schema);
96     foreach (tableDefs() as $table => $def) {
97         $schemaUpdater->register($table, $def);
98     }
99     $schemaUpdater->checkSchema();
100
101     printfnq("DONE.\n");
102 }
103
104 function updateSchemaPlugins()
105 {
106     printfnq("Upgrading plugin schema...");
107
108     Event::handle('BeforePluginCheckSchema');
109     Event::handle('CheckSchema');
110
111     printfnq("DONE.\n");
112 }
113
114 function fixupNoticeConversation()
115 {
116     printfnq("Ensuring all notices have a conversation ID...");
117
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
122     $notice->find();
123
124     while ($notice->fetch()) {
125         try {
126             $cid = null;
127     
128             $orig = clone($notice);
129     
130             if (!empty($notice->reply_to)) {
131                 $reply = Notice::getKV('id', $notice->reply_to);
132
133                 if ($reply instanceof Notice && !empty($reply->conversation)) {
134                     $notice->conversation = $reply->conversation;
135                 }
136                 unset($reply);
137             }
138
139             // if still empty
140             if (empty($notice->conversation)) {
141                 $child = new Notice();
142                 $child->reply_to = $notice->getID();
143                 $child->limit(1);
144                 if ($child->find(true) && !empty($child->conversation)) {
145                     $notice->conversation = $child->conversation;
146                 }
147                 unset($child);
148             }
149
150             // if _still_ empty we just create our own conversation
151             if (empty($notice->conversation)) {
152                 $notice->conversation = $notice->getID();
153             }
154
155             $result = $notice->update($orig);
156
157             unset($orig);
158         } catch (Exception $e) {
159             print("Error setting conversation: " . $e->getMessage());
160         }
161     }
162
163     printfnq("DONE.\n");
164 }
165
166 function fixupGroupURI()
167 {
168     printfnq("Ensuring all groups have an URI...");
169
170     $group = new User_group();
171     $group->whereAdd('uri IS NULL');
172
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);
178         }
179     }
180
181     printfnq("DONE.\n");
182 }
183
184 function initConversation()
185 {
186     if (common_config('fix', 'upgrade_initConversation') <= 1) {
187         printfnq(sprintf("Skipping %s, fixed by previous upgrade.\n", __METHOD__));
188         return;
189     }
190
191     printfnq("Ensuring all conversations have a row in conversation table...");
192
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');
198
199     if ($notice->find()) {
200         printfnq(" fixing {$notice->N} missing conversation entries...");
201     }
202
203     while ($notice->fetch()) {
204
205         $id = $notice->conversation;
206
207         $uri = common_local_url('conversation', array('id' => $id));
208
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')";
213         $sql = sprintf($sql,
214                        $id,
215                        $conv->escape($uri),
216                        $conv->escape(common_sql_now()));
217         $conv->query($sql);
218     }
219
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);
223
224     printfnq("DONE.\n");
225 }
226
227 function fixupConversationURIs()
228 {
229     printfnq("Ensuring all conversations have a URI...");
230
231     $conv = new Conversation();
232     $conv->whereAdd('uri IS NULL');
233
234     if ($conv->find()) {
235         $rounds = 0;
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);
240             $conv->query($sql);
241             if (($conv->N-++$rounds) % 500 == 0) {
242                 printfnq(sprintf(' %d items left...', $conv->N-$rounds));
243             }
244         }
245     }
246
247     printfnq("DONE.\n");
248 }
249
250 function initGroupProfileId()
251 {
252     printfnq("Ensuring all User_group entries have a Profile and profile_id...");
253
254     $group = new User_group();
255     $group->whereAdd('NOT EXISTS (SELECT id FROM profile WHERE id = user_group.profile_id)');
256     $group->find();
257
258     while ($group->fetch()) {
259         try {
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;
270
271             $profile->query('BEGIN');
272             $id = $profile->insert();
273             if (empty($id)) {
274                 $profile->query('ROLLBACK');
275                 throw new Exception('Profile insertion failed, profileurl: '.$profile->profileurl);
276             }
277             $group->query("UPDATE user_group SET profile_id={$id} WHERE id={$group->id}");
278             $profile->query('COMMIT');
279
280             $profile->free();
281         } catch (Exception $e) {
282             printfv("Error initializing Profile for group {$group->nickname}:" . $e->getMessage());
283         }
284     }
285
286     printfnq("DONE.\n");
287 }
288
289 function initLocalGroup()
290 {
291     printfnq("Ensuring all local user groups have a local_group...");
292
293     $group = new User_group();
294     $group->whereAdd('NOT EXISTS (select group_id from local_group where group_id = user_group.id)');
295     $group->find();
296
297     while ($group->fetch()) {
298         try {
299             // Hack to check for local groups
300             if ($group->getUri() == common_local_url('groupbyid', array('id' => $group->id))) {
301                 $lg = new Local_group();
302
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;
307
308                 $lg->insert();
309             }
310         } catch (Exception $e) {
311             printfv("Error initializing local group for {$group->nickname}:" . $e->getMessage());
312         }
313     }
314
315     printfnq("DONE.\n");
316 }
317
318 function initNoticeReshare()
319 {
320     if (common_config('fix', 'upgrade_initNoticeReshare') <= 1) {
321         printfnq(sprintf("Skipping %s, fixed by previous upgrade.\n", __METHOD__));
322         return;
323     }
324
325     printfnq("Ensuring all reshares have the correct verb and object-type...");
326     
327     $notice = new Notice();
328     $notice->whereAdd('repeat_of is not null');
329     $notice->whereAdd('(verb != "'.ActivityVerb::SHARE.'" OR object_type != "'.ActivityObject::ACTIVITY.'")');
330
331     if ($notice->find()) {
332         while ($notice->fetch()) {
333             try {
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());
340             }
341         }
342     }
343
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);
347
348     printfnq("DONE.\n");
349 }
350
351 function initSubscriptionURI()
352 {
353     printfnq("Ensuring all subscriptions have a URI...");
354
355     $sub = new Subscription();
356     $sub->whereAdd('uri IS NULL');
357
358     if ($sub->find()) {
359         while ($sub->fetch()) {
360             try {
361                 $sub->decache();
362                 $sub->query(sprintf('update subscription '.
363                                     'set uri = "%s" '.
364                                     'where subscriber = %d '.
365                                     'and subscribed = %d',
366                                     $sub->escape(Subscription::newUri($sub->getSubscriber(), $sub->getSubscribed(), $sub->created)),
367                                     $sub->subscriber,
368                                     $sub->subscribed));
369             } catch (Exception $e) {
370                 common_log(LOG_ERR, "Error updated subscription URI: " . $e->getMessage());
371             }
372         }
373     }
374
375     printfnq("DONE.\n");
376 }
377
378 function initGroupMemberURI()
379 {
380     printfnq("Ensuring all group memberships have a URI...");
381
382     $mem = new Group_member();
383     $mem->whereAdd('uri IS NULL');
384
385     if ($mem->find()) {
386         while ($mem->fetch()) {
387             try {
388                 $mem->decache();
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),
393                                     $mem->profile_id,
394                                     $mem->group_id));
395             } catch (Exception $e) {
396                 common_log(LOG_ERR, "Error updated membership URI: " . $e->getMessage());  
397           }
398         }
399     }
400
401     printfnq("DONE.\n");
402 }
403
404 function initProfileLists()
405 {
406     printfnq("Ensuring all profile tags have a corresponding list...");
407
408     $ptag = new Profile_tag();
409     $ptag->selectAdd();
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');
416
417     if ($ptag->find()) {
418         while ($ptag->fetch()) {
419             $plist = new Profile_list();
420
421             $plist->tagger   = $ptag->tagger;
422             $plist->tag      = $ptag->tag;
423             $plist->private  = 0;
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));;
429
430             $plist->tagged_count     = $ptag->tagged_count;
431             $plist->subscriber_count = 0;
432
433             $plist->insert();
434
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));
439
440             $plist->update($orig);
441         }
442     }
443
444     printfnq("DONE.\n");
445 }
446
447 /*
448  * Added as we now store interpretd width and height in File table.
449  */
450 function fixupFileGeometry()
451 {
452     printfnq("Ensuring width and height is set for supported local File objects...");
453
454     $file = new File();
455     $file->whereAdd('filename IS NOT NULL');    // local files
456     $file->whereAdd('width IS NULL OR width = 0');
457
458     if ($file->find()) {
459         while ($file->fetch()) {
460             if (DEBUG) printfnq(sprintf('Found file without width: %s\n', _ve($file->getFilename())));
461
462             // Set file geometrical properties if available
463             try {
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'));
468                 continue;
469             }
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);
475
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'));
479                 $image->unlink();
480             }
481             unset($image);
482         }
483     }
484
485     printfnq("DONE.\n");
486 }
487
488 /*
489  * File_thumbnail objects for local Files store their own filenames in the database.
490  */
491 function deleteLocalFileThumbnailsWithoutFilename()
492 {
493     printfnq("Removing all local File_thumbnail entries without filename property...");
494
495     $file = new File();
496     $file->whereAdd('filename IS NOT NULL');    // local files
497
498     if ($file->find()) {
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()) {
506                 continue;
507             }
508             // deleting incomplete entry to allow regeneration
509             while ($thumbs->fetch()) {
510                 $thumbs->delete();
511             }
512         }
513     }
514
515     printfnq("DONE.\n");
516 }
517
518 /*
519  * Delete File_thumbnail entries where the referenced file does not exist.
520  */
521 function deleteMissingLocalFileThumbnails()
522 {
523     printfnq("Removing all local File_thumbnail entries without existing files...");
524
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()) {
530             try {
531                 $thumbs->getPath();
532             } catch (FileNotFoundException $e) {
533                 $thumbs->delete();
534             }
535         }
536     }
537
538     printfnq("DONE.\n");
539 }
540
541 /*
542  * Files are now stored with their hash, so let's generate for previously uploaded files.
543  */
544 function setFilehashOnLocalFiles()
545 {
546     printfnq('Ensuring all local files have the filehash field set...');
547
548     $file = new File();
549     $file->whereAdd('filename IS NOT NULL AND filename != ""');        // local files
550     $file->whereAdd('filehash IS NULL', 'AND');     // without filehash value
551
552     if ($file->find()) {
553         while ($file->fetch()) {
554             try {
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";
560             }
561         }
562     }
563
564     printfnq("DONE.\n");
565 }
566
567 function fixupFileThumbnailUrlhash()
568 {
569     printfnq("Setting urlhash for File_thumbnail entries: ");
570
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
576
577     printfnq("DONE.\n");
578 }
579
580 function migrateProfilePrefs()
581 {
582     printfnq("Finding and possibly migrating Profile_prefs entries: ");
583
584     $prefs = array();   // ['qvitter' => ['cover_photo'=>'profile_banner_url', ...], ...]
585     Event::handle('GetProfilePrefsMigrations', array(&$prefs));
586
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'));
594         $p->find();
595         while ($p->fetch()) {
596             // for each entry, update 'topic' to the new key value
597             $orig = clone($p);
598             $p->topic = $mods[$p->topic];
599             $p->updateWithKeys($orig);
600         }
601     }
602
603     printfnq("DONE.\n");
604 }
605
606 main();