Skip to content

Commit

Permalink
Simplified and aligned handling of mixed entity endpoints
Browse files Browse the repository at this point in the history
Fixes #4444
  • Loading branch information
ssddanbrown committed Sep 10, 2023
1 parent 3928cba commit 2fbf552
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 88 deletions.
53 changes: 14 additions & 39 deletions app/Activity/Controllers/FavouriteController.php
Expand Up @@ -6,11 +6,17 @@
use BookStack\App\Model;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\TopFavourites;
use BookStack\Entities\Tools\MixedEntityRequestHelper;
use BookStack\Http\Controller;
use Illuminate\Http\Request;

class FavouriteController extends Controller
{
public function __construct(
protected MixedEntityRequestHelper $entityHelper,
) {
}

/**
* Show a listing of all favourite items for the current user.
*/
Expand All @@ -36,13 +42,14 @@ public function index(Request $request)
*/
public function add(Request $request)
{
$favouritable = $this->getValidatedModelFromRequest($request);
$favouritable->favourites()->firstOrCreate([
$modelInfo = $this->validate($request, $this->entityHelper->validationRules());
$entity = $this->entityHelper->getVisibleEntityFromRequestData($modelInfo);
$entity->favourites()->firstOrCreate([
'user_id' => user()->id,
]);

$this->showSuccessNotification(trans('activities.favourite_add_notification', [
'name' => $favouritable->name,
'name' => $entity->name,
]));

return redirect()->back();
Expand All @@ -53,48 +60,16 @@ public function add(Request $request)
*/
public function remove(Request $request)
{
$favouritable = $this->getValidatedModelFromRequest($request);
$favouritable->favourites()->where([
$modelInfo = $this->validate($request, $this->entityHelper->validationRules());
$entity = $this->entityHelper->getVisibleEntityFromRequestData($modelInfo);
$entity->favourites()->where([
'user_id' => user()->id,
])->delete();

$this->showSuccessNotification(trans('activities.favourite_remove_notification', [
'name' => $favouritable->name,
'name' => $entity->name,
]));

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;
}
}
43 changes: 4 additions & 39 deletions app/Activity/Controllers/WatchController.php
Expand Up @@ -3,63 +3,28 @@
namespace BookStack\Activity\Controllers;

use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\App\Model;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Tools\MixedEntityRequestHelper;
use BookStack\Http\Controller;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class WatchController extends Controller
{
public function update(Request $request)
public function update(Request $request, MixedEntityRequestHelper $entityHelper)
{
$this->checkPermission('receive-notifications');
$this->preventGuestAccess();

$requestData = $this->validate($request, [
'level' => ['required', 'string'],
...$entityHelper->validationRules()
]);

$watchable = $this->getValidatedModelFromRequest($request);
$watchable = $entityHelper->getVisibleEntityFromRequestData($requestData);
$watchOptions = new UserEntityWatchOptions(user(), $watchable);
$watchOptions->updateLevelByName($requestData['level']);

$this->showSuccessNotification(trans('activities.watch_update_level_notification'));

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 changes: 39 additions & 0 deletions app/Entities/Tools/MixedEntityRequestHelper.php
@@ -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'],
];
}
}
2 changes: 1 addition & 1 deletion resources/views/entities/favourite-action.blade.php
Expand Up @@ -3,7 +3,7 @@
@endphp
<form action="{{ url('/favourites/' . ($isFavourite ? 'remove' : 'add')) }}" method="POST">
{{ 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 }}">
<button type="submit" data-shortcut="favourite" class="icon-list-item text-link">
<span>@icon($isFavourite ? 'star' : 'star-outline')</span>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/entities/watch-action.blade.php
@@ -1,7 +1,7 @@
<form action="{{ url('/watching/update') }}" method="POST">
{{ csrf_field() }}
{{ 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 }}">
<button type="submit"
name="level"
Expand Down
2 changes: 1 addition & 1 deletion resources/views/entities/watch-controls.blade.php
Expand Up @@ -7,7 +7,7 @@ class="dropdown-container block my-xxs">
<form action="{{ url('/watching/update') }}" method="POST">
{{ method_field('PUT') }}
{{ 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 }}">

<ul refs="dropdown@menu" class="dropdown-menu xl-limited anchor-left pb-none">
Expand Down
6 changes: 3 additions & 3 deletions tests/Activity/WatchTest.php
Expand Up @@ -66,7 +66,7 @@ public function test_watch_update()

$this->actingAs($editor)->get($book->getUrl());
$resp = $this->put('/watching/update', [
'type' => get_class($book),
'type' => $book->getMorphClass(),
'id' => $book->id,
'level' => 'comments'
]);
Expand All @@ -81,7 +81,7 @@ public function test_watch_update()
]);

$resp = $this->put('/watching/update', [
'type' => get_class($book),
'type' => $book->getMorphClass(),
'id' => $book->id,
'level' => 'default'
]);
Expand All @@ -101,7 +101,7 @@ public function test_watch_update_fails_for_guest()
$book = $this->entities->book();

$resp = $this->put('/watching/update', [
'type' => get_class($book),
'type' => $book->getMorphClass(),
'id' => $book->id,
'level' => 'comments'
]);
Expand Down
8 changes: 4 additions & 4 deletions tests/FavouriteTest.php
Expand Up @@ -14,10 +14,10 @@ public function test_page_add_favourite_flow()

$resp = $this->actingAs($editor)->get($page->getUrl());
$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', [
'type' => get_class($page),
'type' => $page->getMorphClass(),
'id' => $page->id,
]);
$resp->assertRedirect($page->getUrl());
Expand Down Expand Up @@ -45,7 +45,7 @@ public function test_page_remove_favourite_flow()
$this->withHtml($resp)->assertElementExists('form[method="POST"][action$="/favourites/remove"]');

$resp = $this->post('/favourites/remove', [
'type' => get_class($page),
'type' => $page->getMorphClass(),
'id' => $page->id,
]);
$resp->assertRedirect($page->getUrl());
Expand All @@ -67,7 +67,7 @@ public function test_favourite_flow_with_own_permissions()

$this->actingAs($user)->get($book->getUrl());
$resp = $this->post('/favourites/add', [
'type' => get_class($book),
'type' => $book->getMorphClass(),
'id' => $book->id,
]);
$resp->assertRedirect($book->getUrl());
Expand Down

0 comments on commit 2fbf552

Please sign in to comment.