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
This commit is contained in:
Dan Brown 2020-04-09 16:58:40 +01:00
parent 1962c81742
commit e83d2eedbb
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
6 changed files with 128 additions and 8 deletions

View File

@ -18,7 +18,7 @@ class ClearViews extends Command
* *
* @var string * @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. * Create a new command instance.

View File

@ -23,7 +23,7 @@ class CopyShelfPermissions extends Command
* *
* @var string * @var string
*/ */
protected $description = 'Copy shelf permissions to all child books.'; protected $description = 'Copy shelf permissions to all child books';
/** /**
* @var BookshelfRepo * @var BookshelfRepo

View File

@ -25,7 +25,7 @@ class DeleteUsers extends Command
* *
* @var string * @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) public function __construct(User $user, UserRepo $userRepo)
{ {

View File

@ -3,6 +3,7 @@
namespace BookStack\Console\Commands; namespace BookStack\Console\Commands;
use BookStack\Entities\SearchService; use BookStack\Entities\SearchService;
use DB;
use Illuminate\Console\Command; use Illuminate\Console\Command;
class RegenerateSearch extends Command class RegenerateSearch extends Command
@ -26,7 +27,7 @@ class RegenerateSearch extends Command
/** /**
* Create a new command instance. * Create a new command instance.
* *
* @param \BookStack\Entities\SearchService $searchService * @param SearchService $searchService
*/ */
public function __construct(SearchService $searchService) public function __construct(SearchService $searchService)
{ {
@ -41,14 +42,14 @@ class RegenerateSearch extends Command
*/ */
public function handle() public function handle()
{ {
$connection = \DB::getDefaultConnection(); $connection = DB::getDefaultConnection();
if ($this->option('database') !== null) { if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database')); DB::setDefaultConnection($this->option('database'));
$this->searchService->setConnection(\DB::connection($this->option('database'))); $this->searchService->setConnection(DB::connection($this->option('database')));
} }
$this->searchService->indexAllEntities(); $this->searchService->indexAllEntities();
\DB::setDefaultConnection($connection); DB::setDefaultConnection($connection);
$this->comment('Search index regenerated'); $this->comment('Search index regenerated');
} }
} }

View File

@ -0,0 +1,91 @@
<?php
namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Connection;
class UpdateUrl extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:update-url
{oldUrl : URL to replace}
{newUrl : URL to use as the replacement}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Find and replace the given URLs in your BookStack database';
protected $db;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(Connection $db)
{
$this->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);
}
}

View File

@ -5,6 +5,7 @@ use BookStack\Entities\Bookshelf;
use BookStack\Entities\Page; use BookStack\Entities\Page;
use BookStack\Auth\User; use BookStack\Auth\User;
use BookStack\Entities\Repos\PageRepo; use BookStack\Entities\Repos\PageRepo;
use Symfony\Component\Console\Exception\RuntimeException;
class CommandsTest extends TestCase 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' => 'view', 'role_id' => $editorRole->id]);
$this->assertDatabaseHas('entity_permissions', ['restrictable_id' => $child->id, 'action' => 'update', '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 = '<a href="https://example.com/donkeys"></a>';
$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' => '<a href="https://cats.example.com/donkeys"></a>'
]);
}
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');
}
} }