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'); + } }