<?php namespace BookStack\Actions;
-use BookStack\Ownable;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
+use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* @property string text
* @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;
}
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;
});
// 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);
});
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']);
}]);
}
});
// 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);
});
'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;
$query->where('has_permission', '=', 1)
->orWhere(function ($query2) use ($userId) {
$query2->where('has_permission_own', '=', 1)
- ->where('created_by', '=', $userId);
+ ->where('owned_by', '=', $userId);
});
});
$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);
});
});
});
$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);
});
});
});
$query->where('draft', '=', false)
->orWhere(function (Builder $query) {
$query->where('draft', '=', true)
- ->where('created_by', '=', $this->currentUser()->id);
+ ->where('owned_by', '=', $this->currentUser()->id);
});
});
}
$query->where('draft', '=', false)
->orWhere(function ($query) {
$query->where('draft', '=', true)
- ->where('created_by', '=', $this->currentUser()->id);
+ ->where('owned_by', '=', $this->currentUser()->id);
});
});
}
->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);
});
});
});
->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);
});
});
});
<?php namespace BookStack\Auth;
use Activity;
+use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
* Remove the given user from storage, Delete all related content.
* @throws Exception
*/
- public function destroy(User $user)
+ public function destroy(User $user, ?int $newOwnerId = null)
{
$user->socialAccounts()->delete();
$user->apiTokens()->delete();
foreach ($profileImages as $image) {
Images::destroy($image);
}
+
+ if (!empty($newOwnerId)) {
+ $newOwner = User::query()->find($newOwnerId);
+ if (!is_null($newOwner)) {
+ $this->migrateOwnership($user, $newOwner);
+ }
+ }
+ }
+
+ /**
+ * Migrate ownership of items in the system from one user to another.
+ */
+ protected function migrateOwnership(User $fromUser, User $toUser)
+ {
+ $entities = (new EntityProvider)->all();
+ foreach ($entities as $instance) {
+ $instance->newQuery()->where('owned_by', '=', $fromUser->id)
+ ->update(['owned_by' => $toUser->id]);
+ }
}
/**
/**
* Fetch all core entity types as an associated array
* with their basic names as the keys.
- * @return [string => Entity]
+ * @return array<Entity>
*/
public function all(): array
{
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;
* @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
use BookStack\Actions\ActivityType;
use BookStack\Actions\TagRepo;
+use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage;
use BookStack\Exceptions\ImageUploadException;
$entity->forceFill([
'created_by' => user()->id,
'updated_by' => user()->id,
+ 'owned_by' => user()->id,
]);
$entity->refreshSlug();
$entity->save();
$entity->save();
}
}
-
- /**
- * Update the permissions of an entity.
- */
- public function updatePermissions(Entity $entity, bool $restricted, Collection $permissions = null)
- {
- $entity->restricted = $restricted;
- $entity->permissions()->delete();
-
- if (!is_null($permissions)) {
- $entityPermissionData = $permissions->flatMap(function ($restrictions, $roleId) {
- return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
- return [
- 'role_id' => $roleId,
- 'action' => strtolower($action),
- ] ;
- });
- });
-
- $entity->permissions()->createMany($entityPermissionData);
- }
-
- $entity->save();
- $entity->rebuildPermissions();
- Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE);
- }
}
$this->baseRepo->updateCoverImage($book, $coverImage, $removeImage);
}
- /**
- * Update the permissions of a book.
- */
- public function updatePermissions(Book $book, bool $restricted, Collection $permissions = null)
- {
- $this->baseRepo->updatePermissions($book, $restricted, $permissions);
- }
-
/**
* Remove a book from the system.
* @throws Exception
$this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
}
- /**
- * Update the permissions of a bookshelf.
- */
- public function updatePermissions(Bookshelf $shelf, bool $restricted, Collection $permissions = null)
- {
- $this->baseRepo->updatePermissions($shelf, $restricted, $permissions);
- }
-
/**
* Copy down the permissions of the given shelf to all child books.
*/
return $chapter;
}
- /**
- * Update the permissions of a chapter.
- */
- public function updatePermissions(Chapter $chapter, bool $restricted, Collection $permissions = null)
- {
- $this->baseRepo->updatePermissions($chapter, $restricted, $permissions);
- }
-
/**
* Remove a chapter from the system.
* @throws Exception
$page = (new Page())->forceFill([
'name' => trans('entities.pages_initial_name'),
'created_by' => user()->id,
+ 'owned_by' => user()->id,
'updated_by' => user()->id,
'draft' => true,
]);
return $parentClass::visible()->where('id', '=', $entityId)->first();
}
- /**
- * Update the permissions of a page.
- */
- public function updatePermissions(Page $page, bool $restricted, Collection $permissions = null)
- {
- $this->baseRepo->updatePermissions($page, $restricted, $permissions);
- }
-
/**
* Change the page's parent to the given entity.
*/
--- /dev/null
+<?php namespace BookStack\Entities\Tools;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Entity;
+use BookStack\Facades\Activity;
+use Illuminate\Http\Request;
+use Illuminate\Support\Collection;
+
+class PermissionsUpdater
+{
+
+ /**
+ * Update an entities permissions from a permission form submit request.
+ */
+ public function updateFromPermissionsForm(Entity $entity, Request $request)
+ {
+ $restricted = $request->get('restricted') === 'true';
+ $permissions = $request->get('restrictions', null);
+ $ownerId = $request->get('owned_by', null);
+
+ $entity->restricted = $restricted;
+ $entity->permissions()->delete();
+
+ if (!is_null($permissions)) {
+ $entityPermissionData = $this->formatPermissionsFromRequestToEntityPermissions($permissions);
+ $entity->permissions()->createMany($entityPermissionData);
+ }
+
+ if (!is_null($ownerId)) {
+ $this->updateOwnerFromId($entity, intval($ownerId));
+ }
+
+ $entity->save();
+ $entity->rebuildPermissions();
+
+ Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE);
+ }
+
+ /**
+ * Update the owner of the given entity.
+ * Checks the user exists in the system first.
+ * Does not save the model, just updates it.
+ */
+ protected function updateOwnerFromId(Entity $entity, int $newOwnerId)
+ {
+ $newOwner = User::query()->find($newOwnerId);
+ if (!is_null($newOwner)) {
+ $entity->owned_by = $newOwner->id;
+ }
+ }
+
+ /**
+ * Format permissions provided from a permission form to be
+ * EntityPermission data.
+ */
+ protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): Collection
+ {
+ return collect($permissions)->flatMap(function ($restrictions, $roleId) {
+ return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
+ return [
+ 'role_id' => $roleId,
+ 'action' => strtolower($action),
+ ] ;
+ });
+ });
+ }
+}
use BookStack\Actions\ActivityType;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Exceptions\ImageUploadException;
* Set the restrictions for this book.
* @throws Throwable
*/
- public function permissions(Request $request, string $bookSlug)
+ public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
- $restricted = $request->get('restricted') === 'true';
- $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
- $this->bookRepo->updatePermissions($book, $restricted, $permissions);
+ $permissionsUpdater->updateFromPermissionsForm($book, $request);
$this->showSuccessNotification(trans('entities.books_permissions_updated'));
return redirect($book->getUrl());
use Activity;
use BookStack\Entities\Models\Book;
+use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Exceptions\ImageUploadException;
protected $entityContextManager;
protected $imageRepo;
- /**
- * BookController constructor.
- */
public function __construct(BookshelfRepo $bookshelfRepo, ShelfContext $entityContextManager, ImageRepo $imageRepo)
{
$this->bookshelfRepo = $bookshelfRepo;
/**
* Set the permissions for this bookshelf.
*/
- public function permissions(Request $request, string $slug)
+ public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $slug)
{
$shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
- $restricted = $request->get('restricted') === 'true';
- $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
- $this->bookshelfRepo->updatePermissions($shelf, $restricted, $permissions);
+ $permissionsUpdater->updateFromPermissionsForm($shelf, $request);
$this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
return redirect($shelf->getUrl());
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Repos\ChapterRepo;
+use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Http\Request;
* Set the restrictions for this chapter.
* @throws NotFoundException
*/
- public function permissions(Request $request, string $bookSlug, string $chapterSlug)
+ public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $chapterSlug)
{
$chapter = $this->chapterRepo->getBySlug($bookSlug, $chapterSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
- $restricted = $request->get('restricted') === 'true';
- $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
- $this->chapterRepo->updatePermissions($chapter, $restricted, $permissions);
+ $permissionsUpdater->updateFromPermissionsForm($chapter, $request);
$this->showSuccessNotification(trans('entities.chapters_permissions_success'));
return redirect($chapter->getUrl());
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;
/**
* 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();
use BookStack\Entities\Tools\PageEditActivity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
+use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PermissionsException;
* @throws NotFoundException
* @throws Throwable
*/
- public function permissions(Request $request, string $bookSlug, string $pageSlug)
+ public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug, string $pageSlug)
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
- $restricted = $request->get('restricted') === 'true';
- $permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
- $this->pageRepo->updatePermissions($page, $restricted, $permissions);
+ $permissionsUpdater->updateFromPermissionsForm($page, $request);
$this->showSuccessNotification(trans('entities.pages_permissions_success'));
return redirect($page->getUrl());
* Remove the specified user from storage.
* @throws \Exception
*/
- public function destroy(int $id)
+ public function destroy(Request $request, int $id)
{
$this->preventAccessInDemoMode();
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->userRepo->getById($id);
+ $newOwnerId = $request->get('new_owner_id', null);
if ($this->userRepo->isOnlyAdmin($user)) {
$this->showErrorNotification(trans('errors.users_cannot_delete_only_admin'));
return redirect($user->getEditUrl());
}
- $this->userRepo->destroy($user);
+ $this->userRepo->destroy($user, $newOwnerId);
$this->showSuccessNotification(trans('settings.users_delete_success'));
$this->logActivity(ActivityType::USER_DELETE, $user);
--- /dev/null
+<?php
+
+namespace BookStack\Http\Controllers;
+
+use BookStack\Auth\User;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Http\Request;
+
+class UserSearchController extends Controller
+{
+ /**
+ * Search users in the system, with the response formatted
+ * for use in a select-style list.
+ */
+ public function forSelect(Request $request)
+ {
+ $search = $request->get('search', '');
+ $query = User::query()->orderBy('name', 'desc')
+ ->take(20);
+
+ if (!empty($search)) {
+ $query->where(function (Builder $query) use ($search) {
+ $query->where('email', 'like', '%' . $search . '%')
+ ->orWhere('name', 'like', '%' . $search . '%');
+ });
+ }
+
+ $users = $query->get();
+ return view('components.user-select-list', compact('users'));
+ }
+}
-<?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');
}
--- /dev/null
+<?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');
+ }
+
+}
<?php namespace BookStack\Uploads;
use BookStack\Entities\Models\Page;
-use BookStack\Ownable;
+use BookStack\Model;
+use BookStack\Traits\HasCreatorAndUpdater;
/**
* @property int id
* @property string extension
* @property bool external
*/
-class Attachment extends Ownable
+class Attachment extends Model
{
+ use HasCreatorAndUpdater;
+
protected $fillable = ['name', 'order'];
/**
<?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 = [];
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User;
-use BookStack\Ownable;
+use BookStack\Model;
use BookStack\Settings\SettingService;
/**
* 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);
--- /dev/null
+<?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');
+ });
+ }
+}
$role = Role::getRole('viewer');
$viewerUser->attachRole($role);
- $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id];
+ $byData = ['created_by' => $editorUser->id, 'updated_by' => $editorUser->id, 'owned_by' => $editorUser->id];
factory(\BookStack\Entities\Models\Book::class, 5)->create($byData)
->each(function($book) use ($editorUser, $byData) {
+++ /dev/null
-
-
-class BreadcrumbListing {
-
- constructor(elem) {
- this.elem = elem;
- this.searchInput = elem.querySelector('input');
- this.loadingElem = elem.querySelector('.loading-container');
- this.entityListElem = elem.querySelector('.breadcrumb-listing-entity-list');
-
- // this.loadingElem.style.display = 'none';
- const entityDescriptor = elem.getAttribute('breadcrumb-listing').split(':');
- this.entityType = entityDescriptor[0];
- this.entityId = Number(entityDescriptor[1]);
-
- this.elem.addEventListener('show', this.onShow.bind(this));
- this.searchInput.addEventListener('input', this.onSearch.bind(this));
- }
-
- onShow() {
- this.loadEntityView();
- }
-
- onSearch() {
- const input = this.searchInput.value.toLowerCase().trim();
- const listItems = this.entityListElem.querySelectorAll('.entity-list-item');
- for (let listItem of listItems) {
- const match = !input || listItem.textContent.toLowerCase().includes(input);
- listItem.style.display = match ? 'flex' : 'none';
- listItem.classList.toggle('hidden', !match);
- }
- }
-
- loadEntityView() {
- this.toggleLoading(true);
-
- const params = {
- 'entity_id': this.entityId,
- 'entity_type': this.entityType,
- };
-
- window.$http.get('/search/entity/siblings', params).then(resp => {
- this.entityListElem.innerHTML = resp.data;
- }).catch(err => {
- console.error(err);
- }).then(() => {
- this.toggleLoading(false);
- this.onSearch();
- });
- }
-
- toggleLoading(show = false) {
- this.loadingElem.style.display = show ? 'block' : 'none';
- }
-
-}
-
-export default BreadcrumbListing;
\ No newline at end of file
--- /dev/null
+import {debounce} from "../services/util";
+
+class DropdownSearch {
+
+ setup() {
+ this.elem = this.$el;
+ this.searchInput = this.$refs.searchInput;
+ this.loadingElem = this.$refs.loading;
+ this.listContainerElem = this.$refs.listContainer;
+
+ this.localSearchSelector = this.$opts.localSearchSelector;
+ this.url = this.$opts.url;
+
+ this.elem.addEventListener('show', this.onShow.bind(this));
+ this.searchInput.addEventListener('input', this.onSearch.bind(this));
+
+ this.runAjaxSearch = debounce(this.runAjaxSearch, 300, false);
+ }
+
+ onShow() {
+ this.loadList();
+ }
+
+ onSearch() {
+ const input = this.searchInput.value.toLowerCase().trim();
+ if (this.localSearchSelector) {
+ this.runLocalSearch(input);
+ } else {
+ this.toggleLoading(true);
+ this.runAjaxSearch(input);
+ }
+ }
+
+ runAjaxSearch(searchTerm) {
+ this.loadList(searchTerm);
+ }
+
+ runLocalSearch(searchTerm) {
+ const listItems = this.listContainerElem.querySelectorAll(this.localSearchSelector);
+ for (let listItem of listItems) {
+ const match = !searchTerm || listItem.textContent.toLowerCase().includes(searchTerm);
+ listItem.style.display = match ? 'flex' : 'none';
+ listItem.classList.toggle('hidden', !match);
+ }
+ }
+
+ async loadList(searchTerm = '') {
+ this.listContainerElem.innerHTML = '';
+ this.toggleLoading(true);
+
+ try {
+ const resp = await window.$http.get(this.getAjaxUrl(searchTerm));
+ this.listContainerElem.innerHTML = resp.data;
+ } catch (err) {
+ console.error(err);
+ }
+
+ this.toggleLoading(false);
+ if (this.localSearchSelector) {
+ this.onSearch();
+ }
+ }
+
+ getAjaxUrl(searchTerm = null) {
+ if (!searchTerm) {
+ return this.url;
+ }
+
+ const joiner = this.url.includes('?') ? '&' : '?';
+ return `${this.url}${joiner}search=${encodeURIComponent(searchTerm)}`;
+ }
+
+ toggleLoading(show = false) {
+ this.loadingElem.style.display = show ? 'block' : 'none';
+ }
+
+}
+
+export default DropdownSearch;
\ No newline at end of file
this.body = document.body;
this.showing = false;
this.setupListeners();
+ this.hide = this.hide.bind(this);
}
show(event = null) {
import autoSuggest from "./auto-suggest.js"
import backToTop from "./back-to-top.js"
import bookSort from "./book-sort.js"
-import breadcrumbListing from "./breadcrumb-listing.js"
import chapterToggle from "./chapter-toggle.js"
import codeEditor from "./code-editor.js"
import codeHighlighter from "./code-highlighter.js"
import customCheckbox from "./custom-checkbox.js"
import detailsHighlighter from "./details-highlighter.js"
import dropdown from "./dropdown.js"
+import dropdownSearch from "./dropdown-search.js"
import dropzone from "./dropzone.js"
import editorToolbox from "./editor-toolbox.js"
import entityPermissionsEditor from "./entity-permissions-editor.js"
import templateManager from "./template-manager.js"
import toggleSwitch from "./toggle-switch.js"
import triLayout from "./tri-layout.js"
+import userSelect from "./user-select.js"
import wysiwygEditor from "./wysiwyg-editor.js"
const componentMapping = {
"auto-suggest": autoSuggest,
"back-to-top": backToTop,
"book-sort": bookSort,
- "breadcrumb-listing": breadcrumbListing,
"chapter-toggle": chapterToggle,
"code-editor": codeEditor,
"code-highlighter": codeHighlighter,
"custom-checkbox": customCheckbox,
"details-highlighter": detailsHighlighter,
"dropdown": dropdown,
+ "dropdown-search": dropdownSearch,
"dropzone": dropzone,
"editor-toolbox": editorToolbox,
"entity-permissions-editor": entityPermissionsEditor,
"template-manager": templateManager,
"toggle-switch": toggleSwitch,
"tri-layout": triLayout,
+ "user-select": userSelect,
"wysiwyg-editor": wysiwygEditor,
};
--- /dev/null
+import {onChildEvent} from "../services/dom";
+
+class UserSelect {
+
+ setup() {
+
+ this.input = this.$refs.input;
+ this.userInfoContainer = this.$refs.userInfo;
+
+ this.hide = this.$el.components.dropdown.hide;
+
+ onChildEvent(this.$el, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
+ }
+
+ selectUser(event, userEl) {
+ const id = userEl.getAttribute('data-id');
+ this.input.value = id;
+ this.userInfoContainer.innerHTML = userEl.innerHTML;
+ this.hide();
+ }
+
+}
+
+export default UserSelect;
\ No newline at end of file
'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',
'permissions_intro' => 'Once enabled, These permissions will take priority over any set role permissions.',
'permissions_enable' => 'Enable Custom Permissions',
'permissions_save' => 'Save Permissions',
+ 'permissions_owner' => 'Owner',
// Search
'search_results' => 'Search Results',
'users_delete_named' => 'Delete user :userName',
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
'users_delete_confirm' => 'Are you sure you want to delete this user?',
- 'users_delete_success' => 'Users successfully removed',
+ 'users_migrate_ownership' => 'Migrate Ownership',
+ 'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',
+ 'users_none_selected' => 'No user selected',
+ 'users_delete_success' => 'User successfully removed',
'users_edit' => 'Edit User',
'users_edit_profile' => 'Edit Profile',
'users_edit_success' => 'User successfully updated',
.template-item-actions button:first-child {
border-top: 0;
}
+}
+
+.dropdown-search-dropdown {
+ box-shadow: $bs-med;
+ overflow: hidden;
+ min-height: 100px;
+ width: 240px;
+ display: none;
+ position: absolute;
+ z-index: 80;
+ right: -$-m;
+ @include rtl {
+ right: auto;
+ left: -$-m;
+ }
+ .dropdown-search-search .svg-icon {
+ position: absolute;
+ left: $-s;
+ @include rtl {
+ right: $-s;
+ left: auto;
+ }
+ top: 11px;
+ fill: #888;
+ pointer-events: none;
+ }
+ .dropdown-search-list {
+ max-height: 400px;
+ overflow-y: scroll;
+ text-align: start;
+ }
+ .dropdown-search-item {
+ padding: $-s $-m;
+ &:hover,&:focus {
+ background-color: #F2F2F2;
+ text-decoration: none;
+ }
+ }
+ input {
+ padding-inline-start: $-xl;
+ border-radius: 0;
+ border: 0;
+ border-bottom: 1px solid #DDD;
+ }
+}
+
+@include smaller-than($m) {
+ .dropdown-search-dropdown {
+ position: fixed;
+ right: auto;
+ left: $-m;
+ }
+ .dropdown-search-dropdown .dropdown-search-list {
+ max-height: 240px;
+ }
+}
+
+.custom-select-input {
+ max-width: 280px;
+ border: 1px solid #DDD;
+ border-radius: 4px;
}
\ No newline at end of file
}
}
-.breadcrumb-listing {
+.dropdown-search {
position: relative;
- .breadcrumb-listing-toggle {
+ .dropdown-search-toggle {
padding: 6px;
border: 1px solid transparent;
border-radius: 4px;
}
}
-.breadcrumb-listing-dropdown {
- box-shadow: $bs-med;
- overflow: hidden;
- min-height: 100px;
- width: 240px;
- display: none;
- position: absolute;
- z-index: 80;
- right: -$-m;
- @include rtl {
- right: auto;
- left: -$-m;
- }
- .breadcrumb-listing-search .svg-icon {
- position: absolute;
- left: $-s;
- @include rtl {
- right: $-s;
- left: auto;
- }
- top: 11px;
- fill: #888;
- pointer-events: none;
- }
- .breadcrumb-listing-entity-list {
- max-height: 400px;
- overflow-y: scroll;
- text-align: start;
- }
- input {
- padding-inline-start: $-xl;
- border-radius: 0;
- border: 0;
- border-bottom: 1px solid #DDD;
- }
-}
-
-@include smaller-than($m) {
- .breadcrumb-listing-dropdown {
- position: fixed;
- right: auto;
- left: $-m;
- }
- .breadcrumb-listing-dropdown .breadcrumb-listing-entity-list {
- max-height: 240px;
- }
-}
-
.faded {
a, button, span, span > div {
color: #666;
.justify-center {
justify-content: center;
}
+.items-center {
+ align-items: center;
+}
/**
--- /dev/null
+@foreach($users as $user)
+ <a href="#" class="flex-container-row items-center dropdown-search-item" data-id="{{ $user->id }}">
+ <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}">
+ <span>{{ $user->name }}</span>
+ </a>
+@endforeach
\ No newline at end of file
--- /dev/null
+<div class="dropdown-search custom-select-input" components="dropdown dropdown-search user-select"
+ option:dropdown-search:url="/search/users/select"
+>
+ <input refs="user-select@input" type="hidden" name="{{ $name }}" value="{{ $user->id ?? '' }}">
+ <div refs="dropdown@toggle"
+ class="dropdown-search-toggle flex-container-row items-center"
+ aria-haspopup="true" aria-expanded="false" tabindex="0">
+ <div refs="user-select@user-info" class="flex-container-row items-center px-s">
+ @if($user)
+ <img class="avatar mr-m" src="{{ $user->getAvatar(30) }}" alt="{{ $user->name }}">
+ <span>{{ $user->name }}</span>
+ @else
+ <span>{{ trans('settings.users_none_selected') }}</span>
+ @endif
+ </div>
+ <span style="font-size: 1.5rem; margin-left: auto;">
+ @icon('caret-down')
+ </span>
+ </div>
+ <div refs="dropdown@menu" class="dropdown-search-dropdown card" role="menu">
+ <div class="dropdown-search-search">
+ @icon('search')
+ <input refs="dropdown-search@searchInput"
+ aria-label="{{ trans('common.search') }}"
+ autocomplete="off"
+ placeholder="{{ trans('common.search') }}"
+ type="text">
+ </div>
+ <div refs="dropdown-search@loading" class="text-center">
+ @include('partials.loading-icon')
+ </div>
+ <div refs="dropdown-search@listContainer" class="dropdown-search-list"></div>
+ </div>
+</div>
\ No newline at end of file
{!! csrf_field() !!}
<input type="hidden" name="_method" value="PUT">
- <p class="mb-none">{{ trans('entities.permissions_intro') }}</p>
-
- <div class="form-group">
- @include('form.checkbox', [
- 'name' => 'restricted',
- 'label' => trans('entities.permissions_enable'),
- ])
+ <div class="grid half left-focus v-center">
+ <div>
+ <p class="mb-none mt-m">{{ trans('entities.permissions_intro') }}</p>
+ <div>
+ @include('form.checkbox', [
+ 'name' => 'restricted',
+ 'label' => trans('entities.permissions_enable'),
+ ])
+ </div>
+ </div>
+ <div>
+ <div class="form-group">
+ <label for="owner">{{ trans('entities.permissions_owner') }}</label>
+ @include('components.user-select', ['user' => $model->ownedBy, 'name' => 'owned_by'])
+ </div>
+ </div>
</div>
+ <hr>
+
<table permissions-table class="table permissions-table toggle-switch-list" style="{{ !$model->restricted ? 'display: none' : '' }}">
<tr>
<th>{{ trans('common.role') }}</th>
-<div class="breadcrumb-listing" component="dropdown" breadcrumb-listing="{{ $entity->getType() }}:{{ $entity->id }}">
- <div class="breadcrumb-listing-toggle" refs="dropdown@toggle"
+<div class="dropdown-search" components="dropdown dropdown-search"
+ option:dropdown-search:url="/search/entity/siblings?entity_type={{$entity->getType()}}&entity_id={{ $entity->id }}"
+ option:dropdown-search:local-search-selector=".entity-list-item"
+>
+ <div class="dropdown-search-toggle" refs="dropdown@toggle"
aria-haspopup="true" aria-expanded="false" tabindex="0">
<div class="separator">@icon('chevron-right')</div>
</div>
- <div refs="dropdown@menu" class="breadcrumb-listing-dropdown card" role="menu">
- <div class="breadcrumb-listing-search">
+ <div refs="dropdown@menu" class="dropdown-search-dropdown card" role="menu">
+ <div class="dropdown-search-search">
@icon('search')
- <input autocomplete="off" type="text" name="entity-search" placeholder="{{ trans('common.search') }}" aria-label="{{ trans('common.search') }}">
+ <input refs="dropdown-search@searchInput"
+ aria-label="{{ trans('common.search') }}"
+ autocomplete="off"
+ placeholder="{{ trans('common.search') }}"
+ type="text">
</div>
- @include('partials.loading-icon')
- <div class="breadcrumb-listing-entity-list px-m"></div>
+ <div refs="dropdown-search@loading">
+ @include('partials.loading-icon')
+ </div>
+ <div refs="dropdown-search@listContainer" class="dropdown-search-list px-m"></div>
</div>
</div>
\ No newline at end of file
<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
<p>{{ trans('settings.users_delete_warning', ['userName' => $user->name]) }}</p>
+ <hr class="my-l">
+
+ <div class="grid half gap-xl v-center">
+ <div>
+ <label class="setting-list-label">{{ trans('settings.users_migrate_ownership') }}</label>
+ <p class="small">{{ trans('settings.users_migrate_ownership_desc') }}</p>
+ </div>
+ <div>
+ @include('components.user-select', ['name' => 'new_owner_id', 'user' => null])
+ </div>
+ </div>
+
+ <hr class="my-l">
+
<div class="grid half">
<p class="text-neg"><strong>{{ trans('settings.users_delete_confirm') }}</strong></p>
<div>
Route::get('/search/chapter/{bookId}', 'SearchController@searchChapter');
Route::get('/search/entity/siblings', 'SearchController@searchSiblings');
+ // User Search
+ Route::get('/search/users/select', 'UserSearchController@forSelect');
+
Route::get('/templates', 'PageTemplateController@list');
Route::get('/templates/{templateId}', 'PageTemplateController@get');
<?php namespace Tests;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Auth\Role;
use BookStack\Auth\Permissions\PermissionService;
+use BookStack\Entities\Models\Page;
use BookStack\Settings\SettingService;
+use DB;
use Illuminate\Contracts\Console\Kernel;
+use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\BrowserKitTesting\TestCase;
use Symfony\Component\DomCrawler\Crawler;
public function tearDown() : void
{
- \DB::disconnect();
+ DB::disconnect();
parent::tearDown();
}
/**
* Creates the application.
*
- * @return \Illuminate\Foundation\Application
+ * @return Application
*/
public function createApplication()
{
*/
public function getNormalUser()
{
- return \BookStack\Auth\User::where('system_name', '=', null)->get()->last();
+ return User::where('system_name', '=', null)->get()->last();
}
/**
/**
* Create a group of entities that belong to a specific user.
- * @param $creatorUser
- * @param $updaterUser
- * @return array
*/
- protected function createEntityChainBelongingToUser($creatorUser, $updaterUser = false)
+ protected function createEntityChainBelongingToUser(User $creatorUser, ?User $updaterUser = null): array
{
- if ($updaterUser === false) $updaterUser = $creatorUser;
- $book = factory(\BookStack\Entities\Models\Book::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]);
- $chapter = factory(\BookStack\Entities\Models\Chapter::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]);
- $page = factory(\BookStack\Entities\Models\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id, 'chapter_id' => $chapter->id]);
+ if (empty($updaterUser)) {
+ $updaterUser = $creatorUser;
+ }
+
+ $userAttrs = ['created_by' => $creatorUser->id, 'owned_by' => $creatorUser->id, 'updated_by' => $updaterUser->id];
+ $book = factory(Book::class)->create($userAttrs);
+ $chapter = factory(Chapter::class)->create(array_merge(['book_id' => $book->id], $userAttrs));
+ $page = factory(Page::class)->create(array_merge(['book_id' => $book->id, 'chapter_id' => $chapter->id], $userAttrs));
$restrictionService = $this->app[PermissionService::class];
$restrictionService->buildJointPermissionsForEntity($book);
- return [
- 'book' => $book,
- 'chapter' => $chapter,
- 'page' => $page
- ];
+
+ return compact('book', 'chapter', 'page');
}
/**
*/
protected function getNewBlankUser($attributes = [])
{
- $user = factory(\BookStack\Auth\User::class)->create($attributes);
+ $user = factory(User::class)->create($attributes);
return $user;
}
$resp = $this->actingAs($viewer)->get($page->getUrl());
$resp->assertDontSee($page->getUrl('/copy'));
- $newBook->created_by = $viewer->id;
+ $newBook->owned_by = $viewer->id;
$newBook->save();
$this->giveUserPermissions($viewer, ['page-create-own']);
$this->regenEntityPermissions($newBook);
--- /dev/null
+<?php namespace Tests\Permissions;
+
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Book;
+use BookStack\Entities\Models\Bookshelf;
+use BookStack\Entities\Models\Chapter;
+use BookStack\Entities\Models\Page;
+use Illuminate\Support\Str;
+use Tests\TestCase;
+
+class EntityOwnerChangeTest extends TestCase
+{
+
+ public function test_changing_page_owner()
+ {
+ $page = Page::query()->first();
+ $user = User::query()->where('id', '!=', $page->owned_by)->first();
+
+ $this->asAdmin()->put($page->getUrl('permissions'), ['owned_by' => $user->id]);
+ $this->assertDatabaseHas('pages', ['owned_by' => $user->id, 'id' => $page->id]);
+ }
+
+ public function test_changing_chapter_owner()
+ {
+ $chapter = Chapter::query()->first();
+ $user = User::query()->where('id', '!=', $chapter->owned_by)->first();
+
+ $this->asAdmin()->put($chapter->getUrl('permissions'), ['owned_by' => $user->id]);
+ $this->assertDatabaseHas('chapters', ['owned_by' => $user->id, 'id' => $chapter->id]);
+ }
+
+ public function test_changing_book_owner()
+ {
+ $book = Book::query()->first();
+ $user = User::query()->where('id', '!=', $book->owned_by)->first();
+
+ $this->asAdmin()->put($book->getUrl('permissions'), ['owned_by' => $user->id]);
+ $this->assertDatabaseHas('books', ['owned_by' => $user->id, 'id' => $book->id]);
+ }
+
+ public function test_changing_shelf_owner()
+ {
+ $shelf = Bookshelf::query()->first();
+ $user = User::query()->where('id', '!=', $shelf->owned_by)->first();
+
+ $this->asAdmin()->put($shelf->getUrl('permissions'), ['owned_by' => $user->id]);
+ $this->assertDatabaseHas('bookshelves', ['owned_by' => $user->id, 'id' => $shelf->id]);
+ }
+
+}
\ No newline at end of file
{
$otherShelf = Bookshelf::first();
$ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
- $ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
+ $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
$this->regenEntityPermissions($ownShelf);
$this->checkAccessPermission('bookshelf-update-own', [
$this->giveUserPermissions($this->user, ['bookshelf-update-all']);
$otherShelf = Bookshelf::first();
$ownShelf = $this->newShelf(['name' => 'test-shelf', 'slug' => 'test-shelf']);
- $ownShelf->forceFill(['created_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
+ $ownShelf->forceFill(['owned_by' => $this->user->id, 'updated_by' => $this->user->id])->save();
$this->regenEntityPermissions($ownShelf);
$this->checkAccessPermission('bookshelf-delete-own', [
--- /dev/null
+<?php namespace Tests\User;
+
+use BookStack\Actions\ActivityType;
+use BookStack\Auth\User;
+use BookStack\Entities\Models\Page;
+use Tests\TestCase;
+
+class UserManagementTest extends TestCase
+{
+
+ public function test_delete()
+ {
+ $editor = $this->getEditor();
+ $resp = $this->asAdmin()->delete("settings/users/{$editor->id}");
+ $resp->assertRedirect("/settings/users");
+ $resp = $this->followRedirects($resp);
+
+ $resp->assertSee("User successfully removed");
+ $this->assertActivityExists(ActivityType::USER_DELETE);
+
+ $this->assertDatabaseMissing('users', ['id' => $editor->id]);
+ }
+
+ public function test_delete_offers_migrate_option()
+ {
+ $editor = $this->getEditor();
+ $resp = $this->asAdmin()->get("settings/users/{$editor->id}/delete");
+ $resp->assertSee("Migrate Ownership");
+ $resp->assertSee("new_owner_id");
+ }
+
+ public function test_delete_with_new_owner_id_changes_ownership()
+ {
+ $page = Page::query()->first();
+ $owner = $page->ownedBy;
+ $newOwner = User::query()->where('id', '!=' , $owner->id)->first();
+
+ $this->asAdmin()->delete("settings/users/{$owner->id}", ['new_owner_id' => $newOwner->id]);
+ $this->assertDatabaseHas('pages', [
+ 'id' => $page->id,
+ 'owned_by' => $newOwner->id,
+ ]);
+ }
+}
\ No newline at end of file