Merge pull request #10 from BookStackApp/master

Latest changes
This commit is contained in:
Abijeet Patro 2017-05-03 01:41:08 +05:30 committed by GitHub
commit 3368fe42d8
38 changed files with 680 additions and 432 deletions

View File

@ -49,6 +49,7 @@ class RegeneratePermissions extends Command
$connection = \DB::getDefaultConnection(); $connection = \DB::getDefaultConnection();
if ($this->option('database') !== null) { if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database')); \DB::setDefaultConnection($this->option('database'));
$this->permissionService->setConnection(\DB::connection($this->option('database')));
} }
$this->permissionService->buildJointPermissions(); $this->permissionService->buildJointPermissions();

View File

@ -44,6 +44,7 @@ class RegenerateSearch extends Command
$connection = \DB::getDefaultConnection(); $connection = \DB::getDefaultConnection();
if ($this->option('database') !== null) { if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database')); \DB::setDefaultConnection($this->option('database'));
$this->searchService->setConnection(\DB::connection($this->option('database')));
} }
$this->searchService->indexAllEntities(); $this->searchService->indexAllEntities();

View File

@ -94,17 +94,6 @@ class Entity extends Ownable
->where('action', '=', $action)->count() > 0; ->where('action', '=', $action)->count() > 0;
} }
/**
* Check if this entity has live (active) restrictions in place.
* @param $role_id
* @param $action
* @return bool
*/
public function hasActiveRestriction($role_id, $action)
{
return $this->getRawAttribute('restricted') && $this->hasRestriction($role_id, $action);
}
/** /**
* Get the entity jointPermissions this is connected to. * Get the entity jointPermissions this is connected to.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany * @return \Illuminate\Database\Eloquent\Relations\MorphMany
@ -176,5 +165,11 @@ class Entity extends Ownable
*/ */
public function entityRawQuery(){return '';} public function entityRawQuery(){return '';}
/**
* Get the url of this entity
* @param $path
* @return string
*/
public function getUrl($path){return '/';}
} }

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers; <?php namespace BookStack\Http\Controllers;
use Activity; use Activity;
use BookStack\Book;
use BookStack\Repos\EntityRepo; use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo; use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService; use BookStack\Services\ExportService;
@ -207,13 +208,12 @@ class BookController extends Controller
// Add activity for books // Add activity for books
foreach ($sortedBooks as $bookId) { foreach ($sortedBooks as $bookId) {
/** @var Book $updatedBook */
$updatedBook = $this->entityRepo->getById('book', $bookId); $updatedBook = $this->entityRepo->getById('book', $bookId);
$this->entityRepo->buildJointPermissionsForBook($updatedBook);
Activity::add($updatedBook, 'book_sort', $updatedBook->id); Activity::add($updatedBook, 'book_sort', $updatedBook->id);
} }
// Update permissions on changed models
if (count($updatedModels) === 0) $this->entityRepo->buildJointPermissions($updatedModels);
return redirect($book->getUrl()); return redirect($book->getUrl());
} }

View File

@ -46,7 +46,7 @@ class HomeController extends Controller
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/ */
public function getTranslations() { public function getTranslations() {
$locale = trans()->getLocale(); $locale = app()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale; $cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') { if (cache()->has($cacheKey) && config('app.env') !== 'development') {
$resp = cache($cacheKey); $resp = cache($cacheKey);

View File

@ -15,7 +15,17 @@ class Localization
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
$defaultLang = config('app.locale'); $defaultLang = config('app.locale');
$locale = setting()->getUser(user(), 'language', $defaultLang); if (user()->isDefault()) {
$locale = $defaultLang;
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
if (!in_array($lang, $availableLocales)) continue;
$locale = $lang;
break;
}
} else {
$locale = setting()->getUser(user(), 'language', $defaultLang);
}
app()->setLocale($locale); app()->setLocale($locale);
Carbon::setLocale($locale); Carbon::setLocale($locale);
return $next($request); return $next($request);

View File

@ -2,12 +2,16 @@
namespace BookStack\Notifications; namespace BookStack\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification; use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Messages\MailMessage;
class ConfirmEmail extends Notification class ConfirmEmail extends Notification implements ShouldQueue
{ {
use Queueable;
public $token; public $token;
/** /**

View File

@ -348,6 +348,10 @@ class EntityRepo
foreach ($entities as $entity) { foreach ($entities as $entity) {
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue; if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue;
$parentKey = 'BookStack\\Chapter:' . $entity->chapter_id; $parentKey = 'BookStack\\Chapter:' . $entity->chapter_id;
if (!isset($parents[$parentKey])) {
$tree[] = $entity;
continue;
}
$chapter = $parents[$parentKey]; $chapter = $parents[$parentKey];
$chapter->pages->push($entity); $chapter->pages->push($entity);
} }
@ -529,11 +533,11 @@ class EntityRepo
/** /**
* Alias method to update the book jointPermissions in the PermissionService. * Alias method to update the book jointPermissions in the PermissionService.
* @param Collection $collection collection on entities * @param Book $book
*/ */
public function buildJointPermissions(Collection $collection) public function buildJointPermissionsForBook(Book $book)
{ {
$this->permissionService->buildJointPermissionsForEntities($collection); $this->permissionService->buildJointPermissionsForEntity($book);
} }
/** /**
@ -569,6 +573,7 @@ class EntityRepo
$draftPage->html = $this->formatHtml($input['html']); $draftPage->html = $this->formatHtml($input['html']);
$draftPage->text = strip_tags($draftPage->html); $draftPage->text = strip_tags($draftPage->html);
$draftPage->draft = false; $draftPage->draft = false;
$draftPage->revision_count = 1;
$draftPage->save(); $draftPage->save();
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision')); $this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
@ -593,6 +598,7 @@ class EntityRepo
$revision->created_at = $page->updated_at; $revision->created_at = $page->updated_at;
$revision->type = 'version'; $revision->type = 'version';
$revision->summary = $summary; $revision->summary = $summary;
$revision->revision_number = $page->revision_count;
$revision->save(); $revision->save();
// Clear old revisions // Clear old revisions
@ -724,6 +730,7 @@ class EntityRepo
if ($chapter) $page->chapter_id = $chapter->id; if ($chapter) $page->chapter_id = $chapter->id;
$book->pages()->save($page); $book->pages()->save($page);
$page = $this->page->find($page->id);
$this->permissionService->buildJointPermissionsForEntity($page); $this->permissionService->buildJointPermissionsForEntity($page);
return $page; return $page;
} }
@ -812,6 +819,7 @@ class EntityRepo
$page->text = strip_tags($page->html); $page->text = strip_tags($page->html);
if (setting('app-editor') !== 'markdown') $page->markdown = ''; if (setting('app-editor') !== 'markdown') $page->markdown = '';
$page->updated_by = $userId; $page->updated_by = $userId;
$page->revision_count++;
$page->save(); $page->save();
// Remove all update drafts for this user & page. // Remove all update drafts for this user & page.
@ -920,6 +928,7 @@ class EntityRepo
*/ */
public function restorePageRevision(Page $page, Book $book, $revisionId) public function restorePageRevision(Page $page, Book $book, $revisionId)
{ {
$page->revision_count++;
$this->savePageRevision($page); $this->savePageRevision($page);
$revision = $page->revisions()->where('id', '=', $revisionId)->first(); $revision = $page->revisions()->where('id', '=', $revisionId)->first();
$page->fill($revision->toArray()); $page->fill($revision->toArray());

View File

@ -3,6 +3,7 @@
use BookStack\Book; use BookStack\Book;
use BookStack\Chapter; use BookStack\Chapter;
use BookStack\Entity; use BookStack\Entity;
use BookStack\EntityPermission;
use BookStack\JointPermission; use BookStack\JointPermission;
use BookStack\Ownable; use BookStack\Ownable;
use BookStack\Page; use BookStack\Page;
@ -10,6 +11,7 @@ use BookStack\Role;
use BookStack\User; use BookStack\User;
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\Support\Collection; use Illuminate\Support\Collection;
class PermissionService class PermissionService
@ -28,22 +30,25 @@ class PermissionService
protected $jointPermission; protected $jointPermission;
protected $role; protected $role;
protected $entityPermission;
protected $entityCache; protected $entityCache;
/** /**
* PermissionService constructor. * PermissionService constructor.
* @param JointPermission $jointPermission * @param JointPermission $jointPermission
* @param EntityPermission $entityPermission
* @param Connection $db * @param Connection $db
* @param Book $book * @param Book $book
* @param Chapter $chapter * @param Chapter $chapter
* @param Page $page * @param Page $page
* @param Role $role * @param Role $role
*/ */
public function __construct(JointPermission $jointPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role) public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
{ {
$this->db = $db; $this->db = $db;
$this->jointPermission = $jointPermission; $this->jointPermission = $jointPermission;
$this->entityPermission = $entityPermission;
$this->role = $role; $this->role = $role;
$this->book = $book; $this->book = $book;
$this->chapter = $chapter; $this->chapter = $chapter;
@ -51,6 +56,15 @@ class PermissionService
// TODO - Update so admin still goes through filters // TODO - Update so admin still goes through filters
} }
/**
* Set the database connection
* @param Connection $connection
*/
public function setConnection(Connection $connection)
{
$this->db = $connection;
}
/** /**
* Prepare the local entity cache and ensure it's empty * Prepare the local entity cache and ensure it's empty
*/ */
@ -133,22 +147,48 @@ class PermissionService
$this->readyEntityCache(); $this->readyEntityCache();
// Get all roles (Should be the most limited dimension) // Get all roles (Should be the most limited dimension)
$roles = $this->role->with('permissions')->get(); $roles = $this->role->with('permissions')->get()->all();
// Chunk through all books // Chunk through all books
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) { $this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles); $this->buildJointPermissionsForBooks($books, $roles);
}); });
}
// Chunk through all chapters /**
$this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) { * Get a query for fetching a book with it's children.
$this->createManyJointPermissions($chapters, $roles); * @return QueryBuilder
}); */
protected function bookFetchQuery()
{
return $this->book->newQuery()->select(['id', 'restricted', 'created_by'])->with(['chapters' => function($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
}
// Chunk through all pages /**
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($pages) use ($roles) { * Build joint permissions for an array of books
$this->createManyJointPermissions($pages, $roles); * @param Collection $books
}); * @param array $roles
* @param bool $deleteOld
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false) {
$entities = clone $books;
/** @var Book $book */
foreach ($books->all() as $book) {
foreach ($book->getRelation('chapters') as $chapter) {
$entities->push($chapter);
}
foreach ($book->getRelation('pages') as $page) {
$entities->push($page);
}
}
if ($deleteOld) $this->deleteManyJointPermissionsForEntities($entities->all());
$this->createManyJointPermissions($entities, $roles);
} }
/** /**
@ -157,18 +197,22 @@ class PermissionService
*/ */
public function buildJointPermissionsForEntity(Entity $entity) public function buildJointPermissionsForEntity(Entity $entity)
{ {
$roles = $this->role->get(); $entities = [$entity];
$entities = collect([$entity]);
if ($entity->isA('book')) { if ($entity->isA('book')) {
$entities = $entities->merge($entity->chapters); $books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$entities = $entities->merge($entity->pages); $this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true);
} elseif ($entity->isA('chapter')) { return;
$entities = $entities->merge($entity->pages);
} }
$entities[] = $entity->book;
if ($entity->isA('page') && $entity->chapter_id) $entities[] = $entity->chapter;
if ($entity->isA('chapter')) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}
$this->deleteManyJointPermissionsForEntities($entities); $this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles); $this->buildJointPermissionsForEntities(collect($entities));
} }
/** /**
@ -177,8 +221,8 @@ class PermissionService
*/ */
public function buildJointPermissionsForEntities(Collection $entities) public function buildJointPermissionsForEntities(Collection $entities)
{ {
$roles = $this->role->get(); $roles = $this->role->newQuery()->get();
$this->deleteManyJointPermissionsForEntities($entities); $this->deleteManyJointPermissionsForEntities($entities->all());
$this->createManyJointPermissions($entities, $roles); $this->createManyJointPermissions($entities, $roles);
} }
@ -188,23 +232,12 @@ class PermissionService
*/ */
public function buildJointPermissionForRole(Role $role) public function buildJointPermissionForRole(Role $role)
{ {
$roles = collect([$role]); $roles = [$role];
$this->deleteManyJointPermissionsForRoles($roles); $this->deleteManyJointPermissionsForRoles($roles);
// Chunk through all books // Chunk through all books
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) { $this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles); $this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all chapters
$this->chapter->with('book', 'permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
});
// Chunk through all pages
$this->page->with('book', 'chapter', 'permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
}); });
} }
@ -223,9 +256,10 @@ class PermissionService
*/ */
protected function deleteManyJointPermissionsForRoles($roles) protected function deleteManyJointPermissionsForRoles($roles)
{ {
foreach ($roles as $role) { $roleIds = array_map(function($role) {
$role->jointPermissions()->delete(); return $role->id;
} }, $roles);
$this->jointPermission->newQuery()->whereIn('id', $roleIds)->delete();
} }
/** /**
@ -244,53 +278,88 @@ class PermissionService
protected function deleteManyJointPermissionsForEntities($entities) protected function deleteManyJointPermissionsForEntities($entities)
{ {
if (count($entities) === 0) return; if (count($entities) === 0) return;
$query = $this->jointPermission->newQuery();
foreach ($entities as $entity) { $this->db->transaction(function() use ($entities) {
$query->orWhere(function($query) use ($entity) {
$query->where('entity_id', '=', $entity->id) foreach (array_chunk($entities, 1000) as $entityChunk) {
->where('entity_type', '=', $entity->getMorphClass()); $query = $this->db->table('joint_permissions');
}); foreach ($entityChunk as $entity) {
$query->orWhere(function(QueryBuilder $query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
}
$query->delete();
} }
$query->delete();
});
} }
/** /**
* Create & Save entity jointPermissions for many entities and jointPermissions. * Create & Save entity jointPermissions for many entities and jointPermissions.
* @param Collection $entities * @param Collection $entities
* @param Collection $roles * @param array $roles
*/ */
protected function createManyJointPermissions($entities, $roles) protected function createManyJointPermissions($entities, $roles)
{ {
$this->readyEntityCache(); $this->readyEntityCache();
$jointPermissions = []; $jointPermissions = [];
// Fetch Entity Permissions and create a mapping of entity restricted statuses
$entityRestrictedMap = [];
$permissionFetch = $this->entityPermission->newQuery();
foreach ($entities as $entity) {
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
$permissionFetch->orWhere(function($query) use ($entity) {
$query->where('restrictable_id', '=', $entity->id)->where('restrictable_type', '=', $entity->getMorphClass());
});
}
$permissions = $permissionFetch->get();
// Create a mapping of explicit entity permissions
$permissionMap = [];
foreach ($permissions as $permission) {
$key = $permission->restrictable_type . ':' . $permission->restrictable_id . ':' . $permission->role_id . ':' . $permission->action;
$isRestricted = $entityRestrictedMap[$permission->restrictable_type . ':' . $permission->restrictable_id];
$permissionMap[$key] = $isRestricted;
}
// Create a mapping of role permissions
$rolePermissionMap = [];
foreach ($roles as $role) {
foreach ($role->getRelationValue('permissions') as $permission) {
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
}
}
// Create Joint Permission Data
foreach ($entities as $entity) { foreach ($entities as $entity) {
foreach ($roles as $role) { foreach ($roles as $role) {
foreach ($this->getActions($entity) as $action) { foreach ($this->getActions($entity) as $action) {
$jointPermissions[] = $this->createJointPermissionData($entity, $role, $action); $jointPermissions[] = $this->createJointPermissionData($entity, $role, $action, $permissionMap, $rolePermissionMap);
} }
} }
} }
$this->jointPermission->insert($jointPermissions);
$this->db->transaction(function() use ($jointPermissions) {
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
$this->db->table('joint_permissions')->insert($jointPermissionChunk);
}
});
} }
/** /**
* Get the actions related to an entity. * Get the actions related to an entity.
* @param $entity * @param Entity $entity
* @return array * @return array
*/ */
protected function getActions($entity) protected function getActions(Entity $entity)
{ {
$baseActions = ['view', 'update', 'delete']; $baseActions = ['view', 'update', 'delete'];
if ($entity->isA('chapter') || $entity->isA('book')) $baseActions[] = 'page-create';
if ($entity->isA('chapter')) { if ($entity->isA('book')) $baseActions[] = 'chapter-create';
$baseActions[] = 'page-create'; return $baseActions;
} else if ($entity->isA('book')) {
$baseActions[] = 'page-create';
$baseActions[] = 'chapter-create';
}
return $baseActions;
} }
/** /**
@ -298,14 +367,16 @@ class PermissionService
* for a particular action. * for a particular action.
* @param Entity $entity * @param Entity $entity
* @param Role $role * @param Role $role
* @param $action * @param string $action
* @param array $permissionMap
* @param array $rolePermissionMap
* @return array * @return array
*/ */
protected function createJointPermissionData(Entity $entity, Role $role, $action) protected function createJointPermissionData(Entity $entity, Role $role, $action, $permissionMap, $rolePermissionMap)
{ {
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action; $permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
$roleHasPermission = $role->hasPermission($permissionPrefix . '-all'); $roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
$roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own'); $roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']);
$explodedAction = explode('-', $action); $explodedAction = explode('-', $action);
$restrictionAction = end($explodedAction); $restrictionAction = end($explodedAction);
@ -313,54 +384,46 @@ class PermissionService
return $this->createJointPermissionDataArray($entity, $role, $action, true, true); return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
} }
if ($entity->isA('book')) { if ($entity->restricted) {
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
if (!$entity->restricted) { return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
} else {
$hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
} elseif ($entity->isA('chapter')) {
if (!$entity->restricted) {
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$book->restricted;
return $this->createJointPermissionDataArray($entity, $role, $action,
($hasExplicitAccessToBook || ($roleHasPermission && $hasPermissiveAccessToBook)),
($hasExplicitAccessToBook || ($roleHasPermissionOwn && $hasPermissiveAccessToBook)));
} else {
$hasAccess = $entity->hasActiveRestriction($role->id, $restrictionAction);
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
} elseif ($entity->isA('page')) {
if (!$entity->restricted) {
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToBook = $book->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToBook = !$book->restricted;
$chapter = $this->getChapter($entity->chapter_id);
$hasExplicitAccessToChapter = $chapter && $chapter->hasActiveRestriction($role->id, $restrictionAction);
$hasPermissiveAccessToChapter = $chapter && !$chapter->restricted;
$acknowledgeChapter = ($chapter && $chapter->restricted);
$hasExplicitAccessToParents = $acknowledgeChapter ? $hasExplicitAccessToChapter : $hasExplicitAccessToBook;
$hasPermissiveAccessToParents = $acknowledgeChapter ? $hasPermissiveAccessToChapter : $hasPermissiveAccessToBook;
return $this->createJointPermissionDataArray($entity, $role, $action,
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
);
} else {
$hasAccess = $entity->hasRestriction($role->id, $action);
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
} }
if ($entity->isA('book')) {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
}
// For chapters and pages, Check if explicit permissions are set on the Book.
$book = $this->getBook($entity->book_id);
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $book, $role, $restrictionAction);
$hasPermissiveAccessToParents = !$book->restricted;
// For pages with a chapter, Check if explicit permissions are set on the Chapter
if ($entity->isA('page') && $entity->chapter_id !== 0) {
$chapter = $this->getChapter($entity->chapter_id);
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
if ($chapter->restricted) {
$hasExplicitAccessToParents = $this->mapHasActiveRestriction($permissionMap, $chapter, $role, $restrictionAction);
}
}
return $this->createJointPermissionDataArray($entity, $role, $action,
($hasExplicitAccessToParents || ($roleHasPermission && $hasPermissiveAccessToParents)),
($hasExplicitAccessToParents || ($roleHasPermissionOwn && $hasPermissiveAccessToParents))
);
}
/**
* Check for an active restriction in an entity map.
* @param $entityMap
* @param Entity $entity
* @param Role $role
* @param $action
* @return bool
*/
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action) {
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
return isset($entityMap[$key]) ? $entityMap[$key] : false;
} }
/** /**
@ -375,11 +438,10 @@ class PermissionService
*/ */
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn) protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
{ {
$entityClass = get_class($entity);
return [ return [
'role_id' => $role->getRawAttribute('id'), 'role_id' => $role->getRawAttribute('id'),
'entity_id' => $entity->getRawAttribute('id'), 'entity_id' => $entity->getRawAttribute('id'),
'entity_type' => $entityClass, 'entity_type' => $entity->getMorphClass(),
'action' => $action, 'action' => $action,
'has_permission' => $permissionAll, 'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn, 'has_permission_own' => $permissionOwn,
@ -476,7 +538,7 @@ class PermissionService
* @param integer $book_id * @param integer $book_id
* @param bool $filterDrafts * @param bool $filterDrafts
* @param bool $fetchPageContent * @param bool $fetchPageContent
* @return \Illuminate\Database\Query\Builder * @return QueryBuilder
*/ */
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) { public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false) {
$pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) { $pageSelect = $this->db->table('pages')->selectRaw($this->page->entityRawQuery($fetchPageContent))->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {

View File

@ -50,6 +50,15 @@ class SearchService
$this->permissionService = $permissionService; $this->permissionService = $permissionService;
} }
/**
* Set the database connection
* @param Connection $connection
*/
public function setConnection(Connection $connection)
{
$this->db = $connection;
}
/** /**
* Search all entities in the system. * Search all entities in the system.
* @param string $searchString * @param string $searchString
@ -154,6 +163,7 @@ class SearchService
// Handle normal search terms // Handle normal search terms
if (count($terms['search']) > 0) { if (count($terms['search']) > 0) {
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score')); $subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
$subQuery->where(function(Builder $query) use ($terms) { $subQuery->where(function(Builder $query) use ($terms) {
foreach ($terms['search'] as $inputTerm) { foreach ($terms['search'] as $inputTerm) {
$query->orWhere('term', 'like', $inputTerm .'%'); $query->orWhere('term', 'like', $inputTerm .'%');

View File

@ -64,6 +64,10 @@
"post-update-cmd": [ "post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate", "Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan optimize" "php artisan optimize"
],
"refresh-test-database": [
"php artisan migrate:refresh --database=mysql_testing",
"php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
] ]
}, },
"config": { "config": {

View File

@ -58,6 +58,7 @@ return [
*/ */
'locale' => env('APP_LANG', 'en'), 'locale' => env('APP_LANG', 'en'),
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk'],
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View File

@ -43,7 +43,8 @@ $factory->define(BookStack\Page::class, function ($faker) {
'name' => $faker->sentence, 'name' => $faker->sentence,
'slug' => str_random(10), 'slug' => str_random(10),
'html' => $html, 'html' => $html,
'text' => strip_tags($html) 'text' => strip_tags($html),
'revision_count' => 1
]; ];
}); });

View File

@ -0,0 +1,44 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddRevisionCounts extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('pages', function (Blueprint $table) {
$table->integer('revision_count');
});
Schema::table('page_revisions', function (Blueprint $table) {
$table->integer('revision_number');
$table->index('revision_number');
});
// Update revision count
$pTable = DB::getTablePrefix() . 'pages';
$rTable = DB::getTablePrefix() . 'page_revisions';
DB::statement("UPDATE ${pTable} SET ${pTable}.revision_count=(SELECT count(*) FROM ${rTable} WHERE ${rTable}.page_id=${pTable}.id)");
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('pages', function (Blueprint $table) {
$table->dropColumn('revision_count');
});
Schema::table('page_revisions', function (Blueprint $table) {
$table->dropColumn('revision_number');
});
}
}

View File

@ -28,6 +28,12 @@ class DummyContentSeeder extends Seeder
$book->pages()->saveMany($pages); $book->pages()->saveMany($pages);
}); });
$largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $user->id, 'updated_by' => $user->id]);
$pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
app(\BookStack\Services\PermissionService::class)->buildJointPermissions(); app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities(); app(\BookStack\Services\SearchService::class)->indexAllEntities();
} }

View File

@ -43,6 +43,8 @@ Once done you can run `phpunit` in the application root directory to run all tes
## Translations ## Translations
As part of BookStack v0.14 support for translations has been built in. All text strings can be found in the `resources/lang` folder where each language option has its own folder. To add a new language you should copy the `en` folder to an new folder (eg. `fr` for french) then go through and translate all text strings in those files, leaving the keys and file-names intact. If a language string is missing then the `en` translation will be used. To show the language option in the user preferences language drop-down you will need to add your language to the options found at the bottom of the `resources/lang/en/settings.php` file. A system-wide language can also be set in the `.env` file like so: `APP_LANG=en`. As part of BookStack v0.14 support for translations has been built in. All text strings can be found in the `resources/lang` folder where each language option has its own folder. To add a new language you should copy the `en` folder to an new folder (eg. `fr` for french) then go through and translate all text strings in those files, leaving the keys and file-names intact. If a language string is missing then the `en` translation will be used. To show the language option in the user preferences language drop-down you will need to add your language to the options found at the bottom of the `resources/lang/en/settings.php` file. A system-wide language can also be set in the `.env` file like so: `APP_LANG=en`.
You will also need to add the language to the `locales` array in the `config/app.php` file.
Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time. Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time.

View File

@ -215,7 +215,7 @@ module.exports = function (ngApp, events) {
} }
}]); }]);
const md = new MarkdownIt(); const md = new MarkdownIt({html: true});
md.use(mdTasksLists, {label: true}); md.use(mdTasksLists, {label: true});
/** /**

View File

@ -14,6 +14,7 @@ return [
'recent_activity' => 'Recent Activity', 'recent_activity' => 'Recent Activity',
'create_now' => 'Create one now', 'create_now' => 'Create one now',
'revisions' => 'Revisions', 'revisions' => 'Revisions',
'meta_revision' => 'Revision #:revisionCount',
'meta_created' => 'Created :timeLength', 'meta_created' => 'Created :timeLength',
'meta_created_name' => 'Created :timeLength by :user', 'meta_created_name' => 'Created :timeLength by :user',
'meta_updated' => 'Updated :timeLength', 'meta_updated' => 'Updated :timeLength',
@ -168,6 +169,7 @@ return [
'pages_revision_named' => 'Page Revision for :pageName', 'pages_revision_named' => 'Page Revision for :pageName',
'pages_revisions_created_by' => 'Created By', 'pages_revisions_created_by' => 'Created By',
'pages_revisions_date' => 'Revision Date', 'pages_revisions_date' => 'Revision Date',
'pages_revisions_number' => '#',
'pages_revisions_changelog' => 'Changelog', 'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes', 'pages_revisions_changes' => 'Changes',
'pages_revisions_current' => 'Current Version', 'pages_revisions_current' => 'Current Version',

View File

@ -11,7 +11,7 @@ return [
| |
*/ */
'failed' => 'Las credenciales no concuerdan con nuestros registros.', 'failed' => 'Las credenciales no concuerdan con nuestros registros.',
'throttle' => 'Demasiados intentos fallidos de conexiÃn. Por favor intente nuevamente en :seconds segundos.', 'throttle' => 'Demasiados intentos fallidos de conexión. Por favor intente nuevamente en :seconds segundos.',
/** /**
* Login & Register * Login & Register

View File

@ -18,7 +18,7 @@ return [
'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir', 'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',
'images_deleted' => 'Imágenes borradas', 'images_deleted' => 'Imágenes borradas',
'image_preview' => 'Preview de la imagen', 'image_preview' => 'Preview de la imagen',
'image_upload_success' => 'Imagen subida exitosamente', 'image_upload_success' => 'Imagen subida éxitosamente',
'image_update_success' => 'Detalles de la imagen actualizados exitosamente', 'image_update_success' => 'Detalles de la imagen actualizados exitosamente',
'image_delete_success' => 'Imagen borrada exitosamente' 'image_delete_success' => 'Imagen borrada exitosamente'
]; ];

View File

@ -4,7 +4,7 @@ return [
/** /**
* Shared * Shared
*/ */
'recently_created' => 'Recientemente creadod', 'recently_created' => 'Recientemente creado',
'recently_created_pages' => 'Páginas recientemente creadas', 'recently_created_pages' => 'Páginas recientemente creadas',
'recently_updated_pages' => 'Páginas recientemente actualizadas', 'recently_updated_pages' => 'Páginas recientemente actualizadas',
'recently_created_chapters' => 'Capítulos recientemente creados', 'recently_created_chapters' => 'Capítulos recientemente creados',
@ -139,79 +139,79 @@ return [
'pages_md_editor' => 'Editor', 'pages_md_editor' => 'Editor',
'pages_md_preview' => 'Preview', 'pages_md_preview' => 'Preview',
'pages_md_insert_image' => 'Insertar Imagen', 'pages_md_insert_image' => 'Insertar Imagen',
'pages_md_insert_link' => 'Insert Entity Link', 'pages_md_insert_link' => 'Insertar link de entidad',
'pages_not_in_chapter' => 'Page is not in a chapter', 'pages_not_in_chapter' => 'La página no esá en el caítulo',
'pages_move' => 'Move Page', 'pages_move' => 'Mover página',
'pages_move_success' => 'Page moved to ":parentName"', 'pages_move_success' => 'Página movida a ":parentName"',
'pages_permissions' => 'Page Permissions', 'pages_permissions' => 'Permisos de página',
'pages_permissions_success' => 'Page permissions updated', 'pages_permissions_success' => 'Permisos de página actualizados',
'pages_revisions' => 'Page Revisions', 'pages_revisions' => 'Revisiones de página',
'pages_revisions_named' => 'Page Revisions for :pageName', 'pages_revisions_named' => 'Revisiones de página para :pageName',
'pages_revision_named' => 'Page Revision for :pageName', 'pages_revision_named' => 'Revisión de ágina para :pageName',
'pages_revisions_created_by' => 'Created By', 'pages_revisions_created_by' => 'Creado por',
'pages_revisions_date' => 'Revision Date', 'pages_revisions_date' => 'Fecha de revisión',
'pages_revisions_changelog' => 'Changelog', 'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes', 'pages_revisions_changes' => 'Cambios',
'pages_revisions_current' => 'Current Version', 'pages_revisions_current' => 'Versión actual',
'pages_revisions_preview' => 'Preview', 'pages_revisions_preview' => 'Preview',
'pages_revisions_restore' => 'Restore', 'pages_revisions_restore' => 'Restaurar',
'pages_revisions_none' => 'This page has no revisions', 'pages_revisions_none' => 'Esta página no tiene revisiones',
'pages_copy_link' => 'Copy Link', 'pages_copy_link' => 'Copiar Link',
'pages_permissions_active' => 'Page Permissions Active', 'pages_permissions_active' => 'Permisos de página activos',
'pages_initial_revision' => 'Initial publish', 'pages_initial_revision' => 'Publicación inicial',
'pages_initial_name' => 'New Page', 'pages_initial_name' => 'Página nueva',
'pages_editing_draft_notification' => 'You are currently editing a draft that was last saved :timeDiff.', 'pages_editing_draft_notification' => 'Ud. está actualmente editando un borrador que fue guardado porúltima vez el :timeDiff.',
'pages_draft_edited_notification' => 'This page has been updated by since that time. It is recommended that you discard this draft.', 'pages_draft_edited_notification' => 'Esta página ha sido actualizada desde aquel momento. Se recomienda que cancele este borrador.',
'pages_draft_edit_active' => [ 'pages_draft_edit_active' => [
'start_a' => ':count users have started editing this page', 'start_a' => ':count usuarios han comenzado a editar esta página',
'start_b' => ':userName has started editing this page', 'start_b' => ':userName ha comenzado a editar esta página',
'time_a' => 'since the pages was last updated', 'time_a' => 'desde que las página fue actualizada',
'time_b' => 'in the last :minCount minutes', 'time_b' => 'en los últimos :minCount minutos',
'message' => ':start :time. Take care not to overwrite each other\'s updates!', 'message' => ':start :time. Ten cuidado de no sobreescribir los cambios del otro usuario',
], ],
'pages_draft_discarded' => 'Draft discarded, The editor has been updated with the current page content', 'pages_draft_discarded' => 'Borrador descartado, el editor ha sido actualizado con el contenido de la página actual',
/** /**
* Editor sidebar * Editor sidebar
*/ */
'page_tags' => 'Page Tags', 'page_tags' => 'Etiquetas de página',
'tag' => 'Tag', 'tag' => 'Etiqueta',
'tags' => '', 'tags' => 'Etiquetas',
'tag_value' => 'Tag Value (Optional)', 'tag_value' => 'Valor de la etiqueta (Opcional)',
'tags_explain' => "Add some tags to better categorise your content. \n You can assign a value to a tag for more in-depth organisation.", 'tags_explain' => "Agregar algunas etiquetas para mejorar la categorización de su contenido. \n Ud. puede asignar un valor a una etiqueta para una organizacón a mayor detalle.",
'tags_add' => 'Add another tag', 'tags_add' => 'Agregar otra etiqueta',
'attachments' => 'Attachments', 'attachments' => 'Adjuntos',
'attachments_explain' => 'Upload some files or attach some link to display on your page. These are visible in the page sidebar.', 'attachments_explain' => 'Subir ficheros o agregar links para mostrar en la página. Estos son visibles en la barra lateral de la página.',
'attachments_explain_instant_save' => 'Changes here are saved instantly.', 'attachments_explain_instant_save' => 'Los cambios son guardados de manera instantánea .',
'attachments_items' => 'Attached Items', 'attachments_items' => 'Items adjuntados',
'attachments_upload' => 'Upload File', 'attachments_upload' => 'Fichero adjuntado',
'attachments_link' => 'Attach Link', 'attachments_link' => 'Adjuntar Link',
'attachments_set_link' => 'Set Link', 'attachments_set_link' => 'Setear Link',
'attachments_delete_confirm' => 'Click delete again to confirm you want to delete this attachment.', 'attachments_delete_confirm' => 'Haga click en borrar nuevamente para confirmar que quiere borrar este adjunto.',
'attachments_dropzone' => 'Drop files or click here to attach a file', 'attachments_dropzone' => 'Arrastre ficheros aquío haga click aquípara adjuntar un fichero',
'attachments_no_files' => 'No files have been uploaded', 'attachments_no_files' => 'Ningún fichero ha sido adjuntado',
'attachments_explain_link' => 'You can attach a link if you\'d prefer not to upload a file. This can be a link to another page or a link to a file in the cloud.', 'attachments_explain_link' => 'Ud. puede agregar un link o si lo prefiere puede agregar un fichero. Esto puede ser un link a otra página o un link a un fichero en la nube.',
'attachments_link_name' => 'Link Name', 'attachments_link_name' => 'Nombre de Link',
'attachment_link' => 'Attachment link', 'attachment_link' => 'Link adjunto',
'attachments_link_url' => 'Link to file', 'attachments_link_url' => 'Link a fichero',
'attachments_link_url_hint' => 'Url of site or file', 'attachments_link_url_hint' => 'Url del sitio o fichero',
'attach' => 'Attach', 'attach' => 'Adjuntar',
'attachments_edit_file' => 'Edit File', 'attachments_edit_file' => 'Editar fichero',
'attachments_edit_file_name' => 'File Name', 'attachments_edit_file_name' => 'Nombre del fichero',
'attachments_edit_drop_upload' => 'Drop files or click here to upload and overwrite', 'attachments_edit_drop_upload' => 'Arrastre a los ficheros o haga click aquípara subir o sobreescribir',
'attachments_order_updated' => 'Attachment order updated', 'attachments_order_updated' => 'Orden de adjuntos actualizado',
'attachments_updated_success' => 'Attachment details updated', 'attachments_updated_success' => 'Detalles de adjuntos actualizados',
'attachments_deleted' => 'Attachment deleted', 'attachments_deleted' => 'Adjunto borrado',
'attachments_file_uploaded' => 'File successfully uploaded', 'attachments_file_uploaded' => 'Fichero subido éxitosamente',
'attachments_file_updated' => 'File successfully updated', 'attachments_file_updated' => 'Fichero actualizado éxitosamente',
'attachments_link_attached' => 'Link successfully attached to page', 'attachments_link_attached' => 'Link agregado éxitosamente a la ágina',
/** /**
* Profile View * Profile View
*/ */
'profile_user_for_x' => 'User for :time', 'profile_user_for_x' => 'Usuario para :time',
'profile_created_content' => 'Created Content', 'profile_created_content' => 'Contenido creado',
'profile_not_created_pages' => ':userName has not created any pages', 'profile_not_created_pages' => ':userName no ha creado ninguna página',
'profile_not_created_chapters' => ':userName has not created any chapters', 'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',
'profile_not_created_books' => ':userName has not created any books', 'profile_not_created_books' => ':userName no ha creado ningún libro',
]; ];

View File

@ -7,64 +7,64 @@ return [
*/ */
// Permissions // Permissions
'permission' => 'You do not have permission to access the requested page.', 'permission' => 'Ud. no tiene permisos para visualizar la página solicitada.',
'permissionJson' => 'You do not have permission to perform the requested action.', 'permissionJson' => 'Ud. no tiene permisos para ejecutar la acción solicitada.',
// Auth // Auth
'error_user_exists_different_creds' => 'A user with the email :email already exists but with different credentials.', 'error_user_exists_different_creds' => 'Un usuario con el email :email ya existe pero con credenciales diferentes.',
'email_already_confirmed' => 'Email has already been confirmed, Try logging in.', 'email_already_confirmed' => 'El email ya ha sido confirmado, Intente loguearse en la aplicación.',
'email_confirmation_invalid' => 'This confirmation token is not valid or has already been used, Please try registering again.', 'email_confirmation_invalid' => 'Este token de confirmación no e válido o ya ha sido usado,Intente registrar uno nuevamente.',
'email_confirmation_expired' => 'The confirmation token has expired, A new confirmation email has been sent.', 'email_confirmation_expired' => 'El token de confirmación ha expirado, Un nuevo email de confirmacón ha sido enviado.',
'ldap_fail_anonymous' => 'LDAP access failed using anonymous bind', 'ldap_fail_anonymous' => 'El acceso con LDAP ha fallado usando binding anónimo',
'ldap_fail_authed' => 'LDAP access failed using given dn & password details', 'ldap_fail_authed' => 'El acceso LDAP usando el dn & password detallados',
'ldap_extension_not_installed' => 'LDAP PHP extension not installed', 'ldap_extension_not_installed' => 'La extensión LDAP PHP no se encuentra instalada',
'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed', 'ldap_cannot_connect' => 'No se puede conectar con el servidor ldap, la conexión inicial ha fallado',
'social_no_action_defined' => 'No action defined', 'social_no_action_defined' => 'Acción no definida',
'social_account_in_use' => 'This :socialAccount account is already in use, Try logging in via the :socialAccount option.', 'social_account_in_use' => 'la cuenta :socialAccount ya se encuentra en uso, intente loguearse a través de la opcón :socialAccount .',
'social_account_email_in_use' => 'The email :email is already in use. If you already have an account you can connect your :socialAccount account from your profile settings.', 'social_account_email_in_use' => 'El email :email ya se encuentra en uso. Si ud. ya dispone de una cuenta puede loguearse a través de su cuenta :socialAccount desde la configuración de perfil.',
'social_account_existing' => 'This :socialAccount is already attached to your profile.', 'social_account_existing' => 'La cuenta :socialAccount ya se encuentra asignada a su perfil.',
'social_account_already_used_existing' => 'This :socialAccount account is already used by another user.', 'social_account_already_used_existing' => 'La cuenta :socialAccount ya se encuentra usada por otro usuario.',
'social_account_not_used' => 'This :socialAccount account is not linked to any users. Please attach it in your profile settings. ', 'social_account_not_used' => 'La cuenta :socialAccount no está asociada a ningún usuario. Por favor adjuntela a su configuración de perfil. ',
'social_account_register_instructions' => 'If you do not yet have an account, You can register an account using the :socialAccount option.', 'social_account_register_instructions' => 'Si no dispone de una cuenta, puede registrar una cuenta usando la opción de :socialAccount .',
'social_driver_not_found' => 'Social driver not found', 'social_driver_not_found' => 'Driver social no encontrado',
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.', 'social_driver_not_configured' => 'Su configuración :socialAccount no es correcta.',
// System // System
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.', 'path_not_writable' => 'La ruta :filePath no pudo ser cargada. Asegurese de que es escribible por el servidor.',
'cannot_get_image_from_url' => 'Cannot get image from :url', 'cannot_get_image_from_url' => 'No se puede obtener la imagen desde :url',
'cannot_create_thumbs' => 'The server cannot create thumbnails. Please check you have the GD PHP extension installed.', 'cannot_create_thumbs' => 'El servidor no puede crear la imagen miniatura. Por favor chequee que tiene la extensión GD instalada.',
'server_upload_limit' => 'The server does not allow uploads of this size. Please try a smaller file size.', 'server_upload_limit' => 'El servidor no permite la subida de ficheros de este tamañ. Por favor intente con un fichero de menor tamañ.',
'image_upload_error' => 'An error occurred uploading the image', 'image_upload_error' => 'Ha ocurrido un error al subir la imagen',
// Attachments // Attachments
'attachment_page_mismatch' => 'Page mismatch during attachment update', 'attachment_page_mismatch' => 'Página no coincidente durante la subida del adjunto ',
// Pages // Pages
'page_draft_autosave_fail' => 'Failed to save draft. Ensure you have internet connection before saving this page', 'page_draft_autosave_fail' => 'Fallo al guardar borrador. Asegurese de que tiene conexión a Internet antes de guardar este borrador',
// Entities // Entities
'entity_not_found' => 'Entity not found', 'entity_not_found' => 'Entidad no encontrada',
'book_not_found' => 'Book not found', 'book_not_found' => 'Libro no encontrado',
'page_not_found' => 'Page not found', 'page_not_found' => 'Página no encontrada',
'chapter_not_found' => 'Chapter not found', 'chapter_not_found' => 'Capítulo no encontrado',
'selected_book_not_found' => 'The selected book was not found', 'selected_book_not_found' => 'El libro seleccionado no fue encontrado',
'selected_book_chapter_not_found' => 'The selected Book or Chapter was not found', 'selected_book_chapter_not_found' => 'El libro o capítulo seleccionado no fue encontrado',
'guests_cannot_save_drafts' => 'Guests cannot save drafts', 'guests_cannot_save_drafts' => 'Los invitados no pueden guardar los borradores',
// Users // Users
'users_cannot_delete_only_admin' => 'You cannot delete the only admin', 'users_cannot_delete_only_admin' => 'No se puede borrar el único administrador',
'users_cannot_delete_guest' => 'You cannot delete the guest user', 'users_cannot_delete_guest' => 'No se puede borrar el usuario invitado',
// Roles // Roles
'role_cannot_be_edited' => 'This role cannot be edited', 'role_cannot_be_edited' => 'Este rol no puede ser editado',
'role_system_cannot_be_deleted' => 'This role is a system role and cannot be deleted', 'role_system_cannot_be_deleted' => 'Este rol es un rol de sistema y no puede ser borrado',
'role_registration_default_cannot_delete' => 'This role cannot be deleted while set as the default registration role', 'role_registration_default_cannot_delete' => 'Este rol no puede ser borrado mientras sea el rol por defecto de registro',
// Error pages // Error pages
'404_page_not_found' => 'Page Not Found', '404_page_not_found' => 'Página no encontrada',
'sorry_page_not_found' => 'Sorry, The page you were looking for could not be found.', 'sorry_page_not_found' => 'Lo sentimos, la página que intenta acceder no pudo ser encontrada.',
'return_home' => 'Return to home', 'return_home' => 'Volver al home',
'error_occurred' => 'An Error Occurred', 'error_occurred' => 'Ha ocurrido un error',
'app_down' => ':appName is down right now', 'app_down' => 'La aplicación :appName se encuentra caída en este momento',
'back_soon' => 'It will be back up soon.', 'back_soon' => 'Volverá a estar operativa en corto tiempo.',
]; ];

View File

@ -8,105 +8,105 @@ return [
* including users and roles. * including users and roles.
*/ */
'settings' => 'Settings', 'settings' => 'Ajustes',
'settings_save' => 'Save Settings', 'settings_save' => 'Guardar ajustes',
'settings_save_success' => 'Settings saved', 'settings_save_success' => 'Ajustes guardados',
/** /**
* App settings * App settings
*/ */
'app_settings' => 'App Settings', 'app_settings' => 'Ajustes de App',
'app_name' => 'Application name', 'app_name' => 'Nombre de aplicación',
'app_name_desc' => 'This name is shown in the header and any emails.', 'app_name_desc' => 'Este nombre es mostrado en la cabecera y en cualquier email de la aplicación',
'app_name_header' => 'Show Application name in header?', 'app_name_header' => 'Mostrar el nombre de la aplicación en la cabecera?',
'app_public_viewing' => 'Allow public viewing?', 'app_public_viewing' => 'Permitir vista pública?',
'app_secure_images' => 'Enable higher security image uploads?', 'app_secure_images' => 'Habilitar mayor seguridad para subir imágenes?',
'app_secure_images_desc' => 'For performance reasons, all images are public. This option adds a random, hard-to-guess string in front of image urls. Ensure directory indexes are not enabled to prevent easy access.', 'app_secure_images_desc' => 'Por razones de performance, todas las imágenes son púicas. Esta opción agrega una cadena larga difícil de adivinar, asegúrese que los indices de directorios no esán habilitados para prevenir el acceso fácil a las imágenes.',
'app_editor' => 'Page editor', 'app_editor' => 'Editor de página',
'app_editor_desc' => 'Select which editor will be used by all users to edit pages.', 'app_editor_desc' => 'Seleccione cuál editor ser usado por todos los usuarios para editar páginas.',
'app_custom_html' => 'Custom HTML head content', 'app_custom_html' => 'Contenido de cabecera HTML customizable',
'app_custom_html_desc' => 'Any content added here will be inserted into the bottom of the <head> section of every page. This is handy for overriding styles or adding analytics code.', 'app_custom_html_desc' => 'Cualquier contenido agregado aquíseráinsertado al final de la secón <head> de cada ágina. Esto esútil para sobreescribir estilo o agregar código para anaíticas.',
'app_logo' => 'Application logo', 'app_logo' => 'Logo de la aplicación',
'app_logo_desc' => 'This image should be 43px in height. <br>Large images will be scaled down.', 'app_logo_desc' => 'Esta imagen debería de ser 43px en altura. <br>Iágenes grandes seán escaladas.',
'app_primary_color' => 'Application primary color', 'app_primary_color' => 'Color primario de la aplicación',
'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.', 'app_primary_color_desc' => 'Esto debería ser un valor hexadecimal. <br>Deje el valor vaío para reiniciar al valor por defecto.',
/** /**
* Registration settings * Registration settings
*/ */
'reg_settings' => 'Registration Settings', 'reg_settings' => 'Ajustes de registro',
'reg_allow' => 'Allow registration?', 'reg_allow' => 'Permitir registro?',
'reg_default_role' => 'Default user role after registration', 'reg_default_role' => 'Rol de usuario por defecto despúes del registro',
'reg_confirm_email' => 'Require email confirmation?', 'reg_confirm_email' => 'Requerir email de confirmaación?',
'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and the below value will be ignored.', 'reg_confirm_email_desc' => 'Si la restricción por dominio es usada, entonces la confirmaciónpor email serárequerida y el valor a continuón será ignorado.',
'reg_confirm_restrict_domain' => 'Restrict registration to domain', 'reg_confirm_restrict_domain' => 'Restringir registro al dominio',
'reg_confirm_restrict_domain_desc' => 'Enter a comma separated list of email domains you would like to restrict registration to. Users will be sent an email to confirm their address before being allowed to interact with the application. <br> Note that users will be able to change their email addresses after successful registration.', 'reg_confirm_restrict_domain_desc' => 'Introduzca una lista separada por comasa de los emails del dominio a los que les gustaría restringir el registro por dominio. A los usuarios les seá enviado un emal para confirmar la dirección antes de que se le permita interactuar con la aplicacón. <br> Note que los usuarios se les permitir ácambiar sus direcciones de email luego de un registr éxioso.',
'reg_confirm_restrict_domain_placeholder' => 'No restriction set', 'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida',
/** /**
* Role settings * Role settings
*/ */
'roles' => 'Roles', 'roles' => 'Roles',
'role_user_roles' => 'User Roles', 'role_user_roles' => 'Roles de usuario',
'role_create' => 'Create New Role', 'role_create' => 'Crear nuevo rol',
'role_create_success' => 'Role successfully created', 'role_create_success' => 'Rol creado satisfactoriamente',
'role_delete' => 'Delete Role', 'role_delete' => 'Borrar rol',
'role_delete_confirm' => 'This will delete the role with the name \':roleName\'.', 'role_delete_confirm' => 'Se borrará el rol con nombre \':roleName\'.',
'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.', 'role_delete_users_assigned' => 'Este rol tiene :userCount usuarios asignados. Si ud. quisiera migrar los usuarios de este rol, seleccione un nuevo rol a continuación.',
'role_delete_no_migration' => "Don't migrate users", 'role_delete_no_migration' => "No migrar usuarios",
'role_delete_sure' => 'Are you sure you want to delete this role?', 'role_delete_sure' => 'Está seguro que desea borrar este rol?',
'role_delete_success' => 'Role successfully deleted', 'role_delete_success' => 'Rol borrado satisfactoriamente',
'role_edit' => 'Edit Role', 'role_edit' => 'Editar rol',
'role_details' => 'Role Details', 'role_details' => 'Detalles de rol',
'role_name' => 'Role Name', 'role_name' => 'Nombre de rol',
'role_desc' => 'Short Description of Role', 'role_desc' => 'Descripción corta de rol',
'role_system' => 'System Permissions', 'role_system' => 'Permisos de sistema',
'role_manage_users' => 'Manage users', 'role_manage_users' => 'Gestionar usuarios',
'role_manage_roles' => 'Manage roles & role permissions', 'role_manage_roles' => 'Gestionar roles y permisos de roles',
'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions', 'role_manage_entity_permissions' => 'Gestionar todos los permisos de libros, capítulos y áginas',
'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages', 'role_manage_own_entity_permissions' => 'Gestionar permisos en libros propios, capítulos y páginas',
'role_manage_settings' => 'Manage app settings', 'role_manage_settings' => 'Gestionar ajustes de activos',
'role_asset' => 'Asset Permissions', 'role_asset' => 'Permisos de activos',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.', 'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos a Libros, Capítulos y áginas sobreescribiran estos permisos.',
'role_all' => 'All', 'role_all' => 'Todo',
'role_own' => 'Own', 'role_own' => 'Propio',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to', 'role_controlled_by_asset' => 'Controlado por el actvo al que ha sido subido',
'role_save' => 'Save Role', 'role_save' => 'Guardar rol',
'role_update_success' => 'Role successfully updated', 'role_update_success' => 'Rol actualizado éxitosamente',
'role_users' => 'Users in this role', 'role_users' => 'Usuarios en este rol',
'role_users_none' => 'No users are currently assigned to this role', 'role_users_none' => 'No hay usuarios asignados a este rol',
/** /**
* Users * Users
*/ */
'users' => 'Users', 'users' => 'Usuarios',
'user_profile' => 'User Profile', 'user_profile' => 'Perfil de usuario',
'users_add_new' => 'Add New User', 'users_add_new' => 'Agregar nuevo usuario',
'users_search' => 'Search Users', 'users_search' => 'Buscar usuarios',
'users_role' => 'User Roles', 'users_role' => 'Roles de usuario',
'users_external_auth_id' => 'External Authentication ID', 'users_external_auth_id' => 'ID externo de autenticación',
'users_password_warning' => 'Only fill the below if you would like to change your password:', 'users_password_warning' => 'Solo rellene a continuación si desea cambiar su password:',
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.', 'users_system_public' => 'Este usuario representa cualquier usuario invitado que visita la aplicación. No puede utilizarse para hacer login sio que es asignado automáticamente.',
'users_delete' => 'Delete User', 'users_delete' => 'Borrar usuario',
'users_delete_named' => 'Delete user :userName', 'users_delete_named' => 'Borrar usuario :userName',
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.', 'users_delete_warning' => 'Se borrará completamente el usuario con el nombre \':userName\' del sistema.',
'users_delete_confirm' => 'Are you sure you want to delete this user?', 'users_delete_confirm' => 'Está seguro que desea borrar este usuario?',
'users_delete_success' => 'Users successfully removed', 'users_delete_success' => 'Usuarios removidos éxitosamente',
'users_edit' => 'Edit User', 'users_edit' => 'Editar Usuario',
'users_edit_profile' => 'Edit Profile', 'users_edit_profile' => 'Editar perfil',
'users_edit_success' => 'User successfully updated', 'users_edit_success' => 'Usuario actualizado',
'users_avatar' => 'User Avatar', 'users_avatar' => 'Avatar del usuario',
'users_avatar_desc' => 'This image should be approx 256px square.', 'users_avatar_desc' => 'Esta imagen debe ser aproximadamente 256px por lado.',
'users_preferred_language' => 'Preferred Language', 'users_preferred_language' => 'Lenguaje preferido',
'users_social_accounts' => 'Social Accounts', 'users_social_accounts' => 'Cuentas sociales',
'users_social_accounts_info' => 'Here you can connect your other accounts for quicker and easier login. Disconnecting an account here does not previously authorized access. Revoke access from your profile settings on the connected social account.', 'users_social_accounts_info' => 'Aquí puede conectar sus otras cuentas para un ápido y ás ácil login. Desconectando una cuenta aqu íno revca accesos ya autorizados. Revoque el acceso desde se perfil desde los ajustes de perfil en la cuenta social conectada.',
'users_social_connect' => 'Connect Account', 'users_social_connect' => 'Conectar cuenta',
'users_social_disconnect' => 'Disconnect Account', 'users_social_disconnect' => 'Desconectar cuenta',
'users_social_connected' => ':socialAccount account was successfully attached to your profile.', 'users_social_connected' => 'La cuenta :socialAccount ha sido éxitosamente añadida a su perfil.',
'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.', 'users_social_disconnected' => 'La cuenta :socialAccount ha sido desconectada éxitosamente de su perfil.',
]; ];

View File

@ -13,67 +13,67 @@ return [
| |
*/ */
'accepted' => 'The :attribute must be accepted.', 'accepted' => 'El :attribute debe ser aceptado.',
'active_url' => 'The :attribute is not a valid URL.', 'active_url' => 'El :attribute no es una URl válida.',
'after' => 'The :attribute must be a date after :date.', 'after' => 'El :attribute debe ser una fecha posterior :date.',
'alpha' => 'The :attribute may only contain letters.', 'alpha' => 'El :attribute solo puede contener letras.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', 'alpha_dash' => 'El :attribute solo puede contener letras, números y guiones.',
'alpha_num' => 'The :attribute may only contain letters and numbers.', 'alpha_num' => 'El :attribute solo puede contener letras y número.',
'array' => 'The :attribute must be an array.', 'array' => 'El :attribute debe de ser un array.',
'before' => 'The :attribute must be a date before :date.', 'before' => 'El :attribute debe ser una fecha anterior a :date.',
'between' => [ 'between' => [
'numeric' => 'The :attribute must be between :min and :max.', 'numeric' => 'El :attribute debe estar entre :min y :max.',
'file' => 'The :attribute must be between :min and :max kilobytes.', 'file' => 'El :attribute debe estar entre :min y :max kilobytes.',
'string' => 'The :attribute must be between :min and :max characters.', 'string' => 'El :attribute debe estar entre :min y :max carácteres.',
'array' => 'The :attribute must have between :min and :max items.', 'array' => 'El :attribute debe estar entre :min y :max items.',
], ],
'boolean' => 'The :attribute field must be true or false.', 'boolean' => 'El campo :attribute debe ser true o false.',
'confirmed' => 'The :attribute confirmation does not match.', 'confirmed' => 'La confirmación de :attribute no concuerda.',
'date' => 'The :attribute is not a valid date.', 'date' => 'El :attribute no es una fecha válida.',
'date_format' => 'The :attribute does not match the format :format.', 'date_format' => 'El :attribute no coincide con el formato :format.',
'different' => 'The :attribute and :other must be different.', 'different' => ':attribute y :other deben ser diferentes.',
'digits' => 'The :attribute must be :digits digits.', 'digits' => ':attribute debe ser de :digits dígitos.',
'digits_between' => 'The :attribute must be between :min and :max digits.', 'digits_between' => ':attribute debe ser un valor entre :min y :max dígios.',
'email' => 'The :attribute must be a valid email address.', 'email' => ':attribute debe ser una dirección álida.',
'filled' => 'The :attribute field is required.', 'filled' => 'El campo :attribute es requerido.',
'exists' => 'The selected :attribute is invalid.', 'exists' => 'El :attribute seleccionado es inválido.',
'image' => 'The :attribute must be an image.', 'image' => 'El :attribute debe ser una imagen.',
'in' => 'The selected :attribute is invalid.', 'in' => 'El selected :attribute es inválio.',
'integer' => 'The :attribute must be an integer.', 'integer' => 'El :attribute debe ser un entero.',
'ip' => 'The :attribute must be a valid IP address.', 'ip' => 'El :attribute debe ser una dirección IP álida.',
'max' => [ 'max' => [
'numeric' => 'The :attribute may not be greater than :max.', 'numeric' => ':attribute no puede ser mayor que :max.',
'file' => 'The :attribute may not be greater than :max kilobytes.', 'file' => ':attribute no puede ser mayor que :max kilobytes.',
'string' => 'The :attribute may not be greater than :max characters.', 'string' => ':attribute no puede ser mayor que :max carácteres.',
'array' => 'The :attribute may not have more than :max items.', 'array' => ':attribute no puede contener más de :max items.',
], ],
'mimes' => 'The :attribute must be a file of type: :values.', 'mimes' => ':attribute debe ser un fichero de tipo: :values.',
'min' => [ 'min' => [
'numeric' => 'The :attribute must be at least :min.', 'numeric' => ':attribute debe ser al menos de :min.',
'file' => 'The :attribute must be at least :min kilobytes.', 'file' => ':attribute debe ser al menos :min kilobytes.',
'string' => 'The :attribute must be at least :min characters.', 'string' => ':attribute debe ser al menos :min caracteres.',
'array' => 'The :attribute must have at least :min items.', 'array' => ':attribute debe tener como mínimo :min items.',
], ],
'not_in' => 'The selected :attribute is invalid.', 'not_in' => ':attribute seleccionado es inválio.',
'numeric' => 'The :attribute must be a number.', 'numeric' => ':attribute debe ser numérico.',
'regex' => 'The :attribute format is invalid.', 'regex' => ':attribute con formato inválido',
'required' => 'The :attribute field is required.', 'required' => ':attribute es requerido.',
'required_if' => 'The :attribute field is required when :other is :value.', 'required_if' => ':attribute es requerido cuando :other vale :value.',
'required_with' => 'The :attribute field is required when :values is present.', 'required_with' => 'El campo :attribute es requerido cuando se encuentre entre los valores :values.',
'required_with_all' => 'The :attribute field is required when :values is present.', 'required_with_all' => 'El campo :attribute es requerido cuando los valores sean :values.',
'required_without' => 'The :attribute field is required when :values is not present.', 'required_without' => ':attribute es requerido cuando no se encuentre entre los valores :values.',
'required_without_all' => 'The :attribute field is required when none of :values are present.', 'required_without_all' => ':attribute es requerido cuando ninguno de los valores :values están presentes.',
'same' => 'The :attribute and :other must match.', 'same' => ':attribute y :other deben coincidir.',
'size' => [ 'size' => [
'numeric' => 'The :attribute must be :size.', 'numeric' => ':attribute debe ser :size.',
'file' => 'The :attribute must be :size kilobytes.', 'file' => ':attribute debe ser :size kilobytes.',
'string' => 'The :attribute must be :size characters.', 'string' => ':attribute debe ser :size caracteres.',
'array' => 'The :attribute must contain :size items.', 'array' => ':attribute debe contener :size items.',
], ],
'string' => 'The :attribute must be a string.', 'string' => 'El atributo :attribute debe ser una cadena.',
'timezone' => 'The :attribute must be a valid zone.', 'timezone' => 'El atributo :attribute debe ser una zona válida.',
'unique' => 'The :attribute has already been taken.', 'unique' => 'El atributo :attribute ya ha sido tomado.',
'url' => 'The :attribute format is invalid.', 'url' => 'El atributo :attribute tiene un formato inválid.',
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -88,7 +88,7 @@ return [
'custom' => [ 'custom' => [
'password-confirm' => [ 'password-confirm' => [
'required_with' => 'Password confirmation required', 'required_with' => 'Confirmación de Password requerida',
], ],
], ],

View File

@ -1,5 +1,7 @@
<div class="breadcrumbs"> <div class="breadcrumbs">
@if (userCan('view', $chapter->book))
<a href="{{ $chapter->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a> <a href="{{ $chapter->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $chapter->book->getShortName() }}</a>
<span class="sep">&raquo;</span> <span class="sep">&raquo;</span>
@endif
<a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a> <a href="{{ $chapter->getUrl() }}" class="text-chapter text-button"><i class="zmdi zmdi-collection-bookmark"></i>{{$chapter->getShortName()}}</a>
</div> </div>

View File

@ -1,12 +1,14 @@
<div class="breadcrumbs"> <div class="breadcrumbs">
<a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a> @if (userCan('view', $page->book))
@if($page->hasChapter()) <a href="{{ $page->book->getUrl() }}" class="text-book text-button"><i class="zmdi zmdi-book"></i>{{ $page->book->getShortName() }}</a>
<span class="sep">&raquo;</span> <span class="sep">&raquo;</span>
@endif
@if($page->hasChapter() && userCan('view', $page->chapter))
<a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button"> <a href="{{ $page->chapter->getUrl() }}" class="text-chapter text-button">
<i class="zmdi zmdi-collection-bookmark"></i> <i class="zmdi zmdi-collection-bookmark"></i>
{{ $page->chapter->getShortName() }} {{ $page->chapter->getShortName() }}
</a> </a>
<span class="sep">&raquo;</span>
@endif @endif
<span class="sep">&raquo;</span>
<a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a> <a href="{{ $page->getUrl() }}" class="text-page text-button"><i class="zmdi zmdi-file"></i>{{ $page->getShortName() }}</a>
</div> </div>

View File

@ -19,6 +19,7 @@
<table class="table"> <table class="table">
<tr> <tr>
<th width="3%">{{ trans('entities.pages_revisions_number') }}</th>
<th width="23%">{{ trans('entities.pages_name') }}</th> <th width="23%">{{ trans('entities.pages_name') }}</th>
<th colspan="2" width="8%">{{ trans('entities.pages_revisions_created_by') }}</th> <th colspan="2" width="8%">{{ trans('entities.pages_revisions_created_by') }}</th>
<th width="15%">{{ trans('entities.pages_revisions_date') }}</th> <th width="15%">{{ trans('entities.pages_revisions_date') }}</th>
@ -27,6 +28,7 @@
</tr> </tr>
@foreach($page->revisions as $index => $revision) @foreach($page->revisions as $index => $revision)
<tr> <tr>
<td>{{ $revision->revision_number == 0 ? '' : $revision->revision_number }}</td>
<td>{{ $revision->name }}</td> <td>{{ $revision->name }}</td>
<td style="line-height: 0;"> <td style="line-height: 0;">
@if($revision->createdBy) @if($revision->createdBy)

View File

@ -39,8 +39,10 @@
<h6 class="text-muted">{{ trans('entities.books_navigation') }}</h6> <h6 class="text-muted">{{ trans('entities.books_navigation') }}</h6>
<ul class="sidebar-page-list menu"> <ul class="sidebar-page-list menu">
<li class="book-header"><a href="{{ $book->getUrl() }}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li>
@if (userCan('view', $book))
<li class="book-header"><a href="{{ $book->getUrl() }}" class="book {{ $current->matches($book)? 'selected' : '' }}"><i class="zmdi zmdi-book"></i>{{$book->name}}</a></li>
@endif
@foreach($sidebarTree as $bookChild) @foreach($sidebarTree as $bookChild)
<li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}"> <li class="list-item-{{ $bookChild->getClassName() }} {{ $bookChild->getClassName() }} {{ $bookChild->isA('page') && $bookChild->draft ? 'draft' : '' }}">

View File

@ -1,13 +1,20 @@
<p class="text-muted small"> <p class="text-muted small">
@if ($entity->isA('page')) {{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br> @endif
@if ($entity->createdBy) @if ($entity->createdBy)
{!! trans('entities.meta_created_name', ['timeLength' => $entity->created_at->diffForHumans(), 'user' => "<a href='{$entity->createdBy->getProfileUrl()}'>".htmlentities($entity->createdBy->name). "</a>"]) !!} {!! 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>"
]) !!}
@else @else
{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }} <span title="{{$entity->created_at->toDayDateTimeString()}}">{{ trans('entities.meta_created', ['timeLength' => $entity->created_at->diffForHumans()]) }}</span>
@endif @endif
<br> <br>
@if ($entity->updatedBy) @if ($entity->updatedBy)
{!! trans('entities.meta_updated_name', ['timeLength' => $entity->updated_at->diffForHumans(), 'user' => "<a href='{$entity->updatedBy->getProfileUrl()}'>".htmlentities($entity->updatedBy->name). "</a>"]) !!} {!! 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>"
]) !!}
@else @else
{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }} <span title="{{ $entity->updated_at->toDayDateTimeString() }}">{{ trans('entities.meta_updated', ['timeLength' => $entity->updated_at->diffForHumans()]) }}</span>
@endif @endif
</p> </p>

View File

@ -1,6 +1,7 @@
<?php namespace Tests; <?php namespace Tests;
use BookStack\Role; use BookStack\Role;
use BookStack\Services\PermissionService;
use Illuminate\Contracts\Console\Kernel; use Illuminate\Contracts\Console\Kernel;
use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Foundation\Testing\DatabaseTransactions;
use Laravel\BrowserKitTesting\TestCase; use Laravel\BrowserKitTesting\TestCase;
@ -22,6 +23,12 @@ abstract class BrowserKitTest extends TestCase
private $admin; private $admin;
private $editor; private $editor;
public function tearDown()
{
\DB::disconnect();
parent::tearDown();
}
/** /**
* Creates the application. * Creates the application.
* *
@ -99,11 +106,9 @@ abstract class BrowserKitTest extends TestCase
{ {
if ($updaterUser === false) $updaterUser = $creatorUser; if ($updaterUser === false) $updaterUser = $creatorUser;
$book = factory(\BookStack\Book::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]); $book = factory(\BookStack\Book::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]);
$chapter = factory(\BookStack\Chapter::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id]); $chapter = factory(\BookStack\Chapter::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]);
$page = factory(\BookStack\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id]); $page = factory(\BookStack\Page::class)->create(['created_by' => $creatorUser->id, 'updated_by' => $updaterUser->id, 'book_id' => $book->id, 'chapter_id' => $chapter->id]);
$book->chapters()->saveMany([$chapter]); $restrictionService = $this->app[PermissionService::class];
$chapter->pages()->saveMany([$page]);
$restrictionService = $this->app[\BookStack\Services\PermissionService::class];
$restrictionService->buildJointPermissionsForEntity($book); $restrictionService->buildJointPermissionsForEntity($book);
return [ return [
'book' => $book, 'book' => $book,

View File

@ -42,7 +42,7 @@ class EntitySearchTest extends TestCase
public function test_book_search() public function test_book_search()
{ {
$book = \BookStack\Book::all()->first(); $book = \BookStack\Book::first();
$page = $book->pages->last(); $page = $book->pages->last();
$chapter = $book->chapters->last(); $chapter = $book->chapters->last();

View File

@ -1,5 +1,11 @@
<?php namespace Tests; <?php namespace Tests;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
class EntityTest extends BrowserKitTest class EntityTest extends BrowserKitTest
{ {
@ -18,7 +24,7 @@ class EntityTest extends BrowserKitTest
$this->bookDelete($book); $this->bookDelete($book);
} }
public function bookDelete(\BookStack\Book $book) public function bookDelete(Book $book)
{ {
$this->asAdmin() $this->asAdmin()
->visit($book->getUrl()) ->visit($book->getUrl())
@ -32,7 +38,7 @@ class EntityTest extends BrowserKitTest
->notSeeInDatabase('books', ['id' => $book->id]); ->notSeeInDatabase('books', ['id' => $book->id]);
} }
public function bookUpdate(\BookStack\Book $book) public function bookUpdate(Book $book)
{ {
$newName = $book->name . ' Updated'; $newName = $book->name . ' Updated';
$this->asAdmin() $this->asAdmin()
@ -46,12 +52,12 @@ class EntityTest extends BrowserKitTest
->seePageIs($book->getUrl() . '-updated') ->seePageIs($book->getUrl() . '-updated')
->see($newName); ->see($newName);
return \BookStack\Book::find($book->id); return Book::find($book->id);
} }
public function test_book_sort_page_shows() public function test_book_sort_page_shows()
{ {
$books = \BookStack\Book::all(); $books = Book::all();
$bookToSort = $books[0]; $bookToSort = $books[0];
$this->asAdmin() $this->asAdmin()
->visit($bookToSort->getUrl()) ->visit($bookToSort->getUrl())
@ -65,7 +71,7 @@ class EntityTest extends BrowserKitTest
public function test_book_sort_item_returns_book_content() public function test_book_sort_item_returns_book_content()
{ {
$books = \BookStack\Book::all(); $books = Book::all();
$bookToSort = $books[0]; $bookToSort = $books[0];
$firstPage = $bookToSort->pages[0]; $firstPage = $bookToSort->pages[0];
$firstChapter = $bookToSort->chapters[0]; $firstChapter = $bookToSort->chapters[0];
@ -79,7 +85,7 @@ class EntityTest extends BrowserKitTest
public function pageCreation($chapter) public function pageCreation($chapter)
{ {
$page = factory(\BookStack\Page::class)->make([ $page = factory(Page::class)->make([
'name' => 'My First Page' 'name' => 'My First Page'
]); ]);
@ -88,7 +94,7 @@ class EntityTest extends BrowserKitTest
->visit($chapter->getUrl()) ->visit($chapter->getUrl())
->click('New Page'); ->click('New Page');
$draftPage = \BookStack\Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first(); $draftPage = Page::where('draft', '=', true)->orderBy('created_at', 'desc')->first();
$this->seePageIs($draftPage->getUrl()) $this->seePageIs($draftPage->getUrl())
// Fill out form // Fill out form
@ -99,13 +105,13 @@ class EntityTest extends BrowserKitTest
->seePageIs($chapter->book->getUrl() . '/page/my-first-page') ->seePageIs($chapter->book->getUrl() . '/page/my-first-page')
->see($page->name); ->see($page->name);
$page = \BookStack\Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first(); $page = Page::where('slug', '=', 'my-first-page')->where('chapter_id', '=', $chapter->id)->first();
return $page; return $page;
} }
public function chapterCreation(\BookStack\Book $book) public function chapterCreation(Book $book)
{ {
$chapter = factory(\BookStack\Chapter::class)->make([ $chapter = factory(Chapter::class)->make([
'name' => 'My First Chapter' 'name' => 'My First Chapter'
]); ]);
@ -122,13 +128,13 @@ class EntityTest extends BrowserKitTest
->seePageIs($book->getUrl() . '/chapter/my-first-chapter') ->seePageIs($book->getUrl() . '/chapter/my-first-chapter')
->see($chapter->name)->see($chapter->description); ->see($chapter->name)->see($chapter->description);
$chapter = \BookStack\Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first(); $chapter = Chapter::where('slug', '=', 'my-first-chapter')->where('book_id', '=', $book->id)->first();
return $chapter; return $chapter;
} }
public function bookCreation() public function bookCreation()
{ {
$book = factory(\BookStack\Book::class)->make([ $book = factory(Book::class)->make([
'name' => 'My First Book' 'name' => 'My First Book'
]); ]);
$this->asAdmin() $this->asAdmin()
@ -154,7 +160,7 @@ class EntityTest extends BrowserKitTest
$expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/'; $expectedPattern = '/\/books\/my-first-book-[0-9a-zA-Z]{3}/';
$this->assertRegExp($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n"); $this->assertRegExp($expectedPattern, $this->currentUri, "Did not land on expected page [$expectedPattern].\n");
$book = \BookStack\Book::where('slug', '=', 'my-first-book')->first(); $book = Book::where('slug', '=', 'my-first-book')->first();
return $book; return $book;
} }
@ -165,8 +171,8 @@ class EntityTest extends BrowserKitTest
$updater = $this->getEditor(); $updater = $this->getEditor();
$entities = $this->createEntityChainBelongingToUser($creator, $updater); $entities = $this->createEntityChainBelongingToUser($creator, $updater);
$this->actingAs($creator); $this->actingAs($creator);
app('BookStack\Repos\UserRepo')->destroy($creator); app(UserRepo::class)->destroy($creator);
app('BookStack\Repos\EntityRepo')->savePageRevision($entities['page']); app(EntityRepo::class)->savePageRevision($entities['page']);
$this->checkEntitiesViewable($entities); $this->checkEntitiesViewable($entities);
} }
@ -178,8 +184,8 @@ class EntityTest extends BrowserKitTest
$updater = $this->getEditor(); $updater = $this->getEditor();
$entities = $this->createEntityChainBelongingToUser($creator, $updater); $entities = $this->createEntityChainBelongingToUser($creator, $updater);
$this->actingAs($updater); $this->actingAs($updater);
app('BookStack\Repos\UserRepo')->destroy($updater); app(UserRepo::class)->destroy($updater);
app('BookStack\Repos\EntityRepo')->savePageRevision($entities['page']); app(EntityRepo::class)->savePageRevision($entities['page']);
$this->checkEntitiesViewable($entities); $this->checkEntitiesViewable($entities);
} }
@ -216,7 +222,7 @@ class EntityTest extends BrowserKitTest
public function test_old_page_slugs_redirect_to_new_pages() public function test_old_page_slugs_redirect_to_new_pages()
{ {
$page = \BookStack\Page::first(); $page = Page::first();
$pageUrl = $page->getUrl(); $pageUrl = $page->getUrl();
$newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page'; $newPageUrl = '/books/' . $page->book->slug . '/page/super-test-page';
// Need to save twice since revisions are not generated in seeder. // Need to save twice since revisions are not generated in seeder.
@ -225,7 +231,7 @@ class EntityTest extends BrowserKitTest
->type('super test', '#name') ->type('super test', '#name')
->press('Save Page'); ->press('Save Page');
$page = \BookStack\Page::first(); $page = Page::first();
$pageUrl = $page->getUrl(); $pageUrl = $page->getUrl();
// Second Save // Second Save
@ -242,7 +248,7 @@ class EntityTest extends BrowserKitTest
public function test_recently_updated_pages_on_home() public function test_recently_updated_pages_on_home()
{ {
$page = \BookStack\Page::orderBy('updated_at', 'asc')->first(); $page = Page::orderBy('updated_at', 'asc')->first();
$this->asAdmin()->visit('/') $this->asAdmin()->visit('/')
->dontSeeInElement('#recently-updated-pages', $page->name); ->dontSeeInElement('#recently-updated-pages', $page->name);
$this->visit($page->getUrl() . '/edit') $this->visit($page->getUrl() . '/edit')

View File

@ -0,0 +1,32 @@
<?php namespace Entity;
use BookStack\Page;
use Tests\TestCase;
class PageRevisionTest extends TestCase
{
public function test_page_revision_count_increments_on_update()
{
$page = Page::first();
$startCount = $page->revision_count;
$resp = $this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
$resp->assertStatus(302);
$this->assertTrue(Page::find($page->id)->revision_count === $startCount+1);
}
public function test_revision_count_shown_in_page_meta()
{
$page = Page::first();
$this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
$this->asEditor()->put($page->getUrl(), ['name' => 'Updated page', 'html' => 'new page html', 'summary' => 'Update a']);
$page = Page::find($page->id);
$pageView = $this->get($page->getUrl());
$pageView->assertSee('Revision #' . $page->revision_count);
}
}

View File

@ -1,5 +1,6 @@
<?php namespace Tests; <?php namespace Tests;
use BookStack\Role;
use BookStack\Tag; use BookStack\Tag;
use BookStack\Page; use BookStack\Page;
use BookStack\Services\PermissionService; use BookStack\Services\PermissionService;

View File

@ -14,6 +14,23 @@ class LanguageTest extends TestCase
$this->langs = array_diff(scandir(resource_path('lang')), ['..', '.']); $this->langs = array_diff(scandir(resource_path('lang')), ['..', '.']);
} }
public function test_locales_config_key_set_properly()
{
$configLocales = config('app.locales');
sort($configLocales);
sort($this->langs);
$this->assertTrue(implode(':', $this->langs) === implode(':', $configLocales), 'app.locales configuration variable matches found lang files');
}
public function test_correct_language_if_not_logged_in()
{
$loginReq = $this->get('/login');
$loginReq->assertSee('Log In');
$loginPageFrenchReq = $this->get('/login', ['Accept-Language' => 'fr']);
$loginPageFrenchReq->assertSee('Se Connecter');
}
public function test_js_endpoint_for_each_language() public function test_js_endpoint_for_each_language()
{ {

View File

@ -226,6 +226,7 @@ class RestrictionsTest extends BrowserKitTest
->type('test content', 'html') ->type('test content', 'html')
->press('Save Page') ->press('Save Page')
->seePageIs($chapter->book->getUrl() . '/page/test-page'); ->seePageIs($chapter->book->getUrl() . '/page/test-page');
$this->visit($chapterUrl)->seeInElement('.action-buttons', 'New Page'); $this->visit($chapterUrl)->seeInElement('.action-buttons', 'New Page');
} }
@ -522,4 +523,21 @@ class RestrictionsTest extends BrowserKitTest
->see('Delete Chapter'); ->see('Delete Chapter');
} }
public function test_page_visible_if_has_permissions_when_book_not_visible()
{
$book = \BookStack\Book::first();
$bookChapter = $book->chapters->first();
$bookPage = $bookChapter->pages->first();
$this->setEntityRestrictions($book, []);
$this->setEntityRestrictions($bookPage, ['view']);
$this->actingAs($this->viewer);
$this->get($bookPage->getUrl());
$this->assertResponseOk();
$this->see($bookPage->name);
$this->dontSee(substr($book->name, 0, 15));
$this->dontSee(substr($bookChapter->name, 0, 15));
}
} }

View File

@ -1,5 +1,8 @@
<?php namespace Tests; <?php namespace Tests;
use BookStack\Repos\PermissionsRepo;
use BookStack\Role;
class RolesTest extends BrowserKitTest class RolesTest extends BrowserKitTest
{ {
protected $user; protected $user;
@ -34,11 +37,11 @@ class RolesTest extends BrowserKitTest
/** /**
* Create a new basic role for testing purposes. * Create a new basic role for testing purposes.
* @param array $permissions * @param array $permissions
* @return static * @return Role
*/ */
protected function createNewRole($permissions = []) protected function createNewRole($permissions = [])
{ {
$permissionRepo = app('BookStack\Repos\PermissionsRepo'); $permissionRepo = app(PermissionsRepo::class);
$roleData = factory(\BookStack\Role::class)->make()->toArray(); $roleData = factory(\BookStack\Role::class)->make()->toArray();
$roleData['permissions'] = array_flip($permissions); $roleData['permissions'] = array_flip($permissions);
return $permissionRepo->saveNewRole($roleData); return $permissionRepo->saveNewRole($roleData);
@ -107,16 +110,16 @@ class RolesTest extends BrowserKitTest
public function test_manage_user_permission() public function test_manage_user_permission()
{ {
$this->actingAs($this->user)->visit('/')->visit('/settings/users') $this->actingAs($this->user)->visit('/settings/users')
->seePageIs('/'); ->seePageIs('/');
$this->giveUserPermissions($this->user, ['users-manage']); $this->giveUserPermissions($this->user, ['users-manage']);
$this->actingAs($this->user)->visit('/')->visit('/settings/users') $this->actingAs($this->user)->visit('/settings/users')
->seePageIs('/settings/users'); ->seePageIs('/settings/users');
} }
public function test_user_roles_manage_permission() public function test_user_roles_manage_permission()
{ {
$this->actingAs($this->user)->visit('/')->visit('/settings/roles') $this->actingAs($this->user)->visit('/settings/roles')
->seePageIs('/')->visit('/settings/roles/1')->seePageIs('/'); ->seePageIs('/')->visit('/settings/roles/1')->seePageIs('/');
$this->giveUserPermissions($this->user, ['user-roles-manage']); $this->giveUserPermissions($this->user, ['user-roles-manage']);
$this->actingAs($this->user)->visit('/settings/roles') $this->actingAs($this->user)->visit('/settings/roles')
@ -126,10 +129,10 @@ class RolesTest extends BrowserKitTest
public function test_settings_manage_permission() public function test_settings_manage_permission()
{ {
$this->actingAs($this->user)->visit('/')->visit('/settings') $this->actingAs($this->user)->visit('/settings')
->seePageIs('/'); ->seePageIs('/');
$this->giveUserPermissions($this->user, ['settings-manage']); $this->giveUserPermissions($this->user, ['settings-manage']);
$this->actingAs($this->user)->visit('/')->visit('/settings') $this->actingAs($this->user)->visit('/settings')
->seePageIs('/settings')->press('Save Settings')->see('Settings Saved'); ->seePageIs('/settings')->press('Save Settings')->see('Settings Saved');
} }
@ -181,27 +184,26 @@ class RolesTest extends BrowserKitTest
* @param string $permission * @param string $permission
* @param array $accessUrls Urls that are only accessible after having the permission * @param array $accessUrls Urls that are only accessible after having the permission
* @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission * @param array $visibles Check this text, In the buttons toolbar, is only visible with the permission
* @param null $callback
*/ */
private function checkAccessPermission($permission, $accessUrls = [], $visibles = []) private function checkAccessPermission($permission, $accessUrls = [], $visibles = [])
{ {
foreach ($accessUrls as $url) { foreach ($accessUrls as $url) {
$this->actingAs($this->user)->visit('/')->visit($url) $this->actingAs($this->user)->visit($url)
->seePageIs('/'); ->seePageIs('/');
} }
foreach ($visibles as $url => $text) { foreach ($visibles as $url => $text) {
$this->actingAs($this->user)->visit('/')->visit($url) $this->actingAs($this->user)->visit($url)
->dontSeeInElement('.action-buttons',$text); ->dontSeeInElement('.action-buttons',$text);
} }
$this->giveUserPermissions($this->user, [$permission]); $this->giveUserPermissions($this->user, [$permission]);
foreach ($accessUrls as $url) { foreach ($accessUrls as $url) {
$this->actingAs($this->user)->visit('/')->visit($url) $this->actingAs($this->user)->visit($url)
->seePageIs($url); ->seePageIs($url);
} }
foreach ($visibles as $url => $text) { foreach ($visibles as $url => $text) {
$this->actingAs($this->user)->visit('/')->visit($url) $this->actingAs($this->user)->visit($url)
->see($text); ->see($text);
} }
} }
@ -391,8 +393,8 @@ class RolesTest extends BrowserKitTest
public function test_page_create_own_permissions() public function test_page_create_own_permissions()
{ {
$book = \BookStack\Book::take(1)->get()->first(); $book = \BookStack\Book::first();
$chapter = \BookStack\Chapter::take(1)->get()->first(); $chapter = \BookStack\Chapter::first();
$entities = $this->createEntityChainBelongingToUser($this->user); $entities = $this->createEntityChainBelongingToUser($this->user);
$ownBook = $entities['book']; $ownBook = $entities['book'];
@ -405,7 +407,7 @@ class RolesTest extends BrowserKitTest
$accessUrls = [$createUrl, $createUrlChapter]; $accessUrls = [$createUrl, $createUrlChapter];
foreach ($accessUrls as $url) { foreach ($accessUrls as $url) {
$this->actingAs($this->user)->visit('/')->visit($url) $this->actingAs($this->user)->visit($url)
->seePageIs('/'); ->seePageIs('/');
} }
@ -417,7 +419,7 @@ class RolesTest extends BrowserKitTest
$this->giveUserPermissions($this->user, ['page-create-own']); $this->giveUserPermissions($this->user, ['page-create-own']);
foreach ($accessUrls as $index => $url) { foreach ($accessUrls as $index => $url) {
$this->actingAs($this->user)->visit('/')->visit($url); $this->actingAs($this->user)->visit($url);
$expectedUrl = \BookStack\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); $expectedUrl = \BookStack\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
$this->seePageIs($expectedUrl); $this->seePageIs($expectedUrl);
} }
@ -449,7 +451,7 @@ class RolesTest extends BrowserKitTest
$accessUrls = [$createUrl, $createUrlChapter]; $accessUrls = [$createUrl, $createUrlChapter];
foreach ($accessUrls as $url) { foreach ($accessUrls as $url) {
$this->actingAs($this->user)->visit('/')->visit($url) $this->actingAs($this->user)->visit($url)
->seePageIs('/'); ->seePageIs('/');
} }
@ -461,7 +463,7 @@ class RolesTest extends BrowserKitTest
$this->giveUserPermissions($this->user, ['page-create-all']); $this->giveUserPermissions($this->user, ['page-create-all']);
foreach ($accessUrls as $index => $url) { foreach ($accessUrls as $index => $url) {
$this->actingAs($this->user)->visit('/')->visit($url); $this->actingAs($this->user)->visit($url);
$expectedUrl = \BookStack\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl(); $expectedUrl = \BookStack\Page::where('draft', '=', true)->orderBy('id', 'desc')->first()->getUrl();
$this->seePageIs($expectedUrl); $this->seePageIs($expectedUrl);
} }

View File

@ -1 +1 @@
v0.15-dev v0.16-dev