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