upgrade.php 20 KB

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