From e83d2eedbb9f2c06ca818ddbe0d47205d41c2e3f Mon Sep 17 00:00:00 2001 From: Dan Brown Date: Thu, 9 Apr 2020 16:58:40 +0100 Subject: [PATCH] Added "update-url" command to find/replace url in the database - Also aligned format of command descriptions. Targeted most common columns. Have not done revisions for the sake of keeping that content true to how it was originally stored but could cause unexpected behaviour. For #1225 --- app/Console/Commands/ClearViews.php | 2 +- app/Console/Commands/CopyShelfPermissions.php | 2 +- app/Console/Commands/DeleteUsers.php | 2 +- app/Console/Commands/RegenerateSearch.php | 11 ++- app/Console/Commands/UpdateUrl.php | 91 +++++++++++++++++++ tests/CommandsTest.php | 28 ++++++ 6 files changed, 128 insertions(+), 8 deletions(-) create mode 100644 app/Console/Commands/UpdateUrl.php diff --git a/app/Console/Commands/ClearViews.php b/app/Console/Commands/ClearViews.php index 678c64d33..35356210b 100644 --- a/app/Console/Commands/ClearViews.php +++ b/app/Console/Commands/ClearViews.php @@ -18,7 +18,7 @@ class ClearViews extends Command * * @var string */ - protected $description = 'Clear all view-counts for all entities.'; + protected $description = 'Clear all view-counts for all entities'; /** * Create a new command instance. diff --git a/app/Console/Commands/CopyShelfPermissions.php b/app/Console/Commands/CopyShelfPermissions.php index d9a1c1d72..6b5d35a47 100644 --- a/app/Console/Commands/CopyShelfPermissions.php +++ b/app/Console/Commands/CopyShelfPermissions.php @@ -23,7 +23,7 @@ class CopyShelfPermissions extends Command * * @var string */ - protected $description = 'Copy shelf permissions to all child books.'; + protected $description = 'Copy shelf permissions to all child books'; /** * @var BookshelfRepo diff --git a/app/Console/Commands/DeleteUsers.php b/app/Console/Commands/DeleteUsers.php index 68c5bb738..c73c883de 100644 --- a/app/Console/Commands/DeleteUsers.php +++ b/app/Console/Commands/DeleteUsers.php @@ -25,7 +25,7 @@ class DeleteUsers extends Command * * @var string */ - protected $description = 'Delete users that are not "admin" or system users.'; + protected $description = 'Delete users that are not "admin" or system users'; public function __construct(User $user, UserRepo $userRepo) { diff --git a/app/Console/Commands/RegenerateSearch.php b/app/Console/Commands/RegenerateSearch.php index d27d73edc..dc57f2cea 100644 --- a/app/Console/Commands/RegenerateSearch.php +++ b/app/Console/Commands/RegenerateSearch.php @@ -3,6 +3,7 @@ namespace BookStack\Console\Commands; use BookStack\Entities\SearchService; +use DB; use Illuminate\Console\Command; class RegenerateSearch extends Command @@ -26,7 +27,7 @@ class RegenerateSearch extends Command /** * Create a new command instance. * - * @param \BookStack\Entities\SearchService $searchService + * @param SearchService $searchService */ public function __construct(SearchService $searchService) { @@ -41,14 +42,14 @@ class RegenerateSearch extends Command */ public function handle() { - $connection = \DB::getDefaultConnection(); + $connection = DB::getDefaultConnection(); if ($this->option('database') !== null) { - \DB::setDefaultConnection($this->option('database')); - $this->searchService->setConnection(\DB::connection($this->option('database'))); + DB::setDefaultConnection($this->option('database')); + $this->searchService->setConnection(DB::connection($this->option('database'))); } $this->searchService->indexAllEntities(); - \DB::setDefaultConnection($connection); + DB::setDefaultConnection($connection); $this->comment('Search index regenerated'); } } diff --git a/app/Console/Commands/UpdateUrl.php b/app/Console/Commands/UpdateUrl.php new file mode 100644 index 000000000..b95e277d1 --- /dev/null +++ b/app/Console/Commands/UpdateUrl.php @@ -0,0 +1,91 @@ +db = $db; + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + $oldUrl = str_replace("'", '', $this->argument('oldUrl')); + $newUrl = str_replace("'", '', $this->argument('newUrl')); + + $urlPattern = '/https?:\/\/(.+)/'; + if (!preg_match($urlPattern, $oldUrl) || !preg_match($urlPattern, $newUrl)) { + $this->error("The given urls are expected to be full urls starting with http:// or https://"); + return 1; + } + + if (!$this->checkUserOkayToProceed($oldUrl, $newUrl)) { + return 1; + } + + $columnsToUpdateByTable = [ + "attachments" => ["path"], + "pages" => ["html", "text", "markdown"], + "images" => ["url"], + "comments" => ["html", "text"], + ]; + + foreach ($columnsToUpdateByTable as $table => $columns) { + foreach ($columns as $column) { + $changeCount = $this->db->table($table)->update([ + $column => $this->db->raw("REPLACE({$column}, '{$oldUrl}', '{$newUrl}')") + ]); + $this->info("Updated {$changeCount} rows in {$table}->{$column}"); + } + } + + $this->info("URL update procedure complete."); + return 0; + } + + /** + * Warn the user of the dangers of this operation. + * Returns a boolean indicating if they've accepted the warnings. + */ + protected function checkUserOkayToProceed(string $oldUrl, string $newUrl): bool + { + $dangerWarning = "This will search for \"{$oldUrl}\" in your database and replace it with \"{$newUrl}\".\n"; + $dangerWarning .= "Are you sure you want to proceed?"; + $backupConfirmation = "This operation could cause issues if used incorrectly. Have you made a backup of your existing database?"; + + return $this->confirm($dangerWarning) && $this->confirm($backupConfirmation); + } +} diff --git a/tests/CommandsTest.php b/tests/CommandsTest.php index 099af2939..e55b047d4 100644 --- a/tests/CommandsTest.php +++ b/tests/CommandsTest.php @@ -5,6 +5,7 @@ use BookStack\Entities\Bookshelf; use BookStack\Entities\Page; use BookStack\Auth\User; use BookStack\Entities\Repos\PageRepo; +use Symfony\Component\Console\Exception\RuntimeException; class CommandsTest extends TestCase { @@ -166,4 +167,31 @@ class CommandsTest extends TestCase $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'view', 'role_id' => $editorRole->id]); $this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', 'role_id' => $editorRole->id]); } + + public function test_update_url_command_updates_page_content() + { + $page = Page::query()->first(); + $page->html = ''; + $page->save(); + + $this->artisan('bookstack:update-url https://example.com https://cats.example.com') + ->expectsQuestion("This will search for \"https://example.com\" in your database and replace it with \"https://cats.example.com\".\nAre you sure you want to proceed?", 'y') + ->expectsQuestion("This operation could cause issues if used incorrectly. Have you made a backup of your existing database?", 'y'); + + $this->assertDatabaseHas('pages', [ + 'id' => $page->id, + 'html' => '' + ]); + } + + public function test_update_url_command_requires_valid_url() + { + $badUrlMessage = "The given urls are expected to be full urls starting with http:// or https://"; + $this->artisan('bookstack:update-url //example.com https://cats.example.com')->expectsOutput($badUrlMessage); + $this->artisan('bookstack:update-url https://example.com htts://cats.example.com')->expectsOutput($badUrlMessage); + $this->artisan('bookstack:update-url example.com https://cats.example.com')->expectsOutput($badUrlMessage); + + $this->expectException(RuntimeException::class); + $this->artisan('bookstack:update-url https://cats.example.com'); + } }