Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Attachment search functionality by content type in search #30

Open
wants to merge 2 commits into
base: development
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/App/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
@@ -12,6 +12,7 @@
use BookStack\Http\HttpRequestService;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\SettingService;
use BookStack\Uploads\Attachment;
use BookStack\Util\CspService;
use Illuminate\Contracts\Foundation\ExceptionRenderer;
use Illuminate\Database\Eloquent\Relations\Relation;
@@ -73,6 +74,7 @@ public function boot(): void
'book' => Book::class,
'chapter' => Chapter::class,
'page' => Page::class,
'attachment' => Attachment::class,
]);
}
}
4 changes: 4 additions & 0 deletions app/Entities/EntityProvider.php
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision;
use BookStack\Uploads\Attachment;

/**
* Class EntityProvider.
@@ -23,6 +24,7 @@ class EntityProvider
public Chapter $chapter;
public Page $page;
public PageRevision $pageRevision;
public Attachment $attachment;

public function __construct()
{
@@ -31,6 +33,7 @@ public function __construct()
$this->chapter = new Chapter();
$this->page = new Page();
$this->pageRevision = new PageRevision();
$this->attachment = new Attachment();
}

/**
@@ -46,6 +49,7 @@ public function all(): array
'book' => $this->book,
'chapter' => $this->chapter,
'page' => $this->page,
'attachment' => $this->attachment,
];
}

37 changes: 37 additions & 0 deletions app/Entities/Queries/AttachmentQueries.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace BookStack\Entities\Queries;

use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\ProvidesEntityQueries;
use BookStack\Uploads\Attachment;
use Illuminate\Database\Eloquent\Builder;

class AttachmentQueries implements ProvidesEntityQueries
{
protected static array $listAttributes = [
'id',
'name',
'uploaded_to',
];

public function start(): Builder
{
return Attachment::query();
}

public function findVisibleById(int $id): ?Entity
{
return $this->start()->scopes('visible')->find($id);
}

public function visibleForList(): Builder
{
return $this->start()
->select(array_merge(static::$listAttributes, ['page_slug' => function ($builder) {
$builder->select('slug')
->from('pages')
->whereColumn('pages.id', '=', 'attachments.uploaded_to');
}]));
}
}
2 changes: 2 additions & 0 deletions app/Entities/Queries/EntityQueries.php
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ public function __construct(
public ChapterQueries $chapters,
public PageQueries $pages,
public PageRevisionQueries $revisions,
public AttachmentQueries $attachment,
) {
}

@@ -50,6 +51,7 @@ protected function getQueriesForType(string $type): ProvidesEntityQueries
'chapter' => $this->chapters,
'book' => $this->books,
'bookshelf' => $this->shelves,
'attachment' => $this->attachment,
default => null,
};

5 changes: 4 additions & 1 deletion app/Permissions/PermissionApplicator.php
Original file line number Diff line number Diff line change
@@ -100,7 +100,7 @@ public function checkUserHasEntityPermissionOnAny(string $action, string $entity
public function restrictEntityQuery(Builder $query): Builder
{
return $query->where(function (Builder $parentQuery) {
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) {
$parentQuery->whereHas($parentQuery->getModel()->getTable() === 'attachments' ? 'attachmentJointPermissions' : 'jointPermissions', function (Builder $permissionQuery) {
$permissionQuery->select(['entity_id', 'entity_type'])
->selectRaw('max(owner_id) as owner_id')
->selectRaw('max(status) as status')
@@ -161,6 +161,9 @@ public function filterDeletedFromEntityRelationQuery(Builder $query, string $tab
$joinQuery = function ($query) use ($entityProvider) {
$first = true;
foreach ($entityProvider->all() as $entity) {
if ($entity->getModel()->getTable() === 'attachments') {
continue;
}
/** @var Builder $query */
$entityQuery = function ($query) use ($entity) {
$query->select(['id', 'deleted_at'])
2 changes: 2 additions & 0 deletions app/Search/SearchIndex.php
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Uploads\Attachment;
use BookStack\Util\HtmlDocument;
use DOMNode;
use Illuminate\Database\Eloquent\Builder;
@@ -68,6 +69,7 @@ public function indexAllEntities(?callable $progressCallback = null): void

foreach ($this->entityProvider->all() as $entityModel) {
$indexContentField = $entityModel instanceof Page ? 'html' : 'description';
$indexContentField = $entityModel instanceof Attachment ? 'name' : ($entityModel instanceof Page ? 'html' : 'description');
$selectFields = ['id', 'name', $indexContentField];
/** @var Builder<Entity> $query */
$query = $entityModel->newQuery();
6 changes: 6 additions & 0 deletions app/Search/SearchRunner.php
Original file line number Diff line number Diff line change
@@ -135,6 +135,12 @@ protected function getPageOfDataFromQuery(EloquentBuilder $query, string $entity
};
}

if ($entityType === 'attachment') {
$relations['page'] = function (BelongsTo $query) {
$query->scopes('visible');
};
}

return $query->clone()
->with(array_filter($relations))
->skip(($page - 1) * $count)
48 changes: 41 additions & 7 deletions app/Uploads/Attachment.php
Original file line number Diff line number Diff line change
@@ -2,12 +2,12 @@

namespace BookStack\Uploads;

use BookStack\App\Model;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Permissions\Models\JointPermission;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\HasCreatorAndUpdater;
use BookStack\Users\Models\HasOwner;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -27,17 +27,27 @@
*
* @method static Entity|Builder visible()
*/
class Attachment extends Model
class Attachment extends Entity
{
use HasCreatorAndUpdater;
use HasFactory;
use HasOwner;

public string $textField = 'name';

public string $htmlField = 'name';

protected $fillable = ['name', 'order'];
protected $hidden = ['path', 'page'];
protected $casts = [
'external' => 'bool',
];

public static function bootSoftDeletes()
{
// No operation: override with an empty method
}

/**
* Get the downloadable file name for this upload.
*/
@@ -55,13 +65,13 @@ public function getFileName(): string
*/
public function page(): BelongsTo
{
return $this->belongsTo(Page::class, 'uploaded_to');
return $this->belongsTo(Page::class, 'uploaded_to')->with(['chapter','book']);
}

public function jointPermissions(): HasMany
public function attachmentJointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'uploaded_to')
->where('joint_permissions.entity_type', '=', 'page');
->where('joint_permissions.entity_type', '=', 'page');
}

/**
@@ -110,14 +120,38 @@ public function markdownLink(): string
/**
* Scope the query to those attachments that are visible based upon related page permissions.
*/
public function scopeVisible(): Builder
public function scopeVisible(Builder $query): Builder
{
$permissions = app()->make(PermissionApplicator::class);

return $permissions->restrictPageRelationQuery(
self::query(),
$query,
'attachments',
'uploaded_to'
);
}

protected function performDeleteOnModel()
{
// Perform a direct delete without relying on soft deletes
return $this->newQueryWithoutScopes()->where($this->getKeyName(), $this->getKey())->delete();
}

// Prevent soft deletes by setting the deleted column to null
public function getDeletedAtColumn()
{
return null;
}

public function scopeWithTrashed($query)
{
// No conditions added, so it includes both active and inactive records
return $query;
}

public function scopeOnlyTrashed($query)
{
// No conditions added, so it includes both active and inactive records
return $query;
}
}
6 changes: 4 additions & 2 deletions app/Uploads/AttachmentService.php
Original file line number Diff line number Diff line change
@@ -85,7 +85,7 @@ public function getAttachmentFileSize(Attachment $attachment): int
*
* @throws FileUploadException
*/
public function saveNewUpload(UploadedFile $uploadedFile, int $pageId): Attachment
public function saveNewUpload(UploadedFile $uploadedFile, int $pageId, int $owned_by): Attachment
{
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($uploadedFile);
@@ -99,6 +99,7 @@ public function saveNewUpload(UploadedFile $uploadedFile, int $pageId): Attachme
'uploaded_to' => $pageId,
'created_by' => user()->id,
'updated_by' => user()->id,
'owned_by' => $owned_by,
'order' => $largestExistingOrder + 1,
]);

@@ -132,7 +133,7 @@ public function saveUpdatedUpload(UploadedFile $uploadedFile, Attachment $attach
/**
* Save a new File attachment from a given link and name.
*/
public function saveNewFromLink(string $name, string $link, int $page_id): Attachment
public function saveNewFromLink(string $name, string $link, int $page_id, int $owned_by): Attachment
{
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');

@@ -144,6 +145,7 @@ public function saveNewFromLink(string $name, string $link, int $page_id): Attac
'uploaded_to' => $page_id,
'created_by' => user()->id,
'updated_by' => user()->id,
'owned_by' => $owned_by,
'order' => $largestExistingOrder + 1,
]);
}
7 changes: 4 additions & 3 deletions app/Uploads/Controllers/AttachmentApiController.php
Original file line number Diff line number Diff line change
@@ -28,7 +28,7 @@ public function __construct(
public function list()
{
return $this->apiListingResponse(Attachment::visible(), [
'id', 'name', 'extension', 'uploaded_to', 'external', 'order', 'created_at', 'updated_at', 'created_by', 'updated_by',
'id', 'name', 'extension', 'uploaded_to', 'external', 'order', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by'
]);
}

@@ -54,12 +54,13 @@ public function create(Request $request)

if ($request->hasFile('file')) {
$uploadedFile = $request->file('file');
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $page->id);
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $page->id, $page->owned_by);
} else {
$attachment = $this->attachmentService->saveNewFromLink(
$requestData['name'],
$requestData['link'],
$page->id
$page->id,
$page->owned_by,
);
}

5 changes: 3 additions & 2 deletions app/Uploads/Controllers/AttachmentController.php
Original file line number Diff line number Diff line change
@@ -46,7 +46,8 @@ public function upload(Request $request)
$uploadedFile = $request->file('file');

try {
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId);
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId, $page->owned_by);
$attachment->indexForSearch();
} catch (FileUploadException $e) {
return response($e->getMessage(), 500);
}
@@ -161,7 +162,7 @@ public function attachLink(Request $request)

$attachmentName = $request->get('attachment_link_name');
$link = $request->get('attachment_link_url');
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId), $page->owned_by);

return view('attachments.manager-link-form', [
'pageId' => $pageId,
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('attachments', function (Blueprint $table) {
$table->integer('owned_by')->after('updated_by');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('attachments', function (Blueprint $table) {
//
});
}
};
2 changes: 1 addition & 1 deletion resources/views/entities/list-item-basic.blade.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?php $type = $entity->getType(); ?>
<a href="{{ $entity->getUrl() }}" class="{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}} {{$classes ?? ''}} entity-list-item" data-entity-type="{{$type}}" data-entity-id="{{$entity->id}}">
<a href="{{ $entity->page ? $entity->page->getUrl() : $entity->getUrl() }}" class="{{$type}} {{$type === 'page' && $entity->draft ? 'draft' : ''}} {{$classes ?? ''}} entity-list-item" data-entity-type="{{$type}}" data-entity-id="{{$entity->id}}">
<span role="presentation" class="icon text-{{$type}}">@icon($type)</span>
<div class="content">
<h4 class="entity-list-item-name break-text">{{ $entity->preview_name ?? $entity->name }}</h4>
9 changes: 9 additions & 0 deletions resources/views/entities/list-item.blade.php
Original file line number Diff line number Diff line change
@@ -15,6 +15,15 @@
<span class="text-muted entity-list-item-path-sep">@icon('chevron-right')</span> <span class="text-chapter">{{ $entity->chapter->getShortName(42) }}</span>
@endif
@endif
@if($entity->relationLoaded('page') && $entity->page)
<span class="text-page">{{ $entity->page->getShortName(42) }}</span>
@if($entity->page->chapter)
<span class="text-muted entity-list-item-path-sep">@icon('chevron-right')</span> <span class="text-chapter">{{ $entity->page->chapter->getShortName(42) }}</span>
@if($entity->page->book)
<span class="text-muted entity-list-item-path-sep">@icon('chevron-right')</span> <span class="text-book">{{ $entity->page->book->getShortName(42) }}</span>
@endif
@endif
@endif
@endif

<p class="text-muted break-text">{{ $entity->preview_content ?? $entity->getExcerpt() }}</p>
2 changes: 2 additions & 0 deletions resources/views/search/all.blade.php
Original file line number Diff line number Diff line change
@@ -27,6 +27,8 @@
<br>
@include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('book', $types), 'entity' => 'book', 'transKey' => 'book'])
@include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('bookshelf', $types), 'entity' => 'bookshelf', 'transKey' => 'shelf'])
<br>
@include('search.parts.type-filter', ['checked' => !$hasTypes || in_array('attachment', $types), 'entity' => 'attachment', 'transKey' => 'attachments'])
</div>

<h6>{{ trans('entities.search_exact_matches') }}</h6>
21 changes: 21 additions & 0 deletions tests/Entity/EntitySearchTest.php
Original file line number Diff line number Diff line change
@@ -77,6 +77,27 @@ public function test_chapter_search()
$pageTestResp->assertSee($page->name);
}

public function test_attachment_search()
{
$page = $this->entities->page();

$admin = $this->users->admin();
/** @var Attachment $attachment */
$attachment = $page->attachments()->forceCreate([
'uploaded_to' => $page->id,
'name' => 'My test attachment',
'external' => true,
'order' => 1,
'created_by' => $admin->id,
'updated_by' => $admin->id,
'path' => 'https://attachment.example.com',
]);

$search = $this->asEditor()->get('/search?term=' . urlencode($attachment->name));
$search->assertSee('Search Results');
$search->assertSeeText($attachment->name, true);
}

public function test_tag_search()
{
$newTags = [
1 change: 1 addition & 0 deletions tests/Uploads/AttachmentTest.php
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ public function test_file_upload()
'order' => 1,
'created_by' => $admin->id,
'updated_by' => $admin->id,
'owned_by' => $page->owned_by,
];

$upload = $this->files->uploadAttachmentFile($this, $fileName, $page->id);