upgrade.php 20 KB

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