This commit is contained in:
Abijeet 2017-01-29 09:35:46 +05:30
commit 70991fc1e5
86 changed files with 2912 additions and 1392 deletions

View File

@ -1,11 +1,13 @@
### For Feature Requests
Desired Feature:
### For Bug Reports
PHP Version:
MySQL Version:
* BookStack Version:
* PHP Version:
* MySQL Version:
Expected Behavior:
##### Expected Behavior
Actual Behavior:
##### Actual Behavior

View File

@ -17,9 +17,7 @@ addons:
before_script:
- mysql -u root -e 'create database `bookstack-test`;'
- composer config -g github-oauth.github.com $GITHUB_ACCESS_TOKEN
- phpenv config-rm xdebug.ini
- composer self-update
- composer dump-autoload --no-interaction
- composer install --prefer-dist --no-interaction
- php artisan clear-compiled -n

View File

@ -3,9 +3,9 @@
namespace BookStack\Exceptions;
use Exception;
use Illuminate\Contracts\Validation\ValidationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Validation\ValidationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use PhpSpec\Exception\Example\ErrorException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Auth\Access\AuthorizationException;

View File

@ -208,7 +208,7 @@ class BookController extends Controller
}
// Update permissions on changed models
$this->entityRepo->buildJointPermissions($updatedModels);
if (count($updatedModels) === 0) $this->entityRepo->buildJointPermissions($updatedModels);
return redirect($book->getUrl());
}

View File

@ -4,7 +4,7 @@ namespace BookStack\Http\Controllers;
use BookStack\Ownable;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Http\Exception\HttpResponseException;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Foundation\Validation\ValidatesRequests;

View File

@ -158,13 +158,16 @@ class PageController extends Controller
$this->checkOwnablePermission('page-view', $page);
$pageContent = $this->entityRepo->renderPage($page);
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
$pageNav = $this->entityRepo->getPageNav($page);
$pageNav = $this->entityRepo->getPageNav($pageContent);
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', ['page' => $page, 'book' => $page->book,
'current' => $page, 'sidebarTree' => $sidebarTree, 'pageNav' => $pageNav]);
return view('pages/show', [
'page' => $page,'book' => $page->book,
'current' => $page, 'sidebarTree' => $sidebarTree,
'pageNav' => $pageNav, 'pageContent' => $pageContent]);
}
/**
@ -430,6 +433,7 @@ class PageController extends Controller
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$pdfContent = $this->exportService->pageToPdf($page);
// return $pdfContent;
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'

View File

@ -1,13 +1,8 @@
<?php
<?php namespace BookStack\Http\Controllers;
namespace BookStack\Http\Controllers;
use BookStack\Activity;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use BookStack\Http\Requests;
use BookStack\Repos\UserRepo;
use BookStack\Services\SocialAuthService;
use BookStack\User;
@ -152,7 +147,8 @@ class UserController extends Controller
'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password'
'password-confirm' => 'same:password|required_with:password',
'setting' => 'array'
]);
$user = $this->user->findOrFail($id);
@ -175,6 +171,13 @@ class UserController extends Controller
$user->external_auth_id = $request->get('external_auth_id');
}
// Save an user-specific settings
if ($request->has('setting')) {
foreach ($request->get('setting') as $key => $value) {
setting()->putUser($user, $key, $value);
}
}
$user->save();
session()->flash('success', trans('settings.users_edit_success'));

View File

@ -1,6 +1,4 @@
<?php
namespace BookStack\Http;
<?php namespace BookStack\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
@ -30,6 +28,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\BookStack\Http\Middleware\Localization::class
],
'api' => [
'throttle:60,1',

View File

@ -0,0 +1,23 @@
<?php namespace BookStack\Http\Middleware;
use Carbon\Carbon;
use Closure;
class Localization
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
$locale = setting()->getUser(user(), 'language', $defaultLang);
app()->setLocale($locale);
Carbon::setLocale($locale);
return $next($request);
}
}

View File

@ -1,6 +1,4 @@
<?php
namespace BookStack\Http\Middleware;
<?php namespace BookStack\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Guard;

View File

@ -1,6 +1,5 @@
<?php namespace BookStack\Providers;
use Carbon\Carbon;
use Illuminate\Support\ServiceProvider;
use Validator;
@ -18,8 +17,6 @@ class AppServiceProvider extends ServiceProvider
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
return in_array($value->getMimeType(), $imageMimes);
});
Carbon::setLocale(config('app.locale'));
}
/**

View File

@ -139,7 +139,7 @@ class EntityRepo
*/
public function getById($type, $id, $allowDrafts = false)
{
return $this->entityQuery($type, $allowDrafts)->findOrFail($id);
return $this->entityQuery($type, $allowDrafts)->find($id);
}
/**
@ -318,15 +318,15 @@ class EntityRepo
*/
public function getBookChildren(Book $book, $filterDrafts = false)
{
$q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts);
$q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts)->get();
$entities = [];
$parents = [];
$tree = [];
foreach ($q as $index => $rawEntity) {
if ($rawEntity->entity_type === 'Bookstack\\Page') {
if ($rawEntity->entity_type === 'BookStack\\Page') {
$entities[$index] = $this->page->newFromBuilder($rawEntity);
} else if ($rawEntity->entity_type === 'Bookstack\\Chapter') {
} else if ($rawEntity->entity_type === 'BookStack\\Chapter') {
$entities[$index] = $this->chapter->newFromBuilder($rawEntity);
$key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
$parents[$key] = $entities[$index];
@ -338,7 +338,7 @@ class EntityRepo
foreach ($entities as $entity) {
if ($entity->chapter_id === 0) continue;
$parentKey = 'Bookstack\\Chapter:' . $entity->chapter_id;
$parentKey = 'BookStack\\Chapter:' . $entity->chapter_id;
$chapter = $parents[$parentKey];
$chapter->pages->push($entity);
}
@ -796,6 +796,52 @@ class EntityRepo
return $html;
}
/**
* Render the page for viewing, Parsing and performing features such as page transclusion.
* @param Page $page
* @return mixed|string
*/
public function renderPage(Page $page)
{
$content = $page->html;
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
if (count($matches[0]) === 0) return $content;
foreach ($matches[1] as $index => $includeId) {
$splitInclude = explode('#', $includeId, 2);
$pageId = intval($splitInclude[0]);
if (is_nan($pageId)) continue;
$page = $this->getById('page', $pageId);
if ($page === null) {
$content = str_replace($matches[0][$index], '', $content);
continue;
}
if (count($splitInclude) === 1) {
$content = str_replace($matches[0][$index], $page->html, $content);
continue;
}
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$matchingElem = $doc->getElementById($splitInclude[1]);
if ($matchingElem === null) {
$content = str_replace($matches[0][$index], '', $content);
continue;
}
$innerContent = '';
foreach ($matchingElem->childNodes as $childNode) {
$innerContent .= $doc->saveHTML($childNode);
}
$content = str_replace($matches[0][$index], trim($innerContent), $content);
}
return $content;
}
/**
* Get a new draft page instance.
* @param Book $book
@ -835,19 +881,19 @@ class EntityRepo
/**
* Parse the headers on the page to get a navigation menu
* @param Page $page
* @return Collection
* @param String $pageContent
* @return array
*/
public function getPageNav(Page $page)
public function getPageNav($pageContent)
{
if ($page->html == '') return null;
if ($pageContent == '') return [];
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($page->html, 'HTML-ENTITIES', 'UTF-8'));
$doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
if (is_null($headers)) return null;
if (is_null($headers)) return [];
$tree = collect([]);
foreach ($headers as $header) {
@ -868,7 +914,7 @@ class EntityRepo
return $header;
});
}
return $tree;
return $tree->toArray();
}
/**

View File

@ -93,7 +93,7 @@ class PermissionsRepo
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
if ($role->name === 'admin') {
if ($role->system_name === 'admin') {
$permissions = $this->permission->all()->pluck('id')->toArray();
$role->permissions()->sync($permissions);
}

View File

@ -1,10 +1,22 @@
<?php namespace BookStack\Services;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
class ExportService
{
protected $entityRepo;
/**
* ExportService constructor.
* @param $entityRepo
*/
public function __construct(EntityRepo $entityRepo)
{
$this->entityRepo = $entityRepo;
}
/**
* Convert a page to a self-contained HTML file.
* Includes required CSS & image content. Images are base64 encoded into the HTML.
@ -14,7 +26,7 @@ class ExportService
public function pageToContainedHtml(Page $page)
{
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/export', ['page' => $page, 'css' => $cssContent])->render();
$pageHtml = view('pages/export', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render();
return $this->containHtml($pageHtml);
}
@ -26,7 +38,8 @@ class ExportService
public function pageToPdf(Page $page)
{
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/pdf', ['page' => $page, 'css' => $cssContent])->render();
$pageHtml = view('pages/pdf', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render();
// return $pageHtml;
$useWKHTML = config('snappy.pdf.binary') !== false;
$containedHtml = $this->containHtml($pageHtml);
if ($useWKHTML) {
@ -59,9 +72,13 @@ class ExportService
$pathString = $srcString;
}
if ($isLocal && !file_exists($pathString)) continue;
$imageContent = file_get_contents($pathString);
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
try {
$imageContent = file_get_contents($pathString);
$imageEncoded = 'data:image/' . pathinfo($pathString, PATHINFO_EXTENSION) . ';base64,' . base64_encode($imageContent);
$newImageString = str_replace($srcString, $imageEncoded, $oldImgString);
} catch (\ErrorException $e) {
$newImageString = '';
}
$htmlContent = str_replace($oldImgString, $newImageString, $htmlContent);
}
}
@ -88,14 +105,14 @@ class ExportService
/**
* Converts the page contents into simple plain text.
* This method filters any bad looking content to
* provide a nice final output.
* This method filters any bad looking content to provide a nice final output.
* @param Page $page
* @return mixed
*/
public function pageToPlainText(Page $page)
{
$text = $page->text;
$html = $this->entityRepo->renderPage($page);
$text = strip_tags($html);
// Replace multiple spaces with single spaces
$text = preg_replace('/\ {2,}/', ' ', $text);
// Reduce multiple horrid whitespace characters.

View File

@ -94,4 +94,4 @@ class Ldap
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}
}
}

View File

@ -112,9 +112,13 @@ class LdapService
throw new LdapException(trans('errors.ldap_extension_not_installed'));
}
// Get port from server string if specified.
// Get port from server string and protocol if specified.
$ldapServer = explode(':', $this->config['server']);
$ldapConnection = $this->ldap->connect($ldapServer[0], count($ldapServer) > 1 ? $ldapServer[1] : 389);
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
if (!$hasProtocol) array_unshift($ldapServer, '');
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
if ($ldapConnection === false) {
throw new LdapException(trans('errors.ldap_cannot_connect'));

View File

@ -157,7 +157,7 @@ class PermissionService
*/
public function buildJointPermissionsForEntity(Entity $entity)
{
$roles = $this->role->with('jointPermissions')->get();
$roles = $this->role->get();
$entities = collect([$entity]);
if ($entity->isA('book')) {
@ -177,7 +177,7 @@ class PermissionService
*/
public function buildJointPermissionsForEntities(Collection $entities)
{
$roles = $this->role->with('jointPermissions')->get();
$roles = $this->role->get();
$this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles);
}
@ -243,13 +243,14 @@ 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());
});
}
foreach ($entities as $entity) {
$query->orWhere(function($query) use ($entity) {
$query->where('entity_id', '=', $entity->id)
->where('entity_type', '=', $entity->getMorphClass());
});
}
$query->delete();
}
@ -405,7 +406,7 @@ class PermissionService
$action = end($explodedPermission);
$this->currentAction = $action;
$nonJointPermissions = ['restrictions'];
$nonJointPermissions = ['restrictions', 'image', 'attachment'];
// Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) {
@ -421,7 +422,6 @@ class PermissionService
$this->currentAction = $permission;
}
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
$this->clean();
return $q;
@ -471,49 +471,40 @@ class PermissionService
return $q;
}
/**
* Get the children of a book in an efficient single query, Filtered by the permission system.
* @param integer $book_id
* @param bool $filterDrafts
* @return \Illuminate\Database\Query\Builder
*/
public function bookChildrenQuery($book_id, $filterDrafts = false) {
$pageSelect = $this->db->table('pages')->selectRaw("'BookStack\\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft")->where('book_id', '=', $book_id)->where(function($query) use ($filterDrafts) {
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
});
$chapterSelect = $this->db->table('chapters')->selectRaw("'BookStack\\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft")->where('book_id', '=', $book_id);
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
// Draft setup
$params = [
'userId' => $this->currentUser()->id,
'bookIdPage' => $book_id,
'bookIdChapter' => $book_id
];
if (!$filterDrafts) {
$params['userIdDrafts'] = $this->currentUser()->id;
if (!$this->isAdmin()) {
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
->where(function($query) {
$query->where('jp.has_permission', '=', 1)->orWhere(function($query) {
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
});
});
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
}
// Role setup
$userRoles = $this->getRoles();
$roleBindings = [];
$roleValues = [];
foreach ($userRoles as $index => $roleId) {
$roleBindings[':role'.$index] = $roleId;
$roleValues['role'.$index] = $roleId;
}
// TODO - Clean this up, Maybe extract into a nice class for doing these kind of manual things
// Something which will handle the above role crap in a nice clean way
$roleBindingString = implode(',', array_keys($roleBindings));
$query = "SELECT * from (
(SELECT 'Bookstack\\\Page' as entity_type, id, slug, name, text, '' as description, book_id, priority, chapter_id, draft FROM {$this->page->getTable()}
where book_id = :bookIdPage AND ". ($filterDrafts ? '(draft = 0)' : '(draft = 0 OR (draft = 1 AND created_by = :userIdDrafts))') .")
UNION
(SELECT 'Bookstack\\\Chapter' as entity_type, id, slug, name, '' as text, description, book_id, priority, 0 as chapter_id, 0 as draft FROM {$this->chapter->getTable()} WHERE book_id = :bookIdChapter)
) as U WHERE (
SELECT COUNT(*) FROM {$this->jointPermission->getTable()} jp
WHERE
jp.entity_id=U.id AND
jp.entity_type=U.entity_type AND
jp.action = 'view' AND
jp.role_id IN ({$roleBindingString}) AND
(
jp.has_permission = 1 OR
(jp.has_permission_own = 1 AND jp.created_by = :userId)
)
) > 0
ORDER BY draft desc, priority asc";
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
$this->clean();
return $this->db->select($query, array_replace($roleValues, $params));
return $query;
}
/**
@ -579,6 +570,7 @@ ORDER BY draft desc, priority asc";
});
});
});
$this->clean();
return $q;
}

View File

@ -1,6 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Setting;
use BookStack\User;
use Illuminate\Contracts\Cache\Repository as Cache;
/**
@ -38,10 +39,23 @@ class SettingService
*/
public function get($key, $default = false)
{
if ($default === false) $default = config('setting-defaults.' . $key, false);
$value = $this->getValueFromStore($key, $default);
return $this->formatValue($value, $default);
}
/**
* Get a user-specific setting from the database or cache.
* @param User $user
* @param $key
* @param bool $default
* @return bool|string
*/
public function getUser($user, $key, $default = false)
{
return $this->get($this->userKey($user->id, $key), $default);
}
/**
* Gets a setting value from the cache or database.
* Looks at the system defaults if not cached or in database.
@ -69,14 +83,6 @@ class SettingService
return $value;
}
// Check the defaults set in the app config.
$configPrefix = 'setting-defaults.' . $key;
if (config()->has($configPrefix)) {
$value = config($configPrefix);
$this->cache->forever($cacheKey, $value);
return $value;
}
return $default;
}
@ -118,6 +124,16 @@ class SettingService
return $setting !== null;
}
/**
* Check if a user setting is in the database.
* @param $key
* @return bool
*/
public function hasUser($key)
{
return $this->has($this->userKey($key));
}
/**
* Add a setting to the database.
* @param $key
@ -135,6 +151,28 @@ class SettingService
return true;
}
/**
* Put a user-specific setting into the database.
* @param User $user
* @param $key
* @param $value
* @return bool
*/
public function putUser($user, $key, $value)
{
return $this->put($this->userKey($user->id, $key), $value);
}
/**
* Convert a setting key into a user-specific key.
* @param $key
* @return string
*/
protected function userKey($userId, $key = '')
{
return 'user:' . $userId . ':' . $key;
}
/**
* Removes a setting from the database.
* @param $key
@ -150,6 +188,16 @@ class SettingService
return true;
}
/**
* Delete settings for a given user id.
* @param $userId
* @return mixed
*/
public function deleteUserSettings($userId)
{
return $this->setting->where('setting_key', 'like', $this->userKey($userId) . '%')->delete();
}
/**
* Gets a setting model from the database for the given key.
* @param $key

View File

@ -160,8 +160,16 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getAvatar($size = 50)
{
if ($this->image_id === 0 || $this->image_id === '0' || $this->image_id === null) return baseUrl('/user_avatar.png');
return baseUrl($this->avatar->getThumb($size, $size, false));
$default = baseUrl('/user_avatar.png');
$imageId = $this->image_id;
if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default;
try {
$avatar = baseUrl($this->avatar->getThumb($size, $size, false));
} catch (\Exception $err) {
$avatar = $default;
}
return $avatar;
}
/**

View File

@ -60,11 +60,12 @@ function userCan($permission, Ownable $ownable = null)
* Helper to access system settings.
* @param $key
* @param bool $default
* @return mixed
* @return bool|string|\BookStack\Services\SettingService
*/
function setting($key, $default = false)
function setting($key = null, $default = false)
{
$settingService = app(\BookStack\Services\SettingService::class);
if (is_null($key)) return $settingService;
return $settingService->get($key, $default);
}

View File

@ -6,17 +6,18 @@
"type": "project",
"require": {
"php": ">=5.6.4",
"laravel/framework": "^5.3.4",
"laravel/framework": "5.4.*",
"ext-tidy": "*",
"intervention/image": "^2.3",
"laravel/socialite": "^2.0",
"barryvdh/laravel-ide-helper": "^2.1",
"barryvdh/laravel-debugbar": "^2.2.3",
"laravel/socialite": "^3.0",
"barryvdh/laravel-ide-helper": "^2.2.3",
"barryvdh/laravel-debugbar": "^2.3.2",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "^0.7",
"predis/predis": "^1.1",
"gathercontent/htmldiff": "^0.2.1",
"barryvdh/laravel-snappy": "^0.3.1"
"barryvdh/laravel-snappy": "^0.3.1",
"laravel/browser-kit-testing": "^1.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
@ -35,7 +36,8 @@
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
"tests/TestCase.php",
"tests/BrowserKitTest.php"
]
},
"scripts": {

1510
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -59,4 +59,14 @@ $factory->define(BookStack\Tag::class, function ($faker) {
'name' => $faker->city,
'value' => $faker->sentence(3)
];
});
$factory->define(BookStack\Image::class, function ($faker) {
return [
'name' => $faker->slug . '.jpg',
'url' => $faker->url,
'path' => $faker->url,
'type' => 'gallery',
'uploaded_to' => 0
];
});

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateCacheTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->unique();
$table->text('value');
$table->integer('expiration');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('cache');
}
}

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSessionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->unique();
$table->integer('user_id')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->text('payload');
$table->integer('last_activity');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('sessions');
}
}

View File

@ -16,7 +16,9 @@
"laravel-elixir": "^6.0.0-11",
"laravel-elixir-browserify-official": "^0.1.3",
"marked": "^0.3.5",
"moment": "^2.12.0",
"zeroclipboard": "^2.2.0"
"moment": "^2.12.0"
},
"dependencies": {
"clipboard": "^1.5.16"
}
}

Binary file not shown.

View File

@ -40,13 +40,19 @@ php artisan db:seed --class=DummyContentSeeder --database=mysql_testing
Once done you can run `phpunit` in the application root directory to run all tests.
## Website and Docs
## Translations
The website and project docs are currently stored in the [BookStackApp/website](https://github.com/BookStackApp/website) repo. The docs are stored as markdown files in the `resources/docs` folder
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`.
Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time.
## Website, Docs & Blog
The website project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
## License
BookStack is provided under the MIT License.
The BookStack source is provided under the MIT License.
## Attribution

View File

@ -61,10 +61,9 @@ Controllers(ngApp, window.Events);
// Smooth scrolling
jQuery.fn.smoothScrollTo = function () {
if (this.length === 0) return;
let scrollElem = document.documentElement.scrollTop === 0 ? document.body : document.documentElement;
$(scrollElem).animate({
$('html, body').animate({
scrollTop: this.offset().top - 60 // Adjust to change final scroll position top margin
}, 800); // Adjust to change animations speed (ms)
}, 300); // Adjust to change animations speed (ms)
return this;
};

View File

@ -77,7 +77,7 @@ export default function() {
extended_valid_elements: 'pre[*]',
automatic_uploads: false,
valid_children: "-div[p|pre|h1|h2|h3|h4|h5|h6|blockquote]",
plugins: "image table textcolor paste link fullscreen imagetools code customhr autosave lists",
plugins: "image table textcolor paste link autolink fullscreen imagetools code customhr autosave lists",
imagetools_toolbar: 'imageoptions',
toolbar: "undo redo | styleselect | bold italic underline strikethrough superscript subscript | forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | table image-insert link hr | removeformat code fullscreen",
content_style: "body {padding-left: 15px !important; padding-right: 15px !important; margin:0!important; margin-left:auto!important;margin-right:auto!important;}",

View File

@ -1,13 +1,16 @@
"use strict";
// Configure ZeroClipboard
import zeroClipBoard from "zeroclipboard";
import Clipboard from "clipboard";
export default window.setupPageShow = function (pageId) {
// Set up pointer
let $pointer = $('#pointer').detach();
let pointerShowing = false;
let $pointerInner = $pointer.children('div.pointer').first();
let isSelection = false;
let pointerModeLink = true;
let pointerSectionId = '';
// Select all contents on input click
$pointer.on('click', 'input', function (e) {
@ -15,19 +18,34 @@ export default window.setupPageShow = function (pageId) {
e.stopPropagation();
});
// Set up copy-to-clipboard
zeroClipBoard.config({
swfPath: window.baseUrl('/ZeroClipboard.swf')
// Pointer mode toggle
$pointer.on('click', 'span.icon', event => {
let $icon = $(event.currentTarget);
pointerModeLink = !pointerModeLink;
$icon.html(pointerModeLink ? '<i class="zmdi zmdi-link"></i>' : '<i class="zmdi zmdi-square-down"></i>');
updatePointerContent();
});
new zeroClipBoard($pointer.find('button').first()[0]);
// Set up clipboard
let clipboard = new Clipboard('#pointer button');
// Hide pointer when clicking away
$(document.body).find('*').on('click focus', function (e) {
if (!isSelection) {
$pointer.detach();
}
$(document.body).find('*').on('click focus', event => {
if (!pointerShowing || isSelection) return;
let target = $(event.target);
if (target.is('.zmdi') || $(event.target).closest('#pointer').length === 1) return;
$pointer.detach();
pointerShowing = false;
});
function updatePointerContent() {
let inputText = pointerModeLink ? window.baseUrl(`/link/${pageId}#${pointerSectionId}`) : `{{@${pageId}#${pointerSectionId}}}`;
if (pointerModeLink && inputText.indexOf('http') !== 0) inputText = window.location.protocol + "//" + window.location.host + inputText;
$pointer.find('input').val(inputText);
}
// Show pointer when selecting a single block of tagged content
$('.page-content [id^="bkmrk"]').on('mouseup keyup', function (e) {
e.stopPropagation();
@ -36,12 +54,12 @@ export default window.setupPageShow = function (pageId) {
// Show pointer and set link
let $elem = $(this);
let link = window.baseUrl('/link/' + pageId + '#' + $elem.attr('id'));
if (link.indexOf('http') !== 0) link = window.location.protocol + "//" + window.location.host + link;
$pointer.find('input').val(link);
$pointer.find('button').first().attr('data-clipboard-text', link);
pointerSectionId = $elem.attr('id');
updatePointerContent();
$elem.before($pointer);
$pointer.show();
pointerShowing = true;
// Set pointer to sit near mouse-up position
let pointerLeftOffset = (e.pageX - $elem.offset().left - ($pointerInner.width() / 2));
@ -57,10 +75,12 @@ export default window.setupPageShow = function (pageId) {
// Go to, and highlight if necessary, the specified text.
function goToText(text) {
let idElem = $('.page-content #' + text).first();
if (idElem.length !== 0) {
idElem.smoothScrollTo();
idElem.css('background-color', 'rgba(244, 249, 54, 0.25)');
let idElem = document.getElementById(text);
$('.page-content [data-highlighted]').attr('data-highlighted', '').css('background-color', '');
if (idElem !== null) {
let $idElem = $(idElem);
let color = $('#custom-styles').attr('data-color-light');
$idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo();
} else {
$('.page-content').find(':contains("' + text + '")').smoothScrollTo();
}
@ -72,6 +92,11 @@ export default window.setupPageShow = function (pageId) {
goToText(text);
}
// Sidebar page nav click event
$('.sidebar-page-nav').on('click', 'a', event => {
goToText(event.target.getAttribute('href').substr(1));
});
// Make the book-tree sidebar stick in view on scroll
let $window = $(window);
let $bookTree = $(".book-tree");

View File

@ -136,9 +136,6 @@
background-color: #EEE;
padding: $-s;
display: block;
> * {
display: inline-block;
}
&:before {
font-family: 'Material-Design-Iconic-Font';
padding-right: $-s;

View File

@ -108,5 +108,4 @@ $button-border-radius: 2px;
cursor: default;
box-shadow: none;
}
}
}

View File

@ -70,9 +70,6 @@ body.flexbox-support #entity-selector-wrap .popup-body .form-group {
#entity-selector-wrap .popup-body .form-group {
margin: 0;
}
//body.ie #entity-selector-wrap .popup-body .form-group {
// min-height: 60vh;
//}
.image-manager-body {
min-height: 70vh;

View File

@ -138,6 +138,10 @@
font-size: 18px;
padding-top: 4px;
}
span.icon {
cursor: pointer;
user-select: none;
}
.button {
line-height: 1;
margin: 0 0 0 -4px;

View File

@ -175,6 +175,7 @@ pre code {
background-color: transparent;
border: 0;
font-size: 1em;
display: block;
}
/*
* Text colors

View File

@ -1,4 +1,4 @@
//@import "reset";
@import "reset";
@import "variables";
@import "mixins";
@import "html";

View File

@ -89,6 +89,7 @@ return [
* Chapters
*/
'chapter' => 'Chapter',
'chapters' => 'Chapters',
'chapters_popular' => 'Popular Chapters',
'chapters_new' => 'New Chapter',
'chapters_create' => 'Create New Chapter',

View File

@ -1,13 +1,13 @@
<?php
return [
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
'settings' => 'Settings',
'settings_save' => 'Save Settings',
'settings_save_success' => 'Settings saved',
@ -92,7 +92,7 @@ return [
'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 ser :userName',
'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',
@ -101,40 +101,23 @@ return [
'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.',
// Since these labels are already localized this array does not need to be
// translated in the language-specific files.
// DELETE BELOW IF COPIED FROM EN
///////////////////////////////////
'language_select' => [
'en' => 'English',
'de' => 'Deutsch',
'fr' => 'Français',
'pt_BR' => 'Português do Brasil'
]
///////////////////////////////////
];

View File

@ -0,0 +1,40 @@
<?php
return [
/**
* Activity text strings.
* Is used for all the text within activity logs & notifications.
*/
// Pages
'page_create' => 'a créé la page',
'page_create_notification' => 'Page créée avec succès',
'page_update' => 'a modifié la page',
'page_update_notification' => 'Page modifiée avec succès',
'page_delete' => 'a supprimé la page',
'page_delete_notification' => 'Page supprimée avec succès',
'page_restore' => 'a restauré la page',
'page_restore_notification' => 'Page réstaurée avec succès',
'page_move' => 'a déplacé la page',
// Chapters
'chapter_create' => 'a créé le chapitre',
'chapter_create_notification' => 'Chapitre créé avec succès',
'chapter_update' => 'a modifié le chapitre',
'chapter_update_notification' => 'Chapitre modifié avec succès',
'chapter_delete' => 'a supprimé le chapitre',
'chapter_delete_notification' => 'Chapitre supprimé avec succès',
'chapter_move' => 'a déplacé le chapitre',
// Books
'book_create' => 'a créé le livre',
'book_create_notification' => 'Livre créé avec succès',
'book_update' => 'a modifié le livre',
'book_update_notification' => 'Livre modifié avec succès',
'book_delete' => 'a supprimé le livre',
'book_delete_notification' => 'Livre supprimé avec succès',
'book_sort' => 'a réordonné le livre',
'book_sort_notification' => 'Livre réordonné avec succès',
];

View File

@ -0,0 +1,74 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'Ces informations ne correspondent a aucun compte.',
'throttle' => "Trop d'essais, veuillez réessayer dans :seconds secondes.",
/**
* Login & Register
*/
'sign_up' => "S'inscrire",
'log_in' => 'Se connecter',
'logout' => 'Se déconnecter',
'name' => 'Nom',
'username' => "Nom d'utilisateur",
'email' => 'E-mail',
'password' => 'Mot de passe',
'password_confirm' => 'Confirmez le mot de passe',
'password_hint' => 'Doit faire plus de 5 caractères',
'forgot_password' => 'Mot de passe oublié?',
'remember_me' => 'Se souvenir de moi',
'ldap_email_hint' => "Merci d'entrer une adresse e-mail pour ce compte",
'create_account' => 'Créer un compte',
'social_login' => 'Social Login',
'social_registration' => 'Enregistrement Social',
'social_registration_text' => "S'inscrire et se connecter avec un réseau social",
'register_thanks' => 'Merci pour votre enregistrement',
'register_confirm' => 'Vérifiez vos e-mails et cliquer sur le lien de confirmation pour rejoindre :appName.',
'registrations_disabled' => "L'inscription est désactivée pour le moment",
'registration_email_domain_invalid' => 'Cette adresse e-mail ne peux pas adcéder à l\'application',
'register_success' => 'Merci pour votre inscription. Vous êtes maintenant inscrit(e) et connecté(e)',
/**
* Password Reset
*/
'reset_password' => 'Reset Password',
'reset_password_send_instructions' => 'Entrez votre adresse e-mail ci-dessous et un e-mail avec un lien de réinitialisation de mot de passe vous sera envoyé',
'reset_password_send_button' => 'Envoyer un lien de réinitialisation',
'reset_password_sent_success' => 'Un lien de réinitialisation a été envoyé à :email.',
'reset_password_success' => 'Votre mot de passe a été réinitialisé avec succès.',
'email_reset_subject' => 'Réinitialisez votre mot de passe pour :appName',
'email_reset_text' => 'Vous recevez cet e-mail parceque nous avons reçu une demande de réinitialisation pour votre compte',
'email_reset_not_requested' => 'Si vous n\'avez pas effectué cette demande, vous pouvez ignorer cet e-mail.',
/**
* Email Confirmation
*/
'email_confirm_subject' => 'Confirmez votre adresse e-mail pour :appName',
'email_confirm_greeting' => 'Merci d\'avoir rejoint :appName!',
'email_confirm_text' => 'Merci de confirmer en cliquant sur le lien ci-dessous:',
'email_confirm_action' => 'Confirmez votre adresse e-mail',
'email_confirm_send_error' => 'La confirmation par e-mail est requise mais le système n\'a pas pu envoyer l\'e-mail. Contactez l\'administrateur système.',
'email_confirm_success' => 'Votre adresse e-mail a été confirmée!',
'email_confirm_resent' => 'L\'e-mail de confirmation a été ré-envoyé. Vérifiez votre boîte de récéption.',
'email_not_confirmed' => 'Adresse e-mail non confirmée',
'email_not_confirmed_text' => 'Votre adresse e-mail n\'a pas été confirmée.',
'email_not_confirmed_click_link' => 'Merci de cliquer sur le lien dans l\'e-mail qui vous a été envoyé après l\'enregistrement.',
'email_not_confirmed_resend' => 'Si vous ne retrouvez plus l\'e-mail, vous pouvez renvoyer un e-mail de confirmation en utilisant le formulaire ci-dessous.',
'email_not_confirmed_resend_button' => 'Renvoyez l\'e-mail de confirmation',
];

View File

@ -0,0 +1,58 @@
<?php
return [
/**
* Buttons
*/
'cancel' => 'Annuler',
'confirm' => 'Confirmer',
'back' => 'Retour',
'save' => 'Enregistrer',
'continue' => 'Continuer',
'select' => 'Selectionner',
/**
* Form Labels
*/
'name' => 'Nom',
'description' => 'Description',
'role' => 'Rôle',
/**
* Actions
*/
'actions' => 'Actions',
'view' => 'Voir',
'create' => 'Créer',
'update' => 'Modifier',
'edit' => 'Editer',
'sort' => 'Trier',
'move' => 'Déplacer',
'delete' => 'Supprimer',
'search' => 'Chercher',
'search_clear' => 'Réinitialiser la recherche',
'reset' => 'Réinitialiser',
'remove' => 'Enlever',
/**
* Misc
*/
'deleted_user' => 'Utilisateur supprimé',
'no_activity' => 'Aucune activité',
'no_items' => 'Aucun élément',
'back_to_top' => 'Retour en haut',
'toggle_details' => 'Afficher les détails',
/**
* Header
*/
'view_profile' => 'Voir le profil',
'edit_profile' => 'Modifier le profil',
/**
* Email Content
*/
'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur:',
'email_rights' => 'Tous droits réservés',
];

View File

@ -0,0 +1,24 @@
<?php
return [
/**
* Image Manager
*/
'image_select' => 'Selectionner une image',
'image_all' => 'Toutes',
'image_all_title' => 'Voir toutes les images',
'image_book_title' => 'Voir les images ajoutées à ce livre',
'image_page_title' => 'Voir les images ajoutées à cette page',
'image_search_hint' => 'Rechercher par nom d\'image',
'image_uploaded' => 'Ajoutée le :uploadedDate',
'image_load_more' => 'Charger plus',
'image_image_name' => 'Nom de l\'image',
'image_delete_confirm' => 'Cette image est utilisée dans les pages ci-dessous. Confirmez que vous souhaitez bien supprimer cette image.',
'image_select_image' => 'Selectionner l\'image',
'image_dropzone' => 'Glissez les images ici ou cliquez pour les ajouter',
'images_deleted' => 'Images supprimées',
'image_preview' => 'Prévisualiser l\'image',
'image_upload_success' => 'Image ajoutée avec succès',
'image_update_success' => 'Détails de l\'image mis à jour',
'image_delete_success' => 'Image supprimée avec succès'
];

View File

@ -0,0 +1,225 @@
<?php
return [
/**
* Shared
*/
'recently_created' => 'Créé récemment',
'recently_created_pages' => 'Pages créées récemment',
'recently_updated_pages' => 'Pages mises à jour récemment',
'recently_created_chapters' => 'Chapitres créés récemment',
'recently_created_books' => 'Livres créés récemment',
'recently_update' => 'Mis à jour récemment',
'recently_viewed' => 'Vus récemment',
'recent_activity' => 'Activité récente',
'create_now' => 'En créer un récemment',
'revisions' => 'Révisions',
'meta_created' => 'Créé :timeLength',
'meta_created_name' => 'Créé :timeLength par :user',
'meta_updated' => 'Mis à jour :timeLength',
'meta_updated_name' => 'Mis à jour :timeLength par :user',
'x_pages' => ':count pages',
'entity_select' => 'Sélectionner l\'entité',
'images' => 'Images',
'my_recent_drafts' => 'Mes brouillons récents',
'my_recently_viewed' => 'Vus récemment',
'no_pages_viewed' => 'Vous n\'avez rien visité récemment',
'no_pages_recently_created' => 'Aucune page créée récemment',
'no_pages_recently_updated' => 'Aucune page mise à jour récemment',
/**
* Permissions and restrictions
*/
'permissions' => 'Permissions',
'permissions_intro' => 'Une fois activées ces permission prendont la priorité sur tous les sets de permissions pré-existants.',
'permissions_enable' => 'Activer les permissions personnalisées',
'permissions_save' => 'Enregistrer les permissions',
/**
* Search
*/
'search_results' => 'Résultats de recherche',
'search_results_page' => 'Résultats de recherche des pages',
'search_results_chapter' => 'Résultats de recherche des chapitres',
'search_results_book' => 'Résultats de recherche des livres',
'search_clear' => 'Réinitialiser la recherche',
'search_view_pages' => 'Voir toutes les pages correspondantes',
'search_view_chapters' => 'Voir tous les chapitres correspondants',
'search_view_books' => 'Voir tous les livres correspondants',
'search_no_pages' => 'Aucune page correspondant à cette recherche',
'search_for_term' => 'recherche pour :term',
'search_page_for_term' => 'Recherche de page pour :term',
'search_chapter_for_term' => 'Recherche de chapitre pour :term',
'search_book_for_term' => 'Recherche de livres pour :term',
/**
* Books
*/
'book' => 'Livre',
'books' => 'Livres',
'books_empty' => 'Aucun livre n\'a été créé',
'books_popular' => 'Livres populaires',
'books_recent' => 'Livres récents',
'books_popular_empty' => 'Les livres les plus populaires apparaîtront ici.',
'books_create' => 'Créer un nouveau livre',
'books_delete' => 'Supprimer un livre',
'books_delete_named' => 'Supprimer le livre :bookName',
'books_delete_explain' => 'Ceci va supprimer le livre nommé \':bookName\', Tous les chapitres et pages seront supprimés.',
'books_delete_confirmation' => 'Êtes-vous sûr(e) de vouloir supprimer ce livre?',
'books_edit' => 'Modifier le livre',
'books_edit_named' => 'Modifier le livre :bookName',
'books_form_book_name' => 'Nom du livre',
'books_save' => 'Enregistrer le livre',
'books_permissions' => 'Permissions du livre',
'books_permissions_updated' => 'Permissions du livre mises à jour',
'books_empty_contents' => 'Aucune page ou chapitre n\'a été ajouté à ce livre.',
'books_empty_create_page' => 'Créer une nouvelle page',
'books_empty_or' => 'ou',
'books_empty_sort_current_book' => 'Trier les pages du livre',
'books_empty_add_chapter' => 'Ajouter un chapitre',
'books_permissions_active' => 'Permissions personnalisées activées',
'books_search_this' => 'Chercher dans le livre',
'books_navigation' => 'Navigation dans le livre',
'books_sort' => 'Trier les contenus du livre',
'books_sort_named' => 'Trier le livre :bookName',
'books_sort_show_other' => 'Afficher d\'autres livres',
'books_sort_save' => 'Enregistrer l\'ordre',
/**
* Chapters
*/
'chapter' => 'Chapitre',
'chapters' => 'Chapitres',
'chapters_popular' => 'Chapitres populaires',
'chapters_new' => 'Nouveau chapitre',
'chapters_create' => 'Créer un nouveau chapitre',
'chapters_delete' => 'Supprimer le chapitre',
'chapters_delete_named' => 'Supprimer le chapitre :chapterName',
'chapters_delete_explain' => 'Ceci va supprimer le chapitre \':chapterName\', Toutes les pages seront déplacée dans le livre parent.',
'chapters_delete_confirm' => 'Etes-vous sûr(e) de vouloir supprimer ce chapitre?',
'chapters_edit' => 'Modifier le chapitre',
'chapters_edit_named' => 'Modifier le chapitre :chapterName',
'chapters_save' => 'Enregistrer le chapitre',
'chapters_move' => 'Déplace le chapitre',
'chapters_move_named' => 'Déplacer le chapitre :chapterName',
'chapter_move_success' => 'Chapitre déplacé dans :bookName',
'chapters_permissions' => 'Permissions du chapitre',
'chapters_empty' => 'Il n\'y a pas de pages dans ce chapitre actuellement.',
'chapters_permissions_active' => 'Permissions du chapitre activées',
'chapters_permissions_success' => 'Permissions du chapitres mises à jour',
/**
* Pages
*/
'page' => 'Page',
'pages' => 'Pages',
'pages_popular' => 'Pages populaires',
'pages_new' => 'Nouvelle page',
'pages_attachments' => 'Fichiers joints',
'pages_navigation' => 'Navigation des pages',
'pages_delete' => 'Supprimer la page',
'pages_delete_named' => 'Supprimer la page :pageName',
'pages_delete_draft_named' => 'supprimer le brouillon de la page :pageName',
'pages_delete_draft' => 'Supprimer le brouillon',
'pages_delete_success' => 'Page supprimée',
'pages_delete_draft_success' => 'Brouillon supprimé',
'pages_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cette page?',
'pages_delete_draft_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer ce brouillon?',
'pages_editing_named' => 'Modification de la page :pageName',
'pages_edit_toggle_header' => 'Afficher/cacher l\'en-tête',
'pages_edit_save_draft' => 'Enregistrer le brouillon',
'pages_edit_draft' => 'Modifier le brouillon',
'pages_editing_draft' => 'Modification du brouillon',
'pages_editing_page' => 'Modification de la page',
'pages_edit_draft_save_at' => 'Brouillon sauvé le ',
'pages_edit_delete_draft' => 'Supprimer le brouillon',
'pages_edit_discard_draft' => 'Ecarter le brouillon',
'pages_edit_set_changelog' => 'Remplir le journal des changements',
'pages_edit_enter_changelog_desc' => 'Entrez une brève description des changements effectués',
'pages_edit_enter_changelog' => 'Entrez dans le journal des changements',
'pages_save' => 'Enregistrez la page',
'pages_title' => 'Titre de la page',
'pages_name' => 'Nom de la page',
'pages_md_editor' => 'Editeur',
'pages_md_preview' => 'Prévisualisation',
'pages_md_insert_image' => 'Insérer une image',
'pages_md_insert_link' => 'Insérer un lien',
'pages_not_in_chapter' => 'La page n\'est pas dans un chanpitre',
'pages_move' => 'Déplacer la page',
'pages_move_success' => 'Page déplacée à ":parentName"',
'pages_permissions' => 'Permissions de la page',
'pages_permissions_success' => 'Permissions de la page mises à jour',
'pages_revisions' => 'Révisions de la page',
'pages_revisions_named' => 'Révisions pour :pageName',
'pages_revision_named' => 'Révision pour :pageName',
'pages_revisions_created_by' => 'Créé par',
'pages_revisions_date' => 'Date de révision',
'pages_revisions_changelog' => 'Journal des changements',
'pages_revisions_changes' => 'Changements',
'pages_revisions_current' => 'Version courante',
'pages_revisions_preview' => 'Prévisualisation',
'pages_revisions_restore' => 'Restaurer',
'pages_revisions_none' => 'Cette page n\'a aucune révision',
'pages_export' => 'Exporter',
'pages_export_html' => 'Fichiers web',
'pages_export_pdf' => 'Fichier PDF',
'pages_export_text' => 'Document texte',
'pages_copy_link' => 'Copier le lien',
'pages_permissions_active' => 'Permissions de page actives',
'pages_initial_revision' => 'Publication initiale',
'pages_initial_name' => 'Nouvelle page',
'pages_editing_draft_notification' => 'Vous éditez actuellement un brouillon qui a été sauvé :timeDiff.',
'pages_draft_edited_notification' => 'La page a été mise à jour depuis votre dernière visit. Vous devriez écarter ce brouillon.',
'pages_draft_edit_active' => [
'start_a' => ':count utilisateurs ont commencé a éditer cette page',
'start_b' => ':userName a commencé à éditer cette page',
'time_a' => 'depuis la dernière sauvegarde',
'time_b' => 'dans les :minCount dernières minutes',
'message' => ':start :time. Attention a ne pas écraser les mises à jour de quelqu\'un d\'autre!',
],
'pages_draft_discarded' => 'Brouuillon écarté, la page est dans sa version actuelle.',
/**
* Editor sidebar
*/
'page_tags' => 'Mots-clés de la page',
'tag' => 'Mot-clé',
'tags' => 'Mots-clé',
'tag_value' => 'Valeur du mot-clé (Optionnel)',
'tags_explain' => "Ajouter des mot-clés pour catégoriser votre contenu.",
'tags_add' => 'Ajouter un autre mot-clé',
'attachments' => 'Fichiers joints',
'attachments_explain' => 'Ajouter des fichiers ou des liens pour les afficher sur votre page. Ils seront affichés dans la barre latérale',
'attachments_explain_instant_save' => 'Ces changements sont enregistrés immédiatement.',
'attachments_items' => 'Fichiers joints',
'attachments_upload' => 'Uploader un fichier',
'attachments_link' => 'Attacher un lien',
'attachments_set_link' => 'Définir un lien',
'attachments_delete_confirm' => 'Cliquer une seconde fois sur supprimer pour valider la suppression.',
'attachments_dropzone' => 'Glissez des fichiers ou cliquez ici pour attacher des fichiers',
'attachments_no_files' => 'Aucun fichier ajouté',
'attachments_explain_link' => 'Vous pouvez attacher un lien si vous ne souhaitez pas uploader un fichier.',
'attachments_link_name' => 'Nom du lien',
'attachment_link' => 'Lien de l\'attachement',
'attachments_link_url' => 'Lien sur un fichier',
'attachments_link_url_hint' => 'URL du site ou du fichier',
'attach' => 'Attacher',
'attachments_edit_file' => 'Modifier le fichier',
'attachments_edit_file_name' => 'Nom du fichier',
'attachments_edit_drop_upload' => 'Glissez un fichier ou cliquer pour mettre à jour le fichier',
'attachments_order_updated' => 'Ordre des fichiers joints mis à jour',
'attachments_updated_success' => 'Détails des fichiers joints mis à jour',
'attachments_deleted' => 'Fichier joint supprimé',
'attachments_file_uploaded' => 'Fichier ajouté avec succès',
'attachments_file_updated' => 'Fichier mis à jour avec succès',
'attachments_link_attached' => 'Lien attaché à la page avec succès',
/**
* Profile View
*/
'profile_user_for_x' => 'Utilisateur depuis :time',
'profile_created_content' => 'Contenu créé',
'profile_not_created_pages' => ':userName n\'a pas créé de pages',
'profile_not_created_chapters' => ':userName n\'a pas créé de chapitres',
'profile_not_created_books' => ':userName n\'a pas créé de livres',
];

View File

@ -0,0 +1,70 @@
<?php
return [
/**
* Error text strings.
*/
// Permissions
'permission' => 'Vous n\'avez pas les droits pour accéder à cette page.',
'permissionJson' => 'Vous n\'avez pas les droits pour exécuter cette action.',
// Auth
'error_user_exists_different_creds' => 'Un utilisateur avec l\'adresse :email existe déjà.',
'email_already_confirmed' => 'Cet e-mail a déjà été validé, vous pouvez vous connecter.',
'email_confirmation_invalid' => 'Cette confirmation est invalide. Veuillez essayer de vous inscrire à nouveau.',
'email_confirmation_expired' => 'Le jeton de confirmation est perimé. Un nouvel e-mail vous a été envoyé.',
'ldap_fail_anonymous' => 'L\'accès LDAP anonyme n\'a pas abouti',
'ldap_fail_authed' => 'L\'accès LDAP n\'a pas abouti avec cet utilisateur et ce mot de passe',
'ldap_extension_not_installed' => 'L\'extention LDAP PHP n\'est pas installée',
'ldap_cannot_connect' => 'Cannot connect to ldap server, Initial connection failed',
'social_no_action_defined' => 'No action defined',
'social_account_in_use' => 'Cet compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',
'social_account_email_in_use' => 'L\'email :email Est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.',
'social_account_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.',
'social_account_already_used_existing' => 'Ce compte :socialAccount est déjà utilisé par un autre utilisateur.',
'social_account_not_used' => 'Ce compte :socialAccount n\'est lié à aucun utilisateur. ',
'social_account_register_instructions' => 'Si vous n\'avez pas encore de compte, vous pouvez le lier avec l\'option :socialAccount.',
'social_driver_not_found' => 'Social driver not found',
'social_driver_not_configured' => 'Your :socialAccount social settings are not configured correctly.',
// System
'path_not_writable' => 'File path :filePath could not be uploaded to. Ensure it is writable to the server.',
'cannot_get_image_from_url' => 'Impossible de récupérer l\'image depuis :url',
'cannot_create_thumbs' => 'Le serveur ne peux pas créer de miniatures, vérifier que l\extensions GD PHP est installée.',
'server_upload_limit' => 'La taille du fichier est trop grande.',
'image_upload_error' => 'Une erreur est survenue pendant l\'envoi de l\'image',
// Attachments
'attachment_page_mismatch' => 'Page mismatch during attachment update',
// Pages
'page_draft_autosave_fail' => 'Le brouillon n\'a pas pu être sauvé. Vérifiez votre connexion internet',
// Entities
'entity_not_found' => 'Entité non trouvée',
'book_not_found' => 'Livre non trouvé',
'page_not_found' => 'Page non trouvée',
'chapter_not_found' => 'Chapitre non trouvé',
'selected_book_not_found' => 'Ce livre n\'a pas été trouvé',
'selected_book_chapter_not_found' => 'Ce livre ou chapitre n\'a pas été trouvé',
'guests_cannot_save_drafts' => 'Les invités ne peuvent pas sauver de brouillons',
// Users
'users_cannot_delete_only_admin' => 'Vous ne pouvez pas supprimer le dernier admin',
'users_cannot_delete_guest' => 'Vous ne pouvez pas supprimer l\'utilisateur invité',
// Roles
'role_cannot_be_edited' => 'Ce rôle ne peut pas être modifié',
'role_system_cannot_be_deleted' => 'Ceci est un rôle du système et on ne peut pas le supprimer',
'role_registration_default_cannot_delete' => 'Ce rôle ne peut pas être supprimé tant qu\'il est le rôle par défaut',
// Error pages
'404_page_not_found' => 'Page non trouvée',
'sorry_page_not_found' => 'Désolé, cette page n\'a pas pu être trouvée.',
'return_home' => 'Retour à l\'accueil',
'error_occurred' => 'Une erreur est survenue',
'app_down' => ':appName n\'est pas en service pour le moment',
'back_soon' => 'Nous serons bientôt de retour.',
];

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Précédent',
'next' => 'Suivant &raquo;',
];

View File

@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'password' => 'Les mots de passe doivent faire au moins 6 caractères et correspondre à la confirmation.',
'user' => "Nous n'avons pas trouvé d'utilisateur avec cette adresse.",
'token' => 'Le jeton de réinitialisation est invalide.',
'sent' => 'Nous vous avons envoyé un lien de réinitialisation de mot de passe!',
'reset' => 'Votre mot de passe a été réinitialisé!',
];

View File

@ -0,0 +1,112 @@
<?php
return [
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
'settings' => 'Préférences',
'settings_save' => 'Enregistrer les préférences',
'settings_save_success' => 'Préférences enregistrées',
/**
* App settings
*/
'app_settings' => 'Préférences de l\'application',
'app_name' => 'Nom de l\'application',
'app_name_desc' => 'Ce nom est affiché dans l\'en-tête et les e-mails.',
'app_name_header' => 'Afficher le nom dans l\'en-tête?',
'app_public_viewing' => 'Accepter le visionnage public des pages?',
'app_secure_images' => 'Activer l\'ajout d\'image sécurisé?',
'app_secure_images_desc' => 'Pour des questions de performances, toutes les images sont publiques. Cette option ajoute une chaîne aléatoire difficile à deviner dans les URLs des images.',
'app_editor' => 'Editeur des pages',
'app_editor_desc' => 'Sélectionnez l\'éditeur qui sera utilisé pour modifier les pages.',
'app_custom_html' => 'HTML personnalisé dans l\'en-tête',
'app_custom_html_desc' => 'Le contenu inséré ici sera jouté en bas de la balise <head> de toutes les pages. Vous pouvez l\'utiliser pour ajouter du CSS personnalisé ou un tracker analytique.',
'app_logo' => 'Logo de l\'Application',
'app_logo_desc' => 'Cette image doit faire 43px de hauteur. <br>Les images plus larges seront réduites.',
'app_primary_color' => 'Couleur principale de l\'application',
'app_primary_color_desc' => 'This should be a hex value. <br>Leave empty to reset to the default color.',
/**
* Registration settings
*/
'reg_settings' => 'Préférence pour l\'inscription',
'reg_allow' => 'Accepter l\'inscription?',
'reg_default_role' => 'Rôle par défaut lors de l\'inscription',
'reg_confirm_email' => 'Obliger la confirmation par e-mail?',
'reg_confirm_email_desc' => 'Si la restriction de domaine est activée, la confirmation sera automatiquement obligatoire et cette valeur sera ignorée.',
'reg_confirm_restrict_domain' => 'Restreindre l\'inscription à un domaine',
'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\'inscription, séparés par une virgule. Les utilisateur recevront un e-mail de confirmation à cette adresse. <br> Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.',
'reg_confirm_restrict_domain_placeholder' => 'Aucune restriction en place',
/**
* Role settings
*/
'roles' => 'Rôles',
'role_user_roles' => 'Rôles des utilisateurs',
'role_create' => 'Créer un nouveau rôle',
'role_create_success' => 'Rôle créé avec succès',
'role_delete' => 'Supprimer le rôle',
'role_delete_confirm' => 'Ceci va supprimer le rôle \':roleName\'.',
'role_delete_users_assigned' => 'Ce rôle a :userCount utilisateurs assignés. Vous pouvez choisir un rôle de remplacement pour ces utilisateurs.',
'role_delete_no_migration' => "Ne pas assigner de nouveau rôle",
'role_delete_sure' => 'Êtes vous sûr(e) de vouloir supprimer ce rôle?',
'role_delete_success' => 'Le rôle a été supprimé avec succès',
'role_edit' => 'Modifier le rôle',
'role_details' => 'Détails du rôle',
'role_name' => 'Nom du Rôle',
'role_desc' => 'Courte description du rôle',
'role_system' => 'Permissions système',
'role_manage_users' => 'Gérer les utilisateurs',
'role_manage_roles' => 'Gérer les rôles et permissions',
'role_manage_entity_permissions' => 'Gérer les permissions sur les livres, chapitres et pages',
'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres chapitres et pages',
'role_manage_settings' => 'Gérer les préférences de l\'application',
'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' => 'Tous',
'role_own' => 'Propres',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
'role_save' => 'Enregistrer le rôle',
'role_update_success' => 'Rôle mis à jour avec succès',
'role_users' => 'Utilisateurs ayant ce rôle',
'role_users_none' => 'Aucun utilisateur avec ce rôle actuellement',
/**
* Users
*/
'users' => 'Utilisateurs',
'user_profile' => 'Profil d\'utilisateur',
'users_add_new' => 'Ajouter un nouvel utilisateur',
'users_search' => 'Chercher les utilisateurs',
'users_role' => 'Rôles des utilisateurs',
'users_external_auth_id' => 'Identifiant d\'authentification externe',
'users_password_warning' => 'Remplissez ce fomulaire uniquement si vous souhaitez changer de mot de passe:',
'users_system_public' => 'Cet utilisateur représente les invités visitant votre instance. Il est assigné automatiquement aux invités.',
'users_delete' => 'Supprimer un utilisateur',
'users_delete_named' => 'Supprimer l\'utilisateur :userName',
'users_delete_warning' => 'Ceci va supprimer \':userName\' du système.',
'users_delete_confirm' => 'Êtes-vous sûr(e) de vouloir supprimer cet utilisateur?',
'users_delete_success' => 'Utilisateurs supprimés avec succès',
'users_edit' => 'Modifier l\'utilisateur',
'users_edit_profile' => 'Modifier le profil',
'users_edit_success' => 'Utilisateur mis à jour avec succès',
'users_avatar' => 'Avatar de l\'utilisateur',
'users_avatar_desc' => 'Cette image doit être un carré d\'environ 256px.',
'users_preferred_language' => 'Langue préférée',
'users_social_accounts' => 'Comptes sociaux',
'users_social_accounts_info' => 'Vous pouvez connecter des réseaux sociaux à votre compte pour vous connecter plus rapidement. Déconnecter un compte n\'enlèvera pas les accès autorisés précédemment sur votre compte de réseau social.',
'users_social_connect' => 'Connecter le compte',
'users_social_disconnect' => 'Déconnecter le compte',
'users_social_connected' => 'Votre compte :socialAccount a élté ajouté avec succès.',
'users_social_disconnected' => 'Votre compte :socialAccount a été déconnecté avec succès',
];

View File

@ -0,0 +1,108 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => ':attribute doit être accepté.',
'active_url' => ':attribute n\'est pas une URL valide.',
'after' => ':attribute doit être supérieur à :date.',
'alpha' => ':attribute ne doit contenir que des lettres.',
'alpha_dash' => ':attribute doit contenir uniquement des lettres, chiffres et traits d\'union.',
'alpha_num' => ':attribute doit contenir uniquement des chiffres et des lettres.',
'array' => ':attribute doit être un tableau.',
'before' => ':attribute doit être inférieur à :date.',
'between' => [
'numeric' => ':attribute doit être compris entre :min et :max.',
'file' => ':attribute doit être compris entre :min et :max kilobytes.',
'string' => ':attribute doit être compris entre :min et :max caractères.',
'array' => ':attribute doit être compris entre :min et :max éléments.',
],
'boolean' => ':attribute doit être vrai ou faux.',
'confirmed' => ':attribute la confirmation n\'est pas valide.',
'date' => ':attribute n\'est pas une date valide.',
'date_format' => ':attribute ne correspond pas au format :format.',
'different' => ':attribute et :other doivent être différents l\'un de l\'autre.',
'digits' => ':attribute doit être de longueur :digits.',
'digits_between' => ':attribute doit avoir une longueur entre :min et :max.',
'email' => ':attribute doit être une adresse e-mail valide.',
'filled' => ':attribute est un champ requis.',
'exists' => 'L\'attribut :attribute est invalide.',
'image' => ':attribute doit être une image.',
'in' => 'L\'attribut :attribute est invalide.',
'integer' => ':attribute doit être un chiffre entier.',
'ip' => ':attribute doit être une adresse IP valide.',
'max' => [
'numeric' => ':attribute ne doit pas excéder :max.',
'file' => ':attribute ne doit pas excéder :max kilobytes.',
'string' => ':attribute ne doit pas excéder :max caractères.',
'array' => ':attribute ne doit pas contenir plus de :max éléments.',
],
'mimes' => ':attribute doit être un fichier de type :values.',
'min' => [
'numeric' => ':attribute doit être au moins :min.',
'file' => ':attribute doit faire au moins :min kilobytes.',
'string' => ':attribute doit contenir au moins :min caractères.',
'array' => ':attribute doit contenir au moins :min éléments.',
],
'not_in' => 'L\'attribut sélectionné :attribute est invalide.',
'numeric' => ':attribute doit être un nombre.',
'regex' => ':attribute a un format invalide.',
'required' => ':attribute est un champ requis.',
'required_if' => ':attribute est requis si :other est :value.',
'required_with' => ':attribute est requis si :values est présent.',
'required_with_all' => ':attribute est requis si :values est présent.',
'required_without' => ':attribute est requis si:values n\'est pas présent.',
'required_without_all' => ':attribute est requis si aucun des valeurs :values n\'est présente.',
'same' => ':attribute et :other doivent être identiques.',
'size' => [
'numeric' => ':attribute doit avoir la taille :size.',
'file' => ':attribute doit peser :size kilobytes.',
'string' => ':attribute doit contenir :size caractères.',
'array' => ':attribute doit contenir :size éléments.',
],
'string' => ':attribute doit être une chaîne de caractères.',
'timezone' => ':attribute doit être une zone valide.',
'unique' => ':attribute est déjà utilisé.',
'url' => ':attribute a un format invalide.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'password-confirm' => [
'required_with' => 'La confirmation du mot de passe est requise',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
];

View File

@ -0,0 +1,40 @@
<?php
return [
/**
* Activity text strings.
* Is used for all the text within activity logs & notifications.
*/
// Pages
'page_create' => 'página criada',
'page_create_notification' => 'Página criada com sucesso',
'page_update' => 'página atualizada',
'page_update_notification' => 'Página atualizada com sucesso',
'page_delete' => 'página excluída',
'page_delete_notification' => 'Página excluída com sucesso',
'page_restore' => 'página restaurada',
'page_restore_notification' => 'Página restaurada com sucesso',
'page_move' => 'página movida',
// Chapters
'chapter_create' => 'capítulo criado',
'chapter_create_notification' => 'Capítulo criado com sucesso',
'chapter_update' => 'capítulo atualizado',
'chapter_update_notification' => 'capítulo atualizado com sucesso',
'chapter_delete' => 'capítulo excluído',
'chapter_delete_notification' => 'Capítulo excluído com sucesso',
'chapter_move' => 'capitulo movido',
// Books
'book_create' => 'livro criado',
'book_create_notification' => 'Livro criado com sucesso',
'book_update' => 'livro atualizado',
'book_update_notification' => 'Livro atualizado com sucesso',
'book_delete' => 'livro excluído',
'book_delete_notification' => 'Livro excluído com sucesso',
'book_sort' => 'livro classificado',
'book_sort_notification' => 'Livro reclassificado com sucesso',
];

View File

@ -0,0 +1,74 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'As credenciais fornecidas não puderam ser validadas em nossos registros..',
'throttle' => 'Muitas tentativas de login. Por favor, tente novamente em :seconds segundos.',
/**
* Login & Register
*/
'sign_up' => 'Registrar-se',
'log_in' => 'Entrar',
'logout' => 'Sair',
'name' => 'Nome',
'username' => 'Nome de Usuário',
'email' => 'E-mail',
'password' => 'Senha',
'password_confirm' => 'Confirmar Senha',
'password_hint' => 'Senha deverá ser maior que 5 caracteres',
'forgot_password' => 'Esqueceu a senha?',
'remember_me' => 'Lembrar de mim',
'ldap_email_hint' => 'Por favor, digite um e-mail para essa conta.',
'create_account' => 'Criar conta',
'social_login' => 'Login social',
'social_registration' => 'Registro social',
'social_registration_text' => 'Registre e entre usando outro serviço.',
'register_thanks' => 'Obrigado por efetuar o registro!',
'register_confirm' => 'Por favor, verifique seu e-mail e clique no botão de confirmação para acessar :appName.',
'registrations_disabled' => 'Registros estão temporariamente desabilitados',
'registration_email_domain_invalid' => 'O domínio de e-mail usado não tem acesso permitido a essa aplicação',
'register_success' => 'Obrigado por se registrar! Você agora encontra-se registrado e logado..',
/**
* Password Reset
*/
'reset_password' => 'Resetar senha',
'reset_password_send_instructions' => 'Digite seu e-mail abaixo e o sistema enviará uma mensagem com o link de reset de senha.',
'reset_password_send_button' => 'Enviar o link de reset de senha',
'reset_password_sent_success' => 'Um link de reset de senha foi enviado para :email.',
'reset_password_success' => 'Sua senha foi resetada com sucesso.',
'email_reset_subject' => 'Resetar a senha de :appName',
'email_reset_text' => 'Você recebeu esse e-mail pois recebemos uma solicitação de reset de senha para sua conta.',
'email_reset_not_requested' => 'Caso não tenha sido você a solicitar o reset de senha, ignore esse e-mail.',
/**
* Email Confirmation
*/
'email_confirm_subject' => 'Confirme seu e-mail para :appName',
'email_confirm_greeting' => 'Obrigado por se registrar em :appName!',
'email_confirm_text' => 'Por favor, confirme seu endereço de e-mail clicando no botão abaixo:',
'email_confirm_action' => 'Confirmar E-mail',
'email_confirm_send_error' => 'E-mail de confirmação é requerido, mas o sistema não pôde enviar a mensagem. Por favor, entre em contato com o admin para se certificar que o serviço de envio de e-mails está corretamente configurado.',
'email_confirm_success' => 'Seu e-mail foi confirmado!',
'email_confirm_resent' => 'E-mail de confirmação reenviado. Por favor, cheque sua caixa postal.',
'email_not_confirmed' => 'Endereço de e-mail não foi confirmado',
'email_not_confirmed_text' => 'Seu endereço de e-mail ainda não foi confirmado.',
'email_not_confirmed_click_link' => 'Por favor, clique no link no e-mail que foi enviado após o registro.',
'email_not_confirmed_resend' => 'Caso não encontre o e-mail você poderá reenviar a confirmação usando o formulário abaixo.',
'email_not_confirmed_resend_button' => 'Reenviar o e-mail de confirmação',
];

View File

@ -0,0 +1,58 @@
<?php
return [
/**
* Buttons
*/
'cancel' => 'Cancelar',
'confirm' => 'Confirmar',
'back' => 'Voltar',
'save' => 'Salvar',
'continue' => 'Continuar',
'select' => 'Selecionar',
/**
* Form Labels
*/
'name' => 'Nome',
'description' => 'Descrição',
'role' => 'Regra',
/**
* Actions
*/
'actions' => 'Ações',
'view' => 'Visualizar',
'create' => 'Criar',
'update' => 'Atualizar',
'edit' => 'Editar',
'sort' => 'Ordenar',
'move' => 'Mover',
'delete' => 'Excluir',
'search' => 'Pesquisar',
'search_clear' => 'Limpar Pesquisa',
'reset' => 'Resetar',
'remove' => 'Remover',
/**
* Misc
*/
'deleted_user' => 'Usuário excluído',
'no_activity' => 'Nenhuma atividade a mostrar',
'no_items' => 'Nenhum item disponível',
'back_to_top' => 'Voltar ao topo',
'toggle_details' => 'Alternar Detalhes',
/**
* Header
*/
'view_profile' => 'Visualizar Perfil',
'edit_profile' => 'Editar Perfil',
/**
* Email Content
*/
'email_action_help' => 'Se você estiver tendo problemas ao clicar o botão ":actionText", copie e cole a URL abaixo no seu navegador:',
'email_rights' => 'Todos os direitos reservados',
];

View File

@ -0,0 +1,24 @@
<?php
return [
/**
* Image Manager
*/
'image_select' => 'Selecionar imagem',
'image_all' => 'Todos',
'image_all_title' => 'Visualizar todas as imagens',
'image_book_title' => 'Visualizar imagens relacionadas a esse livro',
'image_page_title' => 'visualizar imagens relacionadas a essa página',
'image_search_hint' => 'Pesquisar imagem por nome',
'image_uploaded' => 'Carregado :uploadedDate',
'image_load_more' => 'Carregar Mais',
'image_image_name' => 'Nome da Imagem',
'image_delete_confirm' => 'Essa imagem é usada nas páginas abaixo. Clique em Excluir novamente para confirmar que você deseja mesmo eliminar a imagem.',
'image_select_image' => 'Selecionar Imagem',
'image_dropzone' => 'Arraste imagens ou clique aqui para fazer upload',
'images_deleted' => 'Imagens excluídas',
'image_preview' => 'Virtualização de Imagem',
'image_upload_success' => 'Upload de imagem efetuado com sucesso',
'image_update_success' => 'Upload de detalhes da imagem efetuado com sucesso',
'image_delete_success' => 'Imagem excluída com sucesso'
];

View File

@ -0,0 +1,226 @@
<?php
return [
/**
* Shared
*/
'recently_created' => 'Recentemente criado',
'recently_created_pages' => 'Páginas recentemente criadas',
'recently_updated_pages' => 'Páginas recentemente atualizadas',
'recently_created_chapters' => 'Capítulos recentemente criados',
'recently_created_books' => 'Livros recentemente criados',
'recently_update' => 'Recentemente atualizado',
'recently_viewed' => 'Recentemente visualizado',
'recent_activity' => 'Atividade recente',
'create_now' => 'Criar um agora',
'revisions' => 'Revisões',
'meta_created' => 'Criado em :timeLength',
'meta_created_name' => 'Criado em :timeLength por :user',
'meta_updated' => 'Atualizado em :timeLength',
'meta_updated_name' => 'Atualizado em :timeLength por :user',
'x_pages' => ':count Páginas',
'entity_select' => 'Seleção de Entidade',
'images' => 'Imagens',
'my_recent_drafts' => 'Meus rascunhos recentes',
'my_recently_viewed' => 'Meus itens recentemente visto',
'no_pages_viewed' => 'Você não visualizou nenhuma página',
'no_pages_recently_created' => 'Nenhuma página recentemente criada',
'no_pages_recently_updated' => 'Nenhuma página recentemente atualizada',
/**
* Permissions and restrictions
*/
'permissions' => 'Permissões',
'permissions_intro' => 'Uma vez habilitado, as permissões terão prioridade sobre outro conjunto de permissões.',
'permissions_enable' => 'Habilitar Permissões Customizadas',
'permissions_save' => 'Salvar Permissões',
/**
* Search
*/
'search_results' => 'Resultado(s) da Pesquisa',
'search_results_page' => 'Resultado(s) de Pesquisa de Página',
'search_results_chapter' => 'Resultado(s) de Pesquisa de Capítulo',
'search_results_book' => 'Resultado(s) de Pesquisa de Livro',
'search_clear' => 'Limpar Pesquisa',
'search_view_pages' => 'Visualizar todas as páginas correspondentes',
'search_view_chapters' => 'Visualizar todos os capítulos correspondentes',
'search_view_books' => 'Visualizar todos os livros correspondentes',
'search_no_pages' => 'Nenhuma página corresponde à pesquisa',
'search_for_term' => 'Pesquisar por :term',
'search_page_for_term' => 'Pesquisar Página por :term',
'search_chapter_for_term' => 'Pesquisar Capítulo por :term',
'search_book_for_term' => 'Pesquisar Livros por :term',
/**
* Books
*/
'book' => 'Livro',
'books' => 'Livros',
'books_empty' => 'Nenhum livro foi criado',
'books_popular' => 'Livros populares',
'books_recent' => 'Livros recentes',
'books_popular_empty' => 'Os livros mais populares aparecerão aqui.',
'books_create' => 'Criar novo Livro',
'books_delete' => 'Excluir Livro',
'books_delete_named' => 'Excluir Livro :bookName',
'books_delete_explain' => 'A ação vai excluír o livro com o nome \':bookName\'. Todas as páginas e capítulos serão removidos.',
'books_delete_confirmation' => 'Você tem certeza que quer excluír o Livro?',
'books_edit' => 'Editar Livro',
'books_edit_named' => 'Editar Livro :bookName',
'books_form_book_name' => 'Nome do Livro',
'books_save' => 'Salvar Livro',
'books_permissions' => 'Permissões do Livro',
'books_permissions_updated' => 'Permissões do Livro Atualizadas',
'books_empty_contents' => 'Nenhuma página ou capítulo criado para esse livro.',
'books_empty_create_page' => 'Criar uma nova página',
'books_empty_or' => 'ou',
'books_empty_sort_current_book' => 'Ordenar o livro atual',
'books_empty_add_chapter' => 'Adicionar um capítulo',
'books_permissions_active' => 'Permissões do Livro ativadas',
'books_search_this' => 'Pesquisar esse livro',
'books_navigation' => 'Navegação do Livro',
'books_sort' => 'Ordenar conteúdos do Livro',
'books_sort_named' => 'Ordenar Livro :bookName',
'books_sort_show_other' => 'Mostrar outros livros',
'books_sort_save' => 'Salvar nova ordenação',
/**
* Chapters
*/
'chapter' => 'Capitulo',
'chapters' => 'Capítulos',
'chapters_popular' => 'Capítulos Populares',
'chapters_new' => 'Novo Capítulo',
'chapters_create' => 'Criar novo Capítulo',
'chapters_delete' => 'Excluír Capítulo',
'chapters_delete_named' => 'Excluir Capítulo :chapterName',
'chapters_delete_explain' => 'A ação vai excluír o capítulo de nome \':chapterName\'. Todas as páginas do capítulo serão removidas
e adicionadas diretamente ao livro pai.',
'chapters_delete_confirm' => 'Tem certeza que deseja excluír o capitulo?',
'chapters_edit' => 'Editar Capítulo',
'chapters_edit_named' => 'Editar capitulo :chapterName',
'chapters_save' => 'Salvar Capítulo',
'chapters_move' => 'Mover Capítulo',
'chapters_move_named' => 'Mover Capítulo :chapterName',
'chapter_move_success' => 'Capítulo movido para :bookName',
'chapters_permissions' => 'Permissões do Capítulo',
'chapters_empty' => 'Nenhuma página existente nesse capítulo.',
'chapters_permissions_active' => 'Permissões de Capítulo ativadas',
'chapters_permissions_success' => 'Permissões de Capítulo atualizadas',
/**
* Pages
*/
'page' => 'Página',
'pages' => 'Páginas',
'pages_popular' => 'Páginas Popular',
'pages_new' => 'Nova Página',
'pages_attachments' => 'Anexos',
'pages_navigation' => 'Página de Navegação',
'pages_delete' => 'Excluír Página',
'pages_delete_named' => 'Excluír Página :pageName',
'pages_delete_draft_named' => 'Excluir rascunho de Página de nome :pageName',
'pages_delete_draft' => 'Excluir rascunho de Página',
'pages_delete_success' => 'Página excluída',
'pages_delete_draft_success' => 'Página de rascunho excluída',
'pages_delete_confirm' => 'Tem certeza que deseja excluir a página?',
'pages_delete_draft_confirm' => 'Tem certeza que deseja excluir o rascunho de página?',
'pages_editing_named' => 'Editando a Página :pageName',
'pages_edit_toggle_header' => 'Alternar cabeçalho',
'pages_edit_save_draft' => 'Salvar Rascunho',
'pages_edit_draft' => 'Editar rascunho de Página',
'pages_editing_draft' => 'Editando Rascunho',
'pages_editing_page' => 'Editando Página',
'pages_edit_draft_save_at' => 'Rascunho salvo em ',
'pages_edit_delete_draft' => 'Excluir rascunho',
'pages_edit_discard_draft' => 'Descartar rascunho',
'pages_edit_set_changelog' => 'Definir Changelog',
'pages_edit_enter_changelog_desc' => 'Digite uma breve descrição das mudanças efetuadas por você',
'pages_edit_enter_changelog' => 'Entrar no Changelog',
'pages_save' => 'Salvar Página',
'pages_title' => 'Título de Página',
'pages_name' => 'Nome da Página',
'pages_md_editor' => 'Editor',
'pages_md_preview' => 'Preview',
'pages_md_insert_image' => 'Inserir Imagem',
'pages_md_insert_link' => 'Inserir Link para Entidade',
'pages_not_in_chapter' => 'Página não está dentro de um Capítulo',
'pages_move' => 'Mover Página',
'pages_move_success' => 'Pagina movida para ":parentName"',
'pages_permissions' => 'Permissões de Página',
'pages_permissions_success' => 'Permissões de Página atualizadas',
'pages_revisions' => 'Revisões de Página',
'pages_revisions_named' => 'Revisões de Página para :pageName',
'pages_revision_named' => 'Revisão de Página para :pageName',
'pages_revisions_created_by' => 'Criado por',
'pages_revisions_date' => 'Data da Revisão',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Mudanças',
'pages_revisions_current' => 'Versão atual',
'pages_revisions_preview' => 'Preview',
'pages_revisions_restore' => 'Restaurar',
'pages_revisions_none' => 'Essa página não tem revisões',
'pages_export' => 'Exportar',
'pages_export_html' => 'Arquivo Web Contained',
'pages_export_pdf' => 'Arquivo PDF',
'pages_export_text' => 'Arquivo Texto',
'pages_copy_link' => 'Copia Link',
'pages_permissions_active' => 'Permissões de Página Ativas',
'pages_initial_revision' => 'Publicação Inicial',
'pages_initial_name' => 'Nova Página',
'pages_editing_draft_notification' => 'Você está atualmente editando um rascunho que foi salvo da última vez em :timeDiff.',
'pages_draft_edited_notification' => 'Essa página foi atualizada desde então. É recomendado que você descarte esse rascunho.',
'pages_draft_edit_active' => [
'start_a' => ':count usuários que iniciaram edição dessa página',
'start_b' => ':userName iniciou a edição dessa página',
'time_a' => 'desde que a página foi atualizada pela última vez',
'time_b' => 'nos últimos :minCount minutos',
'message' => ':start :time. Tome cuidado para não sobrescrever atualizações de outras pessoas!',
],
'pages_draft_discarded' => 'Rascunho descartado. O editor foi atualizado com a página atualizada',
/**
* Editor sidebar
*/
'page_tags' => 'Tags de Página',
'tag' => 'Tag',
'tags' => '',
'tag_value' => 'Valor da Tag (Opcional)',
'tags_explain' => "Adicione algumas tags para melhor categorizar seu conteúdo. \n Você pode atrelar um valor para uma tag para uma organização mais consistente.",
'tags_add' => 'Adicionar outra tag',
'attachments' => 'Anexos',
'attachments_explain' => 'Faça o Upload de alguns arquivos ou anexo algum link para ser mostrado na sua página. Eles estarão visíveis na barra lateral à direita da página.',
'attachments_explain_instant_save' => 'Mudanças são salvas instantaneamente.',
'attachments_items' => 'Itens Anexados',
'attachments_upload' => 'Upload de arquivos',
'attachments_link' => 'Links Anexados',
'attachments_set_link' => 'Definir Link',
'attachments_delete_confirm' => 'Clique novamente em Excluir para confirmar a exclusão desse anexo.',
'attachments_dropzone' => 'Arraste arquivos para cá ou clique para anexar arquivos',
'attachments_no_files' => 'Nenhum arquivo foi enviado',
'attachments_explain_link' => 'Você pode anexar um link se preferir não fazer o upload do arquivo. O link poderá ser para uma outra página ou link para um arquivo na nuvem.',
'attachments_link_name' => 'Nome do Link',
'attachment_link' => 'Link para o Anexo',
'attachments_link_url' => 'Link para o Arquivo',
'attachments_link_url_hint' => 'URL do site ou arquivo',
'attach' => 'Anexar',
'attachments_edit_file' => 'Editar Arquivo',
'attachments_edit_file_name' => 'Nome do Arquivo',
'attachments_edit_drop_upload' => 'Arraste arquivos para cá ou clique para anexar arquivos e sobrescreve-lo',
'attachments_order_updated' => 'Ordem dos anexos atualizada',
'attachments_updated_success' => 'Detalhes dos anexos atualizados',
'attachments_deleted' => 'Anexo excluído',
'attachments_file_uploaded' => 'Upload de arquivo efetuado com sucesso',
'attachments_file_updated' => 'Arquivo atualizado com sucesso',
'attachments_link_attached' => 'Link anexado com sucesso à página',
/**
* Profile View
*/
'profile_user_for_x' => 'Usuário por :time',
'profile_created_content' => 'Conteúdo Criado',
'profile_not_created_pages' => ':userName não criou páginas',
'profile_not_created_chapters' => ':userName não criou capítulos',
'profile_not_created_books' => ':userName não criou livros',
];

View File

@ -0,0 +1,70 @@
<?php
return [
/**
* Error text strings.
*/
// Permissions
'permission' => 'Você não tem permissões para acessar a página requerida.',
'permissionJson' => 'Você não tem permissão para realizar a ação requerida.',
// Auth
'error_user_exists_different_creds' => 'Um usuário com o e-mail :email já existe mas com credenciais diferentes.',
'email_already_confirmed' => 'E-mail já foi confirmado. Tente efetuar o login.',
'email_confirmation_invalid' => 'Esse token de confirmação não é válido ou já foi utilizado. Por favor, tente efetuar o registro novamente.',
'email_confirmation_expired' => 'O token de confirmação já expirou. Um novo e-mail foi enviado.',
'ldap_fail_anonymous' => 'O acesso LDAP falhou ao tentar usar o anonymous bind',
'ldap_fail_authed' => 'O acesso LDAPfalou ao tentar os detalhes do dn e senha fornecidos',
'ldap_extension_not_installed' => 'As extensões LDAP PHP não estão instaladas',
'ldap_cannot_connect' => 'Não foi possível conectar ao servidor LDAP. Conexão inicial falhou',
'social_no_action_defined' => 'Nenhuma ação definida',
'social_account_in_use' => 'Essa conta :socialAccount já está em uso. Por favor, tente se logar usando a opção :socialAccount',
'social_account_email_in_use' => 'O e-mail :email já está e muso. Se você já tem uma conta você poderá se conectar a conta :socialAccount a partir das configurações de seu perfil.',
'social_account_existing' => 'Essa conta :socialAccount já está atrelada a esse perfil.',
'social_account_already_used_existing' => 'Essa conta :socialAccount já está sendo usada por outro usuário.',
'social_account_not_used' => 'Essa conta :socialAccount não está atrelada a nenhum usuário. Por favor, faça o link da conta com suas configurações de perfil. ',
'social_account_register_instructions' => 'Se você não tem uma conta, você poderá fazer o registro usando a opção :socialAccount',
'social_driver_not_found' => 'Social driver não encontrado',
'social_driver_not_configured' => 'Seus parâmetros socials de :socialAccount não estão configurados corretamente.',
// System
'path_not_writable' => 'O caminho de destino (:filePath) de upload de arquivo não possui permissão de escrita. Certifique-se que ele possui direitos de escrita no servidor.',
'cannot_get_image_from_url' => 'Não foi possivel capturar a imagem a partir de :url',
'cannot_create_thumbs' => 'O servidor não pôde criar as miniaturas de imagem. Por favor, verifique se a extensão GD PHP está instalada.',
'server_upload_limit' => 'O servidor não permite o upload de arquivos com esse tamanho. Por favor, tente fazer o upload de arquivos de menor tamanho.',
'image_upload_error' => 'Um erro aconteceu enquanto o servidor tentava efetuar o upload da imagem',
// Attachments
'attachment_page_mismatch' => 'Erro de \'Page mismatch\' durante a atualização do anexo',
// Pages
'page_draft_autosave_fail' => 'Falou ao tentar salvar o rascunho. Certifique-se que a conexão de internet está funcional antes de tentar salvar essa página',
// Entities
'entity_not_found' => 'Entidade não encontrada',
'book_not_found' => 'Livro não encontrado',
'page_not_found' => 'Página não encontrada',
'chapter_not_found' => 'Capítulo não encontrado',
'selected_book_not_found' => 'O livro selecionado não foi encontrado',
'selected_book_chapter_not_found' => 'O Livro selecionado ou Capítulo não foi encontrado',
'guests_cannot_save_drafts' => 'Convidados não podem salvar rascunhos',
// Users
'users_cannot_delete_only_admin' => 'Você não pode excluir o conteúdo, apenas o admin.',
'users_cannot_delete_guest' => 'Você não pode excluir o usuário convidado',
// Roles
'role_cannot_be_edited' => 'Esse perfil não poed ser editado',
'role_system_cannot_be_deleted' => 'Esse perfil é um perfil de sistema e não pode ser excluído',
'role_registration_default_cannot_delete' => 'Esse perfil não poderá se excluído enquando estiver registrado como o perfil padrão',
// Error pages
'404_page_not_found' => 'Página não encontrada',
'sorry_page_not_found' => 'Desculpe, a página que você está procurando não pôde ser encontrada.',
'return_home' => 'Retornar à página principal',
'error_occurred' => 'Um erro ocorreu',
'app_down' => ':appName está fora do ar no momento',
'back_soon' => 'Voltaremos em seguida.',
];

View File

@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Anterior',
'next' => 'Próximo &raquo;',
];

View File

@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reminder Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'password' => 'Senhas devem ter ao menos 6 caraceres e combinar com os atributos mínimos para a senha.',
'user' => "Não pudemos encontrar um usuário com o e-mail fornecido.",
'token' => 'O token de reset de senha é inválido.',
'sent' => 'Enviamos para seu e-mail o link de reset de senha!',
'reset' => 'Sua senha foi resetada com sucesso!',
];

View File

@ -0,0 +1,140 @@
<?php
return [
/**
* Settings text strings
* Contains all text strings used in the general settings sections of BookStack
* including users and roles.
*/
'settings' => 'Configurações',
'settings_save' => 'Salvar Configurações',
'settings_save_success' => 'Configurações Salvas',
/**
* App settings
*/
'app_settings' => 'Configurações do App',
'app_name' => 'Nome da Aplicação',
'app_name_desc' => 'Esse nome será mostrado no cabeçalho e em e-mails.',
'app_name_header' => 'Mostrar o nome da Aplicação no cabeçalho?',
'app_public_viewing' => 'Permitir visualização pública?',
'app_secure_images' => 'Permitir upload de imagens com maior segurança?',
'app_secure_images_desc' => 'Por questões de performance, todas as imagens são públicas. Essa opção adiciona uma string randômica na frente da imagem. Certifique-se de que os índices do diretórios permitem o acesso fácil.',
'app_editor' => 'Editor de Página',
'app_editor_desc' => 'Selecione qual editor a ser usado pelos usuários para editar páginas.',
'app_custom_html' => 'Conteúdo para tag HTML HEAD customizado',
'app_custom_html_desc' => 'Quaisquer conteúdos aqui inseridos serão inseridos no final da seção <head> do HTML de cada página. Essa é uma maneira útil de sobrescrever estilos e adicionar códigos de análise de site.',
'app_logo' => 'Logo da Aplicação',
'app_logo_desc' => 'A imagem deve ter 43px de altura. <br>Imagens mais largas devem ser reduzidas.',
'app_primary_color' => 'Cor primária da Aplicação',
'app_primary_color_desc' => 'Esse valor deverá ser Hexadecimal. <br>Deixe em branco para que o Bookstack assuma a cor padrão.',
/**
* Registration settings
*/
'reg_settings' => 'Parâmetros de Registro',
'reg_allow' => 'Permitir Registro?',
'reg_default_role' => 'Perfil padrão para usuários após o registro',
'reg_confirm_email' => 'Requerer confirmação por e-mail?',
'reg_confirm_email_desc' => 'Se restrições de domínio são usadas a confirmação por e-mail será requerida e o valor abaixo será ignorado.',
'reg_confirm_restrict_domain' => 'Restringir registro ao domínio',
'reg_confirm_restrict_domain_desc' => 'Entre com uma lista de domínios de e-mails separados por vírgula para os quais você deseja restringir os registros. Será enviado um e-mail de confirmação para o usuário validar o e-mail antes de ser permitido interação com a aplicação. <br> Note que os usuários serão capazes de alterar o e-mail cadastrado após o sucesso na confirmação do registro.',
'reg_confirm_restrict_domain_placeholder' => 'Nenhuma restrição configurada',
/**
* Role settings
*/
'roles' => 'Perfis',
'role_user_roles' => 'Perfis de Usuário',
'role_create' => 'Criar novo Perfil',
'role_create_success' => 'Perfil criado com sucesso',
'role_delete' => 'Excluir Perfil',
'role_delete_confirm' => 'A ação vai excluír o Perfil de nome \':roleName\'.',
'role_delete_users_assigned' => 'Esse Perfil tem :userCount usuários assinalados a ele. Se quiser migrar usuários desse Perfil para outro, selecione um novo Perfil.',
'role_delete_no_migration' => "Não migre os usuários",
'role_delete_sure' => 'Tem certeza que deseja excluir esse Perfil?',
'role_delete_success' => 'Perfil excluído com sucesso',
'role_edit' => 'Editar Perfil',
'role_details' => 'Detalhes do Perfil',
'role_name' => 'Nome do Perfil',
'role_desc' => 'Descrição Curta do Perfil',
'role_system' => 'Permissões do Sistema',
'role_manage_users' => 'Gerenciar Usuários',
'role_manage_roles' => 'Gerenciar Perfis & Permissões de Perfis',
'role_manage_entity_permissions' => 'Gerenciar todos os livros, capítulos e permissões de páginas',
'role_manage_own_entity_permissions' => 'Gerenciar permissões de seu próprio livro, capítulo e paginas',
'role_manage_settings' => 'Gerenciar configurações de app',
'role_asset' => 'Permissões de Ativos',
'role_asset_desc' => 'Essas permissões controlam o acesso padrão para os ativos dentro do sistema. Permissões em Livros, Capítulos e Páginas serão sobrescritas por essas permissões.',
'role_all' => 'Todos',
'role_own' => 'Próprio',
'role_controlled_by_asset' => 'Controlado pelos ativos que você fez upload',
'role_save' => 'Salvar Perfil',
'role_update_success' => 'Perfil atualizado com sucesso',
'role_users' => 'Usuários neste Perfil',
'role_users_none' => 'Nenhum usuário está atualmente atrelado a esse Perfil',
/**
* Users
*/
'users' => 'Usuários',
'user_profile' => 'Perfil de Usuário',
'users_add_new' => 'Adicionar Novo Usuário',
'users_search' => 'Pesquisar Usuários',
'users_role' => 'Perfis de Usuário',
'users_external_auth_id' => 'ID de Autenticação Externa',
'users_password_warning' => 'Preencha os dados abaixo caso queira modificar a sua senha:',
'users_system_public' => 'Esse usuário representa quaisquer convidados que visitam o aplicativo. Ele não pode ser usado para login.',
'users_delete' => 'Excluir Usuário',
'users_delete_named' => 'Excluir :userName',
'users_delete_warning' => 'A ação vai excluir completamente o usuário de nome \':userName\' do sistema.',
'users_delete_confirm' => 'Tem certeza que deseja excluir esse usuário?',
'users_delete_success' => 'Usuários excluídos com sucesso',
'users_edit' => 'Editar usuário',
'users_edit_profile' => 'Editar Perfil',
'users_edit_success' => 'Usuário atualizado com sucesso',
'users_avatar' => 'Imagem de Usuário',
'users_avatar_desc' => 'Essa imagem deve ser um quadrado com aproximadamente 256px de altura e largura.',
'users_social_accounts' => 'Contas Sociais',
'users_social_accounts_info' => 'Aqui você pode conectar outras contas para acesso mais rápido. Desconectar uma conta não retira a possibilidade de acesso usando-a. Para revogar o acesso ao perfil através da conta social, você deverá fazê-lo na sua conta social.',
'users_social_connect' => 'Contas conectadas',
'users_social_disconnect' => 'Desconectar Conta',
'users_social_connected' => 'Conta :socialAccount foi conectada com sucesso ao seu perfil.',
'users_social_disconnected' => 'Conta :socialAccount foi desconectada com sucesso de seu perfil.',
];

View File

@ -0,0 +1,108 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'O :attribute deve ser aceito.',
'active_url' => 'O :attribute não é uma URL válida.',
'after' => 'O :attribute deve ser uma data posterior à data :date.',
'alpha' => 'O :attribute deve conter apenas letras.',
'alpha_dash' => 'O :attribute deve conter apenas letras, números e traços.',
'alpha_num' => 'O :attribute deve conter apenas letras e números.',
'array' => 'O :attribute deve ser uma array.',
'before' => 'O :attribute deve ser uma data anterior à data :date.',
'between' => [
'numeric' => 'O :attribute deve ter tamanho entre :min e :max.',
'file' => 'O :attribute deve ter entre :min e :max kilobytes.',
'string' => 'O :attribute deve ter entre :min e :max caracteres.',
'array' => 'O :attribute deve ter entre :min e :max itens.',
],
'boolean' => 'O campo :attribute deve ser verdadeiro ou falso.',
'confirmed' => 'O campo :attribute de confirmação não é igual.',
'date' => 'O campo :attribute não está em um formato de data válido.',
'date_format' => 'O campo :attribute não tem a formatação :format.',
'different' => 'O campo :attribute e o campo :other devem ser diferentes.',
'digits' => 'O campo :attribute deve ter :digits dígitos.',
'digits_between' => 'O campo :attribute deve ter entre :min e :max dígitos.',
'email' => 'O campo :attribute deve ser um e-mail válido.',
'filled' => 'O campo :attribute é requerido.',
'exists' => 'O atributo :attribute selecionado não é válido.',
'image' => 'O campo :attribute deve ser uma imagem.',
'in' => 'The selected :attribute is invalid.',
'integer' => 'O campo :attribute deve ser um número inteiro.',
'ip' => 'O campo :attribute deve ser um IP válido.',
'max' => [
'numeric' => 'O valor para o campo :attribute não deve ser maior que :max.',
'file' => 'O valor para o campo :attribute não deve ter tamanho maior que :max kilobytes.',
'string' => 'O valor para o campo :attribute não deve ter mais que :max caracteres.',
'array' => 'O valor para o campo :attribute não deve ter mais que :max itens.',
],
'mimes' => 'O :attribute deve ser do tipo type: :values.',
'min' => [
'numeric' => 'O valor para o campo :attribute não deve ser menor que :min.',
'file' => 'O valor para o campo :attribute não deve ter tamanho menor que :min kilobytes.',
'string' => 'O valor para o campo :attribute não deve ter menos que :min caracteres.',
'array' => 'O valor para o campo :attribute não deve ter menos que :min itens.',
],
'not_in' => 'O campo selecionado :attribute é inválido.',
'numeric' => 'O campo :attribute deve ser um número.',
'regex' => 'O formato do campo :attribute é inválido.',
'required' => 'O campo :attribute é requerido.',
'required_if' => 'O campo :attribute é requerido quando o campo :other tem valor :value.',
'required_with' => 'O campo :attribute é requerido quando os valores :values estiverem presentes.',
'required_with_all' => 'O campo :attribute é requerido quando os valores :values estiverem presentes.',
'required_without' => 'O campo :attribute é requerido quando os valores :values não estiverem presentes.',
'required_without_all' => 'O campo :attribute é requerido quando nenhum dos valores :values estiverem presentes.',
'same' => 'O campo :attribute e o campo :other devem ser iguais.',
'size' => [
'numeric' => 'O tamanho do campo :attribute deve ser :size.',
'file' => 'O tamanho do arquivo :attribute deve ser de :size kilobytes.',
'string' => 'O tamanho do campo :attribute deve ser de :size caracteres.',
'array' => 'O campo :attribute deve conter :size itens.',
],
'string' => 'O campo :attribute deve ser uma string.',
'timezone' => 'O campo :attribute deve conter uma timezone válida.',
'unique' => 'Já existe um campo/dado de nome :attribute.',
'url' => 'O formato da URL :attribute é inválido.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'password-confirm' => [
'required_with' => 'Confirmação de senha requerida',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap attribute place-holders
| with something more reader friendly such as E-Mail Address instead
| of "email". This simply helps us make messages a little cleaner.
|
*/
'attributes' => [],
];

View File

@ -65,4 +65,4 @@
</div>
@stop
@stop

View File

@ -7,6 +7,6 @@
@if (isset($diff) && $diff)
{!! $diff !!}
@else
{!! $page->html !!}
{!! isset($pageContent) ? $pageContent : $page->html !!}
@endif
</div>

View File

@ -53,9 +53,9 @@
<div class="pointer-container" id="pointer">
<div class="pointer anim">
<i class="zmdi zmdi-link"></i>
<input readonly="readonly" type="text" placeholder="url">
<button class="button icon" title="{{ trans('entities.pages_copy_link') }}" data-clipboard-text=""><i class="zmdi zmdi-copy"></i></button>
<span class="icon text-primary"><i class="zmdi zmdi-link"></i></span>
<input readonly="readonly" type="text" id="pointer-url" placeholder="url">
<button class="button icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}"><i class="zmdi zmdi-copy"></i></button>
</div>
</div>

View File

@ -26,7 +26,7 @@
@endforeach
@endif
@if (isset($pageNav) && $pageNav)
@if (isset($pageNav) && count($pageNav))
<h6 class="text-muted">{{ trans('entities.pages_navigation') }}</h6>
<div class="sidebar-page-nav menu">
@foreach($pageNav as $navItem)

View File

@ -18,7 +18,7 @@
<label>@include('settings/roles/checkbox', ['permission' => 'users-manage']) {{ trans('settings.role_manage_users') }}</label>
<label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) {{ trans('settings.role_manage_roles') }}</label>
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-all']) {{ trans('settings.role_manage_entity_permissions') }}</label>
<label>@include('settings/roles/checkbox', ['permission' => 'permissions']) {{ trans('settings.role_manage_own_entity_permissions') }}</label>
<label>@include('settings/roles/checkbox', ['permission' => 'restrictions-manage-own']) {{ trans('settings.role_manage_own_entity_permissions') }}</label>
<label>@include('settings/roles/checkbox', ['permission' => 'settings-manage']) {{ trans('settings.role_manage_settings') }}</label>
</div>

View File

@ -8,7 +8,7 @@
<div class="row">
<div class="col-sm-12 faded">
<div class="breadcrumbs">
<a href="{{ baseUrl('/settings/users') }}" class="text-button"><i class="zmdi zmdi-accounts"></i>Users</a>
<a href="{{ baseUrl('/settings/users') }}" class="text-button"><i class="zmdi zmdi-accounts"></i>{{ trans('settings.users') }}</a>
</div>
</div>
</div>
@ -16,7 +16,7 @@
</div>
<div class="container small" ng-non-bindable>
<h1>Create User</h1>
<h1>{{ trans('settings.users_add_new') }}</h1>
<form action="{{ baseUrl("/settings/users/create") }}" method="post">
{!! csrf_field() !!}

View File

@ -5,8 +5,6 @@
@include('settings/navbar', ['selected' => 'users'])
<div class="container small">
<form action="{{ baseUrl("/settings/users/{$user->id}") }}" method="post">
<div class="row">
@ -42,7 +40,14 @@
'name' => 'image_id',
'imageClass' => 'avatar large'
])
</div>
<div class="form-group">
<label for="user-language">{{ trans('settings.users_preferred_language') }}</label>
<select name="setting[language]" id="user-language">
@foreach(trans('settings.language_select') as $lang => $label)
<option @if(setting()->getUser($user, 'language') === $lang) selected @endif value="{{ $lang }}">{{ $label }}</option>
@endforeach
</select>
</div>
</div>
</div>

View File

@ -1,10 +1,7 @@
<?php
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class ActivityTrackingTest extends TestCase
class ActivityTrackingTest extends BrowserKitTest
{
public function test_recently_viewed_books()

View File

@ -1,6 +1,6 @@
<?php
class AttachmentTest extends TestCase
class AttachmentTest extends BrowserKitTest
{
/**
* Get a test file that can be uploaded

View File

@ -3,7 +3,7 @@
use BookStack\Notifications\ConfirmEmail;
use Illuminate\Support\Facades\Notification;
class AuthTest extends TestCase
class AuthTest extends BrowserKitTest
{
public function test_auth_working()
@ -220,6 +220,9 @@ class AuthTest extends TestCase
public function test_reset_password_flow()
{
Notification::fake();
$this->visit('/login')->click('Forgot Password?')
->seePageIs('/password/email')
->type('admin@admin.com', 'email')
@ -230,8 +233,13 @@ class AuthTest extends TestCase
'email' => 'admin@admin.com'
]);
$user = \BookStack\User::where('email', '=', 'admin@admin.com')->first();
Notification::assertSentTo($user, \BookStack\Notifications\ResetPassword::class);
$n = Notification::sent($user, \BookStack\Notifications\ResetPassword::class);
$reset = DB::table('password_resets')->where('email', '=', 'admin@admin.com')->first();
$this->visit('/password/reset/' . $reset->token)
$this->visit('/password/reset/' . $n->first()->token)
->see('Reset Password')
->submitForm('Reset Password', [
'email' => 'admin@admin.com',

View File

@ -1,9 +1,7 @@
<?php
use BookStack\Services\LdapService;
use BookStack\User;
class LdapTest extends \TestCase
class LdapTest extends BrowserKitTest
{
protected $mockLdap;

View File

@ -1,6 +1,6 @@
<?php
class SocialAuthTest extends TestCase
class SocialAuthTest extends BrowserKitTest
{
public function test_social_registration()

234
tests/BrowserKitTest.php Normal file
View File

@ -0,0 +1,234 @@
<?php
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Symfony\Component\DomCrawler\Crawler;
abstract class BrowserKitTest extends \Laravel\BrowserKitTesting\TestCase
{
use DatabaseTransactions;
/**
* The base URL to use while testing the application.
*
* @var string
*/
protected $baseUrl = 'http://localhost';
// Local user instances
private $admin;
private $editor;
/**
* Creates the application.
*
* @return \Illuminate\Foundation\Application
*/
public function createApplication()
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
return $app;
}
/**
* Set the current user context to be an admin.
* @return $this
*/
public function asAdmin()
{
return $this->actingAs($this->getAdmin());
}
/**
* Get the current admin user.
* @return mixed
*/
public function getAdmin() {
if($this->admin === null) {
$adminRole = \BookStack\Role::getRole('admin');
$this->admin = $adminRole->users->first();
}
return $this->admin;
}
/**
* Set the current editor context to be an editor.
* @return $this
*/
public function asEditor()
{
if ($this->editor === null) {
$this->editor = $this->getEditor();
}
return $this->actingAs($this->editor);
}
/**
* Get a user that's not a system user such as the guest user.
*/
public function getNormalUser()
{
return \BookStack\User::where('system_name', '=', null)->get()->last();
}
/**
* Quickly sets an array of settings.
* @param $settingsArray
*/
protected function setSettings($settingsArray)
{
$settings = app('BookStack\Services\SettingService');
foreach ($settingsArray as $key => $value) {
$settings->put($key, $value);
}
}
/**
* Create a group of entities that belong to a specific user.
* @param $creatorUser
* @param $updaterUser
* @return array
*/
protected function createEntityChainBelongingToUser($creatorUser, $updaterUser = false)
{
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];
$restrictionService->buildJointPermissionsForEntity($book);
return [
'book' => $book,
'chapter' => $chapter,
'page' => $page
];
}
/**
* Quick way to create a new user
* @param array $attributes
* @return mixed
*/
protected function getEditor($attributes = [])
{
$user = factory(\BookStack\User::class)->create($attributes);
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);;
return $user;
}
/**
* Quick way to create a new user without any permissions
* @param array $attributes
* @return mixed
*/
protected function getNewBlankUser($attributes = [])
{
$user = factory(\BookStack\User::class)->create($attributes);
return $user;
}
/**
* Assert that a given string is seen inside an element.
*
* @param bool|string|null $element
* @param integer $position
* @param string $text
* @param bool $negate
* @return $this
*/
protected function seeInNthElement($element, $position, $text, $negate = false)
{
$method = $negate ? 'assertNotRegExp' : 'assertRegExp';
$rawPattern = preg_quote($text, '/');
$escapedPattern = preg_quote(e($text), '/');
$content = $this->crawler->filter($element)->eq($position)->html();
$pattern = $rawPattern == $escapedPattern
? $rawPattern : "({$rawPattern}|{$escapedPattern})";
$this->$method("/$pattern/i", $content);
return $this;
}
/**
* Assert that the current page matches a given URI.
*
* @param string $uri
* @return $this
*/
protected function seePageUrlIs($uri)
{
$this->assertEquals(
$uri, $this->currentUri, "Did not land on expected page [{$uri}].\n"
);
return $this;
}
/**
* Do a forced visit that does not error out on exception.
* @param string $uri
* @param array $parameters
* @param array $cookies
* @param array $files
* @return $this
*/
protected function forceVisit($uri, $parameters = [], $cookies = [], $files = [])
{
$method = 'GET';
$uri = $this->prepareUrlForRequest($uri);
$this->call($method, $uri, $parameters, $cookies, $files);
$this->clearInputs()->followRedirects();
$this->currentUri = $this->app->make('request')->fullUrl();
$this->crawler = new Crawler($this->response->getContent(), $uri);
return $this;
}
/**
* Click the text within the selected element.
* @param $parentElement
* @param $linkText
* @return $this
*/
protected function clickInElement($parentElement, $linkText)
{
$elem = $this->crawler->filter($parentElement);
$link = $elem->selectLink($linkText);
$this->visit($link->link()->getUri());
return $this;
}
/**
* Check if the page contains the given element.
* @param string $selector
* @return bool
*/
protected function pageHasElement($selector)
{
$elements = $this->crawler->filter($selector);
$this->assertTrue(count($elements) > 0, "The page does not contain an element matching " . $selector);
return $this;
}
/**
* Check if the page contains the given element.
* @param string $selector
* @return bool
*/
protected function pageNotHasElement($selector)
{
$elements = $this->crawler->filter($selector);
$this->assertFalse(count($elements) > 0, "The page contains " . count($elements) . " elements matching " . $selector);
return $this;
}
}

View File

@ -1,8 +1,6 @@
<?php
use Illuminate\Support\Facades\DB;
class EntitySearchTest extends TestCase
class EntitySearchTest extends BrowserKitTest
{
public function test_page_search()

View File

@ -1,8 +1,6 @@
<?php
use Illuminate\Support\Facades\DB;
class EntityTest extends TestCase
class EntityTest extends BrowserKitTest
{
public function test_entity_creation()

View File

@ -1,7 +1,6 @@
<?php
class MarkdownTest extends TestCase
class MarkdownTest extends BrowserKitTest
{
protected $page;

View File

@ -0,0 +1,33 @@
<?php
class PageContentTest extends BrowserKitTest
{
public function test_page_includes()
{
$page = \BookStack\Page::first();
$secondPage = \BookStack\Page::all()->get(2);
$secondPage->html = "<p id='section1'>Hello, This is a test</p><p id='section2'>This is a second block of content</p>";
$secondPage->save();
$this->asAdmin()->visit($page->getUrl())
->dontSee('Hello, This is a test');
$originalHtml = $page->html;
$page->html .= "{{@{$secondPage->id}}}";
$page->save();
$this->asAdmin()->visit($page->getUrl())
->see('Hello, This is a test')
->see('This is a second block of content');
$page->html = $originalHtml . " Well {{@{$secondPage->id}#section2}}";
$page->save();
$this->asAdmin()->visit($page->getUrl())
->dontSee('Hello, This is a test')
->see('Well This is a second block of content');
}
}

View File

@ -1,7 +1,7 @@
<?php
class PageDraftTest extends TestCase
class PageDraftTest extends BrowserKitTest
{
protected $page;
protected $entityRepo;

View File

@ -1,6 +1,6 @@
<?php
class SortTest extends TestCase
class SortTest extends BrowserKitTest
{
protected $book;

View File

@ -1,10 +1,10 @@
<?php namespace Entity;
<?php
use BookStack\Tag;
use BookStack\Page;
use BookStack\Services\PermissionService;
class TagTests extends \TestCase
class TagTest extends BrowserKitTest
{
protected $defaultTagCount = 20;
@ -86,61 +86,16 @@ class TagTests extends \TestCase
$attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color']));
$page = $this->getPageWithTags($attrs);
$this->asAdmin()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
$this->asEditor()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
$this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']);
$this->asEditor()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']);
// Set restricted permission the page
$page->restricted = true;
$page->save();
$permissionService->buildJointPermissionsForEntity($page);
$this->asAdmin()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
$this->asEditor()->get('/ajax/tags/suggest?search=co')->seeJsonEquals([]);
}
public function test_entity_tag_updating()
{
$page = $this->getPageWithTags();
$testJsonData = [
['name' => 'color', 'value' => 'red'],
['name' => 'color', 'value' => ' blue '],
['name' => 'city', 'value' => 'London '],
['name' => 'country', 'value' => ' England'],
];
$testResponseJsonData = [
['name' => 'color', 'value' => 'red'],
['name' => 'color', 'value' => 'blue'],
['name' => 'city', 'value' => 'London'],
['name' => 'country', 'value' => 'England'],
];
// Do update request
$this->asAdmin()->json("POST", "/ajax/tags/update/page/" . $page->id, ['tags' => $testJsonData]);
$updateData = json_decode($this->response->getContent());
// Check data is correct
$testDataCorrect = true;
foreach ($updateData->tags as $data) {
$testItem = ['name' => $data->name, 'value' => $data->value];
if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false;
}
$testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s";
$this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($updateData)));
$this->assertTrue(isset($updateData->message), "No message returned in tag update response");
// Do get request
$this->asAdmin()->get("/ajax/tags/get/page/" . $page->id);
$getResponseData = json_decode($this->response->getContent());
// Check counts
$this->assertTrue(count($getResponseData) === count($testJsonData), "The received tag count is incorrect");
// Check data is correct
$testDataCorrect = true;
foreach ($getResponseData as $data) {
$testItem = ['name' => $data->name, 'value' => $data->value];
if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false;
}
$testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s";
$this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($getResponseData)));
$this->asAdmin()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country']);
$this->asEditor()->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals([]);
}
}

View File

@ -1,6 +1,6 @@
<?php
class ImageTest extends TestCase
class ImageTest extends BrowserKitTest
{
/**

View File

@ -1,6 +1,6 @@
<?php
class RestrictionsTest extends TestCase
class RestrictionsTest extends BrowserKitTest
{
protected $user;
protected $viewer;

View File

@ -1,6 +1,6 @@
<?php
class RolesTest extends TestCase
class RolesTest extends BrowserKitTest
{
protected $user;
@ -578,4 +578,44 @@ class RolesTest extends TestCase
->see('Cannot be deleted');
}
public function test_image_delete_own_permission()
{
$this->giveUserPermissions($this->user, ['image-update-all']);
$page = \BookStack\Page::first();
$image = factory(\BookStack\Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $this->user->id, 'updated_by' => $this->user->id]);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)
->seeStatusCode(403);
$this->giveUserPermissions($this->user, ['image-delete-own']);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)
->seeStatusCode(200)
->dontSeeInDatabase('images', ['id' => $image->id]);
}
public function test_image_delete_all_permission()
{
$this->giveUserPermissions($this->user, ['image-update-all']);
$admin = $this->getAdmin();
$page = \BookStack\Page::first();
$image = factory(\BookStack\Image::class)->create(['uploaded_to' => $page->id, 'created_by' => $admin->id, 'updated_by' => $admin->id]);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)
->seeStatusCode(403);
$this->giveUserPermissions($this->user, ['image-delete-own']);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)
->seeStatusCode(403);
$this->giveUserPermissions($this->user, ['image-delete-all']);
$this->actingAs($this->user)->json('delete', '/images/' . $image->id)
->seeStatusCode(200)
->dontSeeInDatabase('images', ['id' => $image->id]);
}
}

View File

@ -1,6 +1,6 @@
<?php
class PublicActionTest extends TestCase
class PublicActionTest extends BrowserKitTest
{
public function test_app_not_public()

View File

@ -9,15 +9,11 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
use DatabaseTransactions;
/**
* The base URL to use while testing the application.
* The base URL of the application.
*
* @var string
*/
protected $baseUrl = 'http://localhost';
// Local user instances
private $admin;
private $editor;
public $baseUrl = 'http://localhost';
/**
* Creates the application.
@ -28,207 +24,8 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
{
$app = require __DIR__.'/../bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$app->make(Kernel::class)->bootstrap();
return $app;
}
/**
* Set the current user context to be an admin.
* @return $this
*/
public function asAdmin()
{
return $this->actingAs($this->getAdmin());
}
/**
* Get the current admin user.
* @return mixed
*/
public function getAdmin() {
if($this->admin === null) {
$adminRole = \BookStack\Role::getRole('admin');
$this->admin = $adminRole->users->first();
}
return $this->admin;
}
/**
* Set the current editor context to be an editor.
* @return $this
*/
public function asEditor()
{
if($this->editor === null) {
$this->editor = $this->getEditor();
}
return $this->actingAs($this->editor);
}
/**
* Get a user that's not a system user such as the guest user.
*/
public function getNormalUser()
{
return \BookStack\User::where('system_name', '=', null)->get()->last();
}
/**
* Quickly sets an array of settings.
* @param $settingsArray
*/
protected function setSettings($settingsArray)
{
$settings = app('BookStack\Services\SettingService');
foreach ($settingsArray as $key => $value) {
$settings->put($key, $value);
}
}
/**
* Create a group of entities that belong to a specific user.
* @param $creatorUser
* @param $updaterUser
* @return array
*/
protected function createEntityChainBelongingToUser($creatorUser, $updaterUser = false)
{
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];
$restrictionService->buildJointPermissionsForEntity($book);
return [
'book' => $book,
'chapter' => $chapter,
'page' => $page
];
}
/**
* Quick way to create a new user
* @param array $attributes
* @return mixed
*/
protected function getEditor($attributes = [])
{
$user = factory(\BookStack\User::class)->create($attributes);
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);;
return $user;
}
/**
* Quick way to create a new user without any permissions
* @param array $attributes
* @return mixed
*/
protected function getNewBlankUser($attributes = [])
{
$user = factory(\BookStack\User::class)->create($attributes);
return $user;
}
/**
* Assert that a given string is seen inside an element.
*
* @param bool|string|null $element
* @param integer $position
* @param string $text
* @param bool $negate
* @return $this
*/
protected function seeInNthElement($element, $position, $text, $negate = false)
{
$method = $negate ? 'assertNotRegExp' : 'assertRegExp';
$rawPattern = preg_quote($text, '/');
$escapedPattern = preg_quote(e($text), '/');
$content = $this->crawler->filter($element)->eq($position)->html();
$pattern = $rawPattern == $escapedPattern
? $rawPattern : "({$rawPattern}|{$escapedPattern})";
$this->$method("/$pattern/i", $content);
return $this;
}
/**
* Assert that the current page matches a given URI.
*
* @param string $uri
* @return $this
*/
protected function seePageUrlIs($uri)
{
$this->assertEquals(
$uri, $this->currentUri, "Did not land on expected page [{$uri}].\n"
);
return $this;
}
/**
* Do a forced visit that does not error out on exception.
* @param string $uri
* @param array $parameters
* @param array $cookies
* @param array $files
* @return $this
*/
protected function forceVisit($uri, $parameters = [], $cookies = [], $files = [])
{
$method = 'GET';
$uri = $this->prepareUrlForRequest($uri);
$this->call($method, $uri, $parameters, $cookies, $files);
$this->clearInputs()->followRedirects();
$this->currentUri = $this->app->make('request')->fullUrl();
$this->crawler = new Crawler($this->response->getContent(), $uri);
return $this;
}
/**
* Click the text within the selected element.
* @param $parentElement
* @param $linkText
* @return $this
*/
protected function clickInElement($parentElement, $linkText)
{
$elem = $this->crawler->filter($parentElement);
$link = $elem->selectLink($linkText);
$this->visit($link->link()->getUri());
return $this;
}
/**
* Check if the page contains the given element.
* @param string $selector
* @return bool
*/
protected function pageHasElement($selector)
{
$elements = $this->crawler->filter($selector);
$this->assertTrue(count($elements) > 0, "The page does not contain an element matching " . $selector);
return $this;
}
/**
* Check if the page contains the given element.
* @param string $selector
* @return bool
*/
protected function pageNotHasElement($selector)
{
$elements = $this->crawler->filter($selector);
$this->assertFalse(count($elements) > 0, "The page contains " . count($elements) . " elements matching " . $selector);
return $this;
}
}

View File

@ -1,6 +1,6 @@
<?php
class UserProfileTest extends TestCase
class UserProfileTest extends BrowserKitTest
{
protected $user;

View File

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