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();
if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database'));
$this->permissionService->setConnection(\DB::connection($this->option('database')));
}
$this->permissionService->buildJointPermissions();

View File

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

View File

@ -94,17 +94,6 @@ class Entity extends Ownable
->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.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
@ -176,5 +165,11 @@ class Entity extends Ownable
*/
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;
use Activity;
use BookStack\Book;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
@ -207,13 +208,12 @@ class BookController extends Controller
// Add activity for books
foreach ($sortedBooks as $bookId) {
/** @var Book $updatedBook */
$updatedBook = $this->entityRepo->getById('book', $bookId);
$this->entityRepo->buildJointPermissionsForBook($updatedBook);
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
}
// Update permissions on changed models
if (count($updatedModels) === 0) $this->entityRepo->buildJointPermissions($updatedModels);
return redirect($book->getUrl());
}

View File

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

View File

@ -15,7 +15,17 @@ class Localization
public function handle($request, Closure $next)
{
$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);
Carbon::setLocale($locale);
return $next($request);

View File

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

View File

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

View File

@ -3,6 +3,7 @@
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\EntityPermission;
use BookStack\JointPermission;
use BookStack\Ownable;
use BookStack\Page;
@ -10,6 +11,7 @@ use BookStack\Role;
use BookStack\User;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Collection;
class PermissionService
@ -28,22 +30,25 @@ class PermissionService
protected $jointPermission;
protected $role;
protected $entityPermission;
protected $entityCache;
/**
* PermissionService constructor.
* @param JointPermission $jointPermission
* @param EntityPermission $entityPermission
* @param Connection $db
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @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->jointPermission = $jointPermission;
$this->entityPermission = $entityPermission;
$this->role = $role;
$this->book = $book;
$this->chapter = $chapter;
@ -51,6 +56,15 @@ class PermissionService
// 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
*/
@ -133,22 +147,48 @@ class PermissionService
$this->readyEntityCache();
// 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
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($books, $roles);
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
}
// Chunk through all chapters
$this->chapter->with('book', 'permissions')->chunk(500, function ($chapters) use ($roles) {
$this->createManyJointPermissions($chapters, $roles);
});
/**
* Get a query for fetching a book with it's children.
* @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) {
$this->createManyJointPermissions($pages, $roles);
});
/**
* Build joint permissions for an array of books
* @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)
{
$roles = $this->role->get();
$entities = collect([$entity]);
$entities = [$entity];
if ($entity->isA('book')) {
$entities = $entities->merge($entity->chapters);
$entities = $entities->merge($entity->pages);
} elseif ($entity->isA('chapter')) {
$entities = $entities->merge($entity->pages);
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true);
return;
}
$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->createManyJointPermissions($entities, $roles);
$this->buildJointPermissionsForEntities(collect($entities));
}
/**
@ -177,8 +221,8 @@ class PermissionService
*/
public function buildJointPermissionsForEntities(Collection $entities)
{
$roles = $this->role->get();
$this->deleteManyJointPermissionsForEntities($entities);
$roles = $this->role->newQuery()->get();
$this->deleteManyJointPermissionsForEntities($entities->all());
$this->createManyJointPermissions($entities, $roles);
}
@ -188,23 +232,12 @@ class PermissionService
*/
public function buildJointPermissionForRole(Role $role)
{
$roles = collect([$role]);
$roles = [$role];
$this->deleteManyJointPermissionsForRoles($roles);
// Chunk through all books
$this->book->with('permissions')->chunk(500, function ($books) use ($roles) {
$this->createManyJointPermissions($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);
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
}
@ -223,9 +256,10 @@ class PermissionService
*/
protected function deleteManyJointPermissionsForRoles($roles)
{
foreach ($roles as $role) {
$role->jointPermissions()->delete();
}
$roleIds = array_map(function($role) {
return $role->id;
}, $roles);
$this->jointPermission->newQuery()->whereIn('id', $roleIds)->delete();
}
/**
@ -244,53 +278,88 @@ class PermissionService
protected function deleteManyJointPermissionsForEntities($entities)
{
if (count($entities) === 0) return;
$query = $this->jointPermission->newQuery();
foreach ($entities as $entity) {
$query->orWhere(function($query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
$this->db->transaction(function() use ($entities) {
foreach (array_chunk($entities, 1000) as $entityChunk) {
$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.
* @param Collection $entities
* @param Collection $roles
* @param array $roles
*/
protected function createManyJointPermissions($entities, $roles)
{
$this->readyEntityCache();
$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 ($roles as $role) {
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.
* @param $entity
* @param Entity $entity
* @return array
*/
protected function getActions($entity)
protected function getActions(Entity $entity)
{
$baseActions = ['view', 'update', 'delete'];
if ($entity->isA('chapter')) {
$baseActions[] = 'page-create';
} else if ($entity->isA('book')) {
$baseActions[] = 'page-create';
$baseActions[] = 'chapter-create';
}
return $baseActions;
if ($entity->isA('chapter') || $entity->isA('book')) $baseActions[] = 'page-create';
if ($entity->isA('book')) $baseActions[] = 'chapter-create';
return $baseActions;
}
/**
@ -298,14 +367,16 @@ class PermissionService
* for a particular action.
* @param Entity $entity
* @param Role $role
* @param $action
* @param string $action
* @param array $permissionMap
* @param array $rolePermissionMap
* @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;
$roleHasPermission = $role->hasPermission($permissionPrefix . '-all');
$roleHasPermissionOwn = $role->hasPermission($permissionPrefix . '-own');
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
$roleHasPermissionOwn = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-own']);
$explodedAction = explode('-', $action);
$restrictionAction = end($explodedAction);
@ -313,54 +384,46 @@ class PermissionService
return $this->createJointPermissionDataArray($entity, $role, $action, true, true);
}
if ($entity->isA('book')) {
if (!$entity->restricted) {
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->restricted) {
$hasAccess = $this->mapHasActiveRestriction($permissionMap, $entity, $role, $restrictionAction);
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)
{
$entityClass = get_class($entity);
return [
'role_id' => $role->getRawAttribute('id'),
'entity_id' => $entity->getRawAttribute('id'),
'entity_type' => $entityClass,
'entity_type' => $entity->getMorphClass(),
'action' => $action,
'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn,
@ -476,7 +538,7 @@ class PermissionService
* @param integer $book_id
* @param bool $filterDrafts
* @param bool $fetchPageContent
* @return \Illuminate\Database\Query\Builder
* @return QueryBuilder
*/
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) {

View File

@ -50,6 +50,15 @@ class SearchService
$this->permissionService = $permissionService;
}
/**
* Set the database connection
* @param Connection $connection
*/
public function setConnection(Connection $connection)
{
$this->db = $connection;
}
/**
* Search all entities in the system.
* @param string $searchString
@ -154,6 +163,7 @@ class SearchService
// Handle normal search terms
if (count($terms['search']) > 0) {
$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) {
foreach ($terms['search'] as $inputTerm) {
$query->orWhere('term', 'like', $inputTerm .'%');

View File

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

View File

@ -58,6 +58,7 @@ return [
*/
'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,
'slug' => str_random(10),
'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);
});
$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\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
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.

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});
/**

View File

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

View File

@ -11,7 +11,7 @@ return [
|
*/
'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

View File

@ -18,7 +18,7 @@ return [
'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',
'images_deleted' => 'Imágenes borradas',
'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_delete_success' => 'Imagen borrada exitosamente'
];

View File

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

View File

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

View File

@ -8,105 +8,105 @@ return [
* including users and roles.
*/
'settings' => 'Settings',
'settings_save' => 'Save Settings',
'settings_save_success' => 'Settings saved',
'settings' => 'Ajustes',
'settings_save' => 'Guardar ajustes',
'settings_save_success' => 'Ajustes guardados',
/**
* App settings
*/
'app_settings' => 'App Settings',
'app_name' => 'Application name',
'app_name_desc' => 'This name is shown in the header and any emails.',
'app_name_header' => 'Show Application name in header?',
'app_public_viewing' => 'Allow public viewing?',
'app_secure_images' => 'Enable higher security image uploads?',
'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_editor' => 'Page editor',
'app_editor_desc' => 'Select which editor will be used by all users to edit pages.',
'app_custom_html' => 'Custom HTML head content',
'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_logo' => 'Application logo',
'app_logo_desc' => 'This image should be 43px in height. <br>Large images will be scaled down.',
'app_primary_color' => 'Application primary color',
'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
'app_settings' => 'Ajustes de App',
'app_name' => 'Nombre de aplicación',
'app_name_desc' => 'Este nombre es mostrado en la cabecera y en cualquier email de la aplicación',
'app_name_header' => 'Mostrar el nombre de la aplicación en la cabecera?',
'app_public_viewing' => 'Permitir vista pública?',
'app_secure_images' => 'Habilitar mayor seguridad para subir imágenes?',
'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' => 'Editor de página',
'app_editor_desc' => 'Seleccione cuál editor ser usado por todos los usuarios para editar páginas.',
'app_custom_html' => 'Contenido de cabecera HTML customizable',
'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' => 'Logo de la aplicación',
'app_logo_desc' => 'Esta imagen debería de ser 43px en altura. <br>Iágenes grandes seán escaladas.',
'app_primary_color' => 'Color primario de la aplicación',
'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
*/
'reg_settings' => 'Registration Settings',
'reg_allow' => 'Allow registration?',
'reg_default_role' => 'Default user role after registration',
'reg_confirm_email' => 'Require email confirmation?',
'reg_confirm_email_desc' => 'If domain restriction is used then email confirmation will be required and the below value will be ignored.',
'reg_confirm_restrict_domain' => 'Restrict registration to domain',
'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_placeholder' => 'No restriction set',
'reg_settings' => 'Ajustes de registro',
'reg_allow' => 'Permitir registro?',
'reg_default_role' => 'Rol de usuario por defecto despúes del registro',
'reg_confirm_email' => 'Requerir email de confirmaación?',
'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' => 'Restringir registro al dominio',
'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' => 'Ninguna restricción establecida',
/**
* Role settings
*/
'roles' => 'Roles',
'role_user_roles' => 'User Roles',
'role_create' => 'Create New Role',
'role_create_success' => 'Role successfully created',
'role_delete' => 'Delete Role',
'role_delete_confirm' => 'This will delete the role with the name \':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_no_migration' => "Don't migrate users",
'role_delete_sure' => 'Are you sure you want to delete this role?',
'role_delete_success' => 'Role successfully deleted',
'role_edit' => 'Edit Role',
'role_details' => 'Role Details',
'role_name' => 'Role Name',
'role_desc' => 'Short Description of Role',
'role_system' => 'System Permissions',
'role_manage_users' => 'Manage users',
'role_manage_roles' => 'Manage roles & role permissions',
'role_manage_entity_permissions' => 'Manage all book, chapter & page permissions',
'role_manage_own_entity_permissions' => 'Manage permissions on own book, chapter & pages',
'role_manage_settings' => 'Manage app settings',
'role_asset' => 'Asset Permissions',
'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_all' => 'All',
'role_own' => 'Own',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
'role_save' => 'Save Role',
'role_update_success' => 'Role successfully updated',
'role_users' => 'Users in this role',
'role_users_none' => 'No users are currently assigned to this role',
'role_user_roles' => 'Roles de usuario',
'role_create' => 'Crear nuevo rol',
'role_create_success' => 'Rol creado satisfactoriamente',
'role_delete' => 'Borrar rol',
'role_delete_confirm' => 'Se borrará el rol con nombre \':roleName\'.',
'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' => "No migrar usuarios",
'role_delete_sure' => 'Está seguro que desea borrar este rol?',
'role_delete_success' => 'Rol borrado satisfactoriamente',
'role_edit' => 'Editar rol',
'role_details' => 'Detalles de rol',
'role_name' => 'Nombre de rol',
'role_desc' => 'Descripción corta de rol',
'role_system' => 'Permisos de sistema',
'role_manage_users' => 'Gestionar usuarios',
'role_manage_roles' => 'Gestionar roles y permisos de roles',
'role_manage_entity_permissions' => 'Gestionar todos los permisos de libros, capítulos y áginas',
'role_manage_own_entity_permissions' => 'Gestionar permisos en libros propios, capítulos y páginas',
'role_manage_settings' => 'Gestionar ajustes de activos',
'role_asset' => 'Permisos de activos',
'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' => 'Todo',
'role_own' => 'Propio',
'role_controlled_by_asset' => 'Controlado por el actvo al que ha sido subido',
'role_save' => 'Guardar rol',
'role_update_success' => 'Rol actualizado éxitosamente',
'role_users' => 'Usuarios en este rol',
'role_users_none' => 'No hay usuarios asignados a este rol',
/**
* Users
*/
'users' => 'Users',
'user_profile' => 'User Profile',
'users_add_new' => 'Add New User',
'users_search' => 'Search Users',
'users_role' => 'User Roles',
'users_external_auth_id' => 'External Authentication ID',
'users_password_warning' => 'Only fill the below if you would like to change your 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_delete' => 'Delete User',
'users_delete_named' => 'Delete user :userName',
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
'users_delete_confirm' => 'Are you sure you want to delete this user?',
'users_delete_success' => 'Users successfully removed',
'users_edit' => 'Edit User',
'users_edit_profile' => 'Edit Profile',
'users_edit_success' => 'User successfully updated',
'users_avatar' => 'User Avatar',
'users_avatar_desc' => 'This image should be approx 256px square.',
'users_preferred_language' => 'Preferred Language',
'users_social_accounts' => 'Social Accounts',
'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_connect' => 'Connect Account',
'users_social_disconnect' => 'Disconnect Account',
'users_social_connected' => ':socialAccount account was successfully attached to your profile.',
'users_social_disconnected' => ':socialAccount account was successfully disconnected from your profile.',
'users' => 'Usuarios',
'user_profile' => 'Perfil de usuario',
'users_add_new' => 'Agregar nuevo usuario',
'users_search' => 'Buscar usuarios',
'users_role' => 'Roles de usuario',
'users_external_auth_id' => 'ID externo de autenticación',
'users_password_warning' => 'Solo rellene a continuación si desea cambiar su password:',
'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' => 'Borrar usuario',
'users_delete_named' => 'Borrar usuario :userName',
'users_delete_warning' => 'Se borrará completamente el usuario con el nombre \':userName\' del sistema.',
'users_delete_confirm' => 'Está seguro que desea borrar este usuario?',
'users_delete_success' => 'Usuarios removidos éxitosamente',
'users_edit' => 'Editar Usuario',
'users_edit_profile' => 'Editar perfil',
'users_edit_success' => 'Usuario actualizado',
'users_avatar' => 'Avatar del usuario',
'users_avatar_desc' => 'Esta imagen debe ser aproximadamente 256px por lado.',
'users_preferred_language' => 'Lenguaje preferido',
'users_social_accounts' => 'Cuentas sociales',
'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' => 'Conectar cuenta',
'users_social_disconnect' => 'Desconectar cuenta',
'users_social_connected' => 'La cuenta :socialAccount ha sido éxitosamente añadida a su perfil.',
'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.',
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'alpha' => 'The :attribute may only contain letters.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.',
'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'before' => 'The :attribute must be a date before :date.',
'accepted' => 'El :attribute debe ser aceptado.',
'active_url' => 'El :attribute no es una URl válida.',
'after' => 'El :attribute debe ser una fecha posterior :date.',
'alpha' => 'El :attribute solo puede contener letras.',
'alpha_dash' => 'El :attribute solo puede contener letras, números y guiones.',
'alpha_num' => 'El :attribute solo puede contener letras y número.',
'array' => 'El :attribute debe de ser un array.',
'before' => 'El :attribute debe ser una fecha anterior a :date.',
'between' => [
'numeric' => 'The :attribute must be between :min and :max.',
'file' => 'The :attribute must be between :min and :max kilobytes.',
'string' => 'The :attribute must be between :min and :max characters.',
'array' => 'The :attribute must have between :min and :max items.',
'numeric' => 'El :attribute debe estar entre :min y :max.',
'file' => 'El :attribute debe estar entre :min y :max kilobytes.',
'string' => 'El :attribute debe estar entre :min y :max carácteres.',
'array' => 'El :attribute debe estar entre :min y :max items.',
],
'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.',
'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'email' => 'The :attribute must be a valid email address.',
'filled' => 'The :attribute field is required.',
'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.',
'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
'boolean' => 'El campo :attribute debe ser true o false.',
'confirmed' => 'La confirmación de :attribute no concuerda.',
'date' => 'El :attribute no es una fecha válida.',
'date_format' => 'El :attribute no coincide con el formato :format.',
'different' => ':attribute y :other deben ser diferentes.',
'digits' => ':attribute debe ser de :digits dígitos.',
'digits_between' => ':attribute debe ser un valor entre :min y :max dígios.',
'email' => ':attribute debe ser una dirección álida.',
'filled' => 'El campo :attribute es requerido.',
'exists' => 'El :attribute seleccionado es inválido.',
'image' => 'El :attribute debe ser una imagen.',
'in' => 'El selected :attribute es inválio.',
'integer' => 'El :attribute debe ser un entero.',
'ip' => 'El :attribute debe ser una dirección IP álida.',
'max' => [
'numeric' => 'The :attribute may not be greater than :max.',
'file' => 'The :attribute may not be greater than :max kilobytes.',
'string' => 'The :attribute may not be greater than :max characters.',
'array' => 'The :attribute may not have more than :max items.',
'numeric' => ':attribute no puede ser mayor que :max.',
'file' => ':attribute no puede ser mayor que :max kilobytes.',
'string' => ':attribute no puede ser mayor que :max carácteres.',
'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' => [
'numeric' => 'The :attribute must be at least :min.',
'file' => 'The :attribute must be at least :min kilobytes.',
'string' => 'The :attribute must be at least :min characters.',
'array' => 'The :attribute must have at least :min items.',
'numeric' => ':attribute debe ser al menos de :min.',
'file' => ':attribute debe ser al menos :min kilobytes.',
'string' => ':attribute debe ser al menos :min caracteres.',
'array' => ':attribute debe tener como mínimo :min items.',
],
'not_in' => 'The selected :attribute is invalid.',
'numeric' => 'The :attribute must be a number.',
'regex' => 'The :attribute format is invalid.',
'required' => 'The :attribute field is required.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values is present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute and :other must match.',
'not_in' => ':attribute seleccionado es inválio.',
'numeric' => ':attribute debe ser numérico.',
'regex' => ':attribute con formato inválido',
'required' => ':attribute es requerido.',
'required_if' => ':attribute es requerido cuando :other vale :value.',
'required_with' => 'El campo :attribute es requerido cuando se encuentre entre los valores :values.',
'required_with_all' => 'El campo :attribute es requerido cuando los valores sean :values.',
'required_without' => ':attribute es requerido cuando no se encuentre entre los valores :values.',
'required_without_all' => ':attribute es requerido cuando ninguno de los valores :values están presentes.',
'same' => ':attribute y :other deben coincidir.',
'size' => [
'numeric' => 'The :attribute must be :size.',
'file' => 'The :attribute must be :size kilobytes.',
'string' => 'The :attribute must be :size characters.',
'array' => 'The :attribute must contain :size items.',
'numeric' => ':attribute debe ser :size.',
'file' => ':attribute debe ser :size kilobytes.',
'string' => ':attribute debe ser :size caracteres.',
'array' => ':attribute debe contener :size items.',
],
'string' => 'The :attribute must be a string.',
'timezone' => 'The :attribute must be a valid zone.',
'unique' => 'The :attribute has already been taken.',
'url' => 'The :attribute format is invalid.',
'string' => 'El atributo :attribute debe ser una cadena.',
'timezone' => 'El atributo :attribute debe ser una zona válida.',
'unique' => 'El atributo :attribute ya ha sido tomado.',
'url' => 'El atributo :attribute tiene un formato inválid.',
/*
|--------------------------------------------------------------------------
@ -88,7 +88,7 @@ return [
'custom' => [
'password-confirm' => [
'required_with' => 'Password confirmation required',
'required_with' => 'Confirmación de Password requerida',
],
],

View File

@ -1,5 +1,7 @@
<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>
<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>
</div>

View File

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

View File

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

View File

@ -39,8 +39,10 @@
<h6 class="text-muted">{{ trans('entities.books_navigation') }}</h6>
<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)
<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">
@if ($entity->isA('page')) {{ trans('entities.meta_revision', ['revisionCount' => $entity->revision_count]) }} <br> @endif
@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
{{ 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
<br>
@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
{{ 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
</p>

View File

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

View File

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

View File

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

View File

@ -14,6 +14,23 @@ class LanguageTest extends TestCase
$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()
{

View File

@ -226,6 +226,7 @@ class RestrictionsTest extends BrowserKitTest
->type('test content', 'html')
->press('Save Page')
->seePageIs($chapter->book->getUrl() . '/page/test-page');
$this->visit($chapterUrl)->seeInElement('.action-buttons', 'New Page');
}
@ -522,4 +523,21 @@ class RestrictionsTest extends BrowserKitTest
->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;
use BookStack\Repos\PermissionsRepo;
use BookStack\Role;
class RolesTest extends BrowserKitTest
{
protected $user;
@ -34,11 +37,11 @@ class RolesTest extends BrowserKitTest
/**
* Create a new basic role for testing purposes.
* @param array $permissions
* @return static
* @return Role
*/
protected function createNewRole($permissions = [])
{
$permissionRepo = app('BookStack\Repos\PermissionsRepo');
$permissionRepo = app(PermissionsRepo::class);
$roleData = factory(\BookStack\Role::class)->make()->toArray();
$roleData['permissions'] = array_flip($permissions);
return $permissionRepo->saveNewRole($roleData);
@ -107,16 +110,16 @@ class RolesTest extends BrowserKitTest
public function test_manage_user_permission()
{
$this->actingAs($this->user)->visit('/')->visit('/settings/users')
$this->actingAs($this->user)->visit('/settings/users')
->seePageIs('/');
$this->giveUserPermissions($this->user, ['users-manage']);
$this->actingAs($this->user)->visit('/')->visit('/settings/users')
$this->actingAs($this->user)->visit('/settings/users')
->seePageIs('/settings/users');
}
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('/');
$this->giveUserPermissions($this->user, ['user-roles-manage']);
$this->actingAs($this->user)->visit('/settings/roles')
@ -126,10 +129,10 @@ class RolesTest extends BrowserKitTest
public function test_settings_manage_permission()
{
$this->actingAs($this->user)->visit('/')->visit('/settings')
$this->actingAs($this->user)->visit('/settings')
->seePageIs('/');
$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');
}
@ -181,27 +184,26 @@ class RolesTest extends BrowserKitTest
* @param string $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 null $callback
*/
private function checkAccessPermission($permission, $accessUrls = [], $visibles = [])
{
foreach ($accessUrls as $url) {
$this->actingAs($this->user)->visit('/')->visit($url)
$this->actingAs($this->user)->visit($url)
->seePageIs('/');
}
foreach ($visibles as $url => $text) {
$this->actingAs($this->user)->visit('/')->visit($url)
$this->actingAs($this->user)->visit($url)
->dontSeeInElement('.action-buttons',$text);
}
$this->giveUserPermissions($this->user, [$permission]);
foreach ($accessUrls as $url) {
$this->actingAs($this->user)->visit('/')->visit($url)
$this->actingAs($this->user)->visit($url)
->seePageIs($url);
}
foreach ($visibles as $url => $text) {
$this->actingAs($this->user)->visit('/')->visit($url)
$this->actingAs($this->user)->visit($url)
->see($text);
}
}
@ -391,8 +393,8 @@ class RolesTest extends BrowserKitTest
public function test_page_create_own_permissions()
{
$book = \BookStack\Book::take(1)->get()->first();
$chapter = \BookStack\Chapter::take(1)->get()->first();
$book = \BookStack\Book::first();
$chapter = \BookStack\Chapter::first();
$entities = $this->createEntityChainBelongingToUser($this->user);
$ownBook = $entities['book'];
@ -405,7 +407,7 @@ class RolesTest extends BrowserKitTest
$accessUrls = [$createUrl, $createUrlChapter];
foreach ($accessUrls as $url) {
$this->actingAs($this->user)->visit('/')->visit($url)
$this->actingAs($this->user)->visit($url)
->seePageIs('/');
}
@ -417,7 +419,7 @@ class RolesTest extends BrowserKitTest
$this->giveUserPermissions($this->user, ['page-create-own']);
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();
$this->seePageIs($expectedUrl);
}
@ -449,7 +451,7 @@ class RolesTest extends BrowserKitTest
$accessUrls = [$createUrl, $createUrlChapter];
foreach ($accessUrls as $url) {
$this->actingAs($this->user)->visit('/')->visit($url)
$this->actingAs($this->user)->visit($url)
->seePageIs('/');
}
@ -461,7 +463,7 @@ class RolesTest extends BrowserKitTest
$this->giveUserPermissions($this->user, ['page-create-all']);
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();
$this->seePageIs($expectedUrl);
}

View File

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