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
Show file tree
Hide file tree
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
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Up @@ -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.
Expand All @@ -23,6 +24,7 @@ class EntityProvider
public Chapter $chapter;
public Page $page;
public PageRevision $pageRevision;
public Attachment $attachment;

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

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

Expand Down
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
Expand Up @@ -14,6 +14,7 @@ public function __construct(
public ChapterQueries $chapters,
public PageQueries $pages,
public PageRevisionQueries $revisions,
public AttachmentQueries $attachment,
) {
}

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

Expand Down
5 changes: 4 additions & 1 deletion app/Permissions/PermissionApplicator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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'])
Expand Down
2 changes: 2 additions & 0 deletions app/Search/SearchIndex.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions app/Search/SearchRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
48 changes: 41 additions & 7 deletions app/Uploads/Attachment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
*/
Expand All @@ -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');
}

/**
Expand Down Expand Up @@ -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
Expand Up @@ -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);
Expand All @@ -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,
]);

Expand Down Expand Up @@ -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');

Expand All @@ -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,
]);
}
Expand Down
7 changes: 4 additions & 3 deletions app/Uploads/Controllers/AttachmentApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
]);
}

Expand All @@ -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,
);
}

Expand Down
5 changes: 3 additions & 2 deletions app/Uploads/Controllers/AttachmentController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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,
Expand Down
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>
Expand Down
9 changes: 9 additions & 0 deletions resources/views/entities/list-item.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
2 changes: 2 additions & 0 deletions resources/views/search/all.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Loading