]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - scripts/upgrade.php
Make attachment fit better in notice: drop text and link
[quix0rs-gnu-social.git] / scripts / upgrade.php
1 #!/usr/bin/env php
2 <?php
3 // This file is part of GNU social - https://www.gnu.org/software/social
4 //
5 // GNU social is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU Affero General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // GNU social is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU Affero General Public License for more details.
14 //
15 // You should have received a copy of the GNU Affero General Public License
16 // along with GNU social.  If not, see <http://www.gnu.org/licenses/>.
17
18 /**
19  * Upgrade database schema and data to latest software and check DB integrity
20  * Usage: php upgrade.php [options]
21  *
22  * @package   GNUsocial
23  * @author    Bhuvan Krishna <bhuvan@swecha.net>
24  * @author    Evan Prodromou <evan@status.net>
25  * @author    Mikael Nordfeldth <mmn@hethane.se>
26  * @copyright 2010-2019 Free Software Foundation, Inc http://www.fsf.org
27  * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
28  */
29
30 define('INSTALLDIR', dirname(__DIR__));
31 define('PUBLICDIR', INSTALLDIR . DIRECTORY_SEPARATOR . 'public');
32
33 $shortoptions = 'dfx::';
34 $longoptions = ['debug', 'files', 'extensions='];
35
36 $helptext = <<<END_OF_UPGRADE_HELP
37 php upgrade.php [options]
38 Upgrade database schema and data to latest software
39
40 END_OF_UPGRADE_HELP;
41
42 require_once INSTALLDIR.'/scripts/commandline.inc';
43
44
45 if (!defined('DEBUG')) {
46     define('DEBUG', (bool)have_option('d', 'debug'));
47 }
48
49 function main()
50 {
51     // "files" option enables possibly disk/resource intensive operations
52     // that aren't really _required_ for the upgrade
53     $iterate_files = (bool)have_option('f', 'files');
54
55     if (Event::handle('StartUpgrade')) {
56         fixupConversationURIs();
57
58         updateSchemaCore();
59         updateSchemaPlugins();
60
61         // These replace old "fixup_*" scripts
62
63         fixupNoticeConversation();
64         initConversation();
65         fixupGroupURI();
66         if ($iterate_files) {
67             printfnq("Running file iterations:\n");
68             printfnq("* "); fixupFileGeometry();
69             printfnq("* "); deleteLocalFileThumbnailsWithoutFilename();
70             printfnq("* "); deleteMissingLocalFileThumbnails();
71             printfnq("* "); fixupFileThumbnailUrlhash();
72             printfnq("* "); setFilehashOnLocalFiles();
73             printfnq("DONE.\n");
74         } else {
75             printfnq("Skipping intensive/long-running file iteration functions (enable with -f, should be done at least once!)\n");
76         }
77
78         initGroupProfileId();
79         initLocalGroup();
80         initNoticeReshare();
81
82         initSubscriptionURI();
83         initGroupMemberURI();
84
85         initProfileLists();
86
87         migrateProfilePrefs();
88
89         Event::handle('EndUpgrade');
90     }
91 }
92
93 function tableDefs()
94 {
95     $schema = [];
96     require INSTALLDIR . '/db/core.php';
97     return $schema;
98 }
99
100 function updateSchemaCore()
101 {
102     printfnq("Upgrading core schema...");
103
104     $schema = Schema::get();
105     $schemaUpdater = new SchemaUpdater($schema);
106     foreach (tableDefs() as $table => $def) {
107         $schemaUpdater->register($table, $def);
108     }
109     $schemaUpdater->checkSchema();
110
111     printfnq("DONE.\n");
112 }
113
114 function updateSchemaPlugins()
115 {
116     printfnq("Upgrading plugin schema...");
117
118     Event::handle('BeforePluginCheckSchema');
119     Event::handle('CheckSchema');
120
121     printfnq("DONE.\n");
122 }
123
124 function fixupNoticeConversation()
125 {
126     printfnq("Ensuring all notices have a conversation ID...");
127
128     $notice = new Notice();
129     $notice->whereAdd('conversation is null');
130     $notice->whereAdd('conversation = 0', 'OR');
131     $notice->orderBy('id'); // try to get originals before replies
132     $notice->find();
133
134     while ($notice->fetch()) {
135         try {
136             $cid = null;
137
138             $orig = clone($notice);
139
140             if (!empty($notice->reply_to)) {
141                 $reply = Notice::getKV('id', $notice->reply_to);
142
143                 if ($reply instanceof Notice && !empty($reply->conversation)) {
144                     $notice->conversation = $reply->conversation;
145                 }
146                 unset($reply);
147             }
148
149             // if still empty
150             if (empty($notice->conversation)) {
151                 $child = new Notice();
152                 $child->reply_to = $notice->getID();
153                 $child->limit(1);
154                 if ($child->find(true) && !empty($child->conversation)) {
155                     $notice->conversation = $child->conversation;
156                 }
157                 unset($child);
158             }
159
160             // if _still_ empty we just create our own conversation
161             if (empty($notice->conversation)) {
162                 $notice->conversation = $notice->getID();
163             }
164
165             $result = $notice->update($orig);
166
167             unset($orig);
168         } catch (Exception $e) {
169             print("Error setting conversation: " . $e->getMessage());
170         }
171     }
172
173     printfnq("DONE.\n");
174 }
175
176 function fixupGroupURI()
177 {
178     printfnq("Ensuring all groups have an URI...");
179
180     $group = new User_group();
181     $group->whereAdd('uri IS NULL');
182
183     if ($group->find()) {
184         while ($group->fetch()) {
185             $orig = User_group::getKV('id', $group->id);
186             $group->uri = $group->getUri();
187             $group->update($orig);
188         }
189     }
190
191     printfnq("DONE.\n");
192 }
193
194 function initConversation()
195 {
196     if (common_config('fix', 'upgrade_initConversation') <= 1) {
197         printfnq(sprintf("Skipping %s, fixed by previous upgrade.\n", __METHOD__));
198         return;
199     }
200
201     printfnq("Ensuring all conversations have a row in conversation table...");
202
203     $notice = new Notice();
204     $notice->selectAdd();
205     $notice->selectAdd('DISTINCT conversation');
206     $notice->joinAdd(['conversation', 'conversation:id'], 'LEFT');  // LEFT to get the null values for conversation.id
207     $notice->whereAdd('conversation.id IS NULL');
208
209     if ($notice->find()) {
210         printfnq(" fixing {$notice->N} missing conversation entries...");
211     }
212
213     while ($notice->fetch()) {
214         $id = $notice->conversation;
215
216         $uri = common_local_url('conversation', ['id' => $id]);
217
218         // @fixme db_dataobject won't save our value for an autoincrement
219         // so we're bypassing the insert wrappers
220         $conv = new Conversation();
221         $sql = "INSERT INTO conversation (id,uri,created) VALUES (%d,'%s','%s')";
222         $sql = sprintf(
223             $sql,
224             $id,
225             $conv->escape($uri),
226             $conv->escape(common_sql_now())
227         );
228         $conv->query($sql);
229     }
230
231     // This is something we should only have to do once unless introducing new, bad code.
232     if (DEBUG) {
233         printfnq(sprintf('Storing in config that we have done %s', __METHOD__));
234     }
235     common_config_set('fix', 'upgrade_initConversation', 1);
236
237     printfnq("DONE.\n");
238 }
239
240 function fixupConversationURIs()
241 {
242     printfnq("Ensuring all conversations have a URI...");
243
244     $conv = new Conversation();
245     $conv->whereAdd('uri IS NULL');
246
247     if ($conv->find()) {
248         $rounds = 0;
249         while ($conv->fetch()) {
250             $uri = common_local_url('conversation', ['id' => $conv->id]);
251             $sql = sprintf(
252                 'UPDATE conversation SET uri="%1$s" WHERE id="%2$d";',
253                 $conv->escape($uri),
254                 $conv->id
255             );
256             $conv->query($sql);
257             if (($conv->N-++$rounds) % 500 == 0) {
258                 printfnq(sprintf(' %d items left...', $conv->N-$rounds));
259             }
260         }
261     }
262
263     printfnq("DONE.\n");
264 }
265
266 function initGroupProfileId()
267 {
268     printfnq("Ensuring all User_group entries have a Profile and profile_id...");
269
270     $group = new User_group();
271     $group->whereAdd('NOT EXISTS (SELECT id FROM profile WHERE id = user_group.profile_id)');
272     $group->find();
273
274     while ($group->fetch()) {
275         try {
276             // We must create a new, incrementally assigned profile_id
277             $profile = new Profile();
278             $profile->nickname   = $group->nickname;
279             $profile->fullname   = $group->fullname;
280             $profile->profileurl = $group->mainpage;
281             $profile->homepage   = $group->homepage;
282             $profile->bio        = $group->description;
283             $profile->location   = $group->location;
284             $profile->created    = $group->created;
285             $profile->modified   = $group->modified;
286
287             $profile->query('BEGIN');
288             $id = $profile->insert();
289             if (empty($id)) {
290                 $profile->query('ROLLBACK');
291                 throw new Exception('Profile insertion failed, profileurl: '.$profile->profileurl);
292             }
293             $group->query("UPDATE user_group SET profile_id={$id} WHERE id={$group->id}");
294             $profile->query('COMMIT');
295
296             $profile->free();
297         } catch (Exception $e) {
298             printfv("Error initializing Profile for group {$group->nickname}:" . $e->getMessage());
299         }
300     }
301
302     printfnq("DONE.\n");
303 }
304
305 function initLocalGroup()
306 {
307     printfnq("Ensuring all local user groups have a local_group...");
308
309     $group = new User_group();
310     $group->whereAdd('NOT EXISTS (select group_id from local_group where group_id = user_group.id)');
311     $group->find();
312
313     while ($group->fetch()) {
314         try {
315             // Hack to check for local groups
316             if ($group->getUri() == common_local_url('groupbyid', ['id' => $group->id])) {
317                 $lg = new Local_group();
318
319                 $lg->group_id = $group->id;
320                 $lg->nickname = $group->nickname;
321                 $lg->created  = $group->created; // XXX: common_sql_now() ?
322                 $lg->modified = $group->modified;
323
324                 $lg->insert();
325             }
326         } catch (Exception $e) {
327             printfv("Error initializing local group for {$group->nickname}:" . $e->getMessage());
328         }
329     }
330
331     printfnq("DONE.\n");
332 }
333
334 function initNoticeReshare()
335 {
336     if (common_config('fix', 'upgrade_initNoticeReshare') <= 1) {
337         printfnq(sprintf("Skipping %s, fixed by previous upgrade.\n", __METHOD__));
338         return;
339     }
340
341     printfnq("Ensuring all reshares have the correct verb and object-type...");
342
343     $notice = new Notice();
344     $notice->whereAdd('repeat_of is not null');
345     $notice->whereAdd('(verb != "'.ActivityVerb::SHARE.'" OR object_type != "'.ActivityObject::ACTIVITY.'")');
346
347     if ($notice->find()) {
348         while ($notice->fetch()) {
349             try {
350                 $orig = Notice::getKV('id', $notice->id);
351                 $notice->verb = ActivityVerb::SHARE;
352                 $notice->object_type = ActivityObject::ACTIVITY;
353                 $notice->update($orig);
354             } catch (Exception $e) {
355                 printfv("Error updating verb and object_type for {$notice->id}:" . $e->getMessage());
356             }
357         }
358     }
359
360     // This is something we should only have to do once unless introducing new, bad code.
361     if (DEBUG) {
362         printfnq(sprintf('Storing in config that we have done %s', __METHOD__));
363     }
364     common_config_set('fix', 'upgrade_initNoticeReshare', 1);
365
366     printfnq("DONE.\n");
367 }
368
369 function initSubscriptionURI()
370 {
371     printfnq("Ensuring all subscriptions have a URI...");
372
373     $sub = new Subscription();
374     $sub->whereAdd('uri IS NULL');
375
376     if ($sub->find()) {
377         while ($sub->fetch()) {
378             try {
379                 $sub->decache();
380                 $sub->query(sprintf(
381                     'UPDATE subscription '.
382                     'SET uri = "%s" '.
383                     'WHERE subscriber = %d '.
384                       'AND subscribed = %d',
385                     $sub->escape(Subscription::newUri($sub->getSubscriber(), $sub->getSubscribed(), $sub->created)),
386                     $sub->subscriber,
387                     $sub->subscribed
388                 ));
389             } catch (Exception $e) {
390                 common_log(LOG_ERR, "Error updated subscription URI: " . $e->getMessage());
391             }
392         }
393     }
394
395     printfnq("DONE.\n");
396 }
397
398 function initGroupMemberURI()
399 {
400     printfnq("Ensuring all group memberships have a URI...");
401
402     $mem = new Group_member();
403     $mem->whereAdd('uri IS NULL');
404
405     if ($mem->find()) {
406         while ($mem->fetch()) {
407             try {
408                 $mem->decache();
409                 $mem->query(sprintf(
410                     'UPDATE group_member '.
411                     'SET uri = "%s" '.
412                     'WHERE profile_id = %d ' .
413                       'AND group_id = %d',
414                     Group_member::newUri(Profile::getByID($mem->profile_id), User_group::getByID($mem->group_id), $mem->created),
415                     $mem->profile_id,
416                     $mem->group_id
417                 ));
418             } catch (Exception $e) {
419                 common_log(LOG_ERR, "Error updated membership URI: " . $e->getMessage());
420             }
421         }
422     }
423
424     printfnq("DONE.\n");
425 }
426
427 function initProfileLists()
428 {
429     printfnq("Ensuring all profile tags have a corresponding list...");
430
431     $ptag = new Profile_tag();
432     $ptag->selectAdd();
433     $ptag->selectAdd('tagger, tag, COUNT(*) AS tagged_count');
434     $ptag->whereAdd('NOT EXISTS (SELECT tagger, tagged FROM profile_list '.
435                     'WHERE profile_tag.tagger = profile_list.tagger '.
436                     'AND profile_tag.tag = profile_list.tag)');
437     $ptag->groupBy('tagger, tag');
438     $ptag->orderBy('tagger, tag');
439
440     if ($ptag->find()) {
441         while ($ptag->fetch()) {
442             $plist = new Profile_list();
443
444             $plist->tagger   = $ptag->tagger;
445             $plist->tag      = $ptag->tag;
446             $plist->private  = 0;
447             $plist->created  = common_sql_now();
448             $plist->modified = $plist->created;
449             $plist->mainpage = common_local_url(
450                 'showprofiletag',
451                 ['tagger' => $plist->getTagger()->nickname,
452                  'tag'    => $plist->tag]
453             );
454             ;
455
456             $plist->tagged_count     = $ptag->tagged_count;
457             $plist->subscriber_count = 0;
458
459             $plist->insert();
460
461             $orig = clone($plist);
462             // After insert since it uses auto-generated ID
463             $plist->uri = common_local_url(
464                 'profiletagbyid',
465                 ['id'        => $plist->id,
466                  'tagger_id' => $plist->tagger]
467             );
468
469             $plist->update($orig);
470         }
471     }
472
473     printfnq("DONE.\n");
474 }
475
476 /*
477  * Added as we now store interpretd width and height in File table.
478  */
479 function fixupFileGeometry()
480 {
481     printfnq("Ensuring width and height is set for supported local File objects...");
482
483     $file = new File();
484     $file->whereAdd('filename IS NOT NULL');    // local files
485     $file->whereAdd('width IS NULL OR width = 0');
486
487     if ($file->find()) {
488         while ($file->fetch()) {
489             if (DEBUG) {
490                 printfnq(sprintf('Found file without width: %s\n', _ve($file->getFilename())));
491             }
492
493             // Set file geometrical properties if available
494             try {
495                 $image = ImageFile::fromFileObject($file);
496             } catch (ServerException $e) {
497                 // We couldn't make out an image from the file.
498                 if (DEBUG) {
499                     printfnq(sprintf('Could not make an image out of the file.\n'));
500                 }
501                 continue;
502             }
503             $orig = clone($file);
504             $file->width = $image->width;
505             $file->height = $image->height;
506             if (DEBUG) {
507                 printfnq(sprintf('Setting image file and with to %sx%s.\n', $file->width, $file->height));
508             }
509             $file->update($orig);
510
511             // FIXME: Do this more automagically inside ImageFile or so.
512             if ($image->getPath() != $file->getPath()) {
513                 if (DEBUG) {
514                     printfnq(sprintf('Deleting the temporarily stored ImageFile.\n'));
515                 }
516                 $image->unlink();
517             }
518             unset($image);
519         }
520     }
521
522     printfnq("DONE.\n");
523 }
524
525 /*
526  * File_thumbnail objects for local Files store their own filenames in the database.
527  */
528 function deleteLocalFileThumbnailsWithoutFilename()
529 {
530     printfnq("Removing all local File_thumbnail entries without filename property...");
531
532     $file = new File();
533     $file->whereAdd('filename IS NOT NULL');    // local files
534
535     if ($file->find()) {
536         // Looping through local File entries
537         while ($file->fetch()) {
538             $thumbs = new File_thumbnail();
539             $thumbs->file_id = $file->id;
540             $thumbs->whereAdd('filename IS NULL OR filename = ""');
541             // Checking if there were any File_thumbnail entries without filename
542             if (!$thumbs->find()) {
543                 continue;
544             }
545             // deleting incomplete entry to allow regeneration
546             while ($thumbs->fetch()) {
547                 $thumbs->delete();
548             }
549         }
550     }
551
552     printfnq("DONE.\n");
553 }
554
555 /*
556  * Delete File_thumbnail entries where the referenced file does not exist.
557  */
558 function deleteMissingLocalFileThumbnails()
559 {
560     printfnq("Removing all local File_thumbnail entries without existing files...");
561
562     $thumbs = new File_thumbnail();
563     $thumbs->whereAdd('filename IS NOT NULL AND filename != ""');
564     // Checking if there were any File_thumbnail entries without filename
565     if ($thumbs->find()) {
566         while ($thumbs->fetch()) {
567             try {
568                 $thumbs->getPath();
569             } catch (FileNotFoundException $e) {
570                 $thumbs->delete();
571             }
572         }
573     }
574
575     printfnq("DONE.\n");
576 }
577
578 /*
579  * Files are now stored with their hash, so let's generate for previously uploaded files.
580  */
581 function setFilehashOnLocalFiles()
582 {
583     printfnq('Ensuring all local files have the filehash field set...');
584
585     $file = new File();
586     $file->whereAdd('filename IS NOT NULL AND filename != ""');        // local files
587     $file->whereAdd('filehash IS NULL', 'AND');     // without filehash value
588
589     if ($file->find()) {
590         while ($file->fetch()) {
591             try {
592                 $orig = clone($file);
593                 $file->filehash = hash_file(File::FILEHASH_ALG, $file->getPath());
594                 $file->update($orig);
595             } catch (FileNotFoundException $e) {
596                 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";
597             }
598         }
599     }
600
601     printfnq("DONE.\n");
602 }
603
604 function fixupFileThumbnailUrlhash()
605 {
606     printfnq("Setting urlhash for File_thumbnail entries: ");
607
608     $thumb = new File_thumbnail();
609     $thumb->query('UPDATE '.$thumb->escapedTableName().' SET urlhash=SHA2(url, 256) WHERE'.
610                     ' url IS NOT NULL AND'. // find all entries with a url value
611                     ' url != "" AND'.       // precaution against non-null empty strings
612                     ' urlhash IS NULL');    // but don't touch those we've already calculated
613
614     printfnq("DONE.\n");
615 }
616
617 function migrateProfilePrefs()
618 {
619     printfnq("Finding and possibly migrating Profile_prefs entries: ");
620
621     $prefs = [];   // ['qvitter' => ['cover_photo'=>'profile_banner_url', ...], ...]
622     Event::handle('GetProfilePrefsMigrations', [&$prefs]);
623
624     foreach ($prefs as $namespace=>$mods) {
625         echo "$namespace... ";
626         assert(is_array($mods));
627         $p = new Profile_prefs();
628         $p->namespace = $namespace;
629         // find all entries in all modified topics given in this namespace
630         $p->whereAddIn('topic', array_keys($mods), $p->columnType('topic'));
631         $p->find();
632         while ($p->fetch()) {
633             // for each entry, update 'topic' to the new key value
634             $orig = clone($p);
635             $p->topic = $mods[$p->topic];
636             $p->updateWithKeys($orig);
637         }
638     }
639
640     printfnq("DONE.\n");
641 }
642
643 main();