1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-10-25 06:37:36 +03:00

DB: Updated handling of deleted user ID handling in DB

Updated uses of user ID to nullify on delete.
Added testing to cover deletion of user relations.
Added model factories to support changes and potential other tests.
Cleans existing ID references in the DB via migration.
This commit is contained in:
Dan Brown
2025-10-19 19:10:15 +01:00
parent 4c7d6420ee
commit 5754acf2fb
20 changed files with 495 additions and 44 deletions

View File

@@ -0,0 +1,28 @@
<?php
namespace Database\Factories\Access\Mfa;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\BookStack\Access\Mfa\MfaValue>
*/
class MfaValueFactory extends Factory
{
protected $model = \BookStack\Access\Mfa\MfaValue::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'method' => 'totp',
'value' => '123456',
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Database\Factories\Access;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\BookStack\Access\SocialAccount>
*/
class SocialAccountFactory extends Factory
{
protected $model = \BookStack\Access\SocialAccount::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'driver' => 'github',
'driver_id' => '123456',
'avatar' => '',
];
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Database\Factories\Activity\Models;
use BookStack\Activity\ActivityType;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\BookStack\Activity\Models\Activity>
*/
class ActivityFactory extends Factory
{
protected $model = \BookStack\Activity\Models\Activity::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$activities = ActivityType::all();
$activity = $activities[array_rand($activities)];
return [
'type' => $activity,
'detail' => 'Activity detail for ' . $activity,
'user_id' => User::factory(),
'ip' => $this->faker->ipv4(),
'loggable_id' => null,
'loggable_type' => null,
];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Database\Factories\Activity\Models;
use BookStack\Entities\Models\Book;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\BookStack\Activity\Models\Favourite>
*/
class FavouriteFactory extends Factory
{
protected $model = \BookStack\Activity\Models\Favourite::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$book = Book::query()->first();
return [
'user_id' => User::factory(),
'favouritable_id' => $book->id,
'favouritable_type' => 'book',
];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Database\Factories\Activity\Models;
use BookStack\Activity\WatchLevels;
use BookStack\Entities\Models\Book;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\BookStack\Activity\Models\Watch>
*/
class WatchFactory extends Factory
{
protected $model = \BookStack\Activity\Models\Watch::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$book = Book::factory()->create();
return [
'user_id' => User::factory(),
'watchable_id' => $book->id,
'watchable_type' => 'book',
'level' => WatchLevels::NEW,
];
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Database\Factories\Entities\Models;
use BookStack\Entities\Models\Page;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\BookStack\Entities\Models\Deletion>
*/
class DeletionFactory extends Factory
{
protected $model = \BookStack\Entities\Models\Deletion::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'deleted_by' => User::factory(),
'deletable_id' => Page::factory(),
'deletable_type' => 'page',
];
}
}

View File

@@ -17,10 +17,8 @@ class PageFactory extends Factory
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
public function definition(): array
{
$html = '<p>' . implode('</p>', $this->faker->paragraphs(5)) . '</p>';

View File

@@ -0,0 +1,40 @@
<?php
namespace Database\Factories\Entities\Models;
use BookStack\Entities\Models\Page;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
class PageRevisionFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = \BookStack\Entities\Models\PageRevision::class;
/**
* Define the model's default state.
*/
public function definition(): array
{
$html = '<p>' . implode('</p>', $this->faker->paragraphs(5)) . '</p>';
$page = Page::query()->first();
return [
'page_id' => $page->id,
'name' => $this->faker->sentence(),
'html' => $html,
'text' => strip_tags($html),
'created_by' => User::factory(),
'slug' => $page->slug,
'book_slug' => $page->book->slug,
'type' => 'version',
'markdown' => strip_tags($html),
'summary' => $this->faker->sentence(),
'revision_number' => rand(1, 4000),
];
}
}

View File

@@ -62,8 +62,9 @@ return new class extends Migration
});
}
// Convert image zero values to null
// Convert image and activity zero values to null
DB::table('images')->where('uploaded_to', '=', 0)->update(['uploaded_to' => null]);
DB::table('activities')->where('loggable_id', '=', 0)->update(['loggable_id' => null]);
// Rebuild joint permissions if needed
// This was moved here from 2023_01_24_104625_refactor_joint_permissions_storage since the changes

View File

@@ -0,0 +1,80 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
protected static array $toNullify = [
'activities' => ['user_id'],
'attachments' => ['created_by', 'updated_by'],
'comments' => ['created_by', 'updated_by'],
'deletions' => ['deleted_by'],
'entities' => ['created_by', 'updated_by', 'owned_by'],
'images' => ['created_by', 'updated_by'],
'imports' => ['created_by'],
'joint_permissions' => ['owner_id'],
'page_revisions' => ['created_by'],
];
protected static array $toClean = [
'api_tokens' => ['user_id'],
'email_confirmations' => ['user_id'],
'favourites' => ['user_id'],
'mfa_values' => ['user_id'],
'role_user' => ['user_id'],
'sessions' => ['user_id'],
'social_accounts' => ['user_id'],
'user_invites' => ['user_id'],
'views' => ['user_id'],
'watches' => ['user_id'],
];
/**
* Run the migrations.
*/
public function up(): void
{
$idSelectQuery = DB::table('users')->select('id');
foreach (self::$toNullify as $tableName => $columns) {
Schema::table($tableName, function (Blueprint $table) use ($columns) {
foreach ($columns as $columnName) {
$table->unsignedInteger($columnName)->nullable()->change();
}
});
foreach ($columns as $columnName) {
DB::table($tableName)->where($columnName, '=', 0)->update([$columnName => null]);
DB::table($tableName)->whereNotIn($columnName, $idSelectQuery)->update([$columnName => null]);
}
}
foreach (self::$toClean as $tableName => $columns) {
foreach ($columns as $columnName) {
DB::table($tableName)->whereNotIn($columnName, $idSelectQuery)->delete();
}
}
// TODO - Ensure each is fully handled in user delete
// Start by writing tests for each of these columns
}
/**
* Reverse the migrations.
*/
public function down(): void
{
foreach (self::$toNullify as $tableName => $columns) {
foreach ($columns as $columnName) {
DB::table($tableName)->whereNull($columnName)->update([$columnName => 0]);
}
Schema::table($tableName, function (Blueprint $table) use ($columns) {
foreach ($columns as $columnName) {
$table->unsignedInteger($columnName)->nullable(false)->change();
}
});
}
}
};