Started change for entities to have concept of owners

This commit is contained in:
Dan Brown 2020-12-30 18:25:35 +00:00
parent c71f00b2ec
commit b493becadf
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
12 changed files with 151 additions and 57 deletions

View File

@ -1,6 +1,8 @@
<?php namespace BookStack\Actions; <?php namespace BookStack\Actions;
use BookStack\Ownable; use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/** /**
* @property string text * @property string text
@ -8,25 +10,25 @@ use BookStack\Ownable;
* @property int|null parent_id * @property int|null parent_id
* @property int local_id * @property int local_id
*/ */
class Comment extends Ownable class Comment extends Model
{ {
use HasCreatorAndUpdater;
protected $fillable = ['text', 'parent_id']; protected $fillable = ['text', 'parent_id'];
protected $appends = ['created', 'updated']; protected $appends = ['created', 'updated'];
/** /**
* Get the entity that this comment belongs to * Get the entity that this comment belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/ */
public function entity() public function entity(): MorphTo
{ {
return $this->morphTo('entity'); return $this->morphTo('entity');
} }
/** /**
* Check if a comment has been updated since creation. * Check if a comment has been updated since creation.
* @return bool
*/ */
public function isUpdated() public function isUpdated(): bool
{ {
return $this->updated_at->timestamp > $this->created_at->timestamp; return $this->updated_at->timestamp > $this->created_at->timestamp;
} }

View File

@ -5,7 +5,9 @@ use BookStack\Auth\Role;
use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Entity; use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider; use BookStack\Entities\EntityProvider;
use BookStack\Ownable; use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
use Illuminate\Database\Connection; use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\Query\Builder as QueryBuilder;
@ -168,7 +170,7 @@ class PermissionService
}); });
// Chunk through all bookshelves // Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->withTrashed()->select(['id', 'restricted', 'created_by']) $this->entityProvider->bookshelf->newQuery()->withTrashed()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) { ->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles); $this->buildJointPermissionsForShelves($shelves, $roles);
}); });
@ -181,10 +183,10 @@ class PermissionService
protected function bookFetchQuery() protected function bookFetchQuery()
{ {
return $this->entityProvider->book->withTrashed()->newQuery() return $this->entityProvider->book->withTrashed()->newQuery()
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) { ->select(['id', 'restricted', 'owned_by'])->with(['chapters' => function ($query) {
$query->withTrashed()->select(['id', 'restricted', 'created_by', 'book_id']); $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
}, 'pages' => function ($query) { }, 'pages' => function ($query) {
$query->withTrashed()->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']); $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
}]); }]);
} }
@ -286,7 +288,7 @@ class PermissionService
}); });
// Chunk through all bookshelves // Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by']) $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) { ->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles); $this->buildJointPermissionsForShelves($shelves, $roles);
}); });
@ -508,28 +510,24 @@ class PermissionService
'action' => $action, 'action' => $action,
'has_permission' => $permissionAll, 'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn, 'has_permission_own' => $permissionOwn,
'created_by' => $entity->getRawAttribute('created_by') 'owned_by' => $entity->getRawAttribute('owned_by')
]; ];
} }
/** /**
* Checks if an entity has a restriction set upon it. * Checks if an entity has a restriction set upon it.
* @param Ownable $ownable * @param HasCreatorAndUpdater|HasOwner $ownable
* @param $permission
* @return bool
*/ */
public function checkOwnableUserAccess(Ownable $ownable, $permission) public function checkOwnableUserAccess(Model $ownable, string $permission): bool
{ {
$explodedPermission = explode('-', $permission); $explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id); $baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id);
$action = end($explodedPermission); $action = end($explodedPermission);
$this->currentAction = $action; $this->currentAction = $action;
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
// Handle non entity specific jointPermissions // Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) { if (!($ownable instanceof Entity)) {
$allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all'); $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
$ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own'); $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
$this->currentAction = 'view'; $this->currentAction = 'view';
@ -566,7 +564,7 @@ class PermissionService
$query->where('has_permission', '=', 1) $query->where('has_permission', '=', 1)
->orWhere(function ($query2) use ($userId) { ->orWhere(function ($query2) use ($userId) {
$query2->where('has_permission_own', '=', 1) $query2->where('has_permission_own', '=', 1)
->where('created_by', '=', $userId); ->where('owned_by', '=', $userId);
}); });
}); });
@ -615,7 +613,7 @@ class PermissionService
$query->where('has_permission', '=', true) $query->where('has_permission', '=', true)
->orWhere(function ($query) { ->orWhere(function ($query) {
$query->where('has_permission_own', '=', true) $query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id); ->where('owned_by', '=', $this->currentUser()->id);
}); });
}); });
}); });
@ -639,7 +637,7 @@ class PermissionService
$query->where('has_permission', '=', true) $query->where('has_permission', '=', true)
->orWhere(function (Builder $query) { ->orWhere(function (Builder $query) {
$query->where('has_permission_own', '=', true) $query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id); ->where('owned_by', '=', $this->currentUser()->id);
}); });
}); });
}); });
@ -656,7 +654,7 @@ class PermissionService
$query->where('draft', '=', false) $query->where('draft', '=', false)
->orWhere(function (Builder $query) { ->orWhere(function (Builder $query) {
$query->where('draft', '=', true) $query->where('draft', '=', true)
->where('created_by', '=', $this->currentUser()->id); ->where('owned_by', '=', $this->currentUser()->id);
}); });
}); });
} }
@ -676,7 +674,7 @@ class PermissionService
$query->where('draft', '=', false) $query->where('draft', '=', false)
->orWhere(function ($query) { ->orWhere(function ($query) {
$query->where('draft', '=', true) $query->where('draft', '=', true)
->where('created_by', '=', $this->currentUser()->id); ->where('owned_by', '=', $this->currentUser()->id);
}); });
}); });
} }
@ -710,7 +708,7 @@ class PermissionService
->where(function ($query) { ->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) { $query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true) $query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id); ->where('owned_by', '=', $this->currentUser()->id);
}); });
}); });
}); });
@ -746,7 +744,7 @@ class PermissionService
->where(function ($query) { ->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) { $query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true) $query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id); ->where('owned_by', '=', $this->currentUser()->id);
}); });
}); });
}); });

View File

@ -9,7 +9,9 @@ use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Tools\SearchIndex; use BookStack\Entities\Tools\SearchIndex;
use BookStack\Entities\Tools\SlugGenerator; use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Facades\Permissions; use BookStack\Facades\Permissions;
use BookStack\Ownable; use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
@ -35,9 +37,11 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static Builder withLastView() * @method static Builder withLastView()
* @method static Builder withViewCount() * @method static Builder withViewCount()
*/ */
abstract class Entity extends Ownable abstract class Entity extends Model
{ {
use SoftDeletes; use SoftDeletes;
use HasCreatorAndUpdater;
use HasOwner;
/** /**
* @var string - Name of property where the main text content is found * @var string - Name of property where the main text content is found

View File

@ -4,7 +4,8 @@ namespace BookStack\Http\Controllers;
use BookStack\Facades\Activity; use BookStack\Facades\Activity;
use BookStack\Interfaces\Loggable; use BookStack\Interfaces\Loggable;
use BookStack\Ownable; use BookStack\HasCreatorAndUpdater;
use BookStack\Model;
use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Http\Exceptions\HttpResponseException;
@ -72,7 +73,7 @@ abstract class Controller extends BaseController
/** /**
* Check the current user's permissions against an ownable item otherwise throw an exception. * Check the current user's permissions against an ownable item otherwise throw an exception.
*/ */
protected function checkOwnablePermission(string $permission, Ownable $ownable): void protected function checkOwnablePermission(string $permission, Model $ownable): void
{ {
if (!userCan($permission, $ownable)) { if (!userCan($permission, $ownable)) {
$this->showPermissionError(); $this->showPermissionError();

View File

@ -1,27 +1,26 @@
<?php namespace BookStack; <?php namespace BookStack\Traits;
use BookStack\Auth\User; use BookStack\Auth\User;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/** /**
* @property int created_by * @property int created_by
* @property int updated_by * @property int updated_by
*/ */
abstract class Ownable extends Model trait HasCreatorAndUpdater
{ {
/** /**
* Relation for the user that created this entity. * Relation for the user that created this entity.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/ */
public function createdBy() public function createdBy(): BelongsTo
{ {
return $this->belongsTo(User::class, 'created_by'); return $this->belongsTo(User::class, 'created_by');
} }
/** /**
* Relation for the user that updated this entity. * Relation for the user that updated this entity.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/ */
public function updatedBy() public function updatedBy(): BelongsTo
{ {
return $this->belongsTo(User::class, 'updated_by'); return $this->belongsTo(User::class, 'updated_by');
} }

19
app/Traits/HasOwner.php Normal file
View File

@ -0,0 +1,19 @@
<?php namespace BookStack\Traits;
use BookStack\Auth\User;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int owned_by
*/
trait HasOwner
{
/**
* Relation for the user that owns this entity.
*/
public function ownedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'owned_by');
}
}

View File

@ -1,7 +1,8 @@
<?php namespace BookStack\Uploads; <?php namespace BookStack\Uploads;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Ownable; use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
/** /**
* @property int id * @property int id
@ -10,8 +11,10 @@ use BookStack\Ownable;
* @property string extension * @property string extension
* @property bool external * @property bool external
*/ */
class Attachment extends Ownable class Attachment extends Model
{ {
use HasCreatorAndUpdater;
protected $fillable = ['name', 'order']; protected $fillable = ['name', 'order'];
/** /**

View File

@ -1,11 +1,13 @@
<?php namespace BookStack\Uploads; <?php namespace BookStack\Uploads;
use BookStack\Entities\Models\Page; use BookStack\Entities\Models\Page;
use BookStack\Ownable; use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Images; use Images;
class Image extends Ownable class Image extends Model
{ {
use HasCreatorAndUpdater;
protected $fillable = ['name']; protected $fillable = ['name'];
protected $hidden = []; protected $hidden = [];

View File

@ -2,7 +2,7 @@
use BookStack\Auth\Permissions\PermissionService; use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User; use BookStack\Auth\User;
use BookStack\Ownable; use BookStack\Model;
use BookStack\Settings\SettingService; use BookStack\Settings\SettingService;
/** /**
@ -56,7 +56,7 @@ function hasAppAccess(): bool
* Check if the current user has a permission. If an ownable element * Check if the current user has a permission. If an ownable element
* is passed in the jointPermissions are checked against that particular item. * is passed in the jointPermissions are checked against that particular item.
*/ */
function userCan(string $permission, Ownable $ownable = null): bool function userCan(string $permission, Model $ownable = null): bool
{ {
if ($ownable === null) { if ($ownable === null) {
return user() && user()->can($permission); return user() && user()->can($permission);

View File

@ -0,0 +1,49 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class AddOwnedByFieldToEntities extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$tables = ['pages', 'books', 'chapters', 'bookshelves'];
foreach ($tables as $table) {
Schema::table($table, function (Blueprint $table) {
$table->integer('owned_by')->unsigned()->index();
});
DB::table($table)->update(['owned_by' => DB::raw('`created_by`')]);
}
Schema::table('joint_permissions', function (Blueprint $table) {
$table->renameColumn('created_by', 'owned_by');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$tables = ['pages', 'books', 'chapters', 'bookshelves'];
foreach ($tables as $table) {
Schema::table($table, function (Blueprint $table) {
$table->dropColumn('owned_by');
});
}
Schema::table('joint_permissions', function (Blueprint $table) {
$table->renameColumn('owned_by', 'created_by');
});
}
}

View File

@ -22,6 +22,7 @@ return [
'meta_created_name' => 'Created :timeLength by :user', 'meta_created_name' => 'Created :timeLength by :user',
'meta_updated' => 'Updated :timeLength', 'meta_updated' => 'Updated :timeLength',
'meta_updated_name' => 'Updated :timeLength by :user', 'meta_updated_name' => 'Updated :timeLength by :user',
'meta_owned_name' => 'Owned by :user',
'entity_select' => 'Entity Select', 'entity_select' => 'Entity Select',
'images' => 'Images', 'images' => 'Images',
'my_recent_drafts' => 'My Recent Drafts', 'my_recent_drafts' => 'My Recent Drafts',

View File

@ -1,34 +1,50 @@
<div class="entity-meta"> <div class="entity-meta">
@if($entity->isA('revision')) @if($entity->isA('revision'))
@icon('history'){{ trans('entities.pages_revision') }} <div>
{{ trans('entities.pages_revisions_number') }}{{ $entity->revision_number == 0 ? '' : $entity->revision_number }} @icon('history'){{ trans('entities.pages_revision') }}
<br> {{ trans('entities.pages_revisions_number') }}{{ $entity->revision_number == 0 ? '' : $entity->revision_number }}
</div>
@endif @endif
@if ($entity->isA('page')) @if ($entity->isA('page'))
@if (userCan('page-update', $entity)) <a href="{{ $entity->getUrl('/revisions') }}"> @endif <div>
@icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br> @if (userCan('page-update', $entity)) <a href="{{ $entity->getUrl('/revisions') }}"> @endif
@icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }}
@if (userCan('page-update', $entity))</a>@endif @if (userCan('page-update', $entity))</a>@endif
</div>
@endif @endif
@if ($entity->ownedBy && $entity->ownedBy->id !== $entity->createdBy->id)
<div>
@icon('user'){!! trans('entities.meta_owned_name', [
'user' => "<a href='{$entity->ownedBy->getProfileUrl()}'>".e($entity->ownedBy->name). "</a>"
]) !!}
</div>
@endif
@if ($entity->createdBy) @if ($entity->createdBy)
@icon('star'){!! trans('entities.meta_created_name', [ <div>
@icon('star'){!! trans('entities.meta_created_name', [
'timeLength' => '<span title="'.$entity->created_at->toDayDateTimeString().'">'.$entity->created_at->diffForHumans() . '</span>', 'timeLength' => '<span title="'.$entity->created_at->toDayDateTimeString().'">'.$entity->created_at->diffForHumans() . '</span>',
'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>" 'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".e($entity->createdBy->name). "</a>"
]) !!} ]) !!}
</div>
@else @else
@icon('star')<span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span> <div>
@icon('star')<span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
</div>
@endif @endif
<br>
@if ($entity->updatedBy) @if ($entity->updatedBy)
@icon('edit'){!! trans('entities.meta_updated_name', [ <div>
@icon('edit'){!! trans('entities.meta_updated_name', [
'timeLength' => '<span title="' . $entity->updated_at->toDayDateTimeString() .'">' . $entity->updated_at->diffForHumans() .'</span>', 'timeLength' => '<span title="' . $entity->updated_at->toDayDateTimeString() .'">' . $entity->updated_at->diffForHumans() .'</span>',
'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".htmlentities($entity->updatedBy->name). "</a>" 'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".e($entity->updatedBy->name). "</a>"
]) !!} ]) !!}
</div>
@elseif (!$entity->isA('revision')) @elseif (!$entity->isA('revision'))
@icon('edit')<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span> <div>
@icon('edit')<span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
</div>
@endif @endif
</div> </div>