]> BookStack Code Mirror - bookstack/commitdiff
Slugs: Added lookup system using history
authorDan Brown <redacted>
Mon, 24 Nov 2025 13:55:11 +0000 (13:55 +0000)
committerDan Brown <redacted>
Mon, 24 Nov 2025 13:55:11 +0000 (13:55 +0000)
Switched page lookup to use this.

app/Entities/Controllers/PageController.php
app/Entities/Models/SlugHistory.php [new file with mode: 0644]
app/Entities/Queries/EntityQueries.php
app/Entities/Tools/SlugHistory.php

index 603d015ef4a0768aac2cc3c557e8c77aa7f19d71..a648bc298829baf2eb1118f9fae230c7f050e515 100644 (file)
@@ -17,7 +17,6 @@ use BookStack\Entities\Tools\PageContent;
 use BookStack\Entities\Tools\PageEditActivity;
 use BookStack\Entities\Tools\PageEditorData;
 use BookStack\Exceptions\NotFoundException;
-use BookStack\Exceptions\NotifyException;
 use BookStack\Exceptions\PermissionsException;
 use BookStack\Http\Controller;
 use BookStack\Permissions\Permission;
@@ -140,9 +139,7 @@ class PageController extends Controller
         try {
             $page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
         } catch (NotFoundException $e) {
-            $revision = $this->entityQueries->revisions->findLatestVersionBySlugs($bookSlug, $pageSlug);
-            $page = $revision->page ?? null;
-
+            $page = $this->entityQueries->findVisibleByOldSlugs('page', $pageSlug, $bookSlug);
             if (is_null($page)) {
                 throw $e;
             }
diff --git a/app/Entities/Models/SlugHistory.php b/app/Entities/Models/SlugHistory.php
new file mode 100644 (file)
index 0000000..2731fe7
--- /dev/null
@@ -0,0 +1,25 @@
+<?php
+
+namespace BookStack\Entities\Models;
+
+use BookStack\App\Model;
+use BookStack\Permissions\Models\JointPermission;
+use Illuminate\Database\Eloquent\Relations\HasMany;
+
+/**
+ * @property int $id
+ * @property int $sluggable_id
+ * @property string $sluggable_type
+ * @property string $slug
+ * @property ?string $parent_slug
+ */
+class SlugHistory extends Model
+{
+    protected $table = 'slug_history';
+
+    public function jointPermissions(): HasMany
+    {
+        return $this->hasMany(JointPermission::class, 'entity_id', 'sluggable_id')
+            ->whereColumn('joint_permissions.entity_type', '=', 'slug_history.sluggable_type');
+    }
+}
index 91c6a43633d8480ead05df70e459c34a0d180de9..3ffa0adf3dbfb6692f7ca36e3d8a0470c1d978c1 100644 (file)
@@ -4,6 +4,7 @@ namespace BookStack\Entities\Queries;
 
 use BookStack\Entities\Models\Entity;
 use BookStack\Entities\Models\EntityTable;
+use BookStack\Entities\Tools\SlugHistory;
 use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Query\Builder as QueryBuilder;
 use Illuminate\Database\Query\JoinClause;
@@ -18,6 +19,7 @@ class EntityQueries
         public ChapterQueries $chapters,
         public PageQueries $pages,
         public PageRevisionQueries $revisions,
+        protected SlugHistory $slugHistory,
     ) {
     }
 
@@ -31,9 +33,30 @@ class EntityQueries
         $explodedId = explode(':', $identifier);
         $entityType = $explodedId[0];
         $entityId = intval($explodedId[1]);
-        $queries = $this->getQueriesForType($entityType);
 
-        return $queries->findVisibleById($entityId);
+        return $this->findVisibleById($entityType, $entityId);
+    }
+
+    /**
+     * Find an entity by its ID.
+     */
+    public function findVisibleById(string $type, int $id): ?Entity
+    {
+        $queries = $this->getQueriesForType($type);
+        return $queries->findVisibleById($id);
+    }
+
+    /**
+     * Find an entity by looking up old slugs in the slug history.
+     */
+    public function findVisibleByOldSlugs(string $type, string $slug, string $parentSlug = ''): ?Entity
+    {
+        $id = $this->slugHistory->lookupEntityIdUsingSlugs($type, $slug, $parentSlug);
+        if ($id === null) {
+            return null;
+        }
+
+        return $this->findVisibleById($type, $id);
     }
 
     /**
index dc4527a0007add495e5197ae8229b0841925f24a..1584db9cfb106d50ce67a74871ec440502055778 100644 (file)
@@ -4,10 +4,16 @@ namespace BookStack\Entities\Tools;
 
 use BookStack\Entities\Models\BookChild;
 use BookStack\Entities\Models\Entity;
-use Illuminate\Support\Facades\DB;
+use BookStack\Entities\Models\SlugHistory as SlugHistoryModel;
+use BookStack\Permissions\PermissionApplicator;
 
 class SlugHistory
 {
+    public function __construct(
+        protected PermissionApplicator $permissions,
+    ) {
+    }
+
     /**
      * Record the current slugs for the given entity.
      */
@@ -17,31 +23,52 @@ class SlugHistory
             return;
         }
 
-        $latest = $this->getLatestEntryForEntity($entity);
-        if ($latest && $latest->slug === $entity->slug && $latest->parent_slug === $entity->getParent()?->slug) {
-            return;
-        }
-
         $parentSlug = null;
         if ($entity instanceof BookChild) {
             $parentSlug = $entity->book()->first()?->slug;
         }
 
+        $latest = $this->getLatestEntryForEntity($entity);
+        if ($latest && $latest->slug === $entity->slug && $latest->parent_slug === $parentSlug) {
+            return;
+        }
+
         $info = [
             'sluggable_type' => $entity->getMorphClass(),
             'sluggable_id'   => $entity->id,
             'slug'           => $entity->slug,
             'parent_slug'    => $parentSlug,
-            'created_at'     => now(),
-            'updated_at'     => now(),
         ];
 
-        DB::table('slug_history')->insert($info);
+        $entry = new SlugHistoryModel();
+        $entry->forceFill($info);
+        $entry->save();
+    }
+
+    /**
+     * Find the latest visible entry for an entity which uses the given slug(s) in the history.
+     */
+    public function lookupEntityIdUsingSlugs(string $type, string $slug, string $parentSlug = ''): ?int
+    {
+        $query = SlugHistoryModel::query()
+            ->where('sluggable_type', '=', $type)
+            ->where('slug', '=', $slug);
+
+        if ($parentSlug) {
+            $query->where('parent_slug', '=', $parentSlug);
+        }
+
+        $query = $this->permissions->restrictEntityRelationQuery($query, 'slug_history', 'sluggable_id', 'sluggable_type');
+
+        /** @var SlugHistoryModel|null $result */
+        $result = $query->orderBy('created_at', 'desc')->first();
+
+        return $result?->sluggable_id;
     }
 
-    protected function getLatestEntryForEntity(Entity $entity): \stdClass|null
+    protected function getLatestEntryForEntity(Entity $entity): SlugHistoryModel|null
     {
-        return DB::table('slug_history')
+        return SlugHistoryModel::query()
             ->where('sluggable_type', '=', $entity->getMorphClass())
             ->where('sluggable_id', '=', $entity->id)
             ->orderBy('created_at', 'desc')