diff --git a/app/Actions/Comment.php b/app/Actions/Comment.php index 655d45221..f5269e253 100644 --- a/app/Actions/Comment.php +++ b/app/Actions/Comment.php @@ -1,6 +1,8 @@ <?php namespace BookStack\Actions; -use BookStack\Ownable; +use BookStack\Model; +use BookStack\Traits\HasCreatorAndUpdater; +use Illuminate\Database\Eloquent\Relations\MorphTo; /** * @property string text @@ -8,25 +10,25 @@ use BookStack\Ownable; * @property int|null parent_id * @property int local_id */ -class Comment extends Ownable +class Comment extends Model { + use HasCreatorAndUpdater; + protected $fillable = ['text', 'parent_id']; protected $appends = ['created', 'updated']; /** * 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'); } /** * 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; } diff --git a/app/Auth/Permissions/PermissionService.php b/app/Auth/Permissions/PermissionService.php index 5f4648d58..bd4066936 100644 --- a/app/Auth/Permissions/PermissionService.php +++ b/app/Auth/Permissions/PermissionService.php @@ -5,7 +5,9 @@ use BookStack\Auth\Role; use BookStack\Entities\Models\Book; use BookStack\Entities\Models\Entity; 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\Eloquent\Builder; use Illuminate\Database\Query\Builder as QueryBuilder; @@ -168,7 +170,7 @@ class PermissionService }); // 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) { $this->buildJointPermissionsForShelves($shelves, $roles); }); @@ -181,10 +183,10 @@ class PermissionService protected function bookFetchQuery() { return $this->entityProvider->book->withTrashed()->newQuery() - ->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) { - $query->withTrashed()->select(['id', 'restricted', 'created_by', 'book_id']); + ->select(['id', 'restricted', 'owned_by'])->with(['chapters' => function ($query) { + $query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']); }, '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 - $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by']) + $this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'owned_by']) ->chunk(50, function ($shelves) use ($roles) { $this->buildJointPermissionsForShelves($shelves, $roles); }); @@ -508,28 +510,24 @@ class PermissionService 'action' => $action, 'has_permission' => $permissionAll, '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. - * @param Ownable $ownable - * @param $permission - * @return bool + * @param HasCreatorAndUpdater|HasOwner $ownable */ - public function checkOwnableUserAccess(Ownable $ownable, $permission) + public function checkOwnableUserAccess(Model $ownable, string $permission): bool { $explodedPermission = explode('-', $permission); - $baseQuery = $ownable->where('id', '=', $ownable->id); + $baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id); $action = end($explodedPermission); $this->currentAction = $action; - $nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment']; - // Handle non entity specific jointPermissions - if (in_array($explodedPermission[0], $nonJointPermissions)) { + if (!($ownable instanceof Entity)) { $allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all'); $ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own'); $this->currentAction = 'view'; @@ -566,7 +564,7 @@ class PermissionService $query->where('has_permission', '=', 1) ->orWhere(function ($query2) use ($userId) { $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) ->orWhere(function ($query) { $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) ->orWhere(function (Builder $query) { $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) ->orWhere(function (Builder $query) { $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) ->orWhere(function ($query) { $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) { $query->where('has_permission', '=', true)->orWhere(function ($query) { $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) { $query->where('has_permission', '=', true)->orWhere(function ($query) { $query->where('has_permission_own', '=', true) - ->where('created_by', '=', $this->currentUser()->id); + ->where('owned_by', '=', $this->currentUser()->id); }); }); }); diff --git a/app/Entities/Models/Entity.php b/app/Entities/Models/Entity.php index e681a4e22..c6b2468b0 100644 --- a/app/Entities/Models/Entity.php +++ b/app/Entities/Models/Entity.php @@ -9,7 +9,9 @@ use BookStack\Auth\Permissions\JointPermission; use BookStack\Entities\Tools\SearchIndex; use BookStack\Entities\Tools\SlugGenerator; use BookStack\Facades\Permissions; -use BookStack\Ownable; +use BookStack\Model; +use BookStack\Traits\HasCreatorAndUpdater; +use BookStack\Traits\HasOwner; use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; @@ -35,9 +37,11 @@ use Illuminate\Database\Eloquent\SoftDeletes; * @method static Builder withLastView() * @method static Builder withViewCount() */ -abstract class Entity extends Ownable +abstract class Entity extends Model { use SoftDeletes; + use HasCreatorAndUpdater; + use HasOwner; /** * @var string - Name of property where the main text content is found diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 758c85dda..479d5ac15 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -4,7 +4,8 @@ namespace BookStack\Http\Controllers; use BookStack\Facades\Activity; use BookStack\Interfaces\Loggable; -use BookStack\Ownable; +use BookStack\HasCreatorAndUpdater; +use BookStack\Model; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; 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. */ - protected function checkOwnablePermission(string $permission, Ownable $ownable): void + protected function checkOwnablePermission(string $permission, Model $ownable): void { if (!userCan($permission, $ownable)) { $this->showPermissionError(); diff --git a/app/Ownable.php b/app/Traits/HasCreatorAndUpdater.php similarity index 59% rename from app/Ownable.php rename to app/Traits/HasCreatorAndUpdater.php index b118bc742..ad6c3035f 100644 --- a/app/Ownable.php +++ b/app/Traits/HasCreatorAndUpdater.php @@ -1,27 +1,26 @@ -<?php namespace BookStack; +<?php namespace BookStack\Traits; use BookStack\Auth\User; +use Illuminate\Database\Eloquent\Relations\BelongsTo; /** * @property int created_by * @property int updated_by */ -abstract class Ownable extends Model +trait HasCreatorAndUpdater { /** * 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'); } /** * 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'); } diff --git a/app/Traits/HasOwner.php b/app/Traits/HasOwner.php new file mode 100644 index 000000000..9d1eb3df7 --- /dev/null +++ b/app/Traits/HasOwner.php @@ -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'); + } + +} diff --git a/app/Uploads/Attachment.php b/app/Uploads/Attachment.php index 77c7925db..d1060477d 100644 --- a/app/Uploads/Attachment.php +++ b/app/Uploads/Attachment.php @@ -1,7 +1,8 @@ <?php namespace BookStack\Uploads; use BookStack\Entities\Models\Page; -use BookStack\Ownable; +use BookStack\Model; +use BookStack\Traits\HasCreatorAndUpdater; /** * @property int id @@ -10,8 +11,10 @@ use BookStack\Ownable; * @property string extension * @property bool external */ -class Attachment extends Ownable +class Attachment extends Model { + use HasCreatorAndUpdater; + protected $fillable = ['name', 'order']; /** diff --git a/app/Uploads/Image.php b/app/Uploads/Image.php index 029fd3175..dc26af002 100644 --- a/app/Uploads/Image.php +++ b/app/Uploads/Image.php @@ -1,11 +1,13 @@ <?php namespace BookStack\Uploads; use BookStack\Entities\Models\Page; -use BookStack\Ownable; +use BookStack\Model; +use BookStack\Traits\HasCreatorAndUpdater; use Images; -class Image extends Ownable +class Image extends Model { + use HasCreatorAndUpdater; protected $fillable = ['name']; protected $hidden = []; diff --git a/app/helpers.php b/app/helpers.php index 935d4d8da..c090bfd05 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -2,7 +2,7 @@ use BookStack\Auth\Permissions\PermissionService; use BookStack\Auth\User; -use BookStack\Ownable; +use BookStack\Model; use BookStack\Settings\SettingService; /** @@ -56,7 +56,7 @@ function hasAppAccess(): bool * Check if the current user has a permission. If an ownable element * 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) { return user() && user()->can($permission); diff --git a/database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php b/database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php new file mode 100644 index 000000000..bf8bf281f --- /dev/null +++ b/database/migrations/2020_12_30_173528_add_owned_by_field_to_entities.php @@ -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'); + }); + } +} diff --git a/resources/lang/en/entities.php b/resources/lang/en/entities.php index 485ecb7bc..5e6a63deb 100644 --- a/resources/lang/en/entities.php +++ b/resources/lang/en/entities.php @@ -22,6 +22,7 @@ return [ 'meta_created_name' => 'Created :timeLength by :user', 'meta_updated' => 'Updated :timeLength', 'meta_updated_name' => 'Updated :timeLength by :user', + 'meta_owned_name' => 'Owned by :user', 'entity_select' => 'Entity Select', 'images' => 'Images', 'my_recent_drafts' => 'My Recent Drafts', diff --git a/resources/views/partials/entity-meta.blade.php b/resources/views/partials/entity-meta.blade.php index f759ea25b..8996df9bb 100644 --- a/resources/views/partials/entity-meta.blade.php +++ b/resources/views/partials/entity-meta.blade.php @@ -1,34 +1,50 @@ <div class="entity-meta"> @if($entity->isA('revision')) - @icon('history'){{ trans('entities.pages_revision') }} - {{ trans('entities.pages_revisions_number') }}{{ $entity->revision_number == 0 ? '' : $entity->revision_number }} - <br> + <div> + @icon('history'){{ trans('entities.pages_revision') }} + {{ trans('entities.pages_revisions_number') }}{{ $entity->revision_number == 0 ? '' : $entity->revision_number }} + </div> @endif @if ($entity->isA('page')) - @if (userCan('page-update', $entity)) <a href="{{ $entity->getUrl('/revisions') }}"> @endif - @icon('history'){{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br> + <div> + @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 + </div> @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) - @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>', - 'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>" + 'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".e($entity->createdBy->name). "</a>" ]) !!} + </div> @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 - <br> - @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>', - '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')) - @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 </div> \ No newline at end of file