mirror of
				https://github.com/BookStackApp/BookStack.git
				synced 2025-11-03 02:13:16 +03:00 
			
		
		
		
	@@ -6,11 +6,17 @@ use BookStack\Activity\Models\Favouritable;
 | 
				
			|||||||
use BookStack\App\Model;
 | 
					use BookStack\App\Model;
 | 
				
			||||||
use BookStack\Entities\Models\Entity;
 | 
					use BookStack\Entities\Models\Entity;
 | 
				
			||||||
use BookStack\Entities\Queries\TopFavourites;
 | 
					use BookStack\Entities\Queries\TopFavourites;
 | 
				
			||||||
 | 
					use BookStack\Entities\Tools\MixedEntityRequestHelper;
 | 
				
			||||||
use BookStack\Http\Controller;
 | 
					use BookStack\Http\Controller;
 | 
				
			||||||
use Illuminate\Http\Request;
 | 
					use Illuminate\Http\Request;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FavouriteController extends Controller
 | 
					class FavouriteController extends Controller
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
					    public function __construct(
 | 
				
			||||||
 | 
					        protected MixedEntityRequestHelper $entityHelper,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Show a listing of all favourite items for the current user.
 | 
					     * Show a listing of all favourite items for the current user.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
@@ -36,13 +42,14 @@ class FavouriteController extends Controller
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    public function add(Request $request)
 | 
					    public function add(Request $request)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $favouritable = $this->getValidatedModelFromRequest($request);
 | 
					        $modelInfo = $this->validate($request, $this->entityHelper->validationRules());
 | 
				
			||||||
        $favouritable->favourites()->firstOrCreate([
 | 
					        $entity = $this->entityHelper->getVisibleEntityFromRequestData($modelInfo);
 | 
				
			||||||
 | 
					        $entity->favourites()->firstOrCreate([
 | 
				
			||||||
            'user_id' => user()->id,
 | 
					            'user_id' => user()->id,
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->showSuccessNotification(trans('activities.favourite_add_notification', [
 | 
					        $this->showSuccessNotification(trans('activities.favourite_add_notification', [
 | 
				
			||||||
            'name' => $favouritable->name,
 | 
					            'name' => $entity->name,
 | 
				
			||||||
        ]));
 | 
					        ]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return redirect()->back();
 | 
					        return redirect()->back();
 | 
				
			||||||
@@ -53,48 +60,16 @@ class FavouriteController extends Controller
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    public function remove(Request $request)
 | 
					    public function remove(Request $request)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $favouritable = $this->getValidatedModelFromRequest($request);
 | 
					        $modelInfo = $this->validate($request, $this->entityHelper->validationRules());
 | 
				
			||||||
        $favouritable->favourites()->where([
 | 
					        $entity = $this->entityHelper->getVisibleEntityFromRequestData($modelInfo);
 | 
				
			||||||
 | 
					        $entity->favourites()->where([
 | 
				
			||||||
            'user_id' => user()->id,
 | 
					            'user_id' => user()->id,
 | 
				
			||||||
        ])->delete();
 | 
					        ])->delete();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $this->showSuccessNotification(trans('activities.favourite_remove_notification', [
 | 
					        $this->showSuccessNotification(trans('activities.favourite_remove_notification', [
 | 
				
			||||||
            'name' => $favouritable->name,
 | 
					            'name' => $entity->name,
 | 
				
			||||||
        ]));
 | 
					        ]));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return redirect()->back();
 | 
					        return redirect()->back();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @throws \Illuminate\Validation\ValidationException
 | 
					 | 
				
			||||||
     * @throws \Exception
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected function getValidatedModelFromRequest(Request $request): Entity
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $modelInfo = $this->validate($request, [
 | 
					 | 
				
			||||||
            'type' => ['required', 'string'],
 | 
					 | 
				
			||||||
            'id'   => ['required', 'integer'],
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!class_exists($modelInfo['type'])) {
 | 
					 | 
				
			||||||
            throw new \Exception('Model not found');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /** @var Model $model */
 | 
					 | 
				
			||||||
        $model = new $modelInfo['type']();
 | 
					 | 
				
			||||||
        if (!$model instanceof Favouritable) {
 | 
					 | 
				
			||||||
            throw new \Exception('Model not favouritable');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $modelInstance = $model->newQuery()
 | 
					 | 
				
			||||||
            ->where('id', '=', $modelInfo['id'])
 | 
					 | 
				
			||||||
            ->first(['id', 'name', 'owned_by']);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
 | 
					 | 
				
			||||||
        if (is_null($modelInstance) || $inaccessibleEntity) {
 | 
					 | 
				
			||||||
            throw new \Exception('Model instance not found');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return $modelInstance;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,25 +3,23 @@
 | 
				
			|||||||
namespace BookStack\Activity\Controllers;
 | 
					namespace BookStack\Activity\Controllers;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use BookStack\Activity\Tools\UserEntityWatchOptions;
 | 
					use BookStack\Activity\Tools\UserEntityWatchOptions;
 | 
				
			||||||
use BookStack\App\Model;
 | 
					use BookStack\Entities\Tools\MixedEntityRequestHelper;
 | 
				
			||||||
use BookStack\Entities\Models\Entity;
 | 
					 | 
				
			||||||
use BookStack\Http\Controller;
 | 
					use BookStack\Http\Controller;
 | 
				
			||||||
use Exception;
 | 
					 | 
				
			||||||
use Illuminate\Http\Request;
 | 
					use Illuminate\Http\Request;
 | 
				
			||||||
use Illuminate\Validation\ValidationException;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WatchController extends Controller
 | 
					class WatchController extends Controller
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public function update(Request $request)
 | 
					    public function update(Request $request, MixedEntityRequestHelper $entityHelper)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $this->checkPermission('receive-notifications');
 | 
					        $this->checkPermission('receive-notifications');
 | 
				
			||||||
        $this->preventGuestAccess();
 | 
					        $this->preventGuestAccess();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $requestData = $this->validate($request, [
 | 
					        $requestData = $this->validate($request, [
 | 
				
			||||||
            'level' => ['required', 'string'],
 | 
					            'level' => ['required', 'string'],
 | 
				
			||||||
 | 
					            ...$entityHelper->validationRules()
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $watchable = $this->getValidatedModelFromRequest($request);
 | 
					        $watchable = $entityHelper->getVisibleEntityFromRequestData($requestData);
 | 
				
			||||||
        $watchOptions = new UserEntityWatchOptions(user(), $watchable);
 | 
					        $watchOptions = new UserEntityWatchOptions(user(), $watchable);
 | 
				
			||||||
        $watchOptions->updateLevelByName($requestData['level']);
 | 
					        $watchOptions->updateLevelByName($requestData['level']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,37 +27,4 @@ class WatchController extends Controller
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return redirect()->back();
 | 
					        return redirect()->back();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @throws ValidationException
 | 
					 | 
				
			||||||
     * @throws Exception
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    protected function getValidatedModelFromRequest(Request $request): Entity
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        $modelInfo = $this->validate($request, [
 | 
					 | 
				
			||||||
            'type' => ['required', 'string'],
 | 
					 | 
				
			||||||
            'id'   => ['required', 'integer'],
 | 
					 | 
				
			||||||
        ]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (!class_exists($modelInfo['type'])) {
 | 
					 | 
				
			||||||
            throw new Exception('Model not found');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        /** @var Model $model */
 | 
					 | 
				
			||||||
        $model = new $modelInfo['type']();
 | 
					 | 
				
			||||||
        if (!$model instanceof Entity) {
 | 
					 | 
				
			||||||
            throw new Exception('Model not an entity');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $modelInstance = $model->newQuery()
 | 
					 | 
				
			||||||
            ->where('id', '=', $modelInfo['id'])
 | 
					 | 
				
			||||||
            ->first(['id', 'name', 'owned_by']);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        $inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
 | 
					 | 
				
			||||||
        if (is_null($modelInstance) || $inaccessibleEntity) {
 | 
					 | 
				
			||||||
            throw new Exception('Model instance not found');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return $modelInstance;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										39
									
								
								app/Entities/Tools/MixedEntityRequestHelper.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								app/Entities/Tools/MixedEntityRequestHelper.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					<?php
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace BookStack\Entities\Tools;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use BookStack\Entities\EntityProvider;
 | 
				
			||||||
 | 
					use BookStack\Entities\Models\Entity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class MixedEntityRequestHelper
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    public function __construct(
 | 
				
			||||||
 | 
					        protected EntityProvider $entities,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Query out an entity, visible to the current user, for the given
 | 
				
			||||||
 | 
					     * entity request details (this provided in a request validated by
 | 
				
			||||||
 | 
					     * this classes' validationRules method).
 | 
				
			||||||
 | 
					     * @param array{type: string, id: string} $requestData
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function getVisibleEntityFromRequestData(array $requestData): Entity
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        $entityType = $this->entities->get($requestData['type']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return $entityType->newQuery()->scopes(['visible'])->findOrFail($requestData['id']);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /**
 | 
				
			||||||
 | 
					     * Get the validation rules for an abstract entity request.
 | 
				
			||||||
 | 
					     * @return array{type: string[], id: string[]}
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    public function validationRules(): array
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					                'type' => ['required', 'string'],
 | 
				
			||||||
 | 
					                'id'   => ['required', 'integer'],
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
@endphp
 | 
					@endphp
 | 
				
			||||||
<form action="{{ url('/favourites/' . ($isFavourite ? 'remove' : 'add')) }}" method="POST">
 | 
					<form action="{{ url('/favourites/' . ($isFavourite ? 'remove' : 'add')) }}" method="POST">
 | 
				
			||||||
    {{ csrf_field() }}
 | 
					    {{ csrf_field() }}
 | 
				
			||||||
    <input type="hidden" name="type" value="{{ get_class($entity) }}">
 | 
					    <input type="hidden" name="type" value="{{ $entity->getMorphClass() }}">
 | 
				
			||||||
    <input type="hidden" name="id" value="{{ $entity->id }}">
 | 
					    <input type="hidden" name="id" value="{{ $entity->id }}">
 | 
				
			||||||
    <button type="submit" data-shortcut="favourite" class="icon-list-item text-link">
 | 
					    <button type="submit" data-shortcut="favourite" class="icon-list-item text-link">
 | 
				
			||||||
        <span>@icon($isFavourite ? 'star' : 'star-outline')</span>
 | 
					        <span>@icon($isFavourite ? 'star' : 'star-outline')</span>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
<form action="{{ url('/watching/update') }}" method="POST">
 | 
					<form action="{{ url('/watching/update') }}" method="POST">
 | 
				
			||||||
    {{ csrf_field() }}
 | 
					    {{ csrf_field() }}
 | 
				
			||||||
    {{ method_field('PUT') }}
 | 
					    {{ method_field('PUT') }}
 | 
				
			||||||
    <input type="hidden" name="type" value="{{ get_class($entity) }}">
 | 
					    <input type="hidden" name="type" value="{{ $entity->getMorphClass() }}">
 | 
				
			||||||
    <input type="hidden" name="id" value="{{ $entity->id }}">
 | 
					    <input type="hidden" name="id" value="{{ $entity->id }}">
 | 
				
			||||||
    <button type="submit"
 | 
					    <button type="submit"
 | 
				
			||||||
            name="level"
 | 
					            name="level"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@
 | 
				
			|||||||
    <form action="{{ url('/watching/update') }}" method="POST">
 | 
					    <form action="{{ url('/watching/update') }}" method="POST">
 | 
				
			||||||
        {{ method_field('PUT') }}
 | 
					        {{ method_field('PUT') }}
 | 
				
			||||||
        {{ csrf_field() }}
 | 
					        {{ csrf_field() }}
 | 
				
			||||||
        <input type="hidden" name="type" value="{{ get_class($entity) }}">
 | 
					        <input type="hidden" name="type" value="{{ $entity->getMorphClass() }}">
 | 
				
			||||||
        <input type="hidden" name="id" value="{{ $entity->id }}">
 | 
					        <input type="hidden" name="id" value="{{ $entity->id }}">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
 | 
					        <ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -66,7 +66,7 @@ class WatchTest extends TestCase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $this->actingAs($editor)->get($book->getUrl());
 | 
					        $this->actingAs($editor)->get($book->getUrl());
 | 
				
			||||||
        $resp = $this->put('/watching/update', [
 | 
					        $resp = $this->put('/watching/update', [
 | 
				
			||||||
            'type' => get_class($book),
 | 
					            'type' => $book->getMorphClass(),
 | 
				
			||||||
            'id' => $book->id,
 | 
					            'id' => $book->id,
 | 
				
			||||||
            'level' => 'comments'
 | 
					            'level' => 'comments'
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
@@ -81,7 +81,7 @@ class WatchTest extends TestCase
 | 
				
			|||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $resp = $this->put('/watching/update', [
 | 
					        $resp = $this->put('/watching/update', [
 | 
				
			||||||
            'type' => get_class($book),
 | 
					            'type' => $book->getMorphClass(),
 | 
				
			||||||
            'id' => $book->id,
 | 
					            'id' => $book->id,
 | 
				
			||||||
            'level' => 'default'
 | 
					            'level' => 'default'
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
@@ -101,7 +101,7 @@ class WatchTest extends TestCase
 | 
				
			|||||||
        $book = $this->entities->book();
 | 
					        $book = $this->entities->book();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $resp = $this->put('/watching/update', [
 | 
					        $resp = $this->put('/watching/update', [
 | 
				
			||||||
            'type' => get_class($book),
 | 
					            'type' => $book->getMorphClass(),
 | 
				
			||||||
            'id' => $book->id,
 | 
					            'id' => $book->id,
 | 
				
			||||||
            'level' => 'comments'
 | 
					            'level' => 'comments'
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,10 +14,10 @@ class FavouriteTest extends TestCase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $resp = $this->actingAs($editor)->get($page->getUrl());
 | 
					        $resp = $this->actingAs($editor)->get($page->getUrl());
 | 
				
			||||||
        $this->withHtml($resp)->assertElementContains('button', 'Favourite');
 | 
					        $this->withHtml($resp)->assertElementContains('button', 'Favourite');
 | 
				
			||||||
        $this->withHtml($resp)->assertElementExists('form[method="POST"][action$="/favourites/add"]');
 | 
					        $this->withHtml($resp)->assertElementExists('form[method="POST"][action$="/favourites/add"] input[name="type"][value="page"]');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $resp = $this->post('/favourites/add', [
 | 
					        $resp = $this->post('/favourites/add', [
 | 
				
			||||||
            'type' => get_class($page),
 | 
					            'type' => $page->getMorphClass(),
 | 
				
			||||||
            'id'   => $page->id,
 | 
					            'id'   => $page->id,
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
        $resp->assertRedirect($page->getUrl());
 | 
					        $resp->assertRedirect($page->getUrl());
 | 
				
			||||||
@@ -45,7 +45,7 @@ class FavouriteTest extends TestCase
 | 
				
			|||||||
        $this->withHtml($resp)->assertElementExists('form[method="POST"][action$="/favourites/remove"]');
 | 
					        $this->withHtml($resp)->assertElementExists('form[method="POST"][action$="/favourites/remove"]');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $resp = $this->post('/favourites/remove', [
 | 
					        $resp = $this->post('/favourites/remove', [
 | 
				
			||||||
            'type' => get_class($page),
 | 
					            'type' => $page->getMorphClass(),
 | 
				
			||||||
            'id'   => $page->id,
 | 
					            'id'   => $page->id,
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
        $resp->assertRedirect($page->getUrl());
 | 
					        $resp->assertRedirect($page->getUrl());
 | 
				
			||||||
@@ -67,7 +67,7 @@ class FavouriteTest extends TestCase
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        $this->actingAs($user)->get($book->getUrl());
 | 
					        $this->actingAs($user)->get($book->getUrl());
 | 
				
			||||||
        $resp = $this->post('/favourites/add', [
 | 
					        $resp = $this->post('/favourites/add', [
 | 
				
			||||||
            'type' => get_class($book),
 | 
					            'type' => $book->getMorphClass(),
 | 
				
			||||||
            'id'   => $book->id,
 | 
					            'id'   => $book->id,
 | 
				
			||||||
        ]);
 | 
					        ]);
 | 
				
			||||||
        $resp->assertRedirect($book->getUrl());
 | 
					        $resp->assertRedirect($book->getUrl());
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user