Compare commits

...

100 Commits

Author SHA1 Message Date
Dan Brown
1f4260f359 Updated version for release v0.16.2 2017-05-07 19:35:51 +01:00
Dan Brown
dc0bf8ad4e Merge branch 'master' into release 2017-05-07 19:35:34 +01:00
Dan Brown
75981c2412 Fixed chapter check for non-mysqlnd instances
Fixes #383
2017-05-07 19:34:56 +01:00
Dan Brown
102e326e6a Updated JS and version for release v0.16.1 2017-04-30 19:51:23 +01:00
Dan Brown
2b25bf6f3b Merge branch 'master' into release 2017-04-30 19:50:29 +01:00
Dan Brown
f8ae4c335e Made single entity updates more efficent 2017-04-30 19:44:59 +01:00
Dan Brown
5570e858e5 Made more efficiency improvements to permission system 2017-04-30 11:38:58 +01:00
Dan Brown
1859a4d356 Refactored permission system components
Split joint permission creation into chunks

Fixes #374
2017-04-29 22:01:43 +01:00
Dan Brown
ad4642c2c4 Enabled translation when not logged in
Reads from the Accept-Language HTTP header.
Also fixed some encoding for ES translations.

Fixes #375
2017-04-29 16:47:41 +01:00
Dan Brown
92108d710d Re-enabled html in markdown editor
Fixes #378
2017-04-29 16:10:38 +01:00
Dan Brown
f93280696d Updated assets for release v0.16 2017-04-23 20:42:28 +01:00
Dan Brown
1787391b07 Merge branch 'master' into release 2017-04-23 20:41:45 +01:00
Dan Brown
44347ee353 Fixed search system id clash 2017-04-23 20:27:49 +01:00
Dan Brown
9e704fcae4 Updated testing database connection issue 2017-04-23 17:51:01 +01:00
Dan Brown
fdd816b17d Merge pull request #362 from DaneEveritt/patch-1
Queue confirmation emails.
2017-04-23 17:15:06 +01:00
Dan Brown
82e2c523e6 Fixed chapter breadcrumbs and testing issues 2017-04-22 14:08:12 +01:00
Dan Brown
a323b0d49c Allowed child entity permissions to override parent permissions
Updated elements of a page display and sidebar render to allow
child permissions to work even when parent entitites have permission
set. This allows a page with a 'view' permission to be viewable even
when the parent book or chapter is not viewable.

Fixes #366
2017-04-22 13:39:34 +01:00
Dan Brown
4c985aac7e Added page revision counting
Adds stored revision counts to pages and the revisions themselves.
Closes #321
2017-04-20 20:58:54 +01:00
Dan Brown
87e18b8068 Merge pull request #357 from diegoseso/master
Spanish translation completed
2017-04-19 06:40:00 +01:00
Diego Jose Sosa Diaz
607a2c91fc Fixing encoding of files affecting accents. 2017-04-18 23:41:19 +02:00
Dan Brown
fde970ba59 Switched out markdown render
Fixes #304.
Fixes #359.
2017-04-17 12:21:10 +01:00
Dan Brown
ec7be1b08b Merge pull request #363 from solidnerd/add-env-for-logging
Add APP_LOGGING
2017-04-17 11:46:32 +01:00
solidnerd
746a760a23 Add APP_LOGGING
This will add an variable for logging types to make it easier to define outside via .env.

Signed-off-by: solidnerd <niclas@mietz.io>
2017-04-17 09:55:11 +02:00
Dan Brown
1a09d88891 Added fade effect to page content highlighting
Closes #314
2017-04-16 16:46:55 +01:00
Dan Brown
46c01ecba2 Merge pull request #358 from jendrol/master
Add Slovak translation
2017-04-16 15:06:32 +01:00
Dan Brown
544ece03a5 Merge pull request #360 from Abijeet/spellcheck-fix
Fixes #354, Adds the spellchecker option
2017-04-16 15:02:20 +01:00
Dan Brown
5fee7c4db1 Merge pull request #340 from BookStackApp/search_system
Implementation of new search system
2017-04-16 11:01:00 +01:00
Dan Brown
8ed9f75d57 Fixed model extending mis-use 2017-04-16 10:54:23 +01:00
Dan Brown
a15b179676 Updated testcases for new search system.
Finishes implementation of new search system.
Closes #271
Closes #344
Fixes #285
Fixes #269
Closes #64
2017-04-16 10:47:44 +01:00
Dan Brown
73844b9eeb Enabled type search filter in book search 2017-04-15 19:31:11 +01:00
Dan Brown
dcde599709 Added chapter search
Migrated book search to vue-based system.
Updated old tag seached.
Made chapter page layout widths same as book page.
Closes #344
2017-04-15 19:16:07 +01:00
Dan Brown
0e0945ef84 Finished off UI for search system 2017-04-15 15:04:30 +01:00
Dan Brown
ad125327c0 Migrated to custom gulp setup and conintue search interface 2017-04-14 18:47:33 +01:00
Dane Everitt
dfaf20dd83 Actually include the Queueable namespace... 2017-04-13 20:09:38 -04:00
Dane Everitt
786262db3b Queue confirmation emails.
Implements Laravel's queue abilities into the email notification job. Should not affect existing installations that are not using queues as the environment file defaults to `sync`.
2017-04-13 20:03:03 -04:00
Abijeet
29a4110d8f Fixes #354, Adds the spellchecker option
Uses the browser_spellchecker option documented here - https://www.tinymce.com/docs/configure/spelling/#browser_spellcheck
2017-04-13 23:57:57 +05:30
Vlado Jendroľ
9b639f715f Add Slovak translation 2017-04-11 23:20:52 +02:00
Dan Brown
46f3d78c8a Fixed entity type filter bug in new search system 2017-04-09 21:12:13 +01:00
Dan Brown
1338ae2fc3 Started search interface, Added in vue and moved fonts 2017-04-09 20:59:57 +01:00
Dan Brown
37813a223a Improved DB prefix support and removed old search method 2017-04-09 14:44:56 +01:00
Diego Jose Sosa Diaz
b488e969bb Reaching 100 % Spanish translation 2017-04-06 23:12:05 +02:00
Diego Jose Sosa Diaz
1377296ef4 Translating to spanish entities errors and settings 2017-04-06 09:35:24 +02:00
Dan Brown
01cb22af37 Added tag searches and advanced filters to new search 2017-03-27 18:05:34 +01:00
Dan Brown
331305333d Added search term parsing and exact term matches 2017-03-27 11:57:33 +01:00
Dan Brown
0651eae7ec Improve efficiency of single entity search indexing 2017-03-26 19:34:53 +01:00
Dan Brown
1552417598 Developed basic search queries.
Updated search & permission regen commands with ability to specify
database.
2017-03-26 19:24:57 +01:00
Dan Brown
a74a8ee483 Updated version for v0.15.3 2017-03-23 22:22:16 +00:00
Dan Brown
7fa5405cb7 Merge branch 'master' into release 2017-03-23 22:21:04 +00:00
Dan Brown
cc0ce7c630 Fixed bug preventing page revision restore
Added regression tests to cover.
Fixes #341
2017-03-23 22:19:14 +00:00
Dan Brown
070d4aeb6c Started implementation of new search system 2017-03-19 12:48:44 +00:00
Dan Brown
668ce26269 Fixed back button behaviour on books edit
As reported in #339
2017-03-19 08:32:04 +00:00
Dan Brown
6725ddcc41 Updated version for release v0.15.2 2017-03-05 15:50:52 +00:00
Dan Brown
bce941db3f Merge branch 'master' into release 2017-03-05 15:49:47 +00:00
Dan Brown
4499ae84bb Made fixes to es languge files and users page
Fixed PHP formatting error in ES lang file and added tests to cover.
Made user edit page more responsive on smaller devices.
Fixed 'cancel' button on profile screen when the user does not have
permission to manage users.
2017-03-05 15:34:54 +00:00
Dan Brown
d4e790d3cf Added lang tests and update export text keys 2017-03-05 15:10:06 +00:00
Dan Brown
9b35aa42a2 Fixed spanish encoding, Added new lang to settings 2017-03-05 14:43:43 +00:00
Dan Brown
7163997367 Merge pull request #334 from diegoseso/master
First spanish translation effort
2017-03-05 14:18:55 +00:00
Dan Brown
2385a6c29b Merge pull request #325 from arietimmerman/dutchbranch
Dutch Language Files
2017-03-05 14:15:47 +00:00
Dan Brown
36173eb47d Removed extension from translation script link
Also fixed bug causing EN translation backup to not be passed
to javascript translation system.

Closes #328
2017-03-05 14:10:55 +00:00
Diego Jose Sosa Diaz
f7645824d9 First spanish translation effort 2017-03-03 00:21:33 +01:00
Dan Brown
6d926048ec Updated to version v0.15.1 2017-02-27 16:59:10 +00:00
Dan Brown
5335c973b4 Merge branch 'master' into release 2017-02-27 16:58:20 +00:00
Dan Brown
bcafa73faf Set composer to clean bootstrap/cache before an update 2017-02-27 16:55:40 +00:00
Dan Brown
15c3e5c96e Updated assets for release v0.15 2017-02-27 14:58:02 +00:00
Dan Brown
a5d5904969 Merge branch 'master' into release 2017-02-27 14:57:38 +00:00
Dan Brown
e3eefba745 Fixed export testing and updated travis settings 2017-02-26 21:39:15 +00:00
Dan Brown
a90f564980 Made LDAP email attribute configurable via .env
Closes #306
2017-02-26 14:51:49 +00:00
Dan Brown
253132afdf Added chapter export options
Closes #177
2017-02-26 14:25:02 +00:00
Dan Brown
eded8abded Added book export and created export tests to cover
In reference to #177
2017-02-26 13:26:51 +00:00
Dan Brown
0abed1afe5 Added clear activity/revision commands. Cleaned commands.
Added testing to cover each command.
Removed example laravel inspire command.
Standardised command names to be behind 'bookstack' naming.
In reference to #320.
2017-02-26 09:16:24 +00:00
Dan Brown
22077d4181 Updated DOMPDF to latest version 2017-02-25 14:59:56 +00:00
Dan Brown
b0e849f413 Added checkbox sytax parsing to markdown lists
Closes #319
2017-02-25 13:16:26 +00:00
Dan Brown
af3c0e43a5 Prevented custom HTML being inserted on settings page
Gives option for fixing if badly formatted HTML is inserted.
Closes #310
2017-02-25 12:41:32 +00:00
Dan Brown
387047f262 Fixed inaccessible revisions, added regression tests
Fixes #309
2017-02-25 12:29:01 +00:00
Dan Brown
4a2a539c08 Merge pull request #295 from ReeseSebastian/master
Updated and improved german translation
2017-02-23 19:19:21 +00:00
Arie Timmerman
4214fcd2fa Updated Dutch language files 2017-02-11 11:58:45 +01:00
Arie Timmerman
b2b64fb853 Started with Dutch translation 2017-02-10 22:10:41 +01:00
Dan Brown
a6128a1df1 Merge bugfixes from branch 'v0.14' 2017-02-05 21:24:15 +00:00
Dan Brown
598758b991 Updated version for v0.14.3 2017-02-05 21:23:27 +00:00
Dan Brown
9926e23bc8 Merge branch 'v0.14' into release 2017-02-05 21:21:54 +00:00
Dan Brown
6638ee47d3 Fixed entities wrongly visible on 404
Also ensured header state as expected on 404.
In reference to BookStackApp/website#9
2017-02-05 21:19:29 +00:00
Dan Brown
65899a3e91 Prevented settings being overfetched from db/cache 2017-02-05 18:57:57 +00:00
Dan Brown
86625a7642 Neatened up social login/register buttons 2017-02-05 15:28:53 +00:00
Dan Brown
ee495450cc Improved multi-line callout rendering
Closes #300
2017-02-05 14:47:26 +00:00
Dan Brown
d369d315a7 Fixed non-browserkit testcase and seeder issues 2017-02-05 14:37:50 +00:00
Dan Brown
7c9937e924 Converted sort tests to non browserkit testing
Added testing to cover book sort endpoint.
Closes #283
2017-02-05 14:20:59 +00:00
Dan Brown
33a2999a57 Namespaced tests to align with new laravel default 2017-02-04 11:58:42 +00:00
Dan Brown
076693efc9 Added facebook, slack & twitter sign in options.
Also added icon svg blade helper.
Closes #125. Starts #213.
Requires documentation.
2017-02-04 11:01:49 +00:00
Sebastian Reese
54d1fcde5b Merge branch 'master' of https://github.com/BookStackApp/BookStack 2017-02-02 00:43:54 +01:00
Sebastian Reese
1c656d6556 Updated and improved german translation 2017-02-02 00:43:24 +01:00
Dan Brown
2431ce9f86 Merge branch 'v0.14' 2017-02-01 22:28:38 +00:00
Dan Brown
5d3264bc63 Updated assets for release v0.14.2 2017-02-01 22:27:04 +00:00
Dan Brown
d71f819f95 Merge branch 'v0.14' into release 2017-02-01 22:22:38 +00:00
Dan Brown
80f844139c Fixed missing subscript styling
Closes #284
2017-02-01 22:20:44 +00:00
Dan Brown
9eecaea31a Attempt to fix bookchildren and user getThumb
Hopefully Fixes #292 and #294 and #287
2017-02-01 22:16:32 +00:00
Dan Brown
3ccfa0e7fc Fixed readme badge links & added contributing block 2017-01-30 19:31:24 +00:00
Dan Brown
6669998c10 Upgraded to Laravel 5.4 2017-01-25 19:35:40 +00:00
Dan Brown
ee13509760 Updated version number 2017-01-23 22:28:31 +00:00
Dan Brown
82d7bb1f32 Merge branch 'master' into release 2017-01-23 22:28:02 +00:00
Dan Brown
492e2f173e Fixed error causing permissions to be deleted on book sort
Closes #282
2017-01-23 22:27:11 +00:00
172 changed files with 7122 additions and 2329 deletions

9
.gitignore vendored
View File

@@ -13,4 +13,11 @@ _ide_helper.php
/storage/debugbar
.phpstorm.meta.php
yarn.lock
/bin
/bin
.buildpath
.project
.settings/org.eclipse.wst.common.project.facet.core.xml
.settings/org.eclipse.php.core.prefs

View File

@@ -1,5 +1,5 @@
dist: trusty
sudo: required
sudo: false
language: php
php:
- 7.0
@@ -8,15 +8,11 @@ cache:
directories:
- $HOME/.composer/cache
addons:
apt:
packages:
- mysql-server-5.6
- mysql-client-core-5.6
- mysql-client-5.6
before_script:
- mysql -u root -e 'create database `bookstack-test`;'
- mysql -u root -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
- mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
- mysql -u root -e "FLUSH PRIVILEGES;"
- phpenv config-rm xdebug.ini
- composer dump-autoload --no-interaction
- composer install --prefer-dist --no-interaction
@@ -25,5 +21,8 @@ before_script:
- php artisan migrate --force -n --database=mysql_testing
- php artisan db:seed --force -n --class=DummyContentSeeder --database=mysql_testing
after_failure:
- cat storage/logs/laravel.log
script:
- phpunit

View File

@@ -56,4 +56,13 @@ class Book extends Entity
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
return "'BookStack\\\\Book' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@@ -51,4 +51,13 @@ class Chapter extends Entity
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery()
{
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Activity;
use Illuminate\Console\Command;
class ClearActivity extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:clear-activity';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear user activity from the system';
protected $activity;
/**
* Create a new command instance.
*
* @param Activity $activity
*/
public function __construct(Activity $activity)
{
$this->activity = $activity;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->activity->newQuery()->truncate();
$this->comment('System activity cleared');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\PageRevision;
use Illuminate\Console\Command;
class ClearRevisions extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:clear-revisions
{--a|all : Include active update drafts in deletion}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Clear page revisions';
protected $pageRevision;
/**
* Create a new command instance.
*
* @param PageRevision $pageRevision
*/
public function __construct(PageRevision $pageRevision)
{
$this->pageRevision = $pageRevision;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$deleteTypes = $this->option('all') ? ['version', 'update_draft'] : ['version'];
$this->pageRevision->newQuery()->whereIn('type', $deleteTypes)->delete();
$this->comment('Revisions deleted');
}
}

View File

@@ -4,21 +4,21 @@ namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
class ResetViews extends Command
class ClearViews extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'views:reset';
protected $signature = 'bookstack:clear-views';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Reset all view-counts for all entities.';
protected $description = 'Clear all view-counts for all entities.';
/**
* Create a new command instance.
@@ -37,5 +37,6 @@ class ResetViews extends Command
public function handle()
{
\Views::resetAll();
$this->comment('Views cleared');
}
}

View File

@@ -1,33 +0,0 @@
<?php
namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Foundation\Inspiring;
class Inspire extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'inspire';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Display an inspiring quote';
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->comment(PHP_EOL.Inspiring::quote().PHP_EOL);
}
}

View File

@@ -12,7 +12,7 @@ class RegeneratePermissions extends Command
*
* @var string
*/
protected $signature = 'permissions:regen';
protected $signature = 'bookstack:regenerate-permissions {--database= : The database connection to use.}';
/**
* The console command description.
@@ -46,6 +46,15 @@ class RegeneratePermissions extends Command
*/
public function handle()
{
$connection = \DB::getDefaultConnection();
if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database'));
$this->permissionService->setConnection(\DB::connection($this->option('database')));
}
$this->permissionService->buildJointPermissions();
\DB::setDefaultConnection($connection);
$this->comment('Permissions regenerated');
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Services\SearchService;
use Illuminate\Console\Command;
class RegenerateSearch extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-search {--database= : The database connection to use.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
protected $searchService;
/**
* Create a new command instance.
*
* @param SearchService $searchService
*/
public function __construct(SearchService $searchService)
{
parent::__construct();
$this->searchService = $searchService;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$connection = \DB::getDefaultConnection();
if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database'));
$this->searchService->setConnection(\DB::connection($this->option('database')));
}
$this->searchService->indexAllEntities();
\DB::setDefaultConnection($connection);
$this->comment('Search index regenerated');
}
}

View File

@@ -1,6 +1,4 @@
<?php
namespace BookStack\Console;
<?php namespace BookStack\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
@@ -13,9 +11,11 @@ class Kernel extends ConsoleKernel
* @var array
*/
protected $commands = [
\BookStack\Console\Commands\Inspire::class,
\BookStack\Console\Commands\ResetViews::class,
\BookStack\Console\Commands\RegeneratePermissions::class,
Commands\ClearViews::class,
Commands\ClearActivity::class,
Commands\ClearRevisions::class,
Commands\RegeneratePermissions::class,
Commands\RegenerateSearch::class
];
/**
@@ -26,7 +26,6 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('inspire')
->hourly();
//
}
}

View File

@@ -4,7 +4,7 @@
class Entity extends Ownable
{
protected $fieldsToSearch = ['name', 'description'];
public $textField = 'description';
/**
* Compares this entity to another given entity.
@@ -65,6 +65,15 @@ class Entity extends Ownable
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
}
/**
* Get the related search terms.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function searchTerms()
{
return $this->morphMany(SearchTerm::class, 'entity');
}
/**
* Get this entities restrictions.
*/
@@ -85,17 +94,6 @@ class Entity extends Ownable
->where('action', '=', $action)->count() > 0;
}
/**
* Check if this entity has live (active) restrictions in place.
* @param $role_id
* @param $action
* @return bool
*/
public function hasActiveRestriction($role_id, $action)
{
return $this->getRawAttribute('restricted') && $this->hasRestriction($role_id, $action);
}
/**
* Get the entity jointPermissions this is connected to.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
@@ -153,67 +151,25 @@ class Entity extends Ownable
}
/**
* Perform a full-text search on this entity.
* @param string[] $fieldsToSearch
* @param string[] $terms
* @param string[] array $wheres
* Get the body text of this entity.
* @return mixed
*/
public function fullTextSearchQuery($terms, $wheres = [])
public function getText()
{
$exactTerms = [];
$fuzzyTerms = [];
$search = static::newQuery();
foreach ($terms as $key => $term) {
$term = htmlentities($term, ENT_QUOTES);
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
if (preg_match('/&quot;.*?&quot;/', $term) || is_numeric($term)) {
$term = str_replace('&quot;', '', $term);
$exactTerms[] = '%' . $term . '%';
} else {
$term = '' . $term . '*';
if ($term !== '*') $fuzzyTerms[] = $term;
}
}
$isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
// Perform fulltext search if relevant terms exist.
if ($isFuzzy) {
$termString = implode(' ', $fuzzyTerms);
$fields = implode(',', $this->fieldsToSearch);
$search = $search->selectRaw('*, MATCH(name) AGAINST(? IN BOOLEAN MODE) AS title_relevance', [$termString]);
$search = $search->whereRaw('MATCH(' . $fields . ') AGAINST(? IN BOOLEAN MODE)', [$termString]);
}
// Ensure at least one exact term matches if in search
if (count($exactTerms) > 0) {
$search = $search->where(function ($query) use ($exactTerms) {
foreach ($exactTerms as $exactTerm) {
foreach ($this->fieldsToSearch as $field) {
$query->orWhere($field, 'like', $exactTerm);
}
}
});
}
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
// Add additional where terms
foreach ($wheres as $whereTerm) {
$search->where($whereTerm[0], $whereTerm[1], $whereTerm[2]);
}
// Load in relations
if ($this->isA('page')) {
$search = $search->with('book', 'chapter', 'createdBy', 'updatedBy');
} else if ($this->isA('chapter')) {
$search = $search->with('book');
}
return $search->orderBy($orderBy, 'desc');
return $this->{$this->textField};
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string
*/
public function entityRawQuery(){return '';}
/**
* Get the url of this entity
* @param $path
* @return string
*/
public function getUrl($path){return '/';}
}

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

@@ -1,8 +1,10 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Book;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@@ -12,16 +14,19 @@ class BookController extends Controller
protected $entityRepo;
protected $userRepo;
protected $exportService;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
@@ -203,13 +208,12 @@ class BookController extends Controller
// Add activity for books
foreach ($sortedBooks as $bookId) {
/** @var Book $updatedBook */
$updatedBook = $this->entityRepo->getById('book', $bookId);
$this->entityRepo->buildJointPermissionsForBook($updatedBook);
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
}
// Update permissions on changed models
$this->entityRepo->buildJointPermissions($updatedModels);
return redirect($book->getUrl());
}
@@ -258,4 +262,49 @@ class BookController extends Controller
session()->flash('success', trans('entities.books_permissions_updated'));
return redirect($book->getUrl());
}
/**
* Export a book as a PDF file.
* @param string $bookSlug
* @return mixed
*/
public function exportPdf($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$pdfContent = $this->exportService->bookToPdf($book);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
]);
}
/**
* Export a book as a contained HTML file.
* @param string $bookSlug
* @return mixed
*/
public function exportHtml($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$htmlContent = $this->exportService->bookToContainedHtml($book);
return response()->make($htmlContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
]);
}
/**
* Export a book as a plain text file.
* @param $bookSlug
* @return mixed
*/
public function exportPlainText($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$htmlContent = $this->exportService->bookToPlainText($book);
return response()->make($htmlContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
]);
}
}

View File

@@ -3,6 +3,7 @@
use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@@ -12,16 +13,19 @@ class ChapterController extends Controller
protected $userRepo;
protected $entityRepo;
protected $exportService;
/**
* ChapterController constructor.
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param ExportService $exportService
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
parent::__construct();
}
@@ -236,4 +240,52 @@ class ChapterController extends Controller
session()->flash('success', trans('entities.chapters_permissions_success'));
return redirect($chapter->getUrl());
}
/**
* Exports a chapter to pdf .
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportPdf($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$pdfContent = $this->exportService->chapterToPdf($chapter);
return response()->make($pdfContent, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
]);
}
/**
* Export a chapter to a self-contained HTML file.
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportHtml($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
]);
}
/**
* Export a chapter to a simple plaintext .txt file.
* @param string $bookSlug
* @param string $chapterSlug
* @return \Illuminate\Http\Response
*/
public function exportPlainText($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$containedHtml = $this->exportService->chapterToPlainText($chapter);
return response()->make($containedHtml, 200, [
'Content-Type' => 'application/octet-stream',
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
]);
}
}

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

@@ -1,10 +1,7 @@
<?php
namespace BookStack\Http\Controllers;
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Repos\EntityRepo;
use BookStack\Http\Requests;
use Illuminate\Http\Response;
use Views;
@@ -49,7 +46,7 @@ class HomeController extends Controller
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function getTranslations() {
$locale = trans()->getLocale();
$locale = app()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
$resp = cache($cacheKey);
@@ -63,10 +60,10 @@ class HomeController extends Controller
];
if ($locale !== 'en') {
$enTrans = [
'common' => trans('common', [], null, 'en'),
'components' => trans('components', [], null, 'en'),
'entities' => trans('entities', [], null, 'en'),
'errors' => trans('errors', [], null, 'en')
'common' => trans('common', [], 'en'),
'components' => trans('components', [], 'en'),
'entities' => trans('entities', [], 'en'),
'errors' => trans('errors', [], 'en')
];
$translations = array_replace_recursive($enTrans, $translations);
}

View File

@@ -369,10 +369,13 @@ class PageController extends Controller
public function showRevision($bookSlug, $pageSlug, $revisionId)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$revision = $this->entityRepo->getById('page_revision', $revisionId, false);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
abort(404);
}
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [
'page' => $page,
@@ -390,7 +393,10 @@ class PageController extends Controller
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
{
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
$revision = $this->entityRepo->getById('page_revision', $revisionId);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
if ($revision === null) {
abort(404);
}
$prev = $revision->getPrevious();
$prevContent = ($prev === null) ? '' : $prev->html;
@@ -423,7 +429,7 @@ class PageController extends Controller
}
/**
* Exports a page to pdf format using barryvdh/laravel-dompdf wrapper.
* Exports a page to a PDF.
* https://github.com/barryvdh/laravel-dompdf
* @param string $bookSlug
* @param string $pageSlug
@@ -433,7 +439,6 @@ 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,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Repos\EntityRepo;
use BookStack\Services\SearchService;
use BookStack\Services\ViewService;
use Illuminate\Http\Request;
@@ -8,16 +9,19 @@ class SearchController extends Controller
{
protected $entityRepo;
protected $viewService;
protected $searchService;
/**
* SearchController constructor.
* @param EntityRepo $entityRepo
* @param ViewService $viewService
* @param SearchService $searchService
*/
public function __construct(EntityRepo $entityRepo, ViewService $viewService)
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
{
$this->entityRepo = $entityRepo;
$this->viewService = $viewService;
$this->searchService = $searchService;
parent::__construct();
}
@@ -27,84 +31,26 @@ class SearchController extends Controller
* @return \Illuminate\View\View
* @internal param string $searchTerm
*/
public function searchAll(Request $request)
public function search(Request $request)
{
if (!$request->has('term')) {
return redirect()->back();
}
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 10, $paginationAppends);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 10, $paginationAppends);
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
$page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
$hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0;
return view('search/all', [
'pages' => $pages,
'books' => $books,
'chapters' => $chapters,
'searchTerm' => $searchTerm
'entities' => $results['results'],
'totalResults' => $results['total'],
'searchTerm' => $searchTerm,
'hasNextPage' => $hasNextPage,
'nextPageLink' => $nextPageLink
]);
}
/**
* Search only the pages in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchPages(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$pages = $this->entityRepo->getBySearch('page', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_page_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $pages,
'title' => trans('entities.search_results_page'),
'searchTerm' => $searchTerm
]);
}
/**
* Search only the chapters in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchChapters(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_chapter_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $chapters,
'title' => trans('entities.search_results_chapter'),
'searchTerm' => $searchTerm
]);
}
/**
* Search only the books in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View
*/
public function searchBooks(Request $request)
{
if (!$request->has('term')) return redirect()->back();
$searchTerm = $request->get('term');
$paginationAppends = $request->only('term');
$books = $this->entityRepo->getBySearch('book', $searchTerm, [], 20, $paginationAppends);
$this->setPageTitle(trans('entities.search_book_for_term', ['term' => $searchTerm]));
return view('search/entity-search-list', [
'entities' => $books,
'title' => trans('entities.search_results_book'),
'searchTerm' => $searchTerm
]);
}
/**
* Searches all entities within a book.
@@ -115,16 +61,24 @@ class SearchController extends Controller
*/
public function searchBook(Request $request, $bookId)
{
if (!$request->has('term')) {
return redirect()->back();
}
$searchTerm = $request->get('term');
$searchWhereTerms = [['book_id', '=', $bookId]];
$pages = $this->entityRepo->getBySearch('page', $searchTerm, $searchWhereTerms);
$chapters = $this->entityRepo->getBySearch('chapter', $searchTerm, $searchWhereTerms);
return view('search/book', ['pages' => $pages, 'chapters' => $chapters, 'searchTerm' => $searchTerm]);
$term = $request->get('term', '');
$results = $this->searchService->searchBook($bookId, $term);
return view('partials/entity-list', ['entities' => $results]);
}
/**
* Searches all entities within a chapter.
* @param Request $request
* @param integer $chapterId
* @return \Illuminate\View\View
* @internal param string $searchTerm
*/
public function searchChapter(Request $request, $chapterId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchChapter($chapterId, $term);
return view('partials/entity-list', ['entities' => $results]);
}
/**
* Search for a list of entities and return a partial HTML response of matching entities.
@@ -134,18 +88,13 @@ class SearchController extends Controller
*/
public function searchEntitiesAjax(Request $request)
{
$entities = collect();
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
foreach (['page', 'chapter', 'book'] as $entityType) {
if ($entityTypes->contains($entityType)) {
$entities = $entities->merge($this->entityRepo->getBySearch($entityType, $searchTerm)->items());
}
}
$entities = $entities->sortByDesc('title_relevance');
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
$entities = $this->searchService->searchEntities($searchTerm)['results'];
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type);

View File

@@ -13,6 +13,8 @@ class Kernel extends HttpKernel
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
];
/**
@@ -24,8 +26,6 @@ class Kernel extends HttpKernel
'web' => [
\BookStack\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\BookStack\Http\Middleware\Localization::class

View File

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

View File

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

View File

@@ -8,8 +8,7 @@ class Page extends Entity
protected $simpleAttributes = ['name', 'id', 'slug'];
protected $with = ['book'];
protected $fieldsToSearch = ['name', 'text'];
public $textField = 'text';
/**
* Converts this page into a simplified array.
@@ -96,4 +95,14 @@ class Page extends Entity
return mb_convert_encoding($text, 'UTF-8');
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @param bool $withContent
* @return string
*/
public function entityRawQuery($withContent = false)
{ $htmlQuery = $withContent ? 'html' : "'' as html";
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
}
}

View File

@@ -1,5 +1,7 @@
<?php namespace BookStack\Providers;
use BookStack\Services\SettingService;
use BookStack\Setting;
use Illuminate\Support\ServiceProvider;
use Validator;
@@ -17,6 +19,10 @@ 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);
});
\Blade::directive('icon', function($expression) {
return "<?php echo icon($expression); ?>";
});
}
/**
@@ -26,6 +32,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function register()
{
//
$this->app->singleton(SettingService::class, function($app) {
return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
});
}
}

View File

@@ -4,6 +4,7 @@ namespace BookStack\Providers;
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use SocialiteProviders\Manager\SocialiteWasCalled;
class EventServiceProvider extends ServiceProvider
{
@@ -13,8 +14,8 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
'BookStack\Events\SomeEvent' => [
'BookStack\Listeners\EventListener',
SocialiteWasCalled::class => [
'SocialiteProviders\Slack\SlackExtendSocialite@handle',
],
];

View File

@@ -1,36 +0,0 @@
<?php namespace BookStack\Providers;
use Illuminate\Support\ServiceProvider;
class SocialiteServiceProvider extends ServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* @var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bindShared('Laravel\Socialite\Contracts\Factory', function ($app) {
return new SocialiteManager($app);
});
}
/**
* Get the services provided by the provider.
*
* @return array
*/
public function provides()
{
return ['Laravel\Socialite\Contracts\Factory'];
}
}

View File

@@ -8,6 +8,7 @@ use BookStack\Page;
use BookStack\PageRevision;
use BookStack\Services\AttachmentService;
use BookStack\Services\PermissionService;
use BookStack\Services\SearchService;
use BookStack\Services\ViewService;
use Carbon\Carbon;
use DOMDocument;
@@ -59,13 +60,12 @@ class EntityRepo
protected $tagRepo;
/**
* Acceptable operators to be used in a query
* @var array
* @var SearchService
*/
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
protected $searchService;
/**
* EntityService constructor.
* EntityRepo constructor.
* @param Book $book
* @param Chapter $chapter
* @param Page $page
@@ -73,10 +73,12 @@ class EntityRepo
* @param ViewService $viewService
* @param PermissionService $permissionService
* @param TagRepo $tagRepo
* @param SearchService $searchService
*/
public function __construct(
Book $book, Chapter $chapter, Page $page, PageRevision $pageRevision,
ViewService $viewService, PermissionService $permissionService, TagRepo $tagRepo
ViewService $viewService, PermissionService $permissionService,
TagRepo $tagRepo, SearchService $searchService
)
{
$this->book = $book;
@@ -86,12 +88,12 @@ class EntityRepo
$this->entities = [
'page' => $this->page,
'chapter' => $this->chapter,
'book' => $this->book,
'page_revision' => $this->pageRevision
'book' => $this->book
];
$this->viewService = $viewService;
$this->permissionService = $permissionService;
$this->tagRepo = $tagRepo;
$this->searchService = $searchService;
}
/**
@@ -217,6 +219,7 @@ class EntityRepo
* @param int $count
* @param int $page
* @param bool|callable $additionalQuery
* @return Collection
*/
public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
{
@@ -235,6 +238,7 @@ class EntityRepo
* @param int $count
* @param int $page
* @param bool|callable $additionalQuery
* @return Collection
*/
public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
{
@@ -314,11 +318,12 @@ class EntityRepo
* Loads the book slug onto child elements to prevent access database access for getting the slug.
* @param Book $book
* @param bool $filterDrafts
* @param bool $renderPages
* @return mixed
*/
public function getBookChildren(Book $book, $filterDrafts = false)
public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
{
$q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts)->get();
$q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
$entities = [];
$parents = [];
$tree = [];
@@ -326,19 +331,27 @@ class EntityRepo
foreach ($q as $index => $rawEntity) {
if ($rawEntity->entity_type === 'BookStack\\Page') {
$entities[$index] = $this->page->newFromBuilder($rawEntity);
if ($renderPages) {
$entities[$index]->html = $rawEntity->html;
$entities[$index]->html = $this->renderPage($entities[$index]);
};
} 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];
$parents[$key]->setAttribute('pages', collect());
}
if ($entities[$index]->chapter_id === 0) $tree[] = $entities[$index];
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') $tree[] = $entities[$index];
$entities[$index]->book = $book;
}
foreach ($entities as $entity) {
if ($entity->chapter_id === 0) continue;
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') continue;
$parentKey = 'BookStack\\Chapter:' . $entity->chapter_id;
if (!isset($parents[$parentKey])) {
$tree[] = $entity;
continue;
}
$chapter = $parents[$parentKey];
$chapter->pages->push($entity);
}
@@ -350,6 +363,7 @@ class EntityRepo
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param Chapter $chapter
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getChapterChildren(Chapter $chapter)
{
@@ -357,56 +371,6 @@ class EntityRepo
->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
}
/**
* Search entities of a type via a given query.
* @param string $type
* @param string $term
* @param array $whereTerms
* @param int $count
* @param array $paginationAppends
* @return mixed
*/
public function getBySearch($type, $term, $whereTerms = [], $count = 20, $paginationAppends = [])
{
$terms = $this->prepareSearchTerms($term);
$q = $this->permissionService->enforceEntityRestrictions($type, $this->getEntity($type)->fullTextSearchQuery($terms, $whereTerms));
$q = $this->addAdvancedSearchQueries($q, $term);
$entities = $q->paginate($count)->appends($paginationAppends);
$words = join('|', explode(' ', preg_quote(trim($term), '/')));
// Highlight page content
if ($type === 'page') {
//lookahead/behind assertions ensures cut between words
$s = '\s\x00-/:-@\[-`{-~'; //character set for start/end of words
foreach ($entities as $page) {
preg_match_all('#(?<=[' . $s . ']).{1,30}((' . $words . ').{1,30})+(?=[' . $s . '])#uis', $page->text, $matches, PREG_SET_ORDER);
//delimiter between occurrences
$results = [];
foreach ($matches as $line) {
$results[] = htmlspecialchars($line[0], 0, 'UTF-8');
}
$matchLimit = 6;
if (count($results) > $matchLimit) $results = array_slice($results, 0, $matchLimit);
$result = join('... ', $results);
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $result);
if (strlen($result) < 5) $result = $page->getExcerpt(80);
$page->searchSnippet = $result;
}
return $entities;
}
// Highlight chapter/book content
foreach ($entities as $entity) {
//highlight
$result = preg_replace('#' . $words . '#iu', "<span class=\"highlight\">\$0</span>", $entity->getExcerpt(100));
$entity->searchSnippet = $result;
}
return $entities;
}
/**
* Get the next sequential priority for a new child element in the given book.
@@ -488,104 +452,7 @@ class EntityRepo
$this->permissionService->buildJointPermissionsForEntity($entity);
}
/**
* Prepare a string of search terms by turning
* it into an array of terms.
* Keeps quoted terms together.
* @param $termString
* @return array
*/
public function prepareSearchTerms($termString)
{
$termString = $this->cleanSearchTermString($termString);
preg_match_all('/(".*?")/', $termString, $matches);
$terms = [];
if (count($matches[1]) > 0) {
foreach ($matches[1] as $match) {
$terms[] = $match;
}
$termString = trim(preg_replace('/"(.*?)"/', '', $termString));
}
if (!empty($termString)) $terms = array_merge($terms, explode(' ', $termString));
return $terms;
}
/**
* Removes any special search notation that should not
* be used in a full-text search.
* @param $termString
* @return mixed
*/
protected function cleanSearchTermString($termString)
{
// Strip tag searches
$termString = preg_replace('/\[.*?\]/', '', $termString);
// Reduced multiple spacing into single spacing
$termString = preg_replace("/\s{2,}/", " ", $termString);
return $termString;
}
/**
* Get the available query operators as a regex escaped list.
* @return mixed
*/
protected function getRegexEscapedOperators()
{
$escapedOperators = [];
foreach ($this->queryOperators as $operator) {
$escapedOperators[] = preg_quote($operator);
}
return join('|', $escapedOperators);
}
/**
* Parses advanced search notations and adds them to the db query.
* @param $query
* @param $termString
* @return mixed
*/
protected function addAdvancedSearchQueries($query, $termString)
{
$escapedOperators = $this->getRegexEscapedOperators();
// Look for tag searches
preg_match_all("/\[(.*?)((${escapedOperators})(.*?))?\]/", $termString, $tags);
if (count($tags[0]) > 0) {
$this->applyTagSearches($query, $tags);
}
return $query;
}
/**
* Apply extracted tag search terms onto a entity query.
* @param $query
* @param $tags
* @return mixed
*/
protected function applyTagSearches($query, $tags) {
$query->where(function($query) use ($tags) {
foreach ($tags[1] as $index => $tagName) {
$query->whereHas('tags', function($query) use ($tags, $index, $tagName) {
$tagOperator = $tags[3][$index];
$tagValue = $tags[4][$index];
if (!empty($tagOperator) && !empty($tagValue) && in_array($tagOperator, $this->queryOperators)) {
if (is_numeric($tagValue) && $tagOperator !== 'like') {
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
// search the value as a string which prevents being able to do number-based operations
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
$query->where('name', '=', $tagName)->whereRaw("value ${tagOperator} ${tagValue}");
} else {
$query->where('name', '=', $tagName)->where('value', $tagOperator, $tagValue);
}
} else {
$query->where('name', '=', $tagName);
}
});
}
});
return $query;
}
/**
* Create a new entity from request input.
@@ -604,12 +471,13 @@ class EntityRepo
$entity->updated_by = user()->id;
$isChapter ? $book->chapters()->save($entity) : $entity->save();
$this->permissionService->buildJointPermissionsForEntity($entity);
$this->searchService->indexEntity($entity);
return $entity;
}
/**
* Update entity details from request input.
* Use for books and chapters
* Used for books and chapters
* @param string $type
* @param Entity $entityModel
* @param array $input
@@ -624,6 +492,7 @@ class EntityRepo
$entityModel->updated_by = user()->id;
$entityModel->save();
$this->permissionService->buildJointPermissionsForEntity($entityModel);
$this->searchService->indexEntity($entityModel);
return $entityModel;
}
@@ -664,11 +533,11 @@ class EntityRepo
/**
* Alias method to update the book jointPermissions in the PermissionService.
* @param Collection $collection collection on entities
* @param Book $book
*/
public function buildJointPermissions(Collection $collection)
public function buildJointPermissionsForBook(Book $book)
{
$this->permissionService->buildJointPermissionsForEntities($collection);
$this->permissionService->buildJointPermissionsForEntity($book);
}
/**
@@ -704,10 +573,11 @@ class EntityRepo
$draftPage->html = $this->formatHtml($input['html']);
$draftPage->text = strip_tags($draftPage->html);
$draftPage->draft = false;
$draftPage->revision_count = 1;
$draftPage->save();
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
$this->searchService->indexEntity($draftPage);
return $draftPage;
}
@@ -728,6 +598,7 @@ class EntityRepo
$revision->created_at = $page->updated_at;
$revision->type = 'version';
$revision->summary = $summary;
$revision->revision_number = $page->revision_count;
$revision->save();
// Clear old revisions
@@ -859,6 +730,7 @@ class EntityRepo
if ($chapter) $page->chapter_id = $chapter->id;
$book->pages()->save($page);
$page = $this->page->find($page->id);
$this->permissionService->buildJointPermissionsForEntity($page);
return $page;
}
@@ -947,6 +819,7 @@ class EntityRepo
$page->text = strip_tags($page->html);
if (setting('app-editor') !== 'markdown') $page->markdown = '';
$page->updated_by = $userId;
$page->revision_count++;
$page->save();
// Remove all update drafts for this user & page.
@@ -957,6 +830,8 @@ class EntityRepo
$this->savePageRevision($page, $input['summary']);
}
$this->searchService->indexEntity($page);
return $page;
}
@@ -1053,13 +928,15 @@ class EntityRepo
*/
public function restorePageRevision(Page $page, Book $book, $revisionId)
{
$page->revision_count++;
$this->savePageRevision($page);
$revision = $this->getById('page_revision', $revisionId);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
$page->fill($revision->toArray());
$page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
$page->text = strip_tags($page->html);
$page->updated_by = user()->id;
$page->save();
$this->searchService->indexEntity($page);
return $page;
}
@@ -1152,6 +1029,7 @@ class EntityRepo
$book->views()->delete();
$book->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($book);
$this->searchService->deleteEntityTerms($book);
$book->delete();
}
@@ -1171,6 +1049,7 @@ class EntityRepo
$chapter->views()->delete();
$chapter->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($chapter);
$this->searchService->deleteEntityTerms($chapter);
$chapter->delete();
}
@@ -1186,6 +1065,7 @@ class EntityRepo
$page->revisions()->delete();
$page->permissions()->delete();
$this->permissionService->deleteJointPermissionsForEntity($page);
$this->searchService->deleteEntityTerms($page);
// Delete Attached Files
$attachmentService = app(AttachmentService::class);

18
app/SearchTerm.php Normal file
View File

@@ -0,0 +1,18 @@
<?php namespace BookStack;
class SearchTerm extends Model
{
protected $fillable = ['term', 'entity_id', 'entity_type', 'score'];
public $timestamps = false;
/**
* Get the entity that this term belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function entity()
{
return $this->morphTo('entity');
}
}

View File

@@ -1,5 +1,7 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Page;
use BookStack\Repos\EntityRepo;
@@ -25,25 +27,105 @@ class ExportService
*/
public function pageToContainedHtml(Page $page)
{
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/export', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render();
$pageHtml = view('pages/export', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
])->render();
return $this->containHtml($pageHtml);
}
/**
* Convert a page to a pdf file.
* Convert a chapter to a self-contained HTML file.
* @param Chapter $chapter
* @return mixed|string
*/
public function chapterToContainedHtml(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
'chapter' => $chapter,
'pages' => $pages
])->render();
return $this->containHtml($html);
}
/**
* Convert a book to a self-contained HTML file.
* @param Book $book
* @return mixed|string
*/
public function bookToContainedHtml(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
])->render();
return $this->containHtml($html);
}
/**
* Convert a page to a PDF file.
* @param Page $page
* @return mixed|string
*/
public function pageToPdf(Page $page)
{
$cssContent = file_get_contents(public_path('/css/export-styles.css'));
$pageHtml = view('pages/pdf', ['page' => $page, 'pageContent' => $this->entityRepo->renderPage($page), 'css' => $cssContent])->render();
// return $pageHtml;
$html = view('pages/pdf', [
'page' => $page,
'pageContent' => $this->entityRepo->renderPage($page)
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert a chapter to a PDF file.
* @param Chapter $chapter
* @return mixed|string
*/
public function chapterToPdf(Chapter $chapter)
{
$pages = $this->entityRepo->getChapterChildren($chapter);
$pages->each(function($page) {
$page->html = $this->entityRepo->renderPage($page);
});
$html = view('chapters/export', [
'chapter' => $chapter,
'pages' => $pages
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert a book to a PDF file
* @param Book $book
* @return string
*/
public function bookToPdf(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$html = view('books/export', [
'book' => $book,
'bookChildren' => $bookTree
])->render();
return $this->htmlToPdf($html);
}
/**
* Convert normal webpage HTML to a PDF.
* @param $html
* @return string
*/
protected function htmlToPdf($html)
{
$containedHtml = $this->containHtml($html);
$useWKHTML = config('snappy.pdf.binary') !== false;
$containedHtml = $this->containHtml($pageHtml);
if ($useWKHTML) {
$pdf = \SnappyPDF::loadHTML($containedHtml);
$pdf->setOption('print-media-type', true);
} else {
$pdf = \PDF::loadHTML($containedHtml);
}
@@ -123,6 +205,40 @@ class ExportService
return $text;
}
/**
* Convert a chapter into a plain text string.
* @param Chapter $chapter
* @return string
*/
public function chapterToPlainText(Chapter $chapter)
{
$text = $chapter->name . "\n\n";
$text .= $chapter->description . "\n\n";
foreach ($chapter->pages as $page) {
$text .= $this->pageToPlainText($page);
}
return $text;
}
/**
* Convert a book into a plain text string.
* @param Book $book
* @return string
*/
public function bookToPlainText(Book $book)
{
$bookTree = $this->entityRepo->getBookChildren($book, true, true);
$text = $book->name . "\n\n";
foreach ($bookTree as $bookChild) {
if ($bookChild->isA('chapter')) {
$text .= $this->chapterToPlainText($bookChild);
} else {
$text .= $this->pageToPlainText($bookChild);
}
}
return $text;
}
}

View File

@@ -41,7 +41,8 @@ class LdapService
// Find user
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
$baseDn = $this->config['base_dn'];
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', 'mail']);
$emailAttr = $this->config['email_attribute'];
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
if ($users['count'] === 0) return null;
$user = $users[0];
@@ -49,7 +50,7 @@ class LdapService
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'dn' => $user['dn'],
'email' => (isset($user['mail'])) ? $user['mail'][0] : null
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
];
}

View File

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

View File

@@ -0,0 +1,482 @@
<?php namespace BookStack\Services;
use BookStack\Book;
use BookStack\Chapter;
use BookStack\Entity;
use BookStack\Page;
use BookStack\SearchTerm;
use Illuminate\Database\Connection;
use Illuminate\Database\Query\Builder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
class SearchService
{
protected $searchTerm;
protected $book;
protected $chapter;
protected $page;
protected $db;
protected $permissionService;
protected $entities;
/**
* Acceptable operators to be used in a query
* @var array
*/
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
/**
* SearchService constructor.
* @param SearchTerm $searchTerm
* @param Book $book
* @param Chapter $chapter
* @param Page $page
* @param Connection $db
* @param PermissionService $permissionService
*/
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
{
$this->searchTerm = $searchTerm;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
$this->db = $db;
$this->entities = [
'page' => $this->page,
'chapter' => $this->chapter,
'book' => $this->book
];
$this->permissionService = $permissionService;
}
/**
* Set the database connection
* @param Connection $connection
*/
public function setConnection(Connection $connection)
{
$this->db = $connection;
}
/**
* Search all entities in the system.
* @param string $searchString
* @param string $entityType
* @param int $page
* @param int $count
* @return array[int, Collection];
*/
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20)
{
$terms = $this->parseSearchString($searchString);
$entityTypes = array_keys($this->entities);
$entityTypesToSearch = $entityTypes;
$results = collect();
if ($entityType !== 'all') {
$entityTypesToSearch = $entityType;
} else if (isset($terms['filters']['type'])) {
$entityTypesToSearch = explode('|', $terms['filters']['type']);
}
$total = 0;
foreach ($entityTypesToSearch as $entityType) {
if (!in_array($entityType, $entityTypes)) continue;
$search = $this->searchEntityTable($terms, $entityType, $page, $count);
$total += $this->searchEntityTable($terms, $entityType, $page, $count, true);
$results = $results->merge($search);
}
return [
'total' => $total,
'count' => count($results),
'results' => $results->sortByDesc('score')
];
}
/**
* Search a book for entities
* @param integer $bookId
* @param string $searchString
* @return Collection
*/
public function searchBook($bookId, $searchString)
{
$terms = $this->parseSearchString($searchString);
$entityTypes = ['page', 'chapter'];
$entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
$results = collect();
foreach ($entityTypesToSearch as $entityType) {
if (!in_array($entityType, $entityTypes)) continue;
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search);
}
return $results->sortByDesc('score')->take(20);
}
/**
* Search a book for entities
* @param integer $chapterId
* @param string $searchString
* @return Collection
*/
public function searchChapter($chapterId, $searchString)
{
$terms = $this->parseSearchString($searchString);
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
return $pages->sortByDesc('score');
}
/**
* Search across a particular entity type.
* @param array $terms
* @param string $entityType
* @param int $page
* @param int $count
* @param bool $getCount Return the total count of the search
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $getCount = false)
{
$query = $this->buildEntitySearchQuery($terms, $entityType);
if ($getCount) return $query->count();
$query = $query->skip(($page-1) * $count)->take($count);
return $query->get();
}
/**
* Create a search query for an entity
* @param array $terms
* @param string $entityType
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function buildEntitySearchQuery($terms, $entityType = 'page')
{
$entity = $this->getEntity($entityType);
$entitySelect = $entity->newQuery();
// Handle normal search terms
if (count($terms['search']) > 0) {
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
$subQuery->where(function(Builder $query) use ($terms) {
foreach ($terms['search'] as $inputTerm) {
$query->orWhere('term', 'like', $inputTerm .'%');
}
})->groupBy('entity_type', 'entity_id');
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function(JoinClause $join) {
$join->on('id', '=', 'entity_id');
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
$entitySelect->mergeBindings($subQuery);
}
// Handle exact term matching
if (count($terms['exact']) > 0) {
$entitySelect->where(function(\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
foreach ($terms['exact'] as $inputTerm) {
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
$query->where('name', 'like', '%'.$inputTerm .'%')
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
});
}
});
}
// Handle tag searches
foreach ($terms['tags'] as $inputTerm) {
$this->applyTagSearch($entitySelect, $inputTerm);
}
// Handle filters
foreach ($terms['filters'] as $filterTerm => $filterValue) {
$functionName = camel_case('filter_' . $filterTerm);
if (method_exists($this, $functionName)) $this->$functionName($entitySelect, $entity, $filterValue);
}
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, 'view');
}
/**
* Parse a search string into components.
* @param $searchString
* @return array
*/
protected function parseSearchString($searchString)
{
$terms = [
'search' => [],
'exact' => [],
'tags' => [],
'filters' => []
];
$patterns = [
'exact' => '/"(.*?)"/',
'tags' => '/\[(.*?)\]/',
'filters' => '/\{(.*?)\}/'
];
// Parse special terms
foreach ($patterns as $termType => $pattern) {
$matches = [];
preg_match_all($pattern, $searchString, $matches);
if (count($matches) > 0) {
$terms[$termType] = $matches[1];
$searchString = preg_replace($pattern, '', $searchString);
}
}
// Parse standard terms
foreach (explode(' ', trim($searchString)) as $searchTerm) {
if ($searchTerm !== '') $terms['search'][] = $searchTerm;
}
// Split filter values out
$splitFilters = [];
foreach ($terms['filters'] as $filter) {
$explodedFilter = explode(':', $filter, 2);
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
}
$terms['filters'] = $splitFilters;
return $terms;
}
/**
* Get the available query operators as a regex escaped list.
* @return mixed
*/
protected function getRegexEscapedOperators()
{
$escapedOperators = [];
foreach ($this->queryOperators as $operator) {
$escapedOperators[] = preg_quote($operator);
}
return join('|', $escapedOperators);
}
/**
* Apply a tag search term onto a entity query.
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $tagTerm
* @return mixed
*/
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm) {
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
$query->whereHas('tags', function(\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
$tagName = $tagSplit[1];
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
$validOperator = in_array($tagOperator, $this->queryOperators);
if (!empty($tagOperator) && !empty($tagValue) && $validOperator) {
if (!empty($tagName)) $query->where('name', '=', $tagName);
if (is_numeric($tagValue) && $tagOperator !== 'like') {
// We have to do a raw sql query for this since otherwise PDO will quote the value and MySQL will
// search the value as a string which prevents being able to do number-based operations
// on the tag values. We ensure it has a numeric value and then cast it just to be sure.
$tagValue = (float) trim($query->getConnection()->getPdo()->quote($tagValue), "'");
$query->whereRaw("value ${tagOperator} ${tagValue}");
} else {
$query->where('value', $tagOperator, $tagValue);
}
} else {
$query->where('name', '=', $tagName);
}
});
return $query;
}
/**
* Get an entity instance via type.
* @param $type
* @return Entity
*/
protected function getEntity($type)
{
return $this->entities[strtolower($type)];
}
/**
* Index the given entity.
* @param Entity $entity
*/
public function indexEntity(Entity $entity)
{
$this->deleteEntityTerms($entity);
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
$terms = array_merge($nameTerms, $bodyTerms);
foreach ($terms as $index => $term) {
$terms[$index]['entity_type'] = $entity->getMorphClass();
$terms[$index]['entity_id'] = $entity->id;
}
$this->searchTerm->newQuery()->insert($terms);
}
/**
* Index multiple Entities at once
* @param Entity[] $entities
*/
protected function indexEntities($entities) {
$terms = [];
foreach ($entities as $entity) {
$nameTerms = $this->generateTermArrayFromText($entity->name, 5);
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1);
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
$term['entity_id'] = $entity->id;
$term['entity_type'] = $entity->getMorphClass();
$terms[] = $term;
}
}
$chunkedTerms = array_chunk($terms, 500);
foreach ($chunkedTerms as $termChunk) {
$this->searchTerm->newQuery()->insert($termChunk);
}
}
/**
* Delete and re-index the terms for all entities in the system.
*/
public function indexAllEntities()
{
$this->searchTerm->truncate();
// Chunk through all books
$this->book->chunk(1000, function ($books) {
$this->indexEntities($books);
});
// Chunk through all chapters
$this->chapter->chunk(1000, function ($chapters) {
$this->indexEntities($chapters);
});
// Chunk through all pages
$this->page->chunk(1000, function ($pages) {
$this->indexEntities($pages);
});
}
/**
* Delete related Entity search terms.
* @param Entity $entity
*/
public function deleteEntityTerms(Entity $entity)
{
$entity->searchTerms()->delete();
}
/**
* Create a scored term array from the given text.
* @param $text
* @param float|int $scoreAdjustment
* @return array
*/
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
{
$tokenMap = []; // {TextToken => OccurrenceCount}
$splitText = explode(' ', $text);
foreach ($splitText as $token) {
if ($token === '') continue;
if (!isset($tokenMap[$token])) $tokenMap[$token] = 0;
$tokenMap[$token]++;
}
$terms = [];
foreach ($tokenMap as $token => $count) {
$terms[] = [
'term' => $token,
'score' => $count * $scoreAdjustment
];
}
return $terms;
}
/**
* Custom entity search filters
*/
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
$query->where('updated_at', '>=', $date);
}
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
$query->where('updated_at', '<', $date);
}
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
$query->where('created_at', '>=', $date);
}
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
try { $date = date_create($input);
} catch (\Exception $e) {return;}
$query->where('created_at', '<', $date);
}
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
if (!is_numeric($input) && $input !== 'me') return;
if ($input === 'me') $input = user()->id;
$query->where('created_by', '=', $input);
}
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
if (!is_numeric($input) && $input !== 'me') return;
if ($input === 'me') $input = user()->id;
$query->where('updated_by', '=', $input);
}
protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->where('name', 'like', '%' .$input. '%');
}
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input) {$this->filterInName($query, $model, $input);}
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->where($model->textField, 'like', '%' .$input. '%');
}
protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->where('restricted', '=', true);
}
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->whereHas('views', function($query) {
$query->where('user_id', '=', user()->id);
});
}
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
{
$query->whereDoesntHave('views', function($query) {
$query->where('user_id', '=', user()->id);
});
}
}

View File

@@ -16,6 +16,7 @@ class SettingService
protected $setting;
protected $cache;
protected $localCache = [];
protected $cachePrefix = 'setting-';
@@ -40,8 +41,12 @@ class SettingService
public function get($key, $default = false)
{
if ($default === false) $default = config('setting-defaults.' . $key, false);
if (isset($this->localCache[$key])) return $this->localCache[$key];
$value = $this->getValueFromStore($key, $default);
return $this->formatValue($value, $default);
$formatted = $this->formatValue($value, $default);
$this->localCache[$key] = $formatted;
return $formatted;
}
/**
@@ -71,9 +76,8 @@ class SettingService
// Check the cache
$cacheKey = $this->cachePrefix . $key;
if ($this->cache->has($cacheKey)) {
return $this->cache->get($cacheKey);
}
$cacheVal = $this->cache->get($cacheKey, null);
if ($cacheVal !== null) return $cacheVal;
// Check the database
$settingObject = $this->getSettingObjectByKey($key);

View File

@@ -14,7 +14,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter'];
/**
* SocialAuthService constructor.
@@ -181,14 +181,24 @@ class SocialAuthService
public function getActiveDrivers()
{
$activeDrivers = [];
foreach ($this->validSocialDrivers as $driverName) {
if ($this->checkDriverConfigured($driverName)) {
$activeDrivers[$driverName] = true;
foreach ($this->validSocialDrivers as $driverKey) {
if ($this->checkDriverConfigured($driverKey)) {
$activeDrivers[$driverKey] = $this->getDriverName($driverKey);
}
}
return $activeDrivers;
}
/**
* Get the presentational name for a driver.
* @param $driver
* @return mixed
*/
public function getDriverName($driver)
{
return config('services.' . strtolower($driver) . '.name');
}
/**
* @param string $socialDriver
* @param \Laravel\Socialite\Contracts\User $socialUser
@@ -211,7 +221,6 @@ class SocialAuthService
*/
public function detachSocialAccount($socialDriver)
{
session();
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
return redirect(user()->getEditUrl());

View File

@@ -165,7 +165,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
if ($imageId === 0 || $imageId === '0' || $imageId === null) return $default;
try {
$avatar = baseUrl($this->avatar->getThumb($size, $size, false));
$avatar = $this->avatar ? baseUrl($this->avatar->getThumb($size, $size, false)) : $default;
} catch (\Exception $err) {
$avatar = $default;
}

View File

@@ -37,6 +37,15 @@ function user()
return auth()->user() ?: \BookStack\User::getDefault();
}
/**
* Check if current user is a signed in user.
* @return bool
*/
function signedInUser()
{
return auth()->user() && !auth()->user()->isDefault();
}
/**
* Check if the current user has a permission.
* If an ownable element is passed in the jointPermissions are checked against
@@ -64,7 +73,7 @@ function userCan($permission, Ownable $ownable = null)
*/
function setting($key = null, $default = false)
{
$settingService = app(\BookStack\Services\SettingService::class);
$settingService = resolve(\BookStack\Services\SettingService::class);
if (is_null($key)) return $settingService;
return $settingService->get($key, $default);
}
@@ -117,6 +126,16 @@ function redirect($to = null, $status = 302, $headers = [], $secure = null)
return app('redirect')->to($to, $status, $headers, $secure);
}
function icon($name, $attrs = []) {
$iconPath = resource_path('assets/icons/' . $name . '.svg');
$attrString = ' ';
foreach ($attrs as $attrName => $attr) {
$attrString .= $attrName . '="' . $attr . '" ';
}
$fileContents = file_get_contents($iconPath);
return str_replace('<svg', '<svg' . $attrString, $fileContents);
}
/**
* Generate a url with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction
@@ -147,4 +166,4 @@ function sortUrl($path, $data, $overrideData = [])
if (count($queryStringSections) === 0) return $path;
return baseUrl($path . '?' . implode('&', $queryStringSections));
}
}

View File

@@ -6,17 +6,19 @@
"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",
"barryvdh/laravel-dompdf": "^0.8",
"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",
"socialiteproviders/slack": "^3.0"
},
"require-dev": {
"fzaninotto/faker": "~1.4",
@@ -34,9 +36,9 @@
}
},
"autoload-dev": {
"classmap": [
"tests/TestCase.php"
]
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-root-package-install": [
@@ -45,13 +47,27 @@
"post-create-project-cmd": [
"php artisan key:generate"
],
"pre-update-cmd": [
"php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
"php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
],
"pre-install-cmd": [
"php -r \"!file_exists('bootstrap/cache/services.php') || @unlink('bootstrap/cache/services.php');\"",
"php -r \"!file_exists('bootstrap/cache/compiled.php') || @unlink('bootstrap/cache/compiled.php');\""
],
"post-install-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postInstall",
"php artisan optimize"
"php artisan optimize",
"php artisan cache:clear",
"php artisan view:clear"
],
"post-update-cmd": [
"Illuminate\\Foundation\\ComposerScripts::postUpdate",
"php artisan optimize"
],
"refresh-test-database": [
"php artisan migrate:refresh --database=mysql_testing",
"php artisan db:seed --class=DummyContentSeeder --database=mysql_testing"
]
},
"config": {

1691
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,7 @@ return [
*/
'locale' => env('APP_LANG', 'en'),
'locales' => ['en', 'de', 'es', 'fr', 'nl', 'pt_BR', 'sk'],
/*
|--------------------------------------------------------------------------
@@ -100,7 +101,7 @@ return [
|
*/
'log' => 'single',
'log' => env('APP_LOGGING', 'single'),
/*
|--------------------------------------------------------------------------
@@ -139,7 +140,7 @@ return [
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Laravel\Socialite\SocialiteServiceProvider::class,
SocialiteProviders\Manager\ServiceProvider::class,
/**
* Third Party

View File

@@ -82,7 +82,7 @@ return [
'mysql_testing' => [
'driver' => 'mysql',
'host' => 'localhost',
'host' => '127.0.0.1',
'database' => 'bookstack-test',
'username' => env('MYSQL_USER', 'bookstack-test'),
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),

View File

@@ -1,6 +1,6 @@
<?php
return array(
return [
/*
|--------------------------------------------------------------------------
@@ -13,7 +13,7 @@ return array(
*/
'show_warnings' => false, // Throw an Exception on warnings from dompdf
'orientation' => 'portrait',
'defines' => array(
'defines' => [
/**
* The location of the DOMPDF font directory
*
@@ -143,7 +143,7 @@ return array(
* the desired content might be different (e.g. screen or projection view of html file).
* Therefore allow specification of content here.
*/
"DOMPDF_DEFAULT_MEDIA_TYPE" => "screen",
"DOMPDF_DEFAULT_MEDIA_TYPE" => "print",
/**
* The default paper size.
@@ -260,7 +260,7 @@ return array(
"DOMPDF_ENABLE_HTML5PARSER" => true,
),
],
);
];

View File

@@ -41,12 +41,35 @@ return [
'client_id' => env('GITHUB_APP_ID', false),
'client_secret' => env('GITHUB_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/github/callback',
'name' => 'GitHub',
],
'google' => [
'client_id' => env('GOOGLE_APP_ID', false),
'client_secret' => env('GOOGLE_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/google/callback',
'name' => 'Google',
],
'slack' => [
'client_id' => env('SLACK_APP_ID', false),
'client_secret' => env('SLACK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/slack/callback',
'name' => 'Slack',
],
'facebook' => [
'client_id' => env('FACEBOOK_APP_ID', false),
'client_secret' => env('FACEBOOK_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
'name' => 'Facebook',
],
'twitter' => [
'client_id' => env('TWITTER_APP_ID', false),
'client_secret' => env('TWITTER_APP_SECRET', false),
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
'name' => 'Twitter',
],
'ldap' => [
@@ -55,7 +78,8 @@ return [
'pass' => env('LDAP_PASS', false),
'base_dn' => env('LDAP_BASE_DN', false),
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
'version' => env('LDAP_VERSION', false)
'version' => env('LDAP_VERSION', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
]
];

View File

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

View File

@@ -12,9 +12,10 @@ class AddSearchIndexes extends Migration
*/
public function up()
{
DB::statement('ALTER TABLE pages ADD FULLTEXT search(name, text)');
DB::statement('ALTER TABLE books ADD FULLTEXT search(name, description)');
DB::statement('ALTER TABLE chapters ADD FULLTEXT search(name, description)');
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
}
/**

View File

@@ -12,9 +12,10 @@ class FulltextWeighting extends Migration
*/
public function up()
{
DB::statement('ALTER TABLE pages ADD FULLTEXT name_search(name)');
DB::statement('ALTER TABLE books ADD FULLTEXT name_search(name)');
DB::statement('ALTER TABLE chapters ADD FULLTEXT name_search(name)');
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
}
/**

View File

@@ -0,0 +1,63 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateSearchIndexTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('search_terms', function (Blueprint $table) {
$table->increments('id');
$table->string('term', 200);
$table->string('entity_type', 100);
$table->integer('entity_id');
$table->integer('score');
$table->index('term');
$table->index('entity_type');
$table->index(['entity_type', 'entity_id']);
$table->index('score');
});
// Drop search indexes
Schema::table('pages', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
Schema::table('books', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
Schema::table('chapters', function(Blueprint $table) {
$table->dropIndex('search');
$table->dropIndex('name_search');
});
app(\BookStack\Services\SearchService::class)->indexAllEntities();
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$prefix = DB::getTablePrefix();
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT search(name, text)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT search(name, description)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT search(name, description)");
DB::statement("ALTER TABLE {$prefix}pages ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}books ADD FULLTEXT name_search(name)");
DB::statement("ALTER TABLE {$prefix}chapters ADD FULLTEXT name_search(name)");
Schema::dropIfExists('search_terms');
}
}

View File

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

View File

@@ -11,14 +11,14 @@ class DummyContentSeeder extends Seeder
*/
public function run()
{
$user = factory(BookStack\User::class, 1)->create();
$user = factory(\BookStack\User::class)->create();
$role = \BookStack\Role::getRole('editor');
$user->attachRole($role);
$books = factory(BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
factory(\BookStack\Book::class, 20)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($book) use ($user) {
$chapters = factory(BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
$chapters = factory(\BookStack\Chapter::class, 5)->create(['created_by' => $user->id, 'updated_by' => $user->id])
->each(function($chapter) use ($user, $book){
$pages = factory(\BookStack\Page::class, 5)->make(['created_by' => $user->id, 'updated_by' => $user->id, 'book_id' => $book->id]);
$chapter->pages()->saveMany($pages);
@@ -28,7 +28,13 @@ class DummyContentSeeder extends Seeder
$book->pages()->saveMany($pages);
});
$restrictionService = app(\BookStack\Services\PermissionService::class);
$restrictionService->buildJointPermissions();
$largeBook = factory(\BookStack\Book::class)->create(['name' => 'Large book' . str_random(10), 'created_by' => $user->id, 'updated_by' => $user->id]);
$pages = factory(\BookStack\Page::class, 200)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$chapters = factory(\BookStack\Chapter::class, 50)->make(['created_by' => $user->id, 'updated_by' => $user->id]);
$largeBook->pages()->saveMany($pages);
$largeBook->chapters()->saveMany($chapters);
app(\BookStack\Services\PermissionService::class)->buildJointPermissions();
app(\BookStack\Services\SearchService::class)->indexAllEntities();
}
}

View File

@@ -1,8 +1,63 @@
var elixir = require('laravel-elixir');
const argv = require('yargs').argv;
const gulp = require('gulp'),
plumber = require('gulp-plumber');
const autoprefixer = require('gulp-autoprefixer');
const uglify = require('gulp-uglify');
const minifycss = require('gulp-clean-css');
const sass = require('gulp-sass');
const browserify = require("browserify");
const source = require('vinyl-source-stream');
const buffer = require('vinyl-buffer');
const babelify = require("babelify");
const watchify = require("watchify");
const envify = require("envify");
const gutil = require("gulp-util");
elixir(mix => {
mix.sass('styles.scss');
mix.sass('print-styles.scss');
mix.sass('export-styles.scss');
mix.browserify('global.js', './public/js/common.js');
if (argv.production) process.env.NODE_ENV = 'production';
gulp.task('styles', () => {
let chain = gulp.src(['resources/assets/sass/**/*.scss'])
.pipe(plumber({
errorHandler: function (error) {
console.log(error.message);
this.emit('end');
}}))
.pipe(sass())
.pipe(autoprefixer('last 2 versions'));
if (argv.production) chain = chain.pipe(minifycss());
return chain.pipe(gulp.dest('public/css/'));
});
function scriptTask(watch=false) {
let props = {
basedir: 'resources/assets/js',
debug: true,
entries: ['global.js']
};
let bundler = watch ? watchify(browserify(props), { poll: true }) : browserify(props);
bundler.transform(envify, {global: true}).transform(babelify, {presets: ['es2015']});
function rebundle() {
let stream = bundler.bundle();
stream = stream.pipe(source('common.js'));
if (argv.production) stream = stream.pipe(buffer()).pipe(uglify());
return stream.pipe(gulp.dest('public/js/'));
}
bundler.on('update', function() {
rebundle();
gutil.log('Rebundle...');
});
bundler.on('log', gutil.log);
return rebundle();
}
gulp.task('scripts', () => {scriptTask(false)});
gulp.task('scripts-watch', () => {scriptTask(true)});
gulp.task('default', ['styles', 'scripts-watch'], () => {
gulp.watch("resources/assets/sass/**/*.scss", ['styles']);
});
gulp.task('build', ['styles', 'scripts']);

View File

@@ -1,24 +1,44 @@
{
"private": true,
"scripts": {
"build": "gulp --production",
"dev": "gulp watch",
"watch": "gulp watch"
"build": "gulp build",
"production": "gulp build --production",
"dev": "gulp",
"watch": "gulp"
},
"devDependencies": {
"babelify": "^7.3.0",
"browserify": "^14.3.0",
"envify": "^4.0.0",
"gulp": "3.9.1",
"gulp-autoprefixer": "3.1.1",
"gulp-clean-css": "^3.0.4",
"gulp-minify-css": "1.2.4",
"gulp-plumber": "1.1.0",
"gulp-sass": "3.1.0",
"gulp-uglify": "2.1.2",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"watchify": "^3.9.0",
"yargs": "^7.1.0"
},
"dependencies": {
"angular": "^1.5.5",
"angular-animate": "^1.5.5",
"angular-resource": "^1.5.5",
"angular-sanitize": "^1.5.5",
"angular-ui-sortable": "^0.15.0",
"angular-ui-sortable": "^0.17.0",
"axios": "^0.16.1",
"babel-preset-es2015": "^6.24.1",
"clipboard": "^1.5.16",
"dropzone": "^4.0.1",
"gulp": "^3.9.0",
"laravel-elixir": "^6.0.0-11",
"laravel-elixir-browserify-official": "^0.1.3",
"marked": "^0.3.5",
"moment": "^2.12.0"
"gulp-util": "^3.0.8",
"markdown-it": "^8.3.1",
"markdown-it-task-lists": "^2.0.0",
"moment": "^2.12.0",
"vue": "^2.2.6"
},
"dependencies": {
"clipboard": "^1.5.16"
"browser": {
"vue": "vue/dist/vue.common.js"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
.faded-small,.print-hidden,header{display:none}body{font-size:12px}.page-content{margin:0 auto}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}
header{display:none}body{font-size:12px}.faded-small{display:none}.page-content{margin:0 auto}.print-hidden{display:none}.print-full-width{width:100%;float:none;display:block}h2{font-size:2em;line-height:1;margin-top:.6em;margin-bottom:.3em}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
# BookStack
[![GitHub release](https://img.shields.io/github/release/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/releases/latest)
[![license](https://img.shields.io/github/license/ssddanbrown/BookStack.svg?maxAge=2592000)](https://github.com/ssddanbrown/BookStack/blob/master/LICENSE)
[![GitHub release](https://img.shields.io/github/release/BookStackApp/BookStack.svg?maxAge=2592000)](https://github.com/BookStackApp/BookStack/releases/latest)
[![license](https://img.shields.io/github/license/BookStackApp/BookStack.svg?maxAge=2592000)](https://github.com/BookStackApp/BookStack/blob/master/LICENSE)
[![Build Status](https://travis-ci.org/BookStackApp/BookStack.svg)](https://travis-ci.org/BookStackApp/BookStack)
A platform for storing and organising information and documentation. General information and documentation for BookStack can be found at https://www.bookstackapp.com/.
@@ -43,8 +43,16 @@ Once done you can run `phpunit` in the application root directory to run all tes
## Translations
As part of BookStack v0.14 support for translations has been built in. All text strings can be found in the `resources/lang` folder where each language option has its own folder. To add a new language you should copy the `en` folder to an new folder (eg. `fr` for french) then go through and translate all text strings in those files, leaving the keys and file-names intact. If a language string is missing then the `en` translation will be used. To show the language option in the user preferences language drop-down you will need to add your language to the options found at the bottom of the `resources/lang/en/settings.php` file. A system-wide language can also be set in the `.env` file like so: `APP_LANG=en`.
You will also need to add the language to the `locales` array in the `config/app.php` file.
Some strings have colon-prefixed variables in such as `:userName`. Leave these values as they are as they will be replaced at run-time.
## Contributing
Feel free to create issues to request new features or to report bugs and problems. Just please follow the template given when creating the issue.
Pull requests are very welcome. If the scope of your pull request is very large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
## Website, Docs & Blog
@@ -68,7 +76,7 @@ These are the great projects used to help build BookStack:
* [Dropzone.js](http://www.dropzonejs.com/)
* [ZeroClipboard](http://zeroclipboard.org/)
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
* [Marked](https://github.com/chjj/marked)
* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
* [Moment.js](http://momentjs.com/)
* [BarryVD](https://github.com/barryvdh)
* [Debugbar](https://github.com/barryvdh/laravel-debugbar)

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 266.893 266.895"><path fill="#3C5A99" d="M248.082 262.307c7.854 0 14.223-6.37 14.223-14.225V18.812c0-7.857-6.368-14.224-14.223-14.224H18.812c-7.857 0-14.224 6.367-14.224 14.224v229.27c0 7.855 6.366 14.225 14.224 14.225h229.27z"/><path fill="#FFF" d="M182.41 262.307v-99.803h33.498l5.016-38.895H182.41V98.775c0-11.26 3.126-18.935 19.274-18.935l20.596-.01V45.047c-3.562-.474-15.788-1.533-30.012-1.533-29.695 0-50.025 18.126-50.025 51.413v28.684h-33.585v38.894h33.585v99.803h40.166z"/></svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#333333" fill-rule="evenodd" d="M31.9.693c-17.672 0-32 14.327-32 32 0 14.14 9.17 26.132 21.886 30.365 1.6.293 2.184-.695 2.184-1.544 0-.758-.028-2.77-.043-5.44-8.9 1.932-10.78-4.292-10.78-4.292-1.455-3.695-3.553-4.68-3.553-4.68-2.905-1.985.22-1.946.22-1.946 3.212.228 4.9 3.3 4.9 3.3 2.856 4.888 7.492 3.476 9.315 2.66.29-2.07 1.11-3.48 2.03-4.28-7.11-.807-14.58-3.554-14.58-15.816 0-3.493 1.243-6.35 3.29-8.586-.33-.81-1.428-4.063.313-8.47 0 0 2.687-.86 8.8 3.28 2.552-.708 5.29-1.063 8.01-1.075 2.718.01 5.457.36 8.01 1.07 6.11-4.14 8.793-3.28 8.793-3.28 1.747 4.403.65 7.66.32 8.47 2.05 2.233 3.29 5.09 3.29 8.582 0 12.293-7.483 15-14.61 15.79 1.15.99 2.17 2.94 2.17 5.926 0 4.277-.04 7.73-.04 8.777 0 .857.578 1.853 2.2 1.54 12.71-4.235 21.87-16.22 21.87-30.355 0-17.674-14.326-32-32-32"/></svg>

After

Width:  |  Height:  |  Size: 871 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><g fill="none" fill-rule="evenodd"><path fill="#4285f4" d="M62.735 32.712c0-2.27-.204-4.45-.582-6.545H32.015v12.378h17.222c-.742 4-2.997 7.39-6.386 9.658v8.03h10.344c6.05-5.57 9.542-13.775 9.542-23.52z"/><path fill="#34a853" d="M32.015 63.985c8.64 0 15.883-2.865 21.178-7.753l-10.342-8.03c-2.863 1.92-6.53 3.056-10.834 3.056-8.335 0-15.39-5.63-17.906-13.193H3.417v8.29c5.266 10.46 16.088 17.63 28.597 17.63z"/><path fill="#fbbc05" d="M14.11 38.065c-.64-1.92-1.004-3.97-1.004-6.08s.363-4.16 1.003-6.08v-8.29H3.416C1.25 21.935.015 26.82.015 31.985c0 5.163 1.236 10.05 3.403 14.37l10.69-8.29z"/><path fill="#ea4335" d="M32.015 12.712c4.698 0 8.916 1.615 12.233 4.786l9.178-9.178C47.884 3.156 40.64-.015 32.016-.015c-12.51 0-23.332 7.17-28.598 17.63l10.69 8.29c2.518-7.563 9.572-13.193 17.907-13.193z"/><path d="M.015-.015h64v64h-64v-64z"/></g></svg>

After

Width:  |  Height:  |  Size: 906 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 64 64"><style id="style3">.st0{fill:#ECB32D;} .st1{fill:#63C1A0;} .st2{fill:#E01A59;} .st3{fill:#331433;} .st4{fill:#D62027;} .st5{fill:#89D3DF;} .st6{fill:#258B74;} .st7{fill:#819C3C;}</style><g id="g5"><g id="g7"><path id="path9" fill="#ecb32d" d="M41.478 3.945C40.48.95 37.28-.677 34.288.27c-2.992.997-4.62 4.2-3.674 7.195l14.748 45.383c.997 2.784 4.042 4.36 6.928 3.52 3.044-.893 4.88-4.098 3.884-7.04 0-.104-14.696-45.383-14.696-45.383z" class="st0"/><path id="path11" fill="#63c1a0" d="M18.648 11.352c-.997-2.994-4.2-4.623-7.19-3.677-2.992.998-4.62 4.202-3.674 7.196l14.748 45.39c.997 2.784 4.04 4.36 6.928 3.52 3.044-.894 4.88-4.098 3.883-7.04 0-.105-14.695-45.383-14.695-45.383z" class="st1"/><path id="path13" fill="#e01a59" d="M60.058 41.502c2.99-.998 4.618-4.202 3.674-7.196-.997-2.994-4.2-4.622-7.19-3.677L11.14 45.44c-2.78.998-4.356 4.045-3.516 6.934.892 3.046 4.094 4.885 7.033 3.887.104 0 45.398-14.76 45.398-14.76z" class="st2"/><path id="path15" fill="#331433" d="M20.59 54.372c2.94-.946 6.77-2.207 10.864-3.52-.945-2.94-2.204-6.776-3.516-10.873l-10.865 3.514L20.59 54.37z" class="st3"/><path id="path17" fill="#d62027" d="M43.473 46.913c4.094-1.313 7.925-2.574 10.864-3.52-.945-2.94-2.204-6.776-3.516-10.873l-10.86 3.52 3.518 10.873z" class="st4"/><path id="path19" fill="#89d3df" d="M52.605 18.653c2.992-.998 4.62-4.202 3.674-7.196-1-2.994-4.2-4.623-7.19-3.677L3.74 22.54c-2.78.998-4.356 4.045-3.516 6.934.892 3.046 4.094 4.885 7.033 3.887.104 0 45.345-14.703 45.345-14.703z" class="st5"/><path id="path21" fill="#258b74" d="M13.19 31.47c2.94-.946 6.77-2.206 10.864-3.52-1.312-4.097-2.572-7.93-3.517-10.873l-10.864 3.52L13.19 31.47z" class="st6"/><path id="path23" fill="#819c3c" d="M36.02 24.063c4.094-1.313 7.925-2.573 10.864-3.52-1.312-4.096-2.57-7.93-3.516-10.872l-10.864 3.52 3.516 10.877z" class="st7"/></g></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill="#00aced" d="M64 12.145c-2.355 1.045-4.886 1.75-7.54 2.068 2.71-1.625 4.79-4.198 5.772-7.265-2.538 1.505-5.347 2.598-8.338 3.187-2.395-2.552-5.808-4.147-9.584-4.147-7.252 0-13.13 5.88-13.13 13.13 0 1.03.115 2.032.34 2.993-10.914-.543-20.59-5.77-27.065-13.714-1.13 1.94-1.777 4.195-1.777 6.6 0 4.556 2.317 8.575 5.84 10.93-2.15-.068-4.176-.66-5.946-1.642v.166c0 6.36 4.525 11.667 10.53 12.874-1.1.3-2.26.46-3.458.46-.846 0-1.67-.08-2.47-.234 1.67 5.215 6.52 9.012 12.265 9.117-4.498 3.522-10.16 5.62-16.31 5.62-1.06 0-2.107-.06-3.13-.183C5.81 55.827 12.71 58 20.124 58c24.15 0 37.358-20.008 37.358-37.36 0-.568-.013-1.134-.038-1.698 2.566-1.85 4.792-4.163 6.552-6.797"/></svg>

After

Width:  |  Height:  |  Size: 746 B

View File

@@ -1,12 +1,12 @@
"use strict";
import moment from 'moment';
import 'moment/locale/en-gb';
import editorOptions from "./pages/page-form";
const moment = require('moment');
require('moment/locale/en-gb');
const editorOptions = require("./pages/page-form");
moment.locale('en-gb');
export default function (ngApp, events) {
module.exports = function (ngApp, events) {
ngApp.controller('ImageManagerController', ['$scope', '$attrs', '$http', '$timeout', 'imageManagerService',
function ($scope, $attrs, $http, $timeout, imageManagerService) {
@@ -259,39 +259,6 @@ export default function (ngApp, events) {
}]);
ngApp.controller('BookShowController', ['$scope', '$http', '$attrs', '$sce', function ($scope, $http, $attrs, $sce) {
$scope.searching = false;
$scope.searchTerm = '';
$scope.searchResults = '';
$scope.searchBook = function (e) {
e.preventDefault();
let term = $scope.searchTerm;
if (term.length == 0) return;
$scope.searching = true;
$scope.searchResults = '';
let searchUrl = window.baseUrl('/search/book/' + $attrs.bookId);
searchUrl += '?term=' + encodeURIComponent(term);
$http.get(searchUrl).then((response) => {
$scope.searchResults = $sce.trustAsHtml(response.data);
});
};
$scope.checkSearchForm = function () {
if ($scope.searchTerm.length < 1) {
$scope.searching = false;
}
};
$scope.clearSearch = function () {
$scope.searching = false;
$scope.searchTerm = '';
};
}]);
ngApp.controller('PageEditController', ['$scope', '$http', '$attrs', '$interval', '$timeout', '$sce',
function ($scope, $http, $attrs, $interval, $timeout, $sce) {

View File

@@ -1,8 +1,9 @@
"use strict";
import DropZone from "dropzone";
import markdown from "marked";
const DropZone = require("dropzone");
const MarkdownIt = require("markdown-it");
const mdTasksLists = require('markdown-it-task-lists');
export default function (ngApp, events) {
module.exports = function (ngApp, events) {
/**
* Common tab controls using simple jQuery functions.
@@ -214,6 +215,9 @@ export default function (ngApp, events) {
}
}]);
const md = new MarkdownIt({html: true});
md.use(mdTasksLists, {label: true});
/**
* Markdown input
* Handles the logic for just the editor input field.
@@ -231,20 +235,20 @@ export default function (ngApp, events) {
element = element.find('textarea').first();
let content = element.val();
scope.mdModel = content;
scope.mdChange(markdown(content));
scope.mdChange(md.render(content));
element.on('change input', (event) => {
content = element.val();
$timeout(() => {
scope.mdModel = content;
scope.mdChange(markdown(content));
scope.mdChange(md.render(content));
});
});
scope.$on('markdown-update', (event, value) => {
element.val(value);
scope.mdModel = value;
scope.mdChange(markdown(value));
scope.mdChange(md.render(value));
});
}

View File

@@ -1,12 +1,5 @@
"use strict";
// AngularJS - Create application and load components
import angular from "angular";
import "angular-resource";
import "angular-animate";
import "angular-sanitize";
import "angular-ui-sortable";
// Url retrieval function
window.baseUrl = function(path) {
let basePath = document.querySelector('meta[name="base-url"]').getAttribute('content');
@@ -15,11 +8,33 @@ window.baseUrl = function(path) {
return basePath + '/' + path;
};
const Vue = require("vue");
const axios = require("axios");
let axiosInstance = axios.create({
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name=token]').getAttribute('content'),
'baseURL': window.baseUrl('')
}
});
Vue.prototype.$http = axiosInstance;
require("./vues/vues");
// AngularJS - Create application and load components
const angular = require("angular");
require("angular-resource");
require("angular-animate");
require("angular-sanitize");
require("angular-ui-sortable");
let ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Translation setup
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
import Translations from "./translations"
const Translations = require("./translations");
let translator = new Translations(window.translations);
window.trans = translator.get.bind(translator);
@@ -47,11 +62,12 @@ class EventManager {
}
window.Events = new EventManager();
Vue.prototype.$events = window.Events;
// Load in angular specific items
import Services from './services';
import Directives from './directives';
import Controllers from './controllers';
const Services = require('./services');
const Directives = require('./directives');
const Controllers = require('./controllers');
Services(ngApp, window.Events);
Directives(ngApp, window.Events);
Controllers(ngApp, window.Events);
@@ -154,4 +170,4 @@ if(navigator.userAgent.indexOf('MSIE')!==-1
}
// Page specific items
import "./pages/page-show";
require("./pages/page-show");

View File

@@ -60,7 +60,7 @@ function registerEditorShortcuts(editor) {
editor.addShortcut('meta+shift+E', '', ['FormatBlock', false, 'code']);
}
export default function() {
module.exports = function() {
let settings = {
selector: '#html-editor',
content_css: [
@@ -68,6 +68,7 @@ export default function() {
window.baseUrl('/libs/material-design-iconic-font/css/material-design-iconic-font.min.css')
],
body_class: 'page-content',
browser_spellcheck: true,
relative_urls: false,
remove_script_host: false,
document_base_url: window.baseUrl('/'),
@@ -213,4 +214,4 @@ export default function() {
}
};
return settings;
}
};

View File

@@ -1,8 +1,8 @@
"use strict";
// Configure ZeroClipboard
import Clipboard from "clipboard";
const Clipboard = require("clipboard");
export default window.setupPageShow = function (pageId) {
let setupPageShow = window.setupPageShow = function (pageId) {
// Set up pointer
let $pointer = $('#pointer').detach();
@@ -81,6 +81,12 @@ export default window.setupPageShow = function (pageId) {
let $idElem = $(idElem);
let color = $('#custom-styles').attr('data-color-light');
$idElem.css('background-color', color).attr('data-highlighted', 'true').smoothScrollTo();
setTimeout(() => {
$idElem.addClass('anim').addClass('selectFade').css('background-color', '');
setTimeout(() => {
$idElem.removeClass('selectFade');
}, 3000);
}, 100);
} else {
$('.page-content').find(':contains("' + text + '")').smoothScrollTo();
}
@@ -151,3 +157,5 @@ export default window.setupPageShow = function (pageId) {
});
};
module.exports = setupPageShow;

View File

@@ -44,4 +44,4 @@ class Translator {
}
export default Translator
module.exports = Translator;

View File

@@ -0,0 +1,44 @@
let data = {
id: null,
type: '',
searching: false,
searchTerm: '',
searchResults: '',
};
let computed = {
};
let methods = {
searchBook() {
if (this.searchTerm.trim().length === 0) return;
this.searching = true;
this.searchResults = '';
let url = window.baseUrl(`/search/${this.type}/${this.id}`);
url += `?term=${encodeURIComponent(this.searchTerm)}`;
this.$http.get(url).then(resp => {
this.searchResults = resp.data;
});
},
checkSearchForm() {
this.searching = this.searchTerm > 0;
},
clearSearch() {
this.searching = false;
this.searchTerm = '';
}
};
function mounted() {
this.id = Number(this.$el.getAttribute('entity-id'));
this.type = this.$el.getAttribute('entity-type');
}
module.exports = {
data, computed, methods, mounted
};

View File

@@ -0,0 +1,195 @@
const moment = require('moment');
let data = {
terms: '',
termString : '',
search: {
type: {
page: true,
chapter: true,
book: true
},
exactTerms: [],
tagTerms: [],
option: {},
dates: {
updated_after: false,
updated_before: false,
created_after: false,
created_before: false,
}
}
};
let computed = {
};
let methods = {
appendTerm(term) {
this.termString += ' ' + term;
this.termString = this.termString.replace(/\s{2,}/g, ' ');
this.termString = this.termString.replace(/^\s+/, '');
this.termString = this.termString.replace(/\s+$/, '');
},
exactParse(searchString) {
this.search.exactTerms = [];
let exactFilter = /"(.+?)"/g;
let matches;
while ((matches = exactFilter.exec(searchString)) !== null) {
this.search.exactTerms.push(matches[1]);
}
},
exactChange() {
let exactFilter = /"(.+?)"/g;
this.termString = this.termString.replace(exactFilter, '');
let matchesTerm = this.search.exactTerms.filter(term => {
return term.trim() !== '';
}).map(term => {
return `"${term}"`
}).join(' ');
this.appendTerm(matchesTerm);
},
addExact() {
this.search.exactTerms.push('');
setTimeout(() => {
let exactInputs = document.querySelectorAll('.exact-input');
exactInputs[exactInputs.length - 1].focus();
}, 100);
},
removeExact(index) {
this.search.exactTerms.splice(index, 1);
this.exactChange();
},
tagParse(searchString) {
this.search.tagTerms = [];
let tagFilter = /\[(.+?)\]/g;
let matches;
while ((matches = tagFilter.exec(searchString)) !== null) {
this.search.tagTerms.push(matches[1]);
}
},
tagChange() {
let tagFilter = /\[(.+?)\]/g;
this.termString = this.termString.replace(tagFilter, '');
let matchesTerm = this.search.tagTerms.filter(term => {
return term.trim() !== '';
}).map(term => {
return `[${term}]`
}).join(' ');
this.appendTerm(matchesTerm);
},
addTag() {
this.search.tagTerms.push('');
setTimeout(() => {
let tagInputs = document.querySelectorAll('.tag-input');
tagInputs[tagInputs.length - 1].focus();
}, 100);
},
removeTag(index) {
this.search.tagTerms.splice(index, 1);
this.tagChange();
},
typeParse(searchString) {
let typeFilter = /{\s?type:\s?(.*?)\s?}/;
let match = searchString.match(typeFilter);
let type = this.search.type;
if (!match) {
type.page = type.book = type.chapter = true;
return;
}
let splitTypes = match[1].replace(/ /g, '').split('|');
type.page = (splitTypes.indexOf('page') !== -1);
type.chapter = (splitTypes.indexOf('chapter') !== -1);
type.book = (splitTypes.indexOf('book') !== -1);
},
typeChange() {
let typeFilter = /{\s?type:\s?(.*?)\s?}/;
let type = this.search.type;
if (type.page === type.chapter && type.page === type.book) {
this.termString = this.termString.replace(typeFilter, '');
return;
}
let selectedTypes = Object.keys(type).filter(type => {return this.search.type[type];}).join('|');
let typeTerm = '{type:'+selectedTypes+'}';
if (this.termString.match(typeFilter)) {
this.termString = this.termString.replace(typeFilter, typeTerm);
return;
}
this.appendTerm(typeTerm);
},
optionParse(searchString) {
let optionFilter = /{([a-z_\-:]+?)}/gi;
let matches;
while ((matches = optionFilter.exec(searchString)) !== null) {
this.search.option[matches[1].toLowerCase()] = true;
}
},
optionChange(optionName) {
let isChecked = this.search.option[optionName];
if (isChecked) {
this.appendTerm(`{${optionName}}`);
} else {
this.termString = this.termString.replace(`{${optionName}}`, '');
}
},
updateSearch(e) {
e.preventDefault();
window.location = '/search?term=' + encodeURIComponent(this.termString);
},
enableDate(optionName) {
this.search.dates[optionName.toLowerCase()] = moment().format('YYYY-MM-DD');
this.dateChange(optionName);
},
dateParse(searchString) {
let dateFilter = /{([a-z_\-]+?):([a-z_\-0-9]+?)}/gi;
let dateTags = Object.keys(this.search.dates);
let matches;
while ((matches = dateFilter.exec(searchString)) !== null) {
if (dateTags.indexOf(matches[1]) === -1) continue;
this.search.dates[matches[1].toLowerCase()] = matches[2];
}
},
dateChange(optionName) {
let dateFilter = new RegExp('{\\s?'+optionName+'\\s?:([a-z_\\-0-9]+?)}', 'gi');
this.termString = this.termString.replace(dateFilter, '');
if (!this.search.dates[optionName]) return;
this.appendTerm(`{${optionName}:${this.search.dates[optionName]}}`);
},
dateRemove(optionName) {
this.search.dates[optionName] = false;
this.dateChange(optionName);
}
};
function created() {
this.termString = document.querySelector('[name=searchTerm]').value;
this.typeParse(this.termString);
this.exactParse(this.termString);
this.tagParse(this.termString);
this.optionParse(this.termString);
this.dateParse(this.termString);
}
module.exports = {
data, computed, methods, created
};

View File

@@ -0,0 +1,18 @@
const Vue = require("vue");
function exists(id) {
return document.getElementById(id) !== null;
}
let vueMapping = {
'search-system': require('./search'),
'entity-dashboard': require('./entity-search'),
};
Object.keys(vueMapping).forEach(id => {
if (exists(id)) {
let config = vueMapping[id];
config.el = '#' + id;
new Vue(config);
}
});

View File

@@ -2,7 +2,7 @@
.anim.fadeIn {
opacity: 0;
animation-name: fadeIn;
animation-duration: 160ms;
animation-duration: 180ms;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
}
@@ -126,4 +126,8 @@
animation-duration: 180ms;
animation-delay: 0s;
animation-timing-function: cubic-bezier(.62, .28, .23, .99);
}
.anim.selectFade {
transition: background-color ease-in-out 3000ms;
}

View File

@@ -135,11 +135,19 @@
border-left: 3px solid #BBB;
background-color: #EEE;
padding: $-s;
padding-left: $-xl;
display: block;
position: relative;
&:before {
font-family: 'Material-Design-Iconic-Font';
padding-right: $-s;
left: $-xs + 4px;
top: 50%;
margin-top: -9px;
//top: $-xs + 5px;
display: inline-block;
position: absolute;
font-size: 1.222em;
line-height: 1;
}
&.success {
border-left-color: $positive;

View File

@@ -54,6 +54,9 @@ $button-border-radius: 2px;
&.muted {
@include generate-button-colors(#EEE, #888);
}
&.muted-light {
@include generate-button-colors(#666, #e4e4e4);
}
}
.text-button {
@@ -92,6 +95,9 @@ $button-border-radius: 2px;
width: 100%;
text-align: center;
display: block;
&.text-left {
text-align: left;
}
}
.button.icon {
@@ -100,6 +106,19 @@ $button-border-radius: 2px;
}
}
.button.svg {
svg {
display: inline-block;
position: absolute;
left: $-m;
top: $-s - 2px;
width: 24px;
}
padding: $-s $-m;
padding-bottom: $-s - 2px;
padding-left: $-m*2 + 24px;
}
.button[disabled] {
background-color: #BBB;
cursor: default;

View File

@@ -98,19 +98,36 @@ label {
label.radio, label.checkbox {
font-weight: 400;
user-select: none;
input[type="radio"], input[type="checkbox"] {
margin-right: $-xs;
}
}
label.inline.checkbox {
margin-right: $-m;
}
label + p.small {
margin-bottom: 0.8em;
}
input[type="text"], input[type="number"], input[type="email"], input[type="search"], input[type="url"], input[type="password"], select, textarea {
table.form-table {
max-width: 100%;
td {
overflow: hidden;
padding: $-xxs/2 0;
}
}
input[type="text"], input[type="number"], input[type="email"], input[type="date"], input[type="search"], input[type="url"], input[type="password"], select, textarea {
@extend .input-base;
}
input[type=date] {
width: 190px;
}
.toggle-switch {
display: inline-block;
background-color: #BBB;

View File

@@ -55,20 +55,6 @@ div[class^="col-"] img {
}
}
.center-box {
margin: $-xl auto 0 auto;
padding: $-m $-xxl $-xl*2 $-xxl;
max-width: 346px;
display: inline-block;
text-align: left;
vertical-align: top;
&.login {
background-color: #EEE;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #DDD;
}
}
.row {
margin-left: -$-m;
margin-right: -$-m;

View File

@@ -109,6 +109,7 @@
transition-property: right, border;
border-left: 0px solid #FFF;
background-color: #FFF;
max-width: 320px;
&.fixed {
background-color: #FFF;
z-index: 5;

View File

@@ -16,7 +16,7 @@ h2 {
}
h3 {
font-size: 2.333em;
line-height: 1.571428572em;
line-height: 1.221428572em;
margin-top: 0.78571429em;
margin-bottom: 0.43137255em;
}
@@ -71,6 +71,13 @@ a, .link {
padding-right: 0;
padding-left: $-s;
}
&.icon {
display: inline-block;
}
svg {
position: relative;
display: inline-block;
}
}
/*
@@ -84,7 +91,6 @@ p, ul, ol, pre, table, blockquote {
hr {
border: 0;
height: 1px;
border: 0;
background: #EAEAEA;
margin-bottom: $-l;
&.faded {
@@ -119,6 +125,11 @@ sup, .superscript {
font-size: 0.8em;
}
sub, .subscript {
vertical-align: sub;
font-size: 0.8em;
}
pre {
font-family: monospace;
white-space:pre;
@@ -258,16 +269,36 @@ span.highlight {
/*
* Lists
*/
ul, ol {
overflow: hidden;
p {
margin: 0;
}
}
ul {
padding-left: $-m * 1.3;
list-style: disc;
overflow: hidden;
ul {
list-style: circle;
margin-top: 0;
margin-bottom: 0;
}
label {
margin: 0;
}
}
ol {
list-style: decimal;
padding-left: $-m * 2;
overflow: hidden;
}
li.checkbox-item, li.task-list-item {
list-style: none;
margin-left: - ($-m * 1.3);
input[type="checkbox"] {
margin-right: $-xs;
}
}
/*

View File

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

View File

@@ -7,8 +7,8 @@
@import "grid";
@import "blocks";
@import "buttons";
@import "forms";
@import "tables";
@import "forms";
@import "animations";
@import "tinymce";
@import "highlightjs";
@@ -17,7 +17,11 @@
@import "lists";
@import "pages";
[v-cloak], [v-show] {display: none;}
[v-cloak], [v-show] {
display: none; opacity: 0;
animation-name: none !important;
}
[ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important;
@@ -251,15 +255,24 @@ $btt-size: 40px;
}
}
.center-box {
margin: $-xl auto 0 auto;
padding: $-m $-xxl $-xl $-xxl;
width: 420px;
max-width: 100%;
display: inline-block;
text-align: left;
vertical-align: top;
//border: 1px solid #DDD;
input {
width: 100%;
}
&.login {
background-color: #EEE;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.1);
border: 1px solid #DDD;
}
}

View File

@@ -14,7 +14,50 @@ return [
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuchen sie es in :seconds Sekunden erneut.',
/**
* Email Confirmation Text
* Login & Register
*/
'sign_up' => 'Registrieren',
'log_in' => 'Anmelden',
'logout' => 'Abmelden',
'name' => 'Name',
'username' => 'Benutzername',
'email' => 'E-Mail',
'password' => 'Passwort',
'password_confirm' => 'Passwort best&auml;tigen',
'password_hint' => 'Mindestlänge: 5 Zeichen',
'forgot_password' => 'Passwort vergessen?',
'remember_me' => 'Angemeldet bleiben',
'ldap_email_hint' => 'Bitte geben Sie eine E-Mail-Adresse ein, um diese mit dem Account zu nutzen.',
'create_account' => 'Account anlegen',
'social_login' => 'Social Login',
'social_registration' => 'Social Registrierung',
'social_registration_text' => 'Mit einem dieser Möglichkeiten registrieren oder anmelden.',
'register_thanks' => 'Vielen Dank für Ihre Registrierung!',
'register_confirm' => 'Bitte prüfen Sie Ihren E-Mail Eingang und klicken auf den Verifizieren-Button, um :appName nutzen zu können.',
'registrations_disabled' => 'Die Registrierung ist momentan nicht möglich',
'registration_email_domain_invalid' => 'Diese E-Mail-Domain ist für die Benutzer der Applikation nicht freigeschaltet.',
'register_success' => 'Vielen Dank für Ihre Registrierung! Die Daten sind gespeichert und Sie sind angemeldet.',
/**
* Password Reset
*/
'reset_password' => 'Passwort vergessen',
'reset_password_send_instructions' => 'Bitte geben Sie unten Ihre E-Mail-Adresse ein und Sie erhalten eine E-Mail, um Ihr Passwort zurück zu setzen.',
'reset_password_send_button' => 'Passwort zurücksetzen',
'reset_password_sent_success' => 'Eine E-Mail mit den Instruktionen, um Ihr Passwort zurückzusetzen wurde an :email gesendet.',
'reset_password_success' => 'Ihr Passwort wurde erfolgreich zurück gesetzt.',
'email_reset_subject' => 'Passwort zurücksetzen für :appName',
'email_reset_text' => 'Sie erhalten diese E-Mail, weil eine Passwort-Rücksetzung für Ihren Account beantragt wurde.',
'email_reset_not_requested' => 'Wenn Sie die Passwort-Rücksetzung nicht ausgelöst haben, ist kein weiteres Handeln notwendig.',
/**
* Email Confirmation
*/
'email_confirm_subject' => 'Best&auml;tigen sie ihre E-Mail Adresse bei :appName',
'email_confirm_greeting' => 'Danke, dass sie :appName beigetreten sind!',
@@ -23,4 +66,10 @@ return [
'email_confirm_send_error' => 'Best&auml;tigungs-E-Mail ben&ouml;tigt, aber das System konnte die E-Mail nicht versenden. Kontaktieren sie den Administrator, um sicherzustellen, dass das Sytsem korrekt eingerichtet ist.',
'email_confirm_success' => 'Ihre E-Mail Adresse wurde best&auml;tigt!',
'email_confirm_resent' => 'Best&auml;tigungs-E-Mail wurde erneut versendet, bitte &uuml;berpr&uuml;fen sie ihren Posteingang.',
'email_not_confirmed' => 'E-Mail-Adresse ist nicht bestätigt',
'email_not_confirmed_text' => 'Ihre E-Mail-Adresse ist bisher nicht bestätigt.',
'email_not_confirmed_click_link' => 'Bitte klicken Sie auf den Link in der E-Mail, die Sie nach der Registrierung erhalten haben.',
'email_not_confirmed_resend' => 'Wenn Sie die E-Mail nicht erhalten haben, können Sie die Nachricht erneut anfordern. Füllen Sie hierzu bitte das folgende Formular aus:',
'email_not_confirmed_resend_button' => 'Bestätigungs E-Mail erneut senden',
];

View File

@@ -0,0 +1,58 @@
<?php
return [
/**
* Buttons
*/
'cancel' => 'Abbrechen',
'confirm' => 'Bestätigen',
'back' => 'Zurück',
'save' => 'Speichern',
'continue' => 'Weiter',
'select' => 'Auswählen',
/**
* Form Labels
*/
'name' => 'Name',
'description' => 'Beschreibung',
'role' => 'Rolle',
/**
* Actions
*/
'actions' => 'Aktionen',
'view' => 'Anzeigen',
'create' => 'Anlegen',
'update' => 'Aktualisieren',
'edit' => 'Bearbeiten',
'sort' => 'Sortieren',
'move' => 'Verschieben',
'delete' => 'L&ouml;schen',
'search' => 'Suchen',
'search_clear' => 'Suche l&ouml;schen',
'reset' => 'Zurücksetzen',
'remove' => 'Entfernen',
/**
* Misc
*/
'deleted_user' => 'Gel&ouml;schte Benutzer',
'no_activity' => 'Keine Aktivit&auml;ten zum Anzeigen',
'no_items' => 'Keine Eintr&auml;ge gefunden.',
'back_to_top' => 'nach oben',
'toggle_details' => 'Details zeigen/verstecken',
/**
* Header
*/
'view_profile' => 'Profil ansehen',
'edit_profile' => 'Profil bearbeiten',
/**
* Email Content
*/
'email_action_help' => 'Sollte es beim Anklicken des ":actionText" Buttons Probleme geben, kopieren Sie folgende URL und fügen diese in Ihrem Webbrowser ein:',
'email_rights' => 'Alle Rechte vorbehalten',
];

View File

@@ -0,0 +1,24 @@
<?php
return [
/**
* Image Manager
*/
'image_select' => 'Bild auswählen',
'image_all' => 'Alle',
'image_all_title' => 'Alle Bilder anzeigen',
'image_book_title' => 'Zeige alle Bilder, die in dieses Buch hochgeladen wurden',
'image_page_title' => 'Zeige alle Bilder, die auf diese Seite hochgeladen wurden',
'image_search_hint' => 'Nach Bildnamen suchen',
'image_uploaded' => 'Hochgeladen am :uploadedDate',
'image_load_more' => 'Mehr',
'image_image_name' => 'Bildname',
'image_delete_confirm' => 'Dieses Bild wird auf den folgenden Seiten benutzt. Bitte klicken Sie erneut auf löschen, wenn Sie dieses Bild tatsächlich entfernen möchten.',
'image_select_image' => 'Bild auswählen',
'image_dropzone' => 'Ziehen Sie Bilder hier hinein oder klicken Sie hier, um ein Bild auszuwählen',
'images_deleted' => 'Bilder gelöscht',
'image_preview' => 'Bildvorschau',
'image_upload_success' => 'Bild erfolgreich hochgeladen',
'image_update_success' => 'Bilddetails erfolgreich aktualisiert',
'image_delete_success' => 'Bild erfolgreich gelöscht'
];

View File

@@ -0,0 +1,216 @@
<?php
return [
/**
* Shared
*/
'recently_created' => 'K&uuml;rzlich angelegt',
'recently_created_pages' => 'K&uuml;rzlich angelegte Seiten',
'recently_updated_pages' => 'K&uuml;rzlich aktualisierte Seiten',
'recently_created_chapters' => 'K&uuml;rzlich angelegte Kapitel',
'recently_created_books' => 'K&uuml;rzlich angelegte B&uuml;cher',
'recently_update' => 'K&uuml;rzlich aktualisiert',
'recently_viewed' => 'K&uuml;rzlich angesehen',
'recent_activity' => 'K&uuml;rzliche Aktivit&auml;t',
'create_now' => 'Jetzt anlegen',
'revisions' => 'Revisionen',
'meta_created' => 'Angelegt am :timeLength',
'meta_created_name' => 'Angelegt am :timeLength durch :user',
'meta_updated' => 'Aktualisiert am :timeLength',
'meta_updated_name' => 'Aktualisiert am :timeLength durch :user',
'x_pages' => ':count Seiten',
'entity_select' => 'Eintrag ausw&auml;hlen',
'images' => 'Bilder',
'my_recent_drafts' => 'Meine k&uuml;rzlichen Entw&uuml;rfe',
'my_recently_viewed' => 'K&uuml;rzlich von mir angesehen',
'no_pages_viewed' => 'Sie haben bisher keine Seiten angesehen.',
'no_pages_recently_created' => 'Sie haben bisher keine Seiten angelegt.',
'no_pages_recently_updated' => 'Sie haben bisher keine Seiten aktualisiert.',
'export' => 'Exportieren',
'export_html' => 'HTML-Datei',
'export_pdf' => 'PDF-Datei',
'export_text' => 'Text-Datei',
/**
* Permissions and restrictions
*/
'permissions' => 'Berechtigungen',
'permissions_intro' => 'Wenn individuelle Berechtigungen aktiviert werden, &uuml;berschreiben diese Einstellungen durch Rollen zugewiesene Berechtigungen.',
'permissions_enable' => 'Individuelle Berechtigungen aktivieren',
'permissions_save' => 'Berechtigungen speichern',
/**
* Search
*/
'search_results' => 'Suchergebnisse',
'search_clear' => 'Suche zur&uuml;cksetzen',
'search_no_pages' => 'Es wurden keine passenden Suchergebnisse gefunden',
'search_for_term' => 'Suche nach :term',
/**
* Books
*/
'book' => 'Buch',
'books' => 'B&uuml;cher',
'books_empty' => 'Es wurden keine B&uuml;cher angelegt',
'books_popular' => 'Popul&auml;re B&uuml;cher',
'books_recent' => 'K&uuml;rzlich genutzte B&uuml;cher',
'books_popular_empty' => 'Die popul&auml;rsten B&uuml;cher werden hier angezeigt.',
'books_create' => 'Neues Buch anlegen',
'books_delete' => 'Buch l&ouml;schen',
'books_delete_named' => 'Buch :bookName l&ouml;schen',
'books_delete_explain' => 'Sie m&ouml;chten das Buch \':bookName\' l&ouml;schen und alle Seiten und Kapitel entfernen.',
'books_delete_confirmation' => 'Sind Sie sicher, dass Sie dieses Buch l&ouml;schen m&ouml;chten?',
'books_edit' => 'Buch bearbeiten',
'books_edit_named' => 'Buch :bookName bearbeiten',
'books_form_book_name' => 'Buchname',
'books_save' => 'Buch speichern',
'books_permissions' => 'Buch-Berechtigungen',
'books_permissions_updated' => 'Buch-Berechtigungen aktualisiert',
'books_empty_contents' => 'Es sind noch keine Seiten oder Kapitel f&uuml;r dieses Buch angelegt.',
'books_empty_create_page' => 'Neue Seite anlegen',
'books_empty_or' => 'oder',
'books_empty_sort_current_book' => 'Aktuelles Buch sortieren',
'books_empty_add_chapter' => 'Neues Kapitel hinzuf&uuml;gen',
'books_permissions_active' => 'Buch-Berechtigungen aktiv',
'books_search_this' => 'Dieses Buch durchsuchen',
'books_navigation' => 'Buch-Navigation',
'books_sort' => 'Buchinhalte sortieren',
'books_sort_named' => 'Buch :bookName sortieren',
'books_sort_show_other' => 'Andere B&uuml;cher zeigen',
'books_sort_save' => 'Neue Reihenfolge speichern',
/**
* Chapters
*/
'chapter' => 'Kapitel',
'chapters' => 'Kapitel',
'chapters_popular' => 'Popul&auml;re Kapitel',
'chapters_new' => 'Neues Kapitel',
'chapters_create' => 'Neues Kapitel anlegen',
'chapters_delete' => 'Kapitel entfernen',
'chapters_delete_named' => 'Kapitel :chapterName entfernen',
'chapters_delete_explain' => 'Sie m&ouml;chten das Kapitel \':chapterName\' l&ouml;schen und alle Seiten dem direkten Eltern-Buch hinzugef&uuml;gen.',
'chapters_delete_confirm' => 'Sind Sie sicher, dass Sie dieses Kapitel l&ouml;schen m&ouml;chten?',
'chapters_edit' => 'Kapitel bearbeiten',
'chapters_edit_named' => 'Kapitel :chapterName bearbeiten',
'chapters_save' => 'Kapitel speichern',
'chapters_move' => 'Kapitel verschieben',
'chapters_move_named' => 'Kapitel :chapterName verschieben',
'chapter_move_success' => 'Kapitel in das Buch :bookName verschoben.',
'chapters_permissions' => 'Kapitel-Berechtigungen',
'chapters_empty' => 'Aktuell sind keine Kapitel in diesem Buch angelegt.',
'chapters_permissions_active' => 'Kapitel-Berechtigungen aktiv',
'chapters_permissions_success' => 'Kapitel-Berechtigungenen aktualisisert',
/**
* Pages
*/
'page' => 'Seite',
'pages' => 'Seiten',
'pages_popular' => 'Popul&auml;re Seiten',
'pages_new' => 'Neue Seite',
'pages_attachments' => 'Anh&auml;nge',
'pages_navigation' => 'Seitennavigation',
'pages_delete' => 'Seite l&ouml;schen',
'pages_delete_named' => 'Seite :pageName l&ouml;schen',
'pages_delete_draft_named' => 'Seitenentwurf von :pageName l&ouml;schen',
'pages_delete_draft' => 'Seitenentwurf l&ouml;schen',
'pages_delete_success' => 'Seite gel&ouml;scht',
'pages_delete_draft_success' => 'Seitenentwurf gel&ouml;scht',
'pages_delete_confirm' => 'Sind Sie sicher, dass Sie diese Seite l&ouml;schen m&ouml;chen?',
'pages_delete_draft_confirm' => 'Sind Sie sicher, dass Sie diesen Seitenentwurf l&ouml;schen m&ouml;chten?',
'pages_editing_named' => 'Seite :pageName bearbeiten',
'pages_edit_toggle_header' => 'Toggle header',
'pages_edit_save_draft' => 'Entwurf speichern',
'pages_edit_draft' => 'Seitenentwurf bearbeiten',
'pages_editing_draft' => 'Seitenentwurf bearbeiten',
'pages_editing_page' => 'Seite bearbeiten',
'pages_edit_draft_save_at' => 'Entwurf gespeichert um ',
'pages_edit_delete_draft' => 'Entwurf l&ouml;schen',
'pages_edit_discard_draft' => 'Entwurf verwerfen',
'pages_edit_set_changelog' => 'Ver&auml;nderungshinweis setzen',
'pages_edit_enter_changelog_desc' => 'Bitte geben Sie eine kurze Zusammenfassung Ihrer &Auml;nderungen ein',
'pages_edit_enter_changelog' => 'Ver&auml;nderungshinweis eingeben',
'pages_save' => 'Seite speichern',
'pages_title' => 'Seitentitel',
'pages_name' => 'Seitenname',
'pages_md_editor' => 'Redakteur',
'pages_md_preview' => 'Vorschau',
'pages_md_insert_image' => 'Bild einf&uuml;gen',
'pages_md_insert_link' => 'Link zu einem Objekt einf&uuml;gen',
'pages_not_in_chapter' => 'Seite ist in keinem Kapitel',
'pages_move' => 'Seite verschieben',
'pages_move_success' => 'Seite nach ":parentName" verschoben',
'pages_permissions' => 'Seiten Berechtigungen',
'pages_permissions_success' => 'Seiten Berechtigungen aktualisiert',
'pages_revisions' => 'Seitenversionen',
'pages_revisions_named' => 'Seitenversionen von :pageName',
'pages_revision_named' => 'Seitenversion von :pageName',
'pages_revisions_created_by' => 'Angelegt von',
'pages_revisions_date' => 'Versionsdatum',
'pages_revisions_changelog' => 'Ver&auml;nderungshinweise',
'pages_revisions_changes' => 'Ver&auml;nderungen',
'pages_revisions_current' => 'Aktuelle Version',
'pages_revisions_preview' => 'Vorschau',
'pages_revisions_restore' => 'Zur&uuml;ck sichern',
'pages_revisions_none' => 'Diese Seite hat keine &auml;lteren Versionen.',
'pages_copy_link' => 'Link kopieren',
'pages_permissions_active' => 'Seiten-Berechtigungen aktiv',
'pages_initial_revision' => 'Erste Ver&ouml;ffentlichung',
'pages_initial_name' => 'Neue Seite',
'pages_editing_draft_notification' => 'Sie bearbeiten momenten einen Entwurf, der zuletzt um :timeDiff gespeichert wurde.',
'pages_draft_edited_notification' => 'Diese Seite wurde seit diesem Zeitpunkt ver&auml;ndert. Wir empfehlen Ihnen, diesen Entwurf zu verwerfen.',
'pages_draft_edit_active' => [
'start_a' => ':count Benutzer haben die Bearbeitung dieser Seite begonnen.',
'start_b' => ':userName hat die Bearbeitung dieser Seite begonnen.',
'time_a' => 'seit die Seiten zuletzt aktualisiert wurden.',
'time_b' => 'in den letzten :minCount Minuten',
'message' => ':start :time. Achten Sie darauf keine Aktualisierungen von anderen Benutzern zu &uuml;berschreiben!',
],
'pages_draft_discarded' => 'Entwurf verworfen. Der aktuelle Seiteninhalt wurde geladen.',
/**
* Editor sidebar
*/
'page_tags' => 'Seiten-Schlagw&ouml;rter',
'tag' => 'Schlagwort',
'tags' => 'Schlagworte',
'tag_value' => 'Schlagwortinhalt (Optional)',
'tags_explain' => "F&uuml;gen Sie Schlagworte hinzu, um Ihren Inhalt zu kategorisieren. \n Sie k&ouml;nnen einen erkl&auml;renden Inhalt hinzuf&uuml;gen, um eine genauere Unterteilung vorzunehmen.",
'tags_add' => 'Weiteres Schlagwort hinzuf&uuml;gen',
'attachments' => 'Anh&auml;nge',
'attachments_explain' => 'Sie k&ouml;nnen auf Ihrer Seite Dateien hochladen oder Links anf&uuml;gen. Diese werden in der seitlich angezeigt.',
'attachments_explain_instant_save' => '&Auml;nderungen werden direkt gespeichert.',
'attachments_items' => 'Angef&uuml;gte Elemente',
'attachments_upload' => 'Datei hochladen',
'attachments_link' => 'Link anf&uuml;gen',
'attachments_set_link' => 'Link setzen',
'attachments_delete_confirm' => 'Klicken Sie erneut auf l&ouml;schen, um diesen Anhang zu entfernen.',
'attachments_dropzone' => 'Ziehen Sie Dateien hier hinein oder klicken Sie hier, um eine Datei auszuw&auml;hlen',
'attachments_no_files' => 'Es wurden bisher keine Dateien hochgeladen.',
'attachments_explain_link' => 'Wenn Sie keine Datei hochladen m&ouml;chten, k&ouml;nnen Sie stattdessen einen Link anf&uuml;gen. Dieser Link kann auf eine andere Seite oder zu einer Datei in der Cloud weisen.',
'attachments_link_name' => 'Link-Name',
'attachment_link' => 'Link zum Anhang',
'attachments_link_url' => 'Link zu einer Datei',
'attachments_link_url_hint' => 'URL einer Seite oder Datei',
'attach' => 'anf&uuml;gen',
'attachments_edit_file' => 'Datei bearbeiten',
'attachments_edit_file_name' => 'Dateiname',
'attachments_edit_drop_upload' => 'Ziehen Sie Dateien hier hinein, um diese hochzuladen und zu &uuml;berschreiben',
'attachments_order_updated' => 'Reihenfolge der Anh&auml;nge aktualisiert',
'attachments_updated_success' => 'Anhang-Details aktualisiert',
'attachments_deleted' => 'Anhang gel&ouml;scht',
'attachments_file_uploaded' => 'Datei erfolgrecich hochgeladen',
'attachments_file_updated' => 'Datei erfolgreich aktualisisert',
'attachments_link_attached' => 'Link erfolgreich der Seite hinzugef&uuml;gt',
/**
* Profile View
*/
'profile_user_for_x' => 'Benutzer seit :time',
'profile_created_content' => 'Angelegte Inhalte',
'profile_not_created_pages' => ':userName hat bisher keine Seiten angelegt.',
'profile_not_created_chapters' => ':userName hat bisher keine Kapitel angelegt.',
'profile_not_created_books' => ':userName hat bisher keine B&uuml;cher angelegt.',
];

View File

@@ -8,5 +8,63 @@ return [
// Pages
'permission' => 'Sie haben keine Berechtigung auf diese Seite zuzugreifen.',
'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuf&uuml;hren.'
'permissionJson' => 'Sie haben keine Berechtigung die angeforderte Aktion auszuf&uuml;hren.',
// Auth
'error_user_exists_different_creds' => 'Ein Benutzer mit der E-Mail-Adresse :email ist bereits mit anderen Anmeldedaten angelegt.',
'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits best&auml;tigt. Bitte melden Sie sich an.',
'email_confirmation_invalid' => 'Der Best&auml;tigungs-Token ist nicht g&uuml;ltig oder wurde bereits verwendet. Bitte registrieren Sie sich erneut.',
'email_confirmation_expired' => 'Der Best&auml;tigungs-Token ist abgelaufen. Es wurde eine neue Best&auml;tigungs-E-Mail gesendet.',
'ldap_fail_anonymous' => 'Anonymer LDAP Zugriff ist fehlgeschlafgen',
'ldap_fail_authed' => 'LDAP Zugriff mit DN & Passwort ist fehlgeschlagen',
'ldap_extension_not_installed' => 'LDAP PHP Erweiterung ist nicht installiert.',
'ldap_cannot_connect' => 'Die Verbindung zu LDAP-Server ist fehlgeschlagen. Beim initialen Verbindungsaufbau trat ein Fehler auf.',
'social_no_action_defined' => 'Es ist keine Aktion definiert',
'social_account_in_use' => 'Dieses :socialAccount Konto wird bereits verwendet. Bitte melden Sie sich mit dem :socialAccount Konto an.',
'social_account_email_in_use' => 'Die E-Mail-Adresse :email ist bereits registriert. Wenn Sie bereits registriert sind, k&ouml;nnen Sie Ihr :socialAccount Konto in Ihren Profil-Einstellungen verkn&uuml;pfen.',
'social_account_existing' => 'Dieses :socialAccount Konto ist bereits mit Ihrem Profil verkn&uuml;pft.',
'social_account_already_used_existing' => 'Dieses :socialAccount Konto wird bereits durch einen anderen Benutzer verwendet.',
'social_account_not_used' => 'Dieses :socialAccount Konto ist bisher keinem Benutzer zugeordnet. Bitte verkn&uuml;pfen Sie deses in Ihrem Profil-Einstellungen.',
'social_account_register_instructions' => 'Wenn Sie bisher keinen Social-Media Konto besitzen k&ouml;nnen Sie ein solches Konto mit der :socialAccount Option anlegen.',
'social_driver_not_found' => 'Social-Media Konto Treiber nicht gefunden',
'social_driver_not_configured' => 'Ihr :socialAccount Konto ist nicht korrekt konfiguriert.',
// System
'path_not_writable' => 'Die Datei kann nicht in den angegebenen Pfad :filePath hochgeladen werden. Stellen Sie sicher, dass dieser Ordner auf dem Server beschreibbar ist.',
'cannot_get_image_from_url' => 'Bild konnte nicht von der URL :url geladen werden.',
'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte pr&uuml;fen Sie, ob Sie die GD PHP Erweiterung installiert haben.',
'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigr&ouml;&szlig;e. Bitte versuchen Sie es mit einer kleineren Datei.',
'image_upload_error' => 'Beim Hochladen des Bildes trat ein Fehler auf.',
// Attachments
'attachment_page_mismatch' => 'Die Seite stimmt nach dem Hochladen des Anhangs nicht &uuml;berein.',
// Pages
'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stellen Sie sicher, dass Sie mit dem Internet verbunden sind, bevor Sie den Entwurf dieser Seite speichern.',
// Entities
'entity_not_found' => 'Eintrag nicht gefunden',
'book_not_found' => 'Buch nicht gefunden',
'page_not_found' => 'Seite nicht gefunden',
'chapter_not_found' => 'Kapitel nicht gefunden',
'selected_book_not_found' => 'Das gew&auml;hlte Buch wurde nicht gefunden.',
'selected_book_chapter_not_found' => 'Das gew&auml;hlte Buch oder Kapitel wurde nicht gefunden.',
'guests_cannot_save_drafts' => 'G&auml;ste k&ouml;nnen keine Entw&uuml;rfe speichern',
// Users
'users_cannot_delete_only_admin' => 'Sie k&ouml;nnen den einzigen Administrator nicht l&ouml;schen.',
'users_cannot_delete_guest' => 'Sie k&ouml;nnen den Gast-Benutzer nicht l&ouml;schen',
// Roles
'role_cannot_be_edited' => 'Diese Rolle kann nicht bearbeitet werden.',
'role_system_cannot_be_deleted' => 'Dies ist eine Systemrolle und kann nicht gel&ouml;scht werden',
'role_registration_default_cannot_delete' => 'Diese Rolle kann nicht gel&ouml;scht werden solange sie als Standardrolle f&uuml;r neue Registrierungen gesetzt ist',
// Error pages
'404_page_not_found' => 'Seite nicht gefunden',
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Sie angefordert haben wurde nicht gefunden.',
'return_home' => 'Zur&uuml;ck zur Startseite',
'error_occurred' => 'Es ist ein Fehler aufgetreten',
'app_down' => ':appName befindet sich aktuell im Wartungsmodus.',
'back_soon' => 'Wir werden so schnell wie m&ouml;glich wieder online sein.',
];

View File

@@ -16,7 +16,7 @@ return [
'password' => 'Pass&ouml;rter m&uuml;ssen mindestens sechs Zeichen enthalten und die Wiederholung muss identisch sein.',
'user' => "Wir k&ouml;nnen keinen Benutzer mit dieser E-Mail Adresse finden.",
'token' => 'Dieser Passwort-Reset-Token ist ung&uuml;ltig.',
'sent' => 'Wir haben ihnen eine E-Mail mit einem Link zum Zurücksetzen des Passworts zugesendet!',
'sent' => 'Wir haben Ihnen eine E-Mail mit einem Link zum Zur&uuml;cksetzen des Passworts zugesendet!',
'reset' => 'Ihr Passwort wurde zur&uuml;ckgesetzt!',
];

View File

@@ -10,14 +10,19 @@ return [
'settings' => 'Einstellungen',
'settings_save' => 'Einstellungen speichern',
'settings_save_success' => 'Einstellungen gespeichert',
/**
* App settings
*/
'app_settings' => 'Anwendungseinstellungen',
'app_name' => 'Anwendungsname',
'app_name_desc' => 'Dieser Name wird im Header und E-Mails angezeigt.',
'app_name_desc' => 'Dieser Name wird im Header und in E-Mails angezeigt.',
'app_name_header' => 'Anwendungsname im Header anzeigen?',
'app_public_viewing' => '&Ouml;ffentliche Ansicht erlauben?',
'app_secure_images' => 'Erh&ouml;hte Sicherheit f&uuml;r Bilduploads aktivieren?',
'app_secure_images_desc' => 'Aus Leistungsgr&uuml;nden sind alle Bilder &ouml;ffentlich sichtbar. Diese Option f&uuml;gt zuf&auml;llige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugrif zu verhindern.',
'app_secure_images_desc' => 'Aus Leistungsgr&uuml;nden sind alle Bilder &ouml;ffentlich sichtbar. Diese Option f&uuml;gt zuf&auml;llige, schwer zu eratene, Zeichenketten vor die Bild-URLs hinzu. Stellen sie sicher, dass Verzeichnindexes deaktiviert sind, um einen einfachen Zugriff zu verhindern.',
'app_editor' => 'Seiteneditor',
'app_editor_desc' => 'W&auml;hlen sie den Editor aus, der von allen Benutzern genutzt werden soll, um Seiten zu editieren.',
'app_custom_html' => 'Benutzerdefinierter HTML <head> Inhalt',
@@ -25,15 +30,82 @@ return [
'app_logo' => 'Anwendungslogo',
'app_logo_desc' => 'Dieses Bild sollte 43px hoch sein. <br>Gr&ouml;&szlig;ere Bilder werden verkleinert.',
'app_primary_color' => 'Prim&auml;re Anwendungsfarbe',
'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein. <br>Leer lassen des Feldes setzt auf die Standard-Anwendungsfarbe zur&uuml;ck.',
'app_primary_color_desc' => 'Dies sollte ein HEX Wert sein. <br>Wenn Sie nicht eingeben, wird die Anwendung auf die Standardfarbe zur&uuml;ckgesetzt.',
/**
* Registration settings
*/
'reg_settings' => 'Registrierungseinstellungen',
'reg_allow' => 'Registrierung erlauben?',
'reg_default_role' => 'Standard-Benutzerrolle nach Registrierung',
'reg_confirm_email' => 'Best&auml;tigung per E-Mail erforderlich?',
'reg_confirm_email_desc' => 'Falls die Einschr&auml;nkung f&uumlr; Domains genutzt wird, ist die Best&auml;tigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
'reg_confirm_email_desc' => 'Falls die Einschr&auml;nkung f&uuml;r Domains genutzt wird, ist die Best&auml;tigung per E-Mail zwingend erforderlich und der untenstehende Wert wird ignoriert.',
'reg_confirm_restrict_domain' => 'Registrierung auf bestimmte Domains einschr&auml;nken',
'reg_confirm_restrict_domain_desc' => 'F&uuml;gen sie eine, durch Komma getrennte, Liste von E-Mail Domains hinzu, auf die die Registrierung eingeschr&auml;nkt werden soll. Benutzern wird eine E-Mail gesendet, um ihre E-Mail Adresse zu best&auml;tigen, bevor sie diese Anwendung nutzen k&ouml;nnen. <br> Hinweis: Benutzer k&ouml;nnen ihre E-Mail Adresse nach erfolgreicher Registrierung &auml;ndern.',
'reg_confirm_restrict_domain_placeholder' => 'Keine Einschr&auml;nkung gesetzt',
/**
* Role settings
*/
'roles' => 'Rollen',
'role_user_roles' => 'Benutzer-Rollen',
'role_create' => 'Neue Rolle anlegen',
'role_create_success' => 'Rolle erfolgreich angelegt',
'role_delete' => 'Rolle l&ouml;schen',
'role_delete_confirm' => 'Sie m&ouml;chten die Rolle \':roleName\' l&ouml;schen.',
'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Sie k&ouml;nnen unten eine neue Rolle ausw&auml;hlen, die Sie diesen Benutzern zuordnen m&ouml;chten.',
'role_delete_no_migration' => "Den Benutzern keine andere Rolle zuordnen",
'role_delete_sure' => 'Sind Sie sicher, dass Sie diese Rolle l&ouml;schen m&ouml;chten?',
'role_delete_success' => 'Rolle erfolgreich gel&ouml;scht',
'role_edit' => 'Rolle bearbeiten',
'role_details' => 'Rollen-Details',
'role_name' => 'Rollenname',
'role_desc' => 'Kurzbeschreibung der Rolle',
'role_system' => 'System-Berechtigungen',
'role_manage_users' => 'Benutzer verwalten',
'role_manage_roles' => 'Rollen & Rollen-Berechtigungen verwalten',
'role_manage_entity_permissions' => 'Alle Buch-, Kapitel und Seiten-Berechtigungen verwalten',
'role_manage_own_entity_permissions' => 'Nur Berechtigungen eigener B&uuml;cher, Kapitel und Seiten verwalten',
'role_manage_settings' => 'Globaleinstellungen verwalrten',
'role_asset' => 'Berechtigungen',
'role_asset_desc' => 'Diese Berechtigungen gelten f&uuml;r den Standard-Zugriff innerhalb des Systems. Berechtigungen f&uuml;r B&uuml;cher, Kapitel und Seiten &uuml;berschreiben diese Berechtigungenen.',
'role_all' => 'Alle',
'role_own' => 'Eigene',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
'role_save' => 'Rolle speichern',
'role_update_success' => 'Rolle erfolgreich gespeichert',
'role_users' => 'Dieser Rolle zugeordnete Benutzer',
'role_users_none' => 'Bisher sind dieser Rolle keiner Benutzer zugeordnet,',
/**
* Users
*/
'users' => 'Benutzer',
'user_profile' => 'Benutzerprofil',
'users_add_new' => 'Benutzer hinzuf&uuml;gen',
'users_search' => 'Benutzer suchen',
'users_role' => 'Benutzerrollen',
'users_external_auth_id' => 'Externe Authentifizierungs-ID',
'users_password_warning' => 'F&uuml;llen Sie die folgenden Felder nur aus, wenn Sie Ihr Passwort &auml;ndern m&ouml;chten:',
'users_system_public' => 'Dieser Benutzer repr&auml;sentiert alle Gast-Benutzer, die diese Seite betrachten. Er kann nicht zum Anmelden benutzt werden, sondern wird automatisch zugeordnet.',
'users_delete' => 'Benutzer l&ouml;schen',
'users_delete_named' => 'Benutzer :userName l&ouml;schen',
'users_delete_warning' => 'Sie m&ouml;chten den Benutzer \':userName\' g&auml;nzlich aus dem System l&ouml;schen.',
'users_delete_confirm' => 'Sind Sie sicher, dass Sie diesen Benutzer l&ouml;schen m&ouml;chten?',
'users_delete_success' => 'Benutzer erfolgreich gel&ouml;scht.',
'users_edit' => 'Benutzer bearbeiten',
'users_edit_profile' => 'Profil bearbeiten',
'users_edit_success' => 'Benutzer erfolgreich aktualisisert',
'users_avatar' => 'Benutzer-Bild',
'users_avatar_desc' => 'Dieses Bild sollte einen Durchmesser von ca. 256px haben.',
'users_preferred_language' => 'Bevorzugte Sprache',
'users_social_accounts' => 'Social-Media Konten',
'users_social_accounts_info' => 'Hier k&ouml;nnen Sie andere Social-Media Konten f&uuml;r eine schnellere und einfachere Anmeldung verkn&uuml;pfen. Wenn Sie ein Social-Media Konto hier l&ouml;sen, bleibt der Zugriff erhalteb. Entfernen Sie in diesem Falle die Berechtigung in Ihren Profil-Einstellungen des verkn&uuml;pften Social-Media Kontos.',
'users_social_connect' => 'Social-Media Konto verkn&uuml;pfen',
'users_social_disconnect' => 'Social-Media Kontoverkn&uuml;pfung l&ouml;sen',
'users_social_connected' => ':socialAccount Konto wurde erfolgreich mit dem Profil verkn&uuml;pft.',
'users_social_disconnected' => ':socialAccount Konto wurde erfolgreich vom Profil gel&ouml;st.',
];

View File

@@ -18,6 +18,8 @@ return [
*/
'sign_up' => 'Sign up',
'log_in' => 'Log in',
'log_in_with' => 'Login with :socialDriver',
'sign_up_with' => 'Sign up with :socialDriver',
'logout' => 'Logout',
'name' => 'Name',

View File

@@ -33,6 +33,7 @@ return [
'search_clear' => 'Clear Search',
'reset' => 'Reset',
'remove' => 'Remove',
'add' => 'Add',
/**

View File

@@ -14,6 +14,7 @@ return [
'recent_activity' => 'Recent Activity',
'create_now' => 'Create one now',
'revisions' => 'Revisions',
'meta_revision' => 'Revision #:revisionCount',
'meta_created' => 'Created :timeLength',
'meta_created_name' => 'Created :timeLength by :user',
'meta_updated' => 'Updated :timeLength',
@@ -26,6 +27,10 @@ return [
'no_pages_viewed' => 'You have not viewed any pages',
'no_pages_recently_created' => 'No pages have been recently created',
'no_pages_recently_updated' => 'No pages have been recently updated',
'export' => 'Export',
'export_html' => 'Contained Web File',
'export_pdf' => 'PDF File',
'export_text' => 'Plain Text File',
/**
* Permissions and restrictions
@@ -39,18 +44,26 @@ return [
* Search
*/
'search_results' => 'Search Results',
'search_results_page' => 'Page Search Results',
'search_results_chapter' => 'Chapter Search Results',
'search_results_book' => 'Book Search Results',
'search_total_results_found' => ':count result found|:count total results found',
'search_clear' => 'Clear Search',
'search_view_pages' => 'View all matches pages',
'search_view_chapters' => 'View all matches chapters',
'search_view_books' => 'View all matches books',
'search_no_pages' => 'No pages matched this search',
'search_for_term' => 'Search for :term',
'search_page_for_term' => 'Page search for :term',
'search_chapter_for_term' => 'Chapter search for :term',
'search_book_for_term' => 'Books search for :term',
'search_more' => 'More Results',
'search_filters' => 'Search Filters',
'search_content_type' => 'Content Type',
'search_exact_matches' => 'Exact Matches',
'search_tags' => 'Tag Searches',
'search_viewed_by_me' => 'Viewed by me',
'search_not_viewed_by_me' => 'Not viewed by me',
'search_permissions_set' => 'Permissions set',
'search_created_by_me' => 'Created by me',
'search_updated_by_me' => 'Updated by me',
'search_updated_before' => 'Updated before',
'search_updated_after' => 'Updated after',
'search_created_before' => 'Created before',
'search_created_after' => 'Created after',
'search_set_date' => 'Set Date',
'search_update' => 'Update Search',
/**
* Books
@@ -108,6 +121,7 @@ return [
'chapters_empty' => 'No pages are currently in this chapter.',
'chapters_permissions_active' => 'Chapter Permissions Active',
'chapters_permissions_success' => 'Chapter Permissions Updated',
'chapters_search_this' => 'Search this chapter',
/**
* Pages
@@ -155,16 +169,13 @@ return [
'pages_revision_named' => 'Page Revision for :pageName',
'pages_revisions_created_by' => 'Created By',
'pages_revisions_date' => 'Revision Date',
'pages_revisions_number' => '#',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Changes',
'pages_revisions_current' => 'Current Version',
'pages_revisions_preview' => 'Preview',
'pages_revisions_restore' => 'Restore',
'pages_revisions_none' => 'This page has no revisions',
'pages_export' => 'Export',
'pages_export_html' => 'Contained Web File',
'pages_export_pdf' => 'PDF File',
'pages_export_text' => 'Plain Text File',
'pages_copy_link' => 'Copy Link',
'pages_permissions_active' => 'Page Permissions Active',
'pages_initial_revision' => 'Initial publish',

View File

@@ -116,8 +116,11 @@ return [
'language_select' => [
'en' => 'English',
'de' => 'Deutsch',
'es' => 'Español',
'fr' => 'Français',
'pt_BR' => 'Português do Brasil'
'nl' => 'Nederlands',
'pt_BR' => 'Português do Brasil',
'sk' => 'Slovensky',
]
///////////////////////////////////
];

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 creada',
'page_create_notification' => 'Página creada exitosamente',
'page_update' => 'página actualizada',
'page_update_notification' => 'Página actualizada exitosamente',
'page_delete' => 'página borrada',
'page_delete_notification' => 'Página borrada exitosamente',
'page_restore' => 'página restaurada',
'page_restore_notification' => 'Página restaurada exitosamente',
'page_move' => 'página movida',
// Chapters
'chapter_create' => 'capítulo creado',
'chapter_create_notification' => 'Capítulo creado exitosamente',
'chapter_update' => 'capítulo actualizado',
'chapter_update_notification' => 'Capítulo actualizado exitosamente',
'chapter_delete' => 'capítulo borrado',
'chapter_delete_notification' => 'Capítulo borrado exitosamente',
'chapter_move' => 'capítulo movido',
// Books
'book_create' => 'libro creado',
'book_create_notification' => 'Libro creado exitosamente',
'book_update' => 'libro actualizado',
'book_update_notification' => 'Libro actualizado exitosamente',
'book_delete' => 'libro borrado',
'book_delete_notification' => 'Libro borrado exitosamente',
'book_sort' => 'libro ordenado',
'book_sort_notification' => 'Libro re-ordenado exitosamente',
];

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' => 'Las credenciales no concuerdan con nuestros registros.',
'throttle' => 'Demasiados intentos fallidos de conexión. Por favor intente nuevamente en :seconds segundos.',
/**
* Login & Register
*/
'sign_up' => 'Inscribete',
'log_in' => 'Log in',
'logout' => 'Logout',
'name' => 'Nombre',
'username' => 'Username',
'email' => 'Email',
'password' => 'Password',
'password_confirm' => 'Confirmar Password',
'password_hint' => 'Debe contener al menos 5 caracteres',
'forgot_password' => 'Olvidó Password?',
'remember_me' => 'Recordarme',
'ldap_email_hint' => 'Por favor introduzca un mail para utilizar con esta cuenta.',
'create_account' => 'Crear una cuenta',
'social_login' => 'Login Social',
'social_registration' => 'Registro Social',
'social_registration_text' => 'Registrar y entrar utilizando otro servicio.',
'register_thanks' => 'Gracias por registrarse!',
'register_confirm' => 'Por favor chequee su email y haga clic en el botón de confirmación enviado para acceder a :appName.',
'registrations_disabled' => 'Los registros están deshabilitados actualmente',
'registration_email_domain_invalid' => 'Este dominio de Email no tiene acceso a esta aplicación',
'register_success' => 'Gracias por registrarse! Ahora se encuentra registrado y logueado.',
/**
* Password Reset
*/
'reset_password' => 'Reset Password',
'reset_password_send_instructions' => 'Introduzca su email a continuación y le será enviado un correo con un link para la restauración',
'reset_password_send_button' => 'Enviar Link de Reset',
'reset_password_sent_success' => 'Un link para resetear password ha sido enviado a :email.',
'reset_password_success' => 'Su password ha sido reiniciado de manera éxitosa.',
'email_reset_subject' => 'Reset de su password de :appName',
'email_reset_text' => 'Ud. esta recibiendo este email debido a que recibimos una solicitud de reset de password de su cuenta.',
'email_reset_not_requested' => 'Si ud. no solicitó un reset de password, no es requerida ninguna acción.',
/**
* Email Confirmation
*/
'email_confirm_subject' => 'Confirme su email en :appName',
'email_confirm_greeting' => 'Gracias por unirse a :appName!',
'email_confirm_text' => 'Por favor confirme su dirección de email haciendo click en el siguiente botón:',
'email_confirm_action' => 'Confirmar Email',
'email_confirm_send_error' => 'Confirmation de email requerida pero el sistema no pudo enviar el mail. Contacte al administrador para asegurarse que el email está seteado correctamente.',
'email_confirm_success' => 'Su email hasido confirmado!',
'email_confirm_resent' => 'Email de confirmación reenviado, Por favor chequee su Inbox.',
'email_not_confirmed' => 'Dirección de email no confirmada',
'email_not_confirmed_text' => 'Su cuenta de email todavía no ha sido confirmada.',
'email_not_confirmed_click_link' => 'Por favor chequee el email con el link de confirmación que ha sido enviado luego de registrarse.',
'email_not_confirmed_resend' => 'Si no puede encontrar el email, puede solicitar el renvío del email de confirmación rellenando el formulario a continuación.',
'email_not_confirmed_resend_button' => 'Reenviar Email de confirmación',
];

View File

@@ -0,0 +1,58 @@
<?php
return [
/**
* Buttons
*/
'cancel' => 'Cancelar',
'confirm' => 'Confirmar',
'back' => 'Atrás',
'save' => 'Guardar',
'continue' => 'Continuar',
'select' => 'Seleccionar',
/**
* Form Labels
*/
'name' => 'Nombre',
'description' => 'Descripción',
'role' => 'Rol',
/**
* Actions
*/
'actions' => 'Acciones',
'view' => 'Ver',
'create' => 'Crear',
'update' => 'Actualizar',
'edit' => 'Editar',
'sort' => 'Ordenar',
'move' => 'Mover',
'delete' => 'Borrar',
'search' => 'Buscar',
'search_clear' => 'Limpiar búsqueda',
'reset' => 'Reset',
'remove' => 'Remover',
/**
* Misc
*/
'deleted_user' => 'Usuario borrado',
'no_activity' => 'Ninguna actividad para mostrar',
'no_items' => 'No hay items disponibles',
'back_to_top' => 'Volver arriba',
'toggle_details' => 'Alternar detalles',
/**
* Header
*/
'view_profile' => 'Ver Perfil',
'edit_profile' => 'Editar Perfil',
/**
* Email Content
*/
'email_action_help' => 'Si está teniendo problemas haga click en el botón ":actionText", copie y pegue la siguiente URL en su navegador web:',
'email_rights' => 'Todos los derechos reservados',
];

View File

@@ -0,0 +1,24 @@
<?php
return [
/**
* Image Manager
*/
'image_select' => 'Seleccionar Imagen',
'image_all' => 'Todo',
'image_all_title' => 'Ver todas las imágenes',
'image_book_title' => 'Ver las imágenes subidas a este libro',
'image_page_title' => 'Ver las imágenes subidas a esta página',
'image_search_hint' => 'Buscar por nombre de imagen',
'image_uploaded' => 'Subido el :uploadedDate',
'image_load_more' => 'Cargar más',
'image_image_name' => 'Nombre de imagen',
'image_delete_confirm' => 'Esta imagen esta siendo utilizada en las páginas a continuación, haga click de nuevo para confirmar que quiere borrar esta imagen.',
'image_select_image' => 'Seleccionar Imagen',
'image_dropzone' => 'Arrastre las imágenes o hacer click aquí para Subir',
'images_deleted' => 'Imágenes borradas',
'image_preview' => 'Preview de la imagen',
'image_upload_success' => 'Imagen subida éxitosamente',
'image_update_success' => 'Detalles de la imagen actualizados exitosamente',
'image_delete_success' => 'Imagen borrada exitosamente'
];

View File

@@ -0,0 +1,217 @@
<?php
return [
/**
* Shared
*/
'recently_created' => 'Recientemente creado',
'recently_created_pages' => 'Páginas recientemente creadas',
'recently_updated_pages' => 'Páginas recientemente actualizadas',
'recently_created_chapters' => 'Capítulos recientemente creados',
'recently_created_books' => 'Libros recientemente creados',
'recently_update' => 'Recientemente actualizado',
'recently_viewed' => 'Recientemente visto',
'recent_activity' => 'Actividad reciente',
'create_now' => 'Crear uno ahora',
'revisions' => 'Revisiones',
'meta_created' => 'Creado el :timeLength',
'meta_created_name' => 'Creado el :timeLength por :user',
'meta_updated' => 'Actualizado el :timeLength',
'meta_updated_name' => 'Actualizado el :timeLength por :user',
'x_pages' => ':count Páginas',
'entity_select' => 'Seleccione entidad',
'images' => 'Imágenes',
'my_recent_drafts' => 'Mis borradores recientes',
'my_recently_viewed' => 'Mis visualizaciones recientes',
'no_pages_viewed' => 'Ud. no ha visto ninguna página',
'no_pages_recently_created' => 'Ninguna página ha sido creada recientemente',
'no_pages_recently_updated' => 'Ninguna página ha sido actualizada recientemente',
'export' => 'Export',
'export_html' => 'Contained Web File',
'export_pdf' => 'PDF File',
'export_text' => 'Plain Text File',
/**
* Permissions and restrictions
*/
'permissions' => 'Permisos',
'permissions_intro' => 'una vez habilitado, Estos permisos tendrán prioridad por encima de cualquier permiso establecido.',
'permissions_enable' => 'Habilitar permisos custom',
'permissions_save' => 'Guardar permisos',
/**
* Search
*/
'search_results' => 'Buscar resultados',
'search_clear' => 'Limpiar resultados',
'search_no_pages' => 'Ninguna página encontrada para la búsqueda',
'search_for_term' => 'Busqueda por :term',
/**
* Books
*/
'book' => 'Libro',
'books' => 'Libros',
'books_empty' => 'No hay libros creados',
'books_popular' => 'Libros populares',
'books_recent' => 'Libros recientes',
'books_popular_empty' => 'Los libros más populares aparecerán aquí.',
'books_create' => 'Crear nuevo libro',
'books_delete' => 'Borrar libro',
'books_delete_named' => 'Borrar libro :bookName',
'books_delete_explain' => 'Esto borrará el libro con el nombre \':bookName\', Todos las páginas y capítulos serán removios.',
'books_delete_confirmation' => '¿Está seguro de que desea borrar este libro?',
'books_edit' => 'Editar Libro',
'books_edit_named' => 'Editar Libro :bookName',
'books_form_book_name' => 'Nombre de libro',
'books_save' => 'Guardar libro',
'books_permissions' => 'permisos de libro',
'books_permissions_updated' => 'Permisos de libro actualizados',
'books_empty_contents' => 'Ninguna página o capítulo ha sido creada para este libro.',
'books_empty_create_page' => 'Crear una nueva página',
'books_empty_or' => 'ó',
'books_empty_sort_current_book' => 'Organizar el libro actual',
'books_empty_add_chapter' => 'Agregar un capítulo',
'books_permissions_active' => 'Permisos de libro activados',
'books_search_this' => 'Buscar en este libro',
'books_navigation' => 'Navegación de libro',
'books_sort' => 'Organizar contenido de libro',
'books_sort_named' => 'Organizar libro :bookName',
'books_sort_show_other' => 'Mostrar otros libros',
'books_sort_save' => 'Guardar nuevo orden',
/**
* Chapters
*/
'chapter' => 'Capítulo',
'chapters' => 'Capítulos',
'chapters_popular' => 'Capítulos populares',
'chapters_new' => 'Nuevo capítulo',
'chapters_create' => 'Crear nuevo capítulo',
'chapters_delete' => 'Borrar capítulo',
'chapters_delete_named' => 'Borrar capítulo :chapterName',
'chapters_delete_explain' => 'Esto borrará el caítulo con el nombre \':chapterName\', todas las páginas serán removidas
y agregadas directamente al libro padre.',
'chapters_delete_confirm' => 'Está ud. seguro de borrar este capítulo?',
'chapters_edit' => 'Editar capítulo',
'chapters_edit_named' => 'Editar capítulo :chapterName',
'chapters_save' => 'Guardar capítulo',
'chapters_move' => 'Mover capítulo',
'chapters_move_named' => 'Mover Capítulo :chapterName',
'chapter_move_success' => 'Capítulo movido a :bookName',
'chapters_permissions' => 'Permisos de capítulo',
'chapters_empty' => 'No existen páginas en este capítulo.',
'chapters_permissions_active' => 'Permisos de capítulo activado',
'chapters_permissions_success' => 'Permisos de capítulo actualizados',
/**
* Pages
*/
'page' => 'Página',
'pages' => 'Páginas',
'pages_popular' => 'Páginas populares',
'pages_new' => 'Nueva página',
'pages_attachments' => 'Adjuntos',
'pages_navigation' => 'Navegación de página',
'pages_delete' => 'Borrar página',
'pages_delete_named' => 'Borrar página :pageName',
'pages_delete_draft_named' => 'Borrar borrador de página :pageName',
'pages_delete_draft' => 'Borrar borrador de página',
'pages_delete_success' => 'Página borrada',
'pages_delete_draft_success' => 'Borrador de página borrado',
'pages_delete_confirm' => 'Está ud. seguro de borrar esta página ?',
'pages_delete_draft_confirm' => 'Está ud. seguro de que desea borrar este borrador de página?',
'pages_editing_named' => 'Editando página :pageName',
'pages_edit_toggle_header' => 'Toggle Título',
'pages_edit_save_draft' => 'Guardar borrador',
'pages_edit_draft' => 'Editar borrador de página',
'pages_editing_draft' => 'Editando borrador',
'pages_editing_page' => 'Editando página',
'pages_edit_draft_save_at' => 'Borrador guardado el ',
'pages_edit_delete_draft' => 'Borrar borrador',
'pages_edit_discard_draft' => 'Descartar borrador',
'pages_edit_set_changelog' => 'Set Changelog',
'pages_edit_enter_changelog_desc' => 'Introduzca una breve descripción de los cambios que ha realizado',
'pages_edit_enter_changelog' => 'Entrar en Changelog',
'pages_save' => 'Guardar página',
'pages_title' => 'Título de página',
'pages_name' => 'Nombre de página',
'pages_md_editor' => 'Editor',
'pages_md_preview' => 'Preview',
'pages_md_insert_image' => 'Insertar Imagen',
'pages_md_insert_link' => 'Insertar link de entidad',
'pages_not_in_chapter' => 'La página no esá en el caítulo',
'pages_move' => 'Mover página',
'pages_move_success' => 'Página movida a ":parentName"',
'pages_permissions' => 'Permisos de página',
'pages_permissions_success' => 'Permisos de página actualizados',
'pages_revisions' => 'Revisiones de página',
'pages_revisions_named' => 'Revisiones de página para :pageName',
'pages_revision_named' => 'Revisión de ágina para :pageName',
'pages_revisions_created_by' => 'Creado por',
'pages_revisions_date' => 'Fecha de revisión',
'pages_revisions_changelog' => 'Changelog',
'pages_revisions_changes' => 'Cambios',
'pages_revisions_current' => 'Versión actual',
'pages_revisions_preview' => 'Preview',
'pages_revisions_restore' => 'Restaurar',
'pages_revisions_none' => 'Esta página no tiene revisiones',
'pages_copy_link' => 'Copiar Link',
'pages_permissions_active' => 'Permisos de página activos',
'pages_initial_revision' => 'Publicación inicial',
'pages_initial_name' => 'Página nueva',
'pages_editing_draft_notification' => 'Ud. está actualmente editando un borrador que fue guardado porúltima vez el :timeDiff.',
'pages_draft_edited_notification' => 'Esta página ha sido actualizada desde aquel momento. Se recomienda que cancele este borrador.',
'pages_draft_edit_active' => [
'start_a' => ':count usuarios han comenzado a editar esta página',
'start_b' => ':userName ha comenzado a editar esta página',
'time_a' => 'desde que las página fue actualizada',
'time_b' => 'en los últimos :minCount minutos',
'message' => ':start :time. Ten cuidado de no sobreescribir los cambios del otro usuario',
],
'pages_draft_discarded' => 'Borrador descartado, el editor ha sido actualizado con el contenido de la página actual',
/**
* Editor sidebar
*/
'page_tags' => 'Etiquetas de página',
'tag' => 'Etiqueta',
'tags' => 'Etiquetas',
'tag_value' => 'Valor de la etiqueta (Opcional)',
'tags_explain' => "Agregar algunas etiquetas para mejorar la categorización de su contenido. \n Ud. puede asignar un valor a una etiqueta para una organizacón a mayor detalle.",
'tags_add' => 'Agregar otra etiqueta',
'attachments' => 'Adjuntos',
'attachments_explain' => 'Subir ficheros o agregar links para mostrar en la página. Estos son visibles en la barra lateral de la página.',
'attachments_explain_instant_save' => 'Los cambios son guardados de manera instantánea .',
'attachments_items' => 'Items adjuntados',
'attachments_upload' => 'Fichero adjuntado',
'attachments_link' => 'Adjuntar Link',
'attachments_set_link' => 'Setear Link',
'attachments_delete_confirm' => 'Haga click en borrar nuevamente para confirmar que quiere borrar este adjunto.',
'attachments_dropzone' => 'Arrastre ficheros aquío haga click aquípara adjuntar un fichero',
'attachments_no_files' => 'Ningún fichero ha sido adjuntado',
'attachments_explain_link' => 'Ud. puede agregar un link o si lo prefiere puede agregar un fichero. Esto puede ser un link a otra página o un link a un fichero en la nube.',
'attachments_link_name' => 'Nombre de Link',
'attachment_link' => 'Link adjunto',
'attachments_link_url' => 'Link a fichero',
'attachments_link_url_hint' => 'Url del sitio o fichero',
'attach' => 'Adjuntar',
'attachments_edit_file' => 'Editar fichero',
'attachments_edit_file_name' => 'Nombre del fichero',
'attachments_edit_drop_upload' => 'Arrastre a los ficheros o haga click aquípara subir o sobreescribir',
'attachments_order_updated' => 'Orden de adjuntos actualizado',
'attachments_updated_success' => 'Detalles de adjuntos actualizados',
'attachments_deleted' => 'Adjunto borrado',
'attachments_file_uploaded' => 'Fichero subido éxitosamente',
'attachments_file_updated' => 'Fichero actualizado éxitosamente',
'attachments_link_attached' => 'Link agregado éxitosamente a la ágina',
/**
* Profile View
*/
'profile_user_for_x' => 'Usuario para :time',
'profile_created_content' => 'Contenido creado',
'profile_not_created_pages' => ':userName no ha creado ninguna página',
'profile_not_created_chapters' => ':userName no ha creado ningún capítulo',
'profile_not_created_books' => ':userName no ha creado ningún libro',
];

View File

@@ -0,0 +1,70 @@
<?php
return [
/**
* Error text strings.
*/
// Permissions
'permission' => 'Ud. no tiene permisos para visualizar la página solicitada.',
'permissionJson' => 'Ud. no tiene permisos para ejecutar la acción solicitada.',
// Auth
'error_user_exists_different_creds' => 'Un usuario con el email :email ya existe pero con credenciales diferentes.',
'email_already_confirmed' => 'El email ya ha sido confirmado, Intente loguearse en la aplicación.',
'email_confirmation_invalid' => 'Este token de confirmación no e válido o ya ha sido usado,Intente registrar uno nuevamente.',
'email_confirmation_expired' => 'El token de confirmación ha expirado, Un nuevo email de confirmacón ha sido enviado.',
'ldap_fail_anonymous' => 'El acceso con LDAP ha fallado usando binding anónimo',
'ldap_fail_authed' => 'El acceso LDAP usando el dn & password detallados',
'ldap_extension_not_installed' => 'La extensión LDAP PHP no se encuentra instalada',
'ldap_cannot_connect' => 'No se puede conectar con el servidor ldap, la conexión inicial ha fallado',
'social_no_action_defined' => 'Acción no definida',
'social_account_in_use' => 'la cuenta :socialAccount ya se encuentra en uso, intente loguearse a través de la opcón :socialAccount .',
'social_account_email_in_use' => 'El email :email ya se encuentra en uso. Si ud. ya dispone de una cuenta puede loguearse a través de su cuenta :socialAccount desde la configuración de perfil.',
'social_account_existing' => 'La cuenta :socialAccount ya se encuentra asignada a su perfil.',
'social_account_already_used_existing' => 'La cuenta :socialAccount ya se encuentra usada por otro usuario.',
'social_account_not_used' => 'La cuenta :socialAccount no está asociada a ningún usuario. Por favor adjuntela a su configuración de perfil. ',
'social_account_register_instructions' => 'Si no dispone de una cuenta, puede registrar una cuenta usando la opción de :socialAccount .',
'social_driver_not_found' => 'Driver social no encontrado',
'social_driver_not_configured' => 'Su configuración :socialAccount no es correcta.',
// System
'path_not_writable' => 'La ruta :filePath no pudo ser cargada. Asegurese de que es escribible por el servidor.',
'cannot_get_image_from_url' => 'No se puede obtener la imagen desde :url',
'cannot_create_thumbs' => 'El servidor no puede crear la imagen miniatura. Por favor chequee que tiene la extensión GD instalada.',
'server_upload_limit' => 'El servidor no permite la subida de ficheros de este tamañ. Por favor intente con un fichero de menor tamañ.',
'image_upload_error' => 'Ha ocurrido un error al subir la imagen',
// Attachments
'attachment_page_mismatch' => 'Página no coincidente durante la subida del adjunto ',
// Pages
'page_draft_autosave_fail' => 'Fallo al guardar borrador. Asegurese de que tiene conexión a Internet antes de guardar este borrador',
// Entities
'entity_not_found' => 'Entidad no encontrada',
'book_not_found' => 'Libro no encontrado',
'page_not_found' => 'Página no encontrada',
'chapter_not_found' => 'Capítulo no encontrado',
'selected_book_not_found' => 'El libro seleccionado no fue encontrado',
'selected_book_chapter_not_found' => 'El libro o capítulo seleccionado no fue encontrado',
'guests_cannot_save_drafts' => 'Los invitados no pueden guardar los borradores',
// Users
'users_cannot_delete_only_admin' => 'No se puede borrar el único administrador',
'users_cannot_delete_guest' => 'No se puede borrar el usuario invitado',
// Roles
'role_cannot_be_edited' => 'Este rol no puede ser editado',
'role_system_cannot_be_deleted' => 'Este rol es un rol de sistema y no puede ser borrado',
'role_registration_default_cannot_delete' => 'Este rol no puede ser borrado mientras sea el rol por defecto de registro',
// Error pages
'404_page_not_found' => 'Página no encontrada',
'sorry_page_not_found' => 'Lo sentimos, la página que intenta acceder no pudo ser encontrada.',
'return_home' => 'Volver al home',
'error_occurred' => 'Ha ocurrido un error',
'app_down' => 'La aplicación :appName se encuentra caída en este momento',
'back_soon' => 'Volverá a estar operativa en corto tiempo.',
];

View File

@@ -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' => 'Siguiente &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' => 'El Password debe ser como mínimo de seis caracteres y coincidir con la confirmación.',
'user' => "No podemos encontrar un usuario con esta dirección de e-mail.",
'token' => 'El token de reset del password es inválido.',
'sent' => 'Hemos enviado a su cuenta de e-mail un link para restaurar su password!',
'reset' => 'Su password ha sido restaurado!',
];

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' => 'Ajustes',
'settings_save' => 'Guardar ajustes',
'settings_save_success' => 'Ajustes guardados',
/**
* App settings
*/
'app_settings' => 'Ajustes de App',
'app_name' => 'Nombre de aplicación',
'app_name_desc' => 'Este nombre es mostrado en la cabecera y en cualquier email de la aplicación',
'app_name_header' => 'Mostrar el nombre de la aplicación en la cabecera?',
'app_public_viewing' => 'Permitir vista pública?',
'app_secure_images' => 'Habilitar mayor seguridad para subir imágenes?',
'app_secure_images_desc' => 'Por razones de performance, todas las imágenes son púicas. Esta opción agrega una cadena larga difícil de adivinar, asegúrese que los indices de directorios no esán habilitados para prevenir el acceso fácil a las imágenes.',
'app_editor' => 'Editor de página',
'app_editor_desc' => 'Seleccione cuál editor ser usado por todos los usuarios para editar páginas.',
'app_custom_html' => 'Contenido de cabecera HTML customizable',
'app_custom_html_desc' => 'Cualquier contenido agregado aquíseráinsertado al final de la secón <head> de cada ágina. Esto esútil para sobreescribir estilo o agregar código para anaíticas.',
'app_logo' => 'Logo de la aplicación',
'app_logo_desc' => 'Esta imagen debería de ser 43px en altura. <br>Iágenes grandes seán escaladas.',
'app_primary_color' => 'Color primario de la aplicación',
'app_primary_color_desc' => 'Esto debería ser un valor hexadecimal. <br>Deje el valor vaío para reiniciar al valor por defecto.',
/**
* Registration settings
*/
'reg_settings' => 'Ajustes de registro',
'reg_allow' => 'Permitir registro?',
'reg_default_role' => 'Rol de usuario por defecto despúes del registro',
'reg_confirm_email' => 'Requerir email de confirmaación?',
'reg_confirm_email_desc' => 'Si la restricción por dominio es usada, entonces la confirmaciónpor email serárequerida y el valor a continuón será ignorado.',
'reg_confirm_restrict_domain' => 'Restringir registro al dominio',
'reg_confirm_restrict_domain_desc' => 'Introduzca una lista separada por comasa de los emails del dominio a los que les gustaría restringir el registro por dominio. A los usuarios les seá enviado un emal para confirmar la dirección antes de que se le permita interactuar con la aplicacón. <br> Note que los usuarios se les permitir ácambiar sus direcciones de email luego de un registr éxioso.',
'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida',
/**
* Role settings
*/
'roles' => 'Roles',
'role_user_roles' => 'Roles de usuario',
'role_create' => 'Crear nuevo rol',
'role_create_success' => 'Rol creado satisfactoriamente',
'role_delete' => 'Borrar rol',
'role_delete_confirm' => 'Se borrará el rol con nombre \':roleName\'.',
'role_delete_users_assigned' => 'Este rol tiene :userCount usuarios asignados. Si ud. quisiera migrar los usuarios de este rol, seleccione un nuevo rol a continuación.',
'role_delete_no_migration' => "No migrar usuarios",
'role_delete_sure' => 'Está seguro que desea borrar este rol?',
'role_delete_success' => 'Rol borrado satisfactoriamente',
'role_edit' => 'Editar rol',
'role_details' => 'Detalles de rol',
'role_name' => 'Nombre de rol',
'role_desc' => 'Descripción corta de rol',
'role_system' => 'Permisos de sistema',
'role_manage_users' => 'Gestionar usuarios',
'role_manage_roles' => 'Gestionar roles y permisos de roles',
'role_manage_entity_permissions' => 'Gestionar todos los permisos de libros, capítulos y áginas',
'role_manage_own_entity_permissions' => 'Gestionar permisos en libros propios, capítulos y páginas',
'role_manage_settings' => 'Gestionar ajustes de activos',
'role_asset' => 'Permisos de activos',
'role_asset_desc' => 'Estos permisos controlan el acceso por defecto a los activos del sistema. Permisos a Libros, Capítulos y áginas sobreescribiran estos permisos.',
'role_all' => 'Todo',
'role_own' => 'Propio',
'role_controlled_by_asset' => 'Controlado por el actvo al que ha sido subido',
'role_save' => 'Guardar rol',
'role_update_success' => 'Rol actualizado éxitosamente',
'role_users' => 'Usuarios en este rol',
'role_users_none' => 'No hay usuarios asignados a este rol',
/**
* Users
*/
'users' => 'Usuarios',
'user_profile' => 'Perfil de usuario',
'users_add_new' => 'Agregar nuevo usuario',
'users_search' => 'Buscar usuarios',
'users_role' => 'Roles de usuario',
'users_external_auth_id' => 'ID externo de autenticación',
'users_password_warning' => 'Solo rellene a continuación si desea cambiar su password:',
'users_system_public' => 'Este usuario representa cualquier usuario invitado que visita la aplicación. No puede utilizarse para hacer login sio que es asignado automáticamente.',
'users_delete' => 'Borrar usuario',
'users_delete_named' => 'Borrar usuario :userName',
'users_delete_warning' => 'Se borrará completamente el usuario con el nombre \':userName\' del sistema.',
'users_delete_confirm' => 'Está seguro que desea borrar este usuario?',
'users_delete_success' => 'Usuarios removidos éxitosamente',
'users_edit' => 'Editar Usuario',
'users_edit_profile' => 'Editar perfil',
'users_edit_success' => 'Usuario actualizado',
'users_avatar' => 'Avatar del usuario',
'users_avatar_desc' => 'Esta imagen debe ser aproximadamente 256px por lado.',
'users_preferred_language' => 'Lenguaje preferido',
'users_social_accounts' => 'Cuentas sociales',
'users_social_accounts_info' => 'Aquí puede conectar sus otras cuentas para un ápido y ás ácil login. Desconectando una cuenta aqu íno revca accesos ya autorizados. Revoque el acceso desde se perfil desde los ajustes de perfil en la cuenta social conectada.',
'users_social_connect' => 'Conectar cuenta',
'users_social_disconnect' => 'Desconectar cuenta',
'users_social_connected' => 'La cuenta :socialAccount ha sido éxitosamente añadida a su perfil.',
'users_social_disconnected' => 'La cuenta :socialAccount ha sido desconectada éxitosamente de su perfil.',
];

View File

@@ -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' => 'El :attribute debe ser aceptado.',
'active_url' => 'El :attribute no es una URl válida.',
'after' => 'El :attribute debe ser una fecha posterior :date.',
'alpha' => 'El :attribute solo puede contener letras.',
'alpha_dash' => 'El :attribute solo puede contener letras, números y guiones.',
'alpha_num' => 'El :attribute solo puede contener letras y número.',
'array' => 'El :attribute debe de ser un array.',
'before' => 'El :attribute debe ser una fecha anterior a :date.',
'between' => [
'numeric' => 'El :attribute debe estar entre :min y :max.',
'file' => 'El :attribute debe estar entre :min y :max kilobytes.',
'string' => 'El :attribute debe estar entre :min y :max carácteres.',
'array' => 'El :attribute debe estar entre :min y :max items.',
],
'boolean' => 'El campo :attribute debe ser true o false.',
'confirmed' => 'La confirmación de :attribute no concuerda.',
'date' => 'El :attribute no es una fecha válida.',
'date_format' => 'El :attribute no coincide con el formato :format.',
'different' => ':attribute y :other deben ser diferentes.',
'digits' => ':attribute debe ser de :digits dígitos.',
'digits_between' => ':attribute debe ser un valor entre :min y :max dígios.',
'email' => ':attribute debe ser una dirección álida.',
'filled' => 'El campo :attribute es requerido.',
'exists' => 'El :attribute seleccionado es inválido.',
'image' => 'El :attribute debe ser una imagen.',
'in' => 'El selected :attribute es inválio.',
'integer' => 'El :attribute debe ser un entero.',
'ip' => 'El :attribute debe ser una dirección IP álida.',
'max' => [
'numeric' => ':attribute no puede ser mayor que :max.',
'file' => ':attribute no puede ser mayor que :max kilobytes.',
'string' => ':attribute no puede ser mayor que :max carácteres.',
'array' => ':attribute no puede contener más de :max items.',
],
'mimes' => ':attribute debe ser un fichero de tipo: :values.',
'min' => [
'numeric' => ':attribute debe ser al menos de :min.',
'file' => ':attribute debe ser al menos :min kilobytes.',
'string' => ':attribute debe ser al menos :min caracteres.',
'array' => ':attribute debe tener como mínimo :min items.',
],
'not_in' => ':attribute seleccionado es inválio.',
'numeric' => ':attribute debe ser numérico.',
'regex' => ':attribute con formato inválido',
'required' => ':attribute es requerido.',
'required_if' => ':attribute es requerido cuando :other vale :value.',
'required_with' => 'El campo :attribute es requerido cuando se encuentre entre los valores :values.',
'required_with_all' => 'El campo :attribute es requerido cuando los valores sean :values.',
'required_without' => ':attribute es requerido cuando no se encuentre entre los valores :values.',
'required_without_all' => ':attribute es requerido cuando ninguno de los valores :values están presentes.',
'same' => ':attribute y :other deben coincidir.',
'size' => [
'numeric' => ':attribute debe ser :size.',
'file' => ':attribute debe ser :size kilobytes.',
'string' => ':attribute debe ser :size caracteres.',
'array' => ':attribute debe contener :size items.',
],
'string' => 'El atributo :attribute debe ser una cadena.',
'timezone' => 'El atributo :attribute debe ser una zona válida.',
'unique' => 'El atributo :attribute ya ha sido tomado.',
'url' => 'El atributo :attribute tiene un formato inválid.',
/*
|--------------------------------------------------------------------------
| 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' => 'Confirmación de Password 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

@@ -26,6 +26,10 @@ return [
'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',
'export' => 'Exporter',
'export_html' => 'Fichiers web',
'export_pdf' => 'Fichier PDF',
'export_text' => 'Document texte',
/**
* Permissions and restrictions
@@ -39,18 +43,9 @@ return [
* 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
@@ -160,10 +155,6 @@ return [
'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',

Some files were not shown because too many files have changed in this diff Show More