upgrade.php 19 KB

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