upgrade.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717
  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. fixupUserBadNulls();
  53. fixupGroupURI();
  54. if ($iterate_files) {
  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 fixupUserBadNulls(): void
  99. {
  100. printfnq("Ensuring all users have no empty strings for NULLs...");
  101. foreach (['email', 'incomingemail', 'sms', 'smsemail'] as $col) {
  102. $user = new User();
  103. $user->whereAdd("{$col} = ''");
  104. if ($user->find()) {
  105. while ($user->fetch()) {
  106. $user->query(sprintf(
  107. <<<'END'
  108. UPDATE %1$s
  109. SET %2$s = NULL, modified = CURRENT_TIMESTAMP
  110. WHERE id = %3$d
  111. END,
  112. $user->escapedTableName(),
  113. $col,
  114. $user->getID()
  115. ));
  116. }
  117. }
  118. }
  119. printfnq("DONE.\n");
  120. }
  121. function fixupNoticeConversation()
  122. {
  123. printfnq("Ensuring all notices have a conversation ID...");
  124. $notice = new Notice();
  125. $notice->whereAdd('conversation is null');
  126. $notice->whereAdd('conversation = 0', 'OR');
  127. $notice->orderBy('id'); // try to get originals before replies
  128. $notice->find();
  129. while ($notice->fetch()) {
  130. try {
  131. $cid = null;
  132. $orig = clone($notice);
  133. if (!empty($notice->reply_to)) {
  134. $reply = Notice::getKV('id', $notice->reply_to);
  135. if ($reply instanceof Notice && !empty($reply->conversation)) {
  136. $notice->conversation = $reply->conversation;
  137. }
  138. unset($reply);
  139. }
  140. // if still empty
  141. if (empty($notice->conversation)) {
  142. $child = new Notice();
  143. $child->reply_to = $notice->getID();
  144. $child->limit(1);
  145. if ($child->find(true) && !empty($child->conversation)) {
  146. $notice->conversation = $child->conversation;
  147. }
  148. unset($child);
  149. }
  150. // if _still_ empty we just create our own conversation
  151. if (empty($notice->conversation)) {
  152. $notice->conversation = $notice->getID();
  153. }
  154. $result = $notice->update($orig);
  155. unset($orig);
  156. } catch (Exception $e) {
  157. print("Error setting conversation: " . $e->getMessage());
  158. }
  159. }
  160. printfnq("DONE.\n");
  161. }
  162. function fixupGroupURI()
  163. {
  164. printfnq("Ensuring all groups have an URI...");
  165. $group = new User_group();
  166. $group->whereAdd('uri IS NULL');
  167. if ($group->find()) {
  168. while ($group->fetch()) {
  169. $orig = User_group::getKV('id', $group->id);
  170. $group->uri = $group->getUri();
  171. $group->update($orig);
  172. }
  173. }
  174. printfnq("DONE.\n");
  175. }
  176. function initConversation()
  177. {
  178. if (common_config('fix', 'upgrade_initConversation') <= 1) {
  179. printfnq(sprintf("Skipping %s, fixed by previous upgrade.\n", __METHOD__));
  180. return;
  181. }
  182. printfnq("Ensuring all conversations have a row in conversation table...");
  183. $notice = new Notice();
  184. $notice->selectAdd();
  185. $notice->selectAdd('DISTINCT conversation');
  186. $notice->joinAdd(['conversation', 'conversation:id'], 'LEFT'); // LEFT to get the null values for conversation.id
  187. $notice->whereAdd('conversation.id IS NULL');
  188. if ($notice->find()) {
  189. printfnq(" fixing {$notice->N} missing conversation entries...");
  190. }
  191. while ($notice->fetch()) {
  192. $id = $notice->conversation;
  193. $uri = common_local_url('conversation', ['id' => $id]);
  194. // @fixme db_dataobject won't save our value for an autoincrement
  195. // so we're bypassing the insert wrappers
  196. $conv = new Conversation();
  197. $sql = "INSERT INTO conversation (id,uri,created) VALUES (%d,'%s','%s')";
  198. $sql = sprintf(
  199. $sql,
  200. $id,
  201. $conv->escape($uri),
  202. $conv->escape(common_sql_now())
  203. );
  204. $conv->query($sql);
  205. }
  206. // This is something we should only have to do once unless introducing new, bad code.
  207. if (DEBUG) {
  208. printfnq(sprintf('Storing in config that we have done %s', __METHOD__));
  209. }
  210. common_config_set('fix', 'upgrade_initConversation', 1);
  211. printfnq("DONE.\n");
  212. }
  213. function fixupConversationURIs()
  214. {
  215. printfnq("Ensuring all conversations have a URI...");
  216. $conv = new Conversation();
  217. $conv->whereAdd('uri IS NULL');
  218. if ($conv->find()) {
  219. $rounds = 0;
  220. while ($conv->fetch()) {
  221. $uri = common_local_url('conversation', ['id' => $conv->id]);
  222. $sql = sprintf(
  223. <<<'END'
  224. UPDATE conversation
  225. SET uri = %1$s, modified = CURRENT_TIMESTAMP
  226. WHERE id = %2$d;
  227. END,
  228. $conv->_quote($uri),
  229. $conv->id
  230. );
  231. $conv->query($sql);
  232. if (($conv->N-++$rounds) % 500 == 0) {
  233. printfnq(sprintf(' %d items left...', $conv->N-$rounds));
  234. }
  235. }
  236. }
  237. printfnq("DONE.\n");
  238. }
  239. function initGroupProfileId()
  240. {
  241. printfnq("Ensuring all User_group entries have a Profile and profile_id...");
  242. $group = new User_group();
  243. $group->whereAdd('NOT EXISTS (SELECT id FROM profile WHERE id = user_group.profile_id)');
  244. $group->find();
  245. while ($group->fetch()) {
  246. try {
  247. // We must create a new, incrementally assigned profile_id
  248. $profile = new Profile();
  249. $profile->nickname = $group->nickname;
  250. $profile->fullname = $group->fullname;
  251. $profile->profileurl = $group->mainpage;
  252. $profile->homepage = $group->homepage;
  253. $profile->bio = $group->description;
  254. $profile->location = $group->location;
  255. $profile->created = $group->created;
  256. $profile->modified = $group->modified;
  257. $profile->query('START TRANSACTION');
  258. $id = $profile->insert();
  259. if (empty($id)) {
  260. $profile->query('ROLLBACK');
  261. throw new Exception('Profile insertion failed, profileurl: '.$profile->profileurl);
  262. }
  263. $group->query(sprintf(
  264. <<<'END'
  265. UPDATE user_group
  266. SET profile_id = %1$d, modified = CURRENT_TIMESTAMP
  267. WHERE id = %2$d
  268. END,
  269. $id,
  270. $group->getID()
  271. ));
  272. $profile->query('COMMIT');
  273. $profile->free();
  274. } catch (Exception $e) {
  275. printfv("Error initializing Profile for group {$group->nickname}:" . $e->getMessage());
  276. }
  277. }
  278. printfnq("DONE.\n");
  279. }
  280. function initLocalGroup()
  281. {
  282. printfnq("Ensuring all local user groups have a local_group...");
  283. $group = new User_group();
  284. $group->whereAdd('NOT EXISTS (select group_id from local_group where group_id = user_group.id)');
  285. $group->find();
  286. while ($group->fetch()) {
  287. try {
  288. // Hack to check for local groups
  289. if ($group->getUri() == common_local_url('groupbyid', ['id' => $group->id])) {
  290. $lg = new Local_group();
  291. $lg->group_id = $group->id;
  292. $lg->nickname = $group->nickname;
  293. $lg->created = $group->created; // XXX: common_sql_now() ?
  294. $lg->modified = $group->modified;
  295. $lg->insert();
  296. }
  297. } catch (Exception $e) {
  298. printfv("Error initializing local group for {$group->nickname}:" . $e->getMessage());
  299. }
  300. }
  301. printfnq("DONE.\n");
  302. }
  303. function initNoticeReshare()
  304. {
  305. if (common_config('fix', 'upgrade_initNoticeReshare') <= 1) {
  306. printfnq(sprintf("Skipping %s, fixed by previous upgrade.\n", __METHOD__));
  307. return;
  308. }
  309. printfnq("Ensuring all reshares have the correct verb and object-type...");
  310. $notice = new Notice();
  311. $notice->whereAdd('repeat_of IS NOT NULL');
  312. $notice->whereAdd(sprintf(
  313. '(verb <> %1$s OR object_type <> %2$s)',
  314. $notice->_quote(ActivityVerb::SHARE),
  315. $notice->_quote(ActivityObject::ACTIVITY)
  316. ));
  317. if ($notice->find()) {
  318. while ($notice->fetch()) {
  319. try {
  320. $orig = Notice::getKV('id', $notice->id);
  321. $notice->verb = ActivityVerb::SHARE;
  322. $notice->object_type = ActivityObject::ACTIVITY;
  323. $notice->update($orig);
  324. } catch (Exception $e) {
  325. printfv("Error updating verb and object_type for {$notice->id}:" . $e->getMessage());
  326. }
  327. }
  328. }
  329. // This is something we should only have to do once unless introducing new, bad code.
  330. if (DEBUG) {
  331. printfnq(sprintf('Storing in config that we have done %s', __METHOD__));
  332. }
  333. common_config_set('fix', 'upgrade_initNoticeReshare', 1);
  334. printfnq("DONE.\n");
  335. }
  336. function initSubscriptionURI()
  337. {
  338. printfnq("Ensuring all subscriptions have a URI...");
  339. $sub = new Subscription();
  340. $sub->whereAdd('uri IS NULL');
  341. if ($sub->find()) {
  342. while ($sub->fetch()) {
  343. try {
  344. $uri = Subscription::newUri(
  345. $sub->getSubscriber(),
  346. $sub->getSubscribed(),
  347. $sub->created
  348. );
  349. $sub->decache();
  350. $sub->query(sprintf(
  351. <<<'END'
  352. UPDATE subscription
  353. SET uri = %1$s, modified = CURRENT_TIMESTAMP
  354. WHERE subscriber = %2$d AND subscribed = %3$d
  355. END,
  356. $sub->_quote($uri),
  357. $sub->subscriber,
  358. $sub->subscribed
  359. ));
  360. } catch (Exception $e) {
  361. common_log(LOG_ERR, 'Error updating subscription URI: ' . $e->getMessage());
  362. }
  363. }
  364. }
  365. printfnq("DONE.\n");
  366. }
  367. function initGroupMemberURI()
  368. {
  369. printfnq("Ensuring all group memberships have a URI...");
  370. $mem = new Group_member();
  371. $mem->whereAdd('uri IS NULL');
  372. if ($mem->find()) {
  373. while ($mem->fetch()) {
  374. try {
  375. $uri = Group_member::newUri(
  376. Profile::getByID($mem->profile_id),
  377. User_group::getByID($mem->group_id),
  378. $mem->created
  379. );
  380. $mem->decache();
  381. $mem->query(sprintf(
  382. <<<'END'
  383. UPDATE group_member
  384. SET uri = %s, modified = CURRENT_TIMESTAMP
  385. WHERE profile_id = %d AND group_id = %d
  386. END,
  387. $mem->_quote($uri),
  388. $mem->profile_id,
  389. $mem->group_id
  390. ));
  391. } catch (Exception $e) {
  392. common_log(LOG_ERR, "Error updated membership URI: " . $e->getMessage());
  393. }
  394. }
  395. }
  396. printfnq("DONE.\n");
  397. }
  398. function initProfileLists()
  399. {
  400. printfnq("Ensuring all profile tags have a corresponding list...");
  401. $ptag = new Profile_tag();
  402. $ptag->selectAdd();
  403. $ptag->selectAdd('tagger, tag, COUNT(*) AS tagged_count');
  404. $ptag->whereAdd('NOT EXISTS (SELECT tagger, tagged FROM profile_list '.
  405. 'WHERE profile_tag.tagger = profile_list.tagger '.
  406. 'AND profile_tag.tag = profile_list.tag)');
  407. $ptag->groupBy('tagger, tag');
  408. $ptag->orderBy('tagger, tag');
  409. if ($ptag->find()) {
  410. while ($ptag->fetch()) {
  411. $plist = new Profile_list();
  412. $plist->tagger = $ptag->tagger;
  413. $plist->tag = $ptag->tag;
  414. $plist->private = false;
  415. $plist->created = common_sql_now();
  416. $plist->modified = $plist->created;
  417. $plist->mainpage = common_local_url(
  418. 'showprofiletag',
  419. ['tagger' => $plist->getTagger()->nickname,
  420. 'tag' => $plist->tag]
  421. );
  422. ;
  423. $plist->tagged_count = $ptag->tagged_count;
  424. $plist->subscriber_count = 0;
  425. $plist->insert();
  426. $orig = clone($plist);
  427. // After insert since it uses auto-generated ID
  428. $plist->uri = common_local_url(
  429. 'profiletagbyid',
  430. ['id' => $plist->id,
  431. 'tagger_id' => $plist->tagger]
  432. );
  433. $plist->update($orig);
  434. }
  435. }
  436. printfnq("DONE.\n");
  437. }
  438. /*
  439. * Added as we now store interpretd width and height in File table.
  440. */
  441. function fixupFileGeometry()
  442. {
  443. printfnq("Ensuring width and height is set for supported local File objects...");
  444. $file = new File();
  445. $file->whereAdd('filename IS NOT NULL'); // local files
  446. $file->whereAdd('width IS NULL OR width = 0');
  447. if ($file->find()) {
  448. while ($file->fetch()) {
  449. if (DEBUG) {
  450. printfnq(sprintf('Found file without width: %s\n', _ve($file->getFilename())));
  451. }
  452. // Set file geometrical properties if available
  453. try {
  454. $image = ImageFile::fromFileObject($file);
  455. } catch (ServerException $e) {
  456. // We couldn't make out an image from the file.
  457. if (DEBUG) {
  458. printfnq(sprintf('Could not make an image out of the file.\n'));
  459. }
  460. continue;
  461. }
  462. $orig = clone($file);
  463. $file->width = $image->width;
  464. $file->height = $image->height;
  465. if (DEBUG) {
  466. printfnq(sprintf('Setting image file and with to %sx%s.\n', $file->width, $file->height));
  467. }
  468. $file->update($orig);
  469. // FIXME: Do this more automagically inside ImageFile or so.
  470. if ($image->getPath() != $file->getPath()) {
  471. if (DEBUG) {
  472. printfnq(sprintf('Deleting the temporarily stored ImageFile.\n'));
  473. }
  474. $image->unlink();
  475. }
  476. unset($image);
  477. }
  478. }
  479. printfnq("DONE.\n");
  480. }
  481. /*
  482. * File_thumbnail objects for local Files store their own filenames in the database.
  483. */
  484. function deleteLocalFileThumbnailsWithoutFilename()
  485. {
  486. printfnq("Removing all local File_thumbnail entries without filename property...");
  487. $file = new File();
  488. $file->whereAdd('filename IS NOT NULL'); // local files
  489. if ($file->find()) {
  490. // Looping through local File entries
  491. while ($file->fetch()) {
  492. $thumbs = new File_thumbnail();
  493. $thumbs->file_id = $file->id;
  494. $thumbs->whereAdd("filename IS NULL OR filename = ''");
  495. // Checking if there were any File_thumbnail entries without filename
  496. if (!$thumbs->find()) {
  497. continue;
  498. }
  499. // deleting incomplete entry to allow regeneration
  500. while ($thumbs->fetch()) {
  501. $thumbs->delete();
  502. }
  503. }
  504. }
  505. printfnq("DONE.\n");
  506. }
  507. /*
  508. * Delete File_thumbnail entries where the referenced file does not exist.
  509. */
  510. function deleteMissingLocalFileThumbnails()
  511. {
  512. printfnq("Removing all local File_thumbnail entries without existing files...");
  513. $thumbs = new File_thumbnail();
  514. $thumbs->whereAdd("filename IS NOT NULL AND filename <> ''");
  515. // Checking if there were any File_thumbnail entries without filename
  516. if ($thumbs->find()) {
  517. while ($thumbs->fetch()) {
  518. try {
  519. $thumbs->getPath();
  520. } catch (FileNotFoundException $e) {
  521. $thumbs->delete();
  522. }
  523. }
  524. }
  525. printfnq("DONE.\n");
  526. }
  527. /*
  528. * Files are now stored with their hash, so let's generate for previously uploaded files.
  529. */
  530. function setFilehashOnLocalFiles()
  531. {
  532. printfnq('Ensuring all local files have the filehash field set...');
  533. $file = new File();
  534. $file->whereAdd("filename IS NOT NULL AND filename <> ''"); // local files
  535. $file->whereAdd('filehash IS NULL', 'AND'); // without filehash value
  536. if ($file->find()) {
  537. while ($file->fetch()) {
  538. try {
  539. $orig = clone($file);
  540. $file->filehash = hash_file(File::FILEHASH_ALG, $file->getPath());
  541. $file->update($orig);
  542. } catch (FileNotFoundException $e) {
  543. 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";
  544. }
  545. }
  546. }
  547. printfnq("DONE.\n");
  548. }
  549. function fixupFileThumbnailUrlhash()
  550. {
  551. printfnq("Setting urlhash for File_thumbnail entries: ");
  552. switch (common_config('db', 'type')) {
  553. case 'pgsql':
  554. $url_sha256 = 'encode(sha256(CAST("url" AS bytea)), \'hex\')';
  555. break;
  556. case 'mysql':
  557. $url_sha256 = 'sha2(`url`, 256)';
  558. break;
  559. default:
  560. throw new Exception('Unknown DB type selected.');
  561. }
  562. $thumb = new File_thumbnail();
  563. $thumb->query(sprintf(
  564. 'UPDATE %1$s ' .
  565. 'SET urlhash = %2$s, modified = CURRENT_TIMESTAMP ' .
  566. 'WHERE url IS NOT NULL ' . // find all entries with a url value
  567. "AND url <> '' " . // precaution against non-null empty strings
  568. 'AND urlhash IS NULL', // but don't touch those we've already calculated
  569. $thumb->escapedTableName(),
  570. $url_sha256
  571. ));
  572. printfnq("DONE.\n");
  573. }
  574. function migrateProfilePrefs()
  575. {
  576. printfnq("Finding and possibly migrating Profile_prefs entries: ");
  577. $prefs = []; // ['qvitter' => ['cover_photo'=>'profile_banner_url', ...], ...]
  578. Event::handle('GetProfilePrefsMigrations', [&$prefs]);
  579. foreach ($prefs as $namespace=>$mods) {
  580. echo "$namespace... ";
  581. if (!is_array($mods)) {
  582. doom("\$modsは配列ではありません。", __FILE__, __LINE__);
  583. }
  584. $p = new Profile_prefs();
  585. $p->namespace = $namespace;
  586. // find all entries in all modified topics given in this namespace
  587. $p->whereAddIn('topic', array_keys($mods), $p->columnType('topic'));
  588. $p->find();
  589. while ($p->fetch()) {
  590. // for each entry, update 'topic' to the new key value
  591. $orig = clone($p);
  592. $p->topic = $mods[$p->topic];
  593. $p->updateWithKeys($orig);
  594. }
  595. }
  596. printfnq("DONE.\n");
  597. }
  598. main();