Merge remote-tracking branch 'origin' into bookshelves

This commit is contained in:
Dan Brown 2018-08-04 11:35:01 +01:00
commit b5a2d3c1c4
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
70 changed files with 4091 additions and 3319 deletions

2
.browserslistrc Normal file
View File

@ -0,0 +1,2 @@
>0.25%
not op_mini all

View File

@ -56,6 +56,8 @@ TWITCH_APP_SECRET=false
GITLAB_APP_ID=false
GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false
DISCORD_APP_ID=false
DISCORD_APP_SECRET=false
# External services such as Gravatar and Draw.IO
DISABLE_EXTERNAL_SERVICES=false
@ -67,6 +69,13 @@ LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
# Do you want to sync LDAP groups to BookStack roles for a user
LDAP_USER_TO_GROUPS=false
# What is the LDAP attribute for group memberships
LDAP_GROUP_ATTRIBUTE="memberOf"
# Would you like to remove users from roles on BookStack if they do not match on LDAP
# If false, the ldap groups-roles sync will only add users to roles
LDAP_REMOVE_FROM_GROUPS=false
# Mail settings
MAIL_DRIVER=smtp

View File

@ -1,21 +0,0 @@
### For Feature Requests
Desired Feature:
### For Bug Reports
* BookStack Version *(Found in settings, Please don't put 'latest')*:
* PHP Version:
* MySQL Version:
##### Expected Behavior
##### Current Behavior
##### Steps to Reproduce

29
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,29 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**Steps To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Your Configuration (please complete the following information):**
- Exact BookStack Version (Found in settings):
- PHP Version:
- Using Docker or reverse proxy (Yes/No):
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Describe the feature you'd like**
A clear description of the feature you'd like implemented in BookStack.
**Describe the benefits this feature would bring to BookStack users**
Explain the measurable benefits this feature would achieve.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Exceptions\AuthException;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\UserRepo;
use BookStack\Services\LdapService;
use BookStack\Services\SocialAuthService;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
@ -36,18 +37,21 @@ class LoginController extends Controller
protected $redirectAfterLogout = '/login';
protected $socialAuthService;
protected $ldapService;
protected $userRepo;
/**
* Create a new controller instance.
*
* @param SocialAuthService $socialAuthService
* @param LdapService $ldapService
* @param UserRepo $userRepo
*/
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
public function __construct(SocialAuthService $socialAuthService, LdapService $ldapService, UserRepo $userRepo)
{
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
$this->socialAuthService = $socialAuthService;
$this->ldapService = $ldapService;
$this->userRepo = $userRepo;
$this->redirectPath = baseUrl('/');
$this->redirectAfterLogout = baseUrl('/login');
@ -96,6 +100,11 @@ class LoginController extends Controller
auth()->login($user);
}
// Sync LDAP groups if required
if ($this->ldapService->shouldSyncGroups()) {
$this->ldapService->syncGroups($user);
}
$path = session()->pull('url.intended', '/');
$path = baseUrl($path, true);
return redirect($path);
@ -125,6 +134,7 @@ class LoginController extends Controller
* Redirect to the relevant social site.
* @param $socialDriver
* @return \Symfony\Component\HttpFoundation\RedirectResponse
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
*/
public function getSocialLogin($socialDriver)
{

View File

@ -5,7 +5,6 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Repos\EntityRepo;
use BookStack\Repos\UserRepo;
use BookStack\Services\ExportService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@ -38,11 +37,18 @@ class PageController extends Controller
* @param string $chapterSlug
* @return Response
* @internal param bool $pageSlug
* @throws NotFoundException
*/
public function create($bookSlug, $chapterSlug = null)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
if ($chapterSlug !== null) {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
} else {
$chapter = null;
$book = $this->entityRepo->getBySlug('book', $bookSlug);
}
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
@ -52,7 +58,7 @@ class PageController extends Controller
return redirect($draft->getUrl());
}
// Otherwise show edit view
// Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]);
}
@ -71,8 +77,14 @@ class PageController extends Controller
'name' => 'required|string|max:255'
]);
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
if ($chapterSlug !== null) {
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$book = $chapter->book;
} else {
$chapter = null;
$book = $this->entityRepo->getBySlug('book', $bookSlug);
}
$parent = $chapter ? $chapter : $book;
$this->checkOwnablePermission('page-create', $parent);
@ -93,7 +105,7 @@ class PageController extends Controller
public function editDraft($bookSlug, $pageId)
{
$draft = $this->entityRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-create', $draft->book);
$this->checkOwnablePermission('page-create', $draft->parent);
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->signedIn;
@ -119,12 +131,10 @@ class PageController extends Controller
]);
$input = $request->all();
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$draftPage = $this->entityRepo->getById('page', $pageId, true);
$book = $draftPage->book;
$chapterId = intval($draftPage->chapter_id);
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
$parent = $draftPage->parent;
$this->checkOwnablePermission('page-create', $parent);
if ($parent->isA('chapter')) {

View File

@ -78,6 +78,7 @@ class PermissionController extends Controller
* @param $id
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws PermissionsException
*/
public function updateRole($id, Request $request)
{

View File

@ -28,6 +28,15 @@ class Page extends Entity
return $this->belongsTo(Book::class);
}
/**
* Get the parent item
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function parent()
{
return $this->chapter_id ? $this->chapter() : $this->book();
}
/**
* Get the chapter that this page is in, If applicable.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo

View File

@ -20,6 +20,7 @@ class EventServiceProvider extends ServiceProvider
'SocialiteProviders\Okta\OktaExtendSocialite@handle',
'SocialiteProviders\GitLab\GitLabExtendSocialite@handle',
'SocialiteProviders\Twitch\TwitchExtendSocialite@handle',
'SocialiteProviders\Discord\DiscordExtendSocialite@handle',
],
];

View File

@ -3,7 +3,7 @@
class Role extends Model
{
protected $fillable = ['display_name', 'description'];
protected $fillable = ['display_name', 'description', 'external_auth_id'];
/**
* The roles that belong to the role.

View File

@ -1,7 +1,11 @@
<?php namespace BookStack\Services;
use BookStack\Exceptions\LdapException;
use BookStack\Repos\UserRepo;
use BookStack\Role;
use BookStack\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Builder;
/**
* Class LdapService
@ -14,15 +18,55 @@ class LdapService
protected $ldap;
protected $ldapConnection;
protected $config;
protected $userRepo;
protected $enabled;
/**
* LdapService constructor.
* @param Ldap $ldap
* @param UserRepo $userRepo
*/
public function __construct(Ldap $ldap)
public function __construct(Ldap $ldap, UserRepo $userRepo)
{
$this->ldap = $ldap;
$this->config = config('services.ldap');
$this->userRepo = $userRepo;
$this->enabled = config('auth.method') === 'ldap';
}
/**
* Check if groups should be synced.
* @return bool
*/
public function shouldSyncGroups()
{
return $this->enabled && $this->config['user_to_groups'] !== false;
}
/**
* Search for attributes for a specific user on the ldap
* @param string $userName
* @param array $attributes
* @return null|array
* @throws LdapException
*/
private function getUserWithAttributes($userName, $attributes)
{
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
// Find user
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
$baseDn = $this->config['base_dn'];
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes);
if ($users['count'] === 0) {
return null;
}
return $users[0];
}
/**
@ -34,21 +78,13 @@ class LdapService
*/
public function getUserDetails($userName)
{
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
// Find user
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
$baseDn = $this->config['base_dn'];
$emailAttr = $this->config['email_attribute'];
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
if ($users['count'] === 0) {
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
if ($user === null) {
return null;
}
$user = $users[0];
return [
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
@ -162,4 +198,172 @@ class LdapService
}
return strtr($filterString, $newAttrs);
}
/**
* Get the groups a user is a part of on ldap
* @param string $userName
* @return array|null
* @throws LdapException
*/
public function getUserGroups($userName)
{
$groupsAttr = $this->config['group_attribute'];
$user = $this->getUserWithAttributes($userName, [$groupsAttr]);
if ($user === null) {
return null;
}
$userGroups = $this->groupFilter($user);
$userGroups = $this->getGroupsRecursive($userGroups, []);
return $userGroups;
}
/**
* Get the parent groups of an array of groups
* @param array $groupsArray
* @param array $checked
* @return array
* @throws LdapException
*/
private function getGroupsRecursive($groupsArray, $checked)
{
$groups_to_add = [];
foreach ($groupsArray as $groupName) {
if (in_array($groupName, $checked)) {
continue;
}
$groupsToAdd = $this->getGroupGroups($groupName);
$groups_to_add = array_merge($groups_to_add, $groupsToAdd);
$checked[] = $groupName;
}
$groupsArray = array_unique(array_merge($groupsArray, $groups_to_add), SORT_REGULAR);
if (!empty($groups_to_add)) {
return $this->getGroupsRecursive($groupsArray, $checked);
} else {
return $groupsArray;
}
}
/**
* Get the parent groups of a single group
* @param string $groupName
* @return array
* @throws LdapException
*/
private function getGroupGroups($groupName)
{
$ldapConnection = $this->getConnection();
$this->bindSystemUser($ldapConnection);
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
$baseDn = $this->config['base_dn'];
$groupsAttr = strtolower($this->config['group_attribute']);
$groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, 'CN='.$groupName, [$groupsAttr]);
if ($groups['count'] === 0) {
return [];
}
$groupGroups = $this->groupFilter($groups[0]);
return $groupGroups;
}
/**
* Filter out LDAP CN and DN language in a ldap search return
* Gets the base CN (common name) of the string
* @param string $ldapSearchReturn
* @return array
*/
protected function groupFilter($ldapSearchReturn)
{
$groupsAttr = strtolower($this->config['group_attribute']);
$ldapGroups = [];
$count = 0;
if (isset($ldapSearchReturn[$groupsAttr]['count'])) {
$count = (int) $ldapSearchReturn[$groupsAttr]['count'];
}
for ($i=0; $i<$count; $i++) {
$dnComponents = ldap_explode_dn($ldapSearchReturn[$groupsAttr][$i], 1);
if (!in_array($dnComponents[0], $ldapGroups)) {
$ldapGroups[] = $dnComponents[0];
}
}
return $ldapGroups;
}
/**
* Sync the LDAP groups to the user roles for the current user
* @param \BookStack\User $user
* @throws LdapException
*/
public function syncGroups(User $user)
{
$userLdapGroups = $this->getUserGroups($user->external_auth_id);
// Get the ids for the roles from the names
$ldapGroupsAsRoles = $this->matchLdapGroupsToSystemsRoles($userLdapGroups);
// Sync groups
if ($this->config['remove_from_groups']) {
$user->roles()->sync($ldapGroupsAsRoles);
$this->userRepo->attachDefaultRole($user);
} else {
$user->roles()->syncWithoutDetaching($ldapGroupsAsRoles);
}
}
/**
* Match an array of group names from LDAP to BookStack system roles.
* Formats LDAP group names to be lower-case and hyphenated.
* @param array $groupNames
* @return \Illuminate\Support\Collection
*/
protected function matchLdapGroupsToSystemsRoles(array $groupNames)
{
foreach ($groupNames as $i => $groupName) {
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
}
$roles = Role::query()->where(function(Builder $query) use ($groupNames) {
$query->whereIn('name', $groupNames);
foreach ($groupNames as $groupName) {
$query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
}
})->get();
$matchedRoles = $roles->filter(function(Role $role) use ($groupNames) {
return $this->roleMatchesGroupNames($role, $groupNames);
});
return $matchedRoles->pluck('id');
}
/**
* Check a role against an array of group names to see if it matches.
* Checked against role 'external_auth_id' if set otherwise the name of the role.
* @param Role $role
* @param array $groupNames
* @return bool
*/
protected function roleMatchesGroupNames(Role $role, array $groupNames)
{
if ($role->external_auth_id) {
$externalAuthIds = explode(',', strtolower($role->external_auth_id));
foreach ($externalAuthIds as $externalAuthId) {
if (in_array(trim($externalAuthId), $groupNames)) {
return true;
}
}
return false;
}
$roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
return in_array($roleName, $groupNames);
}
}

View File

@ -16,7 +16,7 @@ class SocialAuthService
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch'];
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
/**
* SocialAuthService constructor.

View File

@ -20,7 +20,8 @@
"socialiteproviders/microsoft-azure": "^3.0",
"socialiteproviders/okta": "^1.0",
"socialiteproviders/gitlab": "^3.0",
"socialiteproviders/twitch": "^3.0"
"socialiteproviders/twitch": "^3.0",
"socialiteproviders/discord": "^2.0"
},
"require-dev": {
"filp/whoops": "~2.0",

51
composer.lock generated
View File

@ -1,10 +1,10 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3bf33ab103b15b06ca06c85fd8ae3b78",
"content-hash": "b9ea2a42e2f7780b3a54d4b7327750e0",
"packages": [
{
"name": "aws/aws-sdk-php",
@ -1665,16 +1665,16 @@
},
{
"name": "phenx/php-svg-lib",
"version": "v0.3",
"version": "v0.3.0",
"source": {
"type": "git",
"url": "https://github.com/PhenX/php-svg-lib.git",
"reference": "a85f7fe9fe08d093a4a8583cdd306b553ff918aa"
"reference": "8f543ede60386faec9b0012833536de4b6083bb9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/a85f7fe9fe08d093a4a8583cdd306b553ff918aa",
"reference": "a85f7fe9fe08d093a4a8583cdd306b553ff918aa",
"url": "https://api.github.com/repos/PhenX/php-svg-lib/zipball/8f543ede60386faec9b0012833536de4b6083bb9",
"reference": "8f543ede60386faec9b0012833536de4b6083bb9",
"shasum": ""
},
"require": {
@ -1701,7 +1701,7 @@
],
"description": "A library to read, parse and export to PDF SVG files.",
"homepage": "https://github.com/PhenX/php-svg-lib",
"time": "2017-05-24T10:07:27+00:00"
"time": "2018-04-14T14:36:18+00:00"
},
{
"name": "predis/predis",
@ -2071,6 +2071,43 @@
],
"time": "2016-07-19T19:14:21+00:00"
},
{
"name": "socialiteproviders/discord",
"version": "v2.0.1",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Discord.git",
"reference": "f934ca3f4fa5ea915c1d20852b826e860aa64727"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Discord/zipball/f934ca3f4fa5ea915c1d20852b826e860aa64727",
"reference": "f934ca3f4fa5ea915c1d20852b826e860aa64727",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0",
"socialiteproviders/manager": "~2.0 || ~3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"SocialiteProviders\\Discord\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Christopher Eklund",
"email": "eklundchristopher@gmail.com"
}
],
"description": "Discord OAuth2 Provider for Laravel Socialite",
"time": "2017-08-28T02:20:40+00:00"
},
{
"name": "socialiteproviders/gitlab",
"version": "v3.0.2",

View File

@ -108,6 +108,12 @@ return [
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
'name' => 'Twitch',
],
'discord' => [
'client_id' => env('DISCORD_APP_ID'),
'client_secret' => env('DISCORD_APP_SECRET'),
'redirect' => env('APP_URL') . '/login/service/discord/callback',
'name' => 'Discord',
],
'ldap' => [
'server' => env('LDAP_SERVER', false),
@ -118,6 +124,9 @@ return [
'version' => env('LDAP_VERSION', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
]
'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false),
]
];

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddRoleExternalAuthId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('roles', function (Blueprint $table) {
$table->string('external_auth_id', 200)->default('');
$table->index('external_auth_id');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('roles', function (Blueprint $table) {
$table->dropColumn('external_auth_id');
});
}
}

6312
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,20 +11,20 @@
},
"devDependencies": {
"@babel/core": "^7.0.0-beta.40",
"@babel/polyfill": "^7.0.0-beta.40",
"@babel/preset-env": "^7.0.0-beta.40",
"autoprefixer": "^8.1.0",
"babel-loader": "^8.0.0-beta.0",
"@babel/polyfill": "^7.0.0-beta.40",
"css-loader": "^0.28.10",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"livereload": "^0.7.0",
"node-sass": "^4.7.2",
"node-sass": "^4.9.2",
"npm-run-all": "^4.1.2",
"postcss-loader": "^2.1.1",
"sass-loader": "^7.0.1",
"style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.2.3",
"webpack": "^4.1.1",
"webpack": "^4.16.3",
"webpack-cli": "^2.0.11"
},
"dependencies": {

View File

@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#7289DA;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -18,6 +18,13 @@ class MarkdownEditor {
this.onMarkdownScroll = this.onMarkdownScroll.bind(this);
this.init();
// Scroll to text if needed.
const queryParams = (new URL(window.location)).searchParams;
const scrollText = queryParams.get('content-text');
if (scrollText) {
this.scrollToText(scrollText);
}
}
init() {
@ -387,6 +394,33 @@ class MarkdownEditor {
});
}
// Scroll to a specified text
scrollToText(searchText) {
if (!searchText) {
return;
}
const content = this.cm.getValue();
const lines = content.split(/\r?\n/);
let lineNumber = lines.findIndex(line => {
return line && line.indexOf(searchText) !== -1;
});
if (lineNumber === -1) {
return;
}
this.cm.scrollIntoView({
line: lineNumber,
}, 200);
this.cm.focus();
// set the cursor location.
this.cm.setCursor({
line: lineNumber,
char: lines[lineNumber].length
})
}
}
module.exports = MarkdownEditor ;

View File

@ -6,11 +6,16 @@ class Notification {
this.type = elem.getAttribute('notification');
this.textElem = elem.querySelector('span');
this.autohide = this.elem.hasAttribute('data-autohide');
this.elem.style.display = 'grid';
window.$events.listen(this.type, text => {
this.show(text);
});
elem.addEventListener('click', this.hide.bind(this));
if (elem.hasAttribute('data-show')) this.show(this.textElem.textContent);
if (elem.hasAttribute('data-show')) {
setTimeout(() => this.show(this.textElem.textContent), 100);
}
this.hideCleanup = this.hideCleanup.bind(this);
}

View File

@ -20,7 +20,7 @@ class PageDisplay {
// Sidebar page nav click event
$('.sidebar-page-nav').on('click', 'a', event => {
goToText(event.target.getAttribute('href').substr(1));
this.goToText(event.target.getAttribute('href').substr(1));
});
}
@ -74,11 +74,23 @@ class PageDisplay {
pointerShowing = false;
});
let updatePointerContent = () => {
let updatePointerContent = ($elem) => {
let inputText = pointerModeLink ? window.baseUrl(`/link/${this.pageId}#${pointerSectionId}`) : `{{@${this.pageId}#${pointerSectionId}}}`;
if (pointerModeLink && inputText.indexOf('http') !== 0) inputText = window.location.protocol + "//" + window.location.host + inputText;
$pointer.find('input').val(inputText);
// update anchor if present
const $editAnchor = $pointer.find('#pointer-edit');
if ($editAnchor.length !== 0 && $elem) {
const editHref = $editAnchor.data('editHref');
const element = $elem[0];
const elementId = element.id;
// get the first 50 characters.
let queryContent = element.textContent && element.textContent.substring(0, 50);
$editAnchor[0].href = `${editHref}?content-id=${elementId}&content-text=${encodeURIComponent(queryContent)}`;
}
};
// Show pointer when selecting a single block of tagged content
@ -90,7 +102,7 @@ class PageDisplay {
// Show pointer and set link
let $elem = $(this);
pointerSectionId = $elem.attr('id');
updatePointerContent();
updatePointerContent($elem);
$elem.before($pointer);
$pointer.show();
@ -219,7 +231,6 @@ class PageDisplay {
}
}
}
}
module.exports = PageDisplay;
module.exports = PageDisplay;

View File

@ -483,13 +483,36 @@ class WysiwygEditor {
},
setup: function (editor) {
editor.on('init ExecCommand change input NodeChange ObjectResized', editorChange);
editor.on('ExecCommand change input NodeChange ObjectResized', editorChange);
editor.on('init', () => {
editorChange();
// Scroll to the content if needed.
const queryParams = (new URL(window.location)).searchParams;
const scrollId = queryParams.get('content-id');
if (scrollId) {
scrollToText(scrollId);
}
});
function editorChange() {
let content = editor.getContent();
window.$events.emit('editor-html-change', content);
}
function scrollToText(scrollId) {
const element = editor.dom.get(encodeURIComponent(scrollId).replace(/!/g, '%21'));
if (!element) {
return;
}
// scroll the element into the view and put the cursor at the end.
element.scrollIntoView();
editor.selection.select(element, true);
editor.selection.collapse(false);
editor.focus();
}
window.$events.listen('editor-html-update', html => {
editor.setContent(html);
editor.selection.select(editor.getBody(), true);

View File

@ -1,5 +1,4 @@
// Global Polyfills
import "@babel/polyfill"
import "./services/dom-polyfills"
// Url retrieval function

View File

@ -12,4 +12,13 @@ export function utcTimeStampToLocalTime(timestamp) {
let hours = date.getHours();
let mins = date.getMinutes();
return `${(hours>9?'':'0') + hours}:${(mins>9?'':'0') + mins}`;
}
export function formatDateTime(date) {
let month = date.getMonth() + 1;
let day = date.getDate();
let hours = date.getHours();
let mins = date.getMinutes();
return `${date.getFullYear()}-${(month>9?'':'0') + month}-${(day>9?'':'0') + day} ${(hours>9?'':'0') + hours}:${(mins>9?'':'0') + mins}`;
}

View File

@ -52,7 +52,9 @@ let methods = {
},
deleteFile(file) {
if (!file.deleting) return file.deleting = true;
if (!file.deleting) {
return this.$set(file, 'deleting', true);
}
this.$http.delete(window.baseUrl(`/attachments/${file.id}`)).then(resp => {
this.$events.emit('success', resp.data.message);

View File

@ -1,3 +1,6 @@
import * as Dates from "../services/dates";
const dropzone = require('./components/dropzone');
let page = 0;
@ -168,7 +171,7 @@ const methods = {
},
getDate(stringDate) {
return new Date(stringDate);
return Dates.formatDateTime(new Date(stringDate));
},
uploadSuccess(event) {

View File

@ -4,17 +4,18 @@
top: 0;
right: 0;
margin: $-xl*2 $-xl;
padding: $-l $-xl;
padding: $-m $-l;
background-color: #EEE;
border-radius: 3px;
box-shadow: $bs-med;
box-shadow: $bs-card;
z-index: 999999;
cursor: pointer;
max-width: 360px;
transition: transform ease-in-out 280ms;
transform: translate3d(580px, 0, 0);
transform: translateX(580px);
display: grid;
grid-template-columns: 64px 1fr;
grid-template-columns: 42px 1fr;
color: #FFF;
span, svg {
vertical-align: middle;
justify-self: center;
@ -22,9 +23,9 @@
}
svg {
fill: #EEEEEE;
width: 4em;
height: 4em;
padding-right: $-m;
width: 2.8rem;
height: 2.8rem;
padding-right: $-s;
}
span {
vertical-align: middle;
@ -32,18 +33,15 @@
}
&.pos {
background-color: $positive;
color: #EEE;
}
&.neg {
background-color: $negative;
color: #EEE;
}
&.warning {
background-color: $secondary;
color: #EEE;
}
&.showing {
transform: translate3d(0, 0, 0);
transform: translateX(0);
}
&.showing:hover {
transform: translate3d(0, -2px, 0);

View File

@ -266,22 +266,13 @@ ul.pagination {
padding: $-xxs $-s;
border: 1px solid #CCC;
margin-left: -1px;
color: #888;
fill: #888;
user-select: none;
&.disabled {
cursor: not-allowed;
}
}
li.active span {
background-color: rgba($primary, 0.8);
color: #EEE;
fill: #EEE;
border-color: rgba($primary, 0.8);
}
a {
color: $primary;
fill: $primary;
color: #FFF;
}
}

View File

@ -89,6 +89,12 @@
del {
background: #FFECEC;
}
&.page-revision {
pre code {
white-space: pre-wrap;
}
}
}
// Page content pointers
@ -107,8 +113,13 @@
position: absolute;
top: -60px;
background-color:#FFF;
width: 272px;
width: 275px;
z-index: 55;
&.is-page-editable {
width: 328px;
}
&:before {
position: absolute;
left: 50%;
@ -132,12 +143,13 @@
width: 172px;
z-index: 40;
}
input, button {
input, button, a {
position: relative;
border-radius: 0;
height: 28px;
font-size: 12px;
vertical-align: top;
padding: 5px 16px;
}
> i {
color: #888;
@ -148,11 +160,22 @@
cursor: pointer;
user-select: none;
}
.button {
.input-group .button {
line-height: 1;
margin: 0 0 0 -4px;
box-shadow: none;
}
a.button {
margin: 0 0 0 0;
&:hover {
fill: #fff;
}
}
.svg-icon {
width: 1.2em;
height: 1.2em;
}
}
// Attribute form

View File

@ -59,4 +59,5 @@ $text-light: #EEE;
// Shadows
$bs-light: 0 0 4px 1px #CCC;
$bs-med: 0 1px 3px 1px rgba(76, 76, 76, 0.26);
$bs-card: 0 1px 3px 1px rgba(76, 76, 76, 0.26), 0 1px 12px 0px rgba(76, 76, 76, 0.2);
$bs-hover: 0 2px 2px 1px rgba(0,0,0,.13);

View File

@ -8,34 +8,34 @@ return [
*/
// Pages
'page_create' => 'hat Seite erstellt:',
'page_create_notification' => 'hat Seite erfolgreich erstellt:',
'page_update' => 'hat Seite aktualisiert:',
'page_update_notification' => 'hat Seite erfolgreich aktualisiert:',
'page_delete' => 'hat Seite gelöscht:',
'page_delete_notification' => 'hat Seite erfolgreich gelöscht:',
'page_restore' => 'hat Seite wiederhergstellt:',
'page_restore_notification' => 'hat Seite erfolgreich wiederhergstellt:',
'page_move' => 'hat Seite verschoben:',
'page_create' => 'Seite erstellt',
'page_create_notification' => 'Die Seite wurde erfolgreich erstellt.',
'page_update' => 'Seite aktualisiert',
'page_update_notification' => 'Die Seite wurde erfolgreich aktualisiert.',
'page_delete' => 'Seite gelöscht',
'page_delete_notification' => 'Die Seite wurde erfolgreich gelöscht.',
'page_restore' => 'Seite wiederhergstellt',
'page_restore_notification' => 'Die Seite wurde erfolgreich wiederhergstellt.',
'page_move' => 'Seite verschoben',
// Chapters
'chapter_create' => 'hat Kapitel erstellt:',
'chapter_create_notification' => 'hat Kapitel erfolgreich erstellt:',
'chapter_update' => 'hat Kapitel aktualisiert:',
'chapter_update_notification' => 'hat Kapitel erfolgreich aktualisiert:',
'chapter_delete' => 'hat Kapitel gelöscht',
'chapter_delete_notification' => 'hat Kapitel erfolgreich gelöscht:',
'chapter_move' => 'hat Kapitel verschoben:',
'chapter_create' => 'Kapitel erstellt',
'chapter_create_notification' => 'Das Kapitel wurde erfolgreich erstellt.',
'chapter_update' => 'Kapitel aktualisiert',
'chapter_update_notification' => 'Das Kapitel wurde erfolgreich aktualisiert.',
'chapter_delete' => 'Kapitel gelöscht',
'chapter_delete_notification' => 'Das Kapitel wurde erfolgreich gelöscht.',
'chapter_move' => 'Kapitel verschoben',
// Books
'book_create' => 'hat Buch erstellt:',
'book_create_notification' => 'hat Buch erfolgreich erstellt:',
'book_update' => 'hat Buch aktualisiert:',
'book_update_notification' => 'hat Buch erfolgreich aktualisiert:',
'book_delete' => 'hat Buch gelöscht:',
'book_delete_notification' => 'hat Buch erfolgreich gelöscht:',
'book_sort' => 'hat Buch sortiert:',
'book_sort_notification' => 'hat Buch erfolgreich neu sortiert:',
'book_create' => 'Buch erstellt',
'book_create_notification' => 'Das Buch wurde erfolgreich erstellt.',
'book_update' => 'Buch aktualisiert',
'book_update_notification' => 'Das Buch wurde erfolgreich aktualisiert.',
'book_delete' => 'Buch gelöscht',
'book_delete_notification' => 'Das Buch wurde erfolgreich gelöscht.',
'book_sort' => 'Buch sortiert',
'book_sort_notification' => 'Das Buch wurde erfolgreich neu sortiert.',
// Other
'commented_on' => 'kommentierte',

View File

@ -179,6 +179,7 @@ return [
'pages_revisions_restore' => 'Wiederherstellen',
'pages_revisions_none' => 'Diese Seite hat keine älteren Versionen.',
'pages_copy_link' => 'Link kopieren',
'pages_edit_content_link' => 'Inhalt bearbeiten',
'pages_permissions_active' => 'Seiten-Berechtigungen aktiv',
'pages_initial_revision' => 'Erste Veröffentlichung',
'pages_initial_name' => 'Neue Seite',

View File

@ -185,6 +185,7 @@ return [
'pages_revisions_restore' => 'Restore',
'pages_revisions_none' => 'This page has no revisions',
'pages_copy_link' => 'Copy Link',
'pages_edit_content_link' => 'Edit Content',
'pages_permissions_active' => 'Page Permissions Active',
'pages_initial_revision' => 'Initial publish',
'pages_initial_name' => 'New Page',

View File

@ -82,6 +82,7 @@ return [
'role_details' => 'Role Details',
'role_name' => 'Role Name',
'role_desc' => 'Short Description of Role',
'role_external_auth_id' => 'External Authentication IDs',
'role_system' => 'System Permissions',
'role_manage_users' => 'Manage users',
'role_manage_roles' => 'Manage roles & role permissions',

View File

@ -35,6 +35,8 @@ return [
'book_delete' => 'libro borrado',
'book_delete_notification' => 'Libro borrado exitosamente',
'book_sort' => 'libro ordenado',
'book_sort_notification' => 'Libro re-ordenado exitosamente',
'book_sort_notification' => 'Libro reordenado exitosamente',
// Other
'commented_on' => 'comentada el',
];

View File

@ -31,6 +31,7 @@ return [
'edit' => 'Editar',
'sort' => 'Ordenar',
'move' => 'Mover',
'copy' => 'Copiar',
'reply' => 'Responder',
'delete' => 'Borrar',
'search' => 'Buscar',

View File

@ -166,6 +166,9 @@ return [
'pages_not_in_chapter' => 'La página no está en un capítulo',
'pages_move' => 'Mover página',
'pages_move_success' => 'Página movida a ":parentName"',
'pages_copy' => 'Copiar página',
'pages_copy_desination' => 'Destino de la copia',
'pages_copy_success' => 'Página copiada a correctamente',
'pages_permissions' => 'Permisos de página',
'pages_permissions_success' => 'Permisos de página actualizados',
'pages_revision' => 'Revisión',
@ -182,6 +185,7 @@ return [
'pages_revisions_restore' => 'Restaurar',
'pages_revisions_none' => 'Esta página no tiene revisiones',
'pages_copy_link' => 'Copiar Enlace',
'pages_edit_content_link' => 'Contenido editado',
'pages_permissions_active' => 'Permisos de página activos',
'pages_initial_revision' => 'Publicación inicial',
'pages_initial_name' => 'Página nueva',

View File

@ -7,7 +7,7 @@ return [
*/
// Permissions
'permission' => 'UNo tiene permisos para visualizar la página solicitada.',
'permission' => 'No tiene permisos para visualizar la página solicitada.',
'permissionJson' => 'No tiene permisos para ejecutar la acción solicitada.',
// Auth
@ -65,7 +65,7 @@ return [
'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 nuevos registros',
// Comments
// Comments
'comment_list' => 'Se ha producido un error al buscar los comentarios.',
'cannot_add_comment_to_draft' => 'No puedes añadir comentarios a un borrador.',
'comment_add' => 'Se ha producido un error al añadir el comentario.',

View File

@ -34,6 +34,7 @@ return [
'app_homepage' => 'Página de inicio',
'app_homepage_desc' => 'Elija la página que se mostrará al inicio en lugar de la vista predeterminada. Se ignorarán los permisos de la página seleccionada.',
'app_homepage_default' => 'Página de inicio seleccionada',
'app_homepage_books' => 'O selecciona la página de libros como página de inicio. Esto prevalecerá sobre cualquier página seleccionada como página de inicio.',
'app_disable_comments' => 'Deshabilitar comentarios',
'app_disable_comments_desc' => 'Deshabilita los comentarios en todas las páginas de la aplicación. Los comentarios existentes no se muestran.',
@ -50,6 +51,19 @@ return [
'reg_confirm_restrict_domain_desc' => 'Introduzca una lista separada por comas de los dominio a los que les gustaría restringir el registro de usuarios. A los usuarios les será enviado un correo electrónico para confirmar la dirección antes de que se le permita interactuar con la aplicación. <br> Tenga en cuenta que los usuarios podrán cambiar sus direcciones de correo electrónico después de registrarse exitosamente.',
'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida',
/**
* Maintenance settings
*/
'maint' => 'Mantenimiento',
'maint_image_cleanup' => 'Limpiar imágenes',
'maint_image_cleanup_desc' => "Analiza las páginas y sus revisiones para comprobar qué imágenes y dibujos están siendo utilizadas y cuales no son necesarias. Asegúrate de crear una copia completa de la base de datos y de las imágenes antes de lanzar esta opción.",
'maint_image_cleanup_ignore_revisions' => 'Ignorar imágenes en revisiones',
'maint_image_cleanup_run' => 'Lanzar limpieza',
'maint_image_cleanup_warning' => 'Se han encontrado :count imágenes posiblemente no utilizadas . ¿Estás seguro de querer borrar estas imágenes?',
'maint_image_cleanup_success' => '¡Se han encontrado y borrado :count imágenes posiblemente no utilizadas!',
'maint_image_cleanup_nothing_found' => '¡No se han encontrado imágenes sin utilizar, no se han borrado imágenes!',
/**
* Role settings
*/

View File

@ -124,7 +124,7 @@ return [
'chapters_permissions_active' => 'Permisos de capítulo activado',
'chapters_permissions_success' => 'Permisos de capítulo actualizados',
'chapters_search_this' => 'Buscar en este capítulo',
/**
* Pages
*/
@ -185,6 +185,7 @@ return [
'pages_revisions_restore' => 'Restaurar',
'pages_revisions_none' => 'Esta página no tiene revisiones',
'pages_copy_link' => 'Copiar enlace',
'pages_edit_content_link' => 'Contenido editado',
'pages_permissions_active' => 'Permisos de página activos',
'pages_initial_revision' => 'Publicación inicial',
'pages_initial_name' => 'Página nueva',

View File

@ -34,10 +34,10 @@ return [
'app_homepage' => 'Página de inicio de la Aplicación',
'app_homepage_desc' => 'Seleccione una página de inicio para mostrar en lugar de la vista por defecto. Se ignoran los permisos de página para las páginas seleccionadas.',
'app_homepage_default' => 'Página de inicio por defecto seleccionadad',
'app_homepage_books' => 'O seleccione la página de libros como su página de inicio. Esto tendrá preferencia sobre cualquier página seleccionada como página de inicio.',
'app_disable_comments' => 'Deshabilitar comentarios',
'app_disable_comments_desc' => 'Deshabilitar comentarios en todas las páginas de la aplicación. Los comentarios existentes no se muestran.',
/**
* Registration settings
*/
@ -51,6 +51,19 @@ return [
'reg_confirm_restrict_domain_desc' => 'Introduzca una lista separada por comas de los correos electrónicos del dominio a los que les gustaría restringir el registro por dominio. A los usuarios les será enviado un correo elctrónico para confirmar la dirección antes de que se le permita interactuar con la aplicación. <br> Note que a los usuarios se les permitirá cambiar sus direcciones de correo electrónico luego de un registro éxioso.',
'reg_confirm_restrict_domain_placeholder' => 'Ninguna restricción establecida',
/**
* Maintenance settings
*/
'maint' => 'Mantenimiento',
'maint_image_cleanup' => 'Limpiar imágenes',
'maint_image_cleanup_desc' => "Analizar contenido de páginas y revisiones para detectar cuáles imágenes y dibujos están en uso y cuáles son redundantes. Asegúrese de crear un respaldo completo de imágenes y base de datos antes de ejecutar esta tarea.",
'maint_image_cleanup_ignore_revisions' => 'Ignorar imágenes en revisión',
'maint_image_cleanup_run' => 'Ejecutar limpieza',
'maint_image_cleanup_warning' => 'Se encontraron :count imágenes pontencialmente sin uso. Está seguro de que quiere eliminarlas?',
'maint_image_cleanup_success' => 'Se encontraron y se eliminaron :count imágenes pontencialmente sin uso!',
'maint_image_cleanup_nothing_found' => 'No se encotraron imágenes sin usar, Nada eliminado!',
/**
* Role settings
*/

View File

@ -15,7 +15,7 @@ return [
'page_delete' => 'a supprimé la page',
'page_delete_notification' => 'Page supprimée avec succès',
'page_restore' => 'a restauré la page',
'page_restore_notification' => 'Page réstaurée avec succès',
'page_restore_notification' => 'Page restaurée avec succès',
'page_move' => 'a déplacé la page',
// Chapters
@ -39,5 +39,4 @@ return [
// Other
'commented_on' => 'a commenté'
];

View File

@ -73,4 +73,4 @@ return [
'email_not_confirmed_click_link' => 'Merci de cliquer sur le lien dans l\'e-mail qui vous a été envoyé après l\'enregistrement.',
'email_not_confirmed_resend' => 'Si vous ne retrouvez plus l\'e-mail, vous pouvez renvoyer un e-mail de confirmation en utilisant le formulaire ci-dessous.',
'email_not_confirmed_resend_button' => 'Renvoyez l\'e-mail de confirmation',
];
];

View File

@ -20,6 +20,7 @@ return [
'role' => 'Rôle',
'cover_image' => 'Image de couverture',
'cover_image_description' => 'Cette image doit être environ 300x170px.',
/**
* Actions
*/
@ -30,6 +31,7 @@ return [
'edit' => 'Editer',
'sort' => 'Trier',
'move' => 'Déplacer',
'copy' => 'Copier',
'reply' => 'Répondre',
'delete' => 'Supprimer',
'search' => 'Chercher',
@ -38,7 +40,6 @@ return [
'remove' => 'Enlever',
'add' => 'Ajouter',
/**
* Misc
*/
@ -63,4 +64,4 @@ return [
*/
'email_action_help' => 'Si vous rencontrez des problèmes pour cliquer sur le bouton ":actionText", copiez et collez l\'adresse ci-dessous dans votre navigateur :',
'email_rights' => 'Tous droits réservés',
];
];

View File

@ -4,7 +4,7 @@ return [
/**
* Image Manager
*/
'image_select' => 'Selectionner une image',
'image_select' => 'Sélectionner une image',
'image_all' => 'Toutes',
'image_all_title' => 'Voir toutes les images',
'image_book_title' => 'Voir les images ajoutées à ce livre',
@ -15,19 +15,20 @@ return [
'image_image_name' => 'Nom de l\'image',
'image_delete_used' => 'Cette image est utilisée dans les pages ci-dessous.',
'image_delete_confirm' => 'Confirmez que vous souhaitez bien supprimer cette image.',
'image_select_image' => 'Selectionner l\'image',
'image_select_image' => 'Sélectionner l\'image',
'image_dropzone' => 'Glissez les images ici ou cliquez pour les ajouter',
'images_deleted' => 'Images supprimées',
'image_preview' => 'Prévisualiser l\'image',
'image_upload_success' => 'Image ajoutée avec succès',
'image_update_success' => 'Détails de l\'image mis à jour',
'image_delete_success' => 'Image supprimée avec succès',
'image_upload_remove' => 'Supprimer',
/**
* Code editor
*/
'code_editor' => 'Editer le code',
'code_language' => 'Language du code',
'code_language' => 'Langage du code',
'code_content' => 'Contenu du code',
'code_save' => 'Enregistrer le code',
];

View File

@ -19,7 +19,6 @@ return [
'meta_created_name' => 'Créé :timeLength par :user',
'meta_updated' => 'Mis à jour :timeLength',
'meta_updated_name' => 'Mis à jour :timeLength par :user',
'x_pages' => ':count pages',
'entity_select' => 'Sélectionner l\'entité',
'images' => 'Images',
'my_recent_drafts' => 'Mes brouillons récents',
@ -36,7 +35,7 @@ return [
* Permissions and restrictions
*/
'permissions' => 'Permissions',
'permissions_intro' => 'Une fois activées ces permission prendont la priorité sur tous les sets de permissions pré-existants.',
'permissions_intro' => 'Une fois activées ces permissions prendront la priorité sur tous les sets de permissions préexistants.',
'permissions_enable' => 'Activer les permissions personnalisées',
'permissions_save' => 'Enregistrer les permissions',
@ -131,6 +130,7 @@ return [
*/
'page' => 'Page',
'pages' => 'Pages',
'x_pages' => ':count Page|:count Pages',
'pages_popular' => 'Pages populaires',
'pages_new' => 'Nouvelle page',
'pages_attachments' => 'Fichiers joints',
@ -166,6 +166,9 @@ return [
'pages_not_in_chapter' => 'La page n\'est pas dans un chapitre',
'pages_move' => 'Déplacer la page',
'pages_move_success' => 'Page déplacée à ":parentName"',
'pages_copy' => 'Copier la page',
'pages_copy_desination' => 'Destination de la copie',
'pages_copy_success' => 'Page copiée avec succès',
'pages_permissions' => 'Permissions de la page',
'pages_permissions_success' => 'Permissions de la page mises à jour',
'pages_revision' => 'Révision',
@ -182,6 +185,7 @@ return [
'pages_revisions_restore' => 'Restaurer',
'pages_revisions_none' => 'Cette page n\'a aucune révision',
'pages_copy_link' => 'Copier le lien',
'pages_edit_content_link' => 'Modifier le contenu',
'pages_permissions_active' => 'Permissions de page actives',
'pages_initial_revision' => 'Publication initiale',
'pages_initial_name' => 'Nouvelle page',
@ -200,10 +204,12 @@ return [
* Editor sidebar
*/
'page_tags' => 'Mots-clés de la page',
'chapter_tags' => 'Mots-clés du chapitre',
'book_tags' => 'Mots-clés du livre',
'tag' => 'Mot-clé',
'tags' => 'Mots-clé',
'tags' => 'Mots-clés',
'tag_value' => 'Valeur du mot-clé (Optionnel)',
'tags_explain' => "Ajouter des mot-clés pour catégoriser votre contenu.",
'tags_explain' => "Ajouter des mots-clés pour catégoriser votre contenu.",
'tags_add' => 'Ajouter un autre mot-clé',
'attachments' => 'Fichiers joints',
'attachments_explain' => 'Ajouter des fichiers ou des liens pour les afficher sur votre page. Ils seront affichés dans la barre latérale',
@ -257,6 +263,6 @@ return [
'comment_deleted_success' => 'Commentaire supprimé',
'comment_created_success' => 'Commentaire ajouté',
'comment_updated_success' => 'Commentaire mis à jour',
'comment_delete_confirm' => 'Etes-vous sûr de vouloir supprimer ce commentaire?',
'comment_delete_confirm' => 'Etes-vous sûr de vouloir supprimer ce commentaire ?',
'comment_in_reply_to' => 'En réponse à :commentId',
];
];

View File

@ -14,12 +14,13 @@ return [
'error_user_exists_different_creds' => 'Un utilisateur avec l\'adresse :email existe déjà.',
'email_already_confirmed' => 'Cet e-mail a déjà été validé, vous pouvez vous connecter.',
'email_confirmation_invalid' => 'Cette confirmation est invalide. Veuillez essayer de vous inscrire à nouveau.',
'email_confirmation_expired' => 'Le jeton de confirmation est perimé. Un nouvel e-mail vous a été envoyé.',
'email_confirmation_expired' => 'Le jeton de confirmation est périmé. Un nouvel e-mail vous a été envoyé.',
'ldap_fail_anonymous' => 'L\'accès LDAP anonyme n\'a pas abouti',
'ldap_fail_authed' => 'L\'accès LDAP n\'a pas abouti avec cet utilisateur et ce mot de passe',
'ldap_extension_not_installed' => 'L\'extention LDAP PHP n\'est pas installée',
'ldap_extension_not_installed' => 'L\'extension LDAP PHP n\'est pas installée',
'ldap_cannot_connect' => 'Impossible de se connecter au serveur LDAP, la connexion initiale a échoué',
'social_no_action_defined' => 'Pas d\'action définie',
'social_login_bad_response' => "Erreur pendant la tentative de connexion à :socialAccount : \n:error",
'social_account_in_use' => 'Ce compte :socialAccount est déjà utilisé. Essayez de vous connecter via :socialAccount.',
'social_account_email_in_use' => 'L\'email :email est déjà utilisé. Si vous avez déjà un compte :socialAccount, vous pouvez le joindre à votre profil existant.',
'social_account_existing' => 'Ce compte :socialAccount est déjà rattaché à votre profil.',
@ -34,13 +35,17 @@ return [
'cannot_get_image_from_url' => 'Impossible de récupérer l\'image depuis :url',
'cannot_create_thumbs' => 'Le serveur ne peut pas créer de miniature, vérifier que l\'extension PHP GD est installée.',
'server_upload_limit' => 'La taille du fichier est trop grande.',
'uploaded' => 'Le serveur n\'autorise pas l\'envoi d\'un fichier de cette taille. Veuillez essayer avec une taille de fichier réduite.',
'image_upload_error' => 'Une erreur est survenue pendant l\'envoi de l\'image',
'image_upload_type_error' => 'LE format de l\'image envoyée n\'est pas valide',
// Attachments
'attachment_page_mismatch' => 'Page mismatch during attachment update',
'attachment_page_mismatch' => 'Page incorrecte durant la mise à jour du fichier joint',
'attachment_not_found' => 'Fichier joint non trouvé',
// Pages
'page_draft_autosave_fail' => 'Le brouillon n\'a pas pu être sauvé. Vérifiez votre connexion internet',
'page_custom_home_deletion' => 'Impossible de supprimer une page définie comme page d\'accueil',
// Entities
'entity_not_found' => 'Entité non trouvée',

View File

@ -34,8 +34,10 @@ return [
'app_homepage' => 'Page d\'accueil de l\'application',
'app_homepage_desc' => 'Choisissez une page à afficher sur la page d\'accueil au lieu de la vue par défaut. Les permissions sont ignorées pour les pages sélectionnées.',
'app_homepage_default' => 'Page d\'accueil par défaut sélectionnée',
'app_homepage_books' => 'Ou sélectionner la page des livres comme page d\'accueil. Cela va ignorer la page séléctionnée comme page d\'accueil.',
'app_disable_comments' => 'Désactiver les commentaires',
'app_disable_comments_desc' => 'Désactive les commentaires sur toutes les pages de l\'application. Les commentaires existants ne sont pas affichés.',
/**
* Registration settings
*/
@ -46,9 +48,22 @@ return [
'reg_confirm_email' => 'Obliger la confirmation par e-mail ?',
'reg_confirm_email_desc' => 'Si la restriction de domaine est activée, la confirmation sera automatiquement obligatoire et cette valeur sera ignorée.',
'reg_confirm_restrict_domain' => 'Restreindre l\'inscription à un domaine',
'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\'inscription, séparés par une virgule. Les utilisateur recevront un e-mail de confirmation à cette adresse. <br> Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.',
'reg_confirm_restrict_domain_desc' => 'Entrez une liste de domaines acceptés lors de l\'inscription, séparés par une virgule. Les utilisateurs recevront un e-mail de confirmation à cette adresse. <br> Les utilisateurs pourront changer leur adresse après inscription s\'ils le souhaitent.',
'reg_confirm_restrict_domain_placeholder' => 'Aucune restriction en place',
/**
* Maintenance settings
*/
'maint' => 'Maintenance',
'maint_image_cleanup' => 'Nettoyer les images',
'maint_image_cleanup_desc' => "Scan le contenu des pages et des révisions pour vérifier les images et les dessins en cours d'utilisation et lesquels sont redondant. Veuillez à faire une sauvegarde de la base de données et des images avant de lancer ceci.",
'maint_image_cleanup_ignore_revisions' => 'Ignorer les images dans les révisions',
'maint_image_cleanup_run' => 'Lancer le nettoyage',
'maint_image_cleanup_warning' => ':count images potentiellement inutilisées trouvées. Etes-vous sûr de vouloir supprimer ces images ?',
'maint_image_cleanup_success' => ':count images potentiellement inutilisées trouvées et supprimées !',
'maint_image_cleanup_nothing_found' => 'Aucune image inutilisée trouvée, rien à supprimer !',
/**
* Role settings
*/
@ -61,23 +76,24 @@ return [
'role_delete_confirm' => 'Ceci va supprimer le rôle \':roleName\'.',
'role_delete_users_assigned' => 'Ce rôle a :userCount utilisateurs assignés. Vous pouvez choisir un rôle de remplacement pour ces utilisateurs.',
'role_delete_no_migration' => "Ne pas assigner de nouveau rôle",
'role_delete_sure' => 'Êtes vous sûr(e) de vouloir supprimer ce rôle ?',
'role_delete_sure' => 'Êtes-vous sûr de vouloir supprimer ce rôle ?',
'role_delete_success' => 'Le rôle a été supprimé avec succès',
'role_edit' => 'Modifier le rôle',
'role_details' => 'Détails du rôle',
'role_name' => 'Nom du rôle',
'role_desc' => 'Courte description du rôle',
'role_external_auth_id' => 'Identifiants d\'authentification externes',
'role_system' => 'Permissions système',
'role_manage_users' => 'Gérer les utilisateurs',
'role_manage_roles' => 'Gérer les rôles et permissions',
'role_manage_entity_permissions' => 'Gérer les permissions sur les livres, chapitres et pages',
'role_manage_own_entity_permissions' => 'Gérer les permissions de ses propres livres, chapitres, et pages',
'role_manage_settings' => 'Gérer les préférences de l\'application',
'role_asset' => 'Asset Permissions',
'role_asset_desc' => 'These permissions control default access to the assets within the system. Permissions on Books, Chapters and Pages will override these permissions.',
'role_asset' => 'Permissions des ressources',
'role_asset_desc' => 'Ces permissions contrôlent l\'accès par défaut des ressources dans le système. Les permissions dans les livres, les chapitres et les pages ignoreront ces permissions',
'role_all' => 'Tous',
'role_own' => 'Propres',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
'role_controlled_by_asset' => 'Contrôlé par les ressources les ayant envoyés',
'role_save' => 'Enregistrer le rôle',
'role_update_success' => 'Rôle mis à jour avec succès',
'role_users' => 'Utilisateurs ayant ce rôle',
@ -93,7 +109,7 @@ return [
'users_search' => 'Chercher les utilisateurs',
'users_role' => 'Rôles des utilisateurs',
'users_external_auth_id' => 'Identifiant d\'authentification externe',
'users_password_warning' => 'Remplissez ce fomulaire uniquement si vous souhaitez changer de mot de passe:',
'users_password_warning' => 'Remplissez ce formulaire uniquement si vous souhaitez changer de mot de passe:',
'users_system_public' => 'Cet utilisateur représente les invités visitant votre instance. Il est assigné automatiquement aux invités.',
'users_books_view_type' => 'Disposition d\'affichage préférée pour les livres',
'users_delete' => 'Supprimer un utilisateur',

View File

@ -182,6 +182,7 @@ return [
'pages_revisions_restore' => 'Ripristina',
'pages_revisions_none' => 'Questa pagina non ha versioni',
'pages_copy_link' => 'Copia Link',
'pages_edit_content_link' => 'Modifica contenuto',
'pages_permissions_active' => 'Permessi Pagina Attivi',
'pages_initial_revision' => 'Pubblicazione iniziale',
'pages_initial_name' => 'Nuova Pagina',

View File

@ -179,6 +179,7 @@ return [
'pages_revisions_restore' => '復元',
'pages_revisions_none' => 'このページにはリビジョンがありません',
'pages_copy_link' => 'リンクをコピー',
'pages_edit_content_link' => 'コンテンツの編集',
'pages_permissions_active' => 'ページの権限は有効です',
'pages_initial_revision' => '初回の公開',
'pages_initial_name' => '新規ページ',

View File

@ -14,7 +14,7 @@ return [
'recent_activity' => 'Recente Activiteit',
'create_now' => 'Maak er zelf één',
'revisions' => 'Revisies',
'meta_revision' => 'Revisie #:revisionCount',
'meta_revision' => 'Revisie #:revisionCount',
'meta_created' => 'Aangemaakt :timeLength',
'meta_created_name' => 'Aangemaakt: :timeLength door :user',
'meta_updated' => ':timeLength Aangepast',
@ -44,7 +44,7 @@ return [
* Search
*/
'search_results' => 'Zoekresultaten',
'search_total_results_found' => ':count resultaten gevonden|:count resultaten gevonden',
'search_total_results_found' => ':count resultaten gevonden|:count resultaten gevonden',
'search_clear' => 'Zoekopdracht wissen',
'search_no_pages' => 'Er zijn geen pagina\'s gevonden',
'search_for_term' => 'Zoeken op :term',
@ -105,7 +105,7 @@ return [
*/
'chapter' => 'Hoofdstuk',
'chapters' => 'Hoofdstukken',
'x_chapters' => ':count Hoofdstuk|:count Hoofdstukken',
'x_chapters' => ':count Hoofdstuk|:count Hoofdstukken',
'chapters_popular' => 'Populaire Hoofdstukken',
'chapters_new' => 'Nieuw Hoofdstuk',
'chapters_create' => 'Hoofdstuk Toevoegen',
@ -124,14 +124,14 @@ return [
'chapters_empty' => 'Er zijn geen pagina\'s in dit hoofdstuk aangemaakt.',
'chapters_permissions_active' => 'Hoofdstuk Permissies Actief',
'chapters_permissions_success' => 'Hoofdstuk Permissies Bijgewerkt',
'chapters_search_this' => 'Doorzoek dit hoofdstuk',
'chapters_search_this' => 'Doorzoek dit hoofdstuk',
/**
* Pages
*/
'page' => 'Pagina',
'pages' => 'Pagina\'s',
'x_pages' => ':count Pagina|:count Pagina\'s',
'x_pages' => ':count Pagina|:count Pagina\'s',
'pages_popular' => 'Populaire Pagina\'s',
'pages_new' => 'Nieuwe Pagina',
'pages_attachments' => 'Bijlages',
@ -168,7 +168,7 @@ return [
'pages_move_success' => 'Pagina verplaatst naar ":parentName"',
'pages_permissions' => 'Pagina Permissies',
'pages_permissions_success' => 'Pagina Permissies bijgwerkt',
'pages_revision' => 'Revisie',
'pages_revision' => 'Revisie',
'pages_revisions' => 'Pagina Revisies',
'pages_revisions_named' => 'Pagina Revisies voor :pageName',
'pages_revision_named' => 'Pagina Revisie voor :pageName',
@ -182,6 +182,7 @@ return [
'pages_revisions_restore' => 'Herstellen',
'pages_revisions_none' => 'Deze pagina heeft geen revisies',
'pages_copy_link' => 'Link Kopiëren',
'pages_edit_content_link' => 'Bewerk inhoud',
'pages_permissions_active' => 'Pagina Permissies Actief',
'pages_initial_revision' => 'Eerste publicatie',
'pages_initial_name' => 'Nieuwe Pagina',

View File

@ -179,6 +179,7 @@ return [
'pages_revisions_restore' => 'Przywróć',
'pages_revisions_none' => 'Ta strona nie posiada żadnych rewizji',
'pages_copy_link' => 'Kopiuj link',
'pages_edit_content_link' => 'Edytuj zawartość',
'pages_permissions_active' => 'Uprawnienia strony aktywne',
'pages_initial_revision' => 'Wydanie pierwotne',
'pages_initial_name' => 'Nowa strona',

View File

@ -20,6 +20,7 @@ return [
'role' => 'Regra',
'cover_image' => 'Imagem de capa',
'cover_image_description' => 'Esta imagem deve ser aproximadamente 300x170px.',
/**
* Actions
*/
@ -30,6 +31,7 @@ return [
'edit' => 'Editar',
'sort' => 'Ordenar',
'move' => 'Mover',
'copy' => 'Copiar',
'reply' => 'Responder',
'delete' => 'Excluir',
'search' => 'Pesquisar',
@ -48,6 +50,8 @@ return [
'toggle_details' => 'Alternar Detalhes',
'toggle_thumbnails' => 'Alternar Miniaturas',
'details' => 'Detalhes',
'grid_view' => 'Visualização em Grade',
'list_view' => 'Visualização em Lista',
/**
* Header

View File

@ -181,6 +181,7 @@ return [
'pages_revisions_restore' => 'Restaurar',
'pages_revisions_none' => 'Essa página não tem revisões',
'pages_copy_link' => 'Copia Link',
'pages_edit_content_link' => 'Editar conteúdo',
'pages_permissions_active' => 'Permissões de Página Ativas',
'pages_initial_revision' => 'Publicação Inicial',
'pages_initial_name' => 'Nova Página',

View File

@ -181,6 +181,7 @@ return [
'pages_revisions_restore' => 'Восстановить',
'pages_revisions_none' => 'У этой страницы нет других версий',
'pages_copy_link' => 'Копировать ссылку',
'pages_edit_content_link' => 'Изменить содержание',
'pages_permissions_active' => 'Действующие разрешения на страницу',
'pages_initial_revision' => 'Первоначальное издание',
'pages_initial_name' => 'Новая страница',

View File

@ -166,6 +166,7 @@ return [
'pages_revisions_restore' => 'Obnoviť',
'pages_revisions_none' => 'Táto stránka nemá žiadne revízie',
'pages_copy_link' => 'Kopírovať odkaz',
'pages_edit_content_link' => 'Upraviť obsah',
'pages_permissions_active' => 'Oprávnienia stránky aktívne',
'pages_initial_revision' => 'Prvé zverejnenie',
'pages_initial_name' => 'Nová stránka',

View File

@ -182,6 +182,7 @@ return [
'pages_revisions_restore' => 'Återställ',
'pages_revisions_none' => 'Sidan har inga revisioner',
'pages_copy_link' => 'Kopiera länk',
'pages_edit_content_link' => 'Redigera innehåll',
'pages_permissions_active' => 'Anpassade rättigheter är i bruk',
'pages_initial_revision' => 'Första publicering',
'pages_initial_name' => 'Ny sida',

View File

@ -181,6 +181,7 @@ return [
'pages_revisions_restore' => '恢复',
'pages_revisions_none' => '此页面没有修订',
'pages_copy_link' => '复制链接',
'pages_edit_content_link' => '编辑内容',
'pages_permissions_active' => '有效的页面权限',
'pages_initial_revision' => '初始发布',
'pages_initial_name' => '新页面',

View File

@ -182,6 +182,7 @@ return [
'pages_revisions_restore' => '恢複',
'pages_revisions_none' => '此頁面沒有修訂',
'pages_copy_link' => '複製連結',
'pages_edit_content_link' => '编辑内容',
'pages_permissions_active' => '有效的頁面權限',
'pages_initial_revision' => '初次發布',
'pages_initial_name' => '新頁面',

View File

@ -42,8 +42,8 @@
<span class="text-primary small" @click="file.deleting = false;">{{ trans('common.cancel') }}</span>
</div>
</div>
<div @click="startEdit(file)" class="drag-card-action text-center text-primary" style="padding: 0;">@icon('edit')</div>
<div @click="deleteFile(file)" class="drag-card-action text-center text-neg" style="padding: 0;">@icon('close')</div>
<div @click="startEdit(file)" class="drag-card-action text-center text-primary">@icon('edit')</div>
<div @click="deleteFile(file)" class="drag-card-action text-center text-neg">@icon('close')</div>
</div>
</draggable>
<p class="small muted" v-if="files.length === 0">

View File

@ -14,7 +14,7 @@
<div class="container">
<div class="row">
<div class="col-md-9">
<div class="page-content">
<div class="page-content page-revision">
@include('pages.page-display')
</div>
</div>

View File

@ -124,10 +124,16 @@
<div class="page-content flex" page-display="{{ $page->id }}">
<div class="pointer-container" id="pointer">
<div class="pointer anim" >
<div class="pointer anim {{ userCan('page-update', $page) ? 'is-page-editable' : ''}}" >
<span class="icon text-primary">@icon('link') @icon('include', ['style' => 'display:none;'])</span>
<input readonly="readonly" type="text" id="pointer-url" placeholder="url">
<button class="button icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
<span class="input-group">
<input readonly="readonly" type="text" id="pointer-url" placeholder="url">
<button class="button icon" data-clipboard-target="#pointer-url" type="button" title="{{ trans('entities.pages_copy_link') }}">@icon('copy')</button>
</span>
@if(userCan('page-update', $page))
<a href="{{ $page->getUrl('/edit') }}" id="pointer-edit" data-edit-href="{{ $page->getUrl('/edit') }}"
class="button icon heading-edit-icon" title="{{ trans('entities.pages_edit_content_link')}}">@icon('edit')</a>
@endif
</div>
</div>

View File

@ -1,12 +1,11 @@
<div notification="success" data-autohide class="pos" @if(session()->has('success')) data-show @endif>
<div notification="success" style="display: none;" data-autohide class="pos" @if(session()->has('success')) data-show @endif>
@icon('check-circle') <span>{!! nl2br(htmlentities(session()->get('success'))) !!}</span>
</div>
<div notification="warning" class="warning" @if(session()->has('warning')) data-show @endif>
<div notification="warning" style="display: none;" class="warning" @if(session()->has('warning')) data-show @endif>
@icon('info') <span>{!! nl2br(htmlentities(session()->get('warning'))) !!}</span>
</div>
<div notification="error" class="neg" @if(session()->has('error')) data-show @endif>
<div notification="error" style="display: none;" class="neg" @if(session()->has('error')) data-show @endif>
@icon('danger') <span>{!! nl2br(htmlentities(session()->get('error'))) !!}</span>
</div>

View File

@ -15,6 +15,14 @@
<label for="name">{{ trans('settings.role_desc') }}</label>
@include('form/text', ['name' => 'description'])
</div>
@if(config('auth.method') === 'ldap')
<div class="form-group">
<label for="name">{{ trans('settings.role_external_auth_id') }}</label>
@include('form/text', ['name' => 'external_auth_id'])
</div>
@endif
<h5>{{ trans('settings.role_system') }}</h5>
<label>@include('settings/roles/checkbox', ['permission' => 'users-manage']) {{ trans('settings.role_manage_users') }}</label>
<label>@include('settings/roles/checkbox', ['permission' => 'user-roles-manage']) {{ trans('settings.role_manage_roles') }}</label>

View File

@ -0,0 +1,36 @@
@if ($paginator->hasPages())
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="disabled"><span>&laquo;</span></li>
@else
<li><a href="{{ $paginator->previousPageUrl() }}" rel="prev">&laquo;</a></li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="disabled"><span>{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="active primary-background"><span>{{ $page }}</span></li>
@else
<li><a href="{{ $url }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li><a href="{{ $paginator->nextPageUrl() }}" rel="next">&raquo;</a></li>
@else
<li class="disabled"><span>&raquo;</span></li>
@endif
</ul>
@endif

View File

@ -1,10 +1,17 @@
<?php namespace Tests;
use BookStack\Role;
use BookStack\Services\Ldap;
use BookStack\User;
use Mockery\MockInterface;
class LdapTest extends BrowserKitTest
{
/**
* @var MockInterface
*/
protected $mockLdap;
protected $mockUser;
protected $resourceId = 'resource-test';
@ -12,9 +19,15 @@ class LdapTest extends BrowserKitTest
{
parent::setUp();
if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
app('config')->set(['auth.method' => 'ldap', 'services.ldap.base_dn' => 'dc=ldap,dc=local', 'auth.providers.users.driver' => 'ldap']);
$this->mockLdap = \Mockery::mock(\BookStack\Services\Ldap::class);
$this->app['BookStack\Services\Ldap'] = $this->mockLdap;
app('config')->set([
'auth.method' => 'ldap',
'services.ldap.base_dn' => 'dc=ldap,dc=local',
'services.ldap.email_attribute' => 'mail',
'services.ldap.user_to_groups' => false,
'auth.providers.users.driver' => 'ldap',
]);
$this->mockLdap = \Mockery::mock(Ldap::class);
$this->app[Ldap::class] = $this->mockLdap;
$this->mockUser = factory(User::class)->make();
}
@ -133,4 +146,156 @@ class LdapTest extends BrowserKitTest
->dontSee('External Authentication');
}
public function test_login_maps_roles_and_retains_existsing_roles()
{
$roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
$roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
$existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
$this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
$this->mockUser->attachRole($existingRole);
app('config')->set([
'services.ldap.user_to_groups' => true,
'services.ldap.group_attribute' => 'memberOf',
'services.ldap.remove_from_groups' => false,
]);
$this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->times(2);
$this->mockLdap->shouldReceive('setOption')->times(5);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(5)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test' . config('services.ldap.base_dn')],
'mail' => [$this->mockUser->email],
'memberof' => [
'count' => 2,
0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
]
]]);
$this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
'user_id' => $user->id,
'role_id' => $roleToReceive->id
]);
$this->seeInDatabase('role_user', [
'user_id' => $user->id,
'role_id' => $roleToReceive2->id
]);
$this->seeInDatabase('role_user', [
'user_id' => $user->id,
'role_id' => $existingRole->id
]);
}
public function test_login_maps_roles_and_removes_old_roles_if_set()
{
$roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
$existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
$this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
$this->mockUser->attachRole($existingRole);
app('config')->set([
'services.ldap.user_to_groups' => true,
'services.ldap.group_attribute' => 'memberOf',
'services.ldap.remove_from_groups' => true,
]);
$this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->times(2);
$this->mockLdap->shouldReceive('setOption')->times(4);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test' . config('services.ldap.base_dn')],
'mail' => [$this->mockUser->email],
'memberof' => [
'count' => 1,
0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
]
]]);
$this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
'user_id' => $user->id,
'role_id' => $roleToReceive->id
]);
$this->dontSeeInDatabase('role_user', [
'user_id' => $user->id,
'role_id' => $existingRole->id
]);
}
public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
{
$role = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
$this->asAdmin()->visit('/settings/roles/' . $role->id)
->see('ex-auth-a');
}
public function test_login_maps_roles_using_external_auth_ids_if_set()
{
$roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
$roleToNotReceive = factory(Role::class)->create(['name' => 'ldaptester-not-receive', 'display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
app('config')->set([
'services.ldap.user_to_groups' => true,
'services.ldap.group_attribute' => 'memberOf',
'services.ldap.remove_from_groups' => true,
]);
$this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
$this->mockLdap->shouldReceive('setVersion')->times(2);
$this->mockLdap->shouldReceive('setOption')->times(4);
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
->andReturn(['count' => 1, 0 => [
'uid' => [$this->mockUser->name],
'cn' => [$this->mockUser->name],
'dn' => ['dc=test' . config('services.ldap.base_dn')],
'mail' => [$this->mockUser->email],
'memberof' => [
'count' => 1,
0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com",
]
]]);
$this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
$this->visit('/login')
->see('Username')
->type($this->mockUser->name, '#username')
->type($this->mockUser->password, '#password')
->press('Log In')
->seePageIs('/');
$user = User::where('email', $this->mockUser->email)->first();
$this->seeInDatabase('role_user', [
'user_id' => $user->id,
'role_id' => $roleToReceive->id
]);
$this->dontSeeInDatabase('role_user', [
'user_id' => $user->id,
'role_id' => $roleToNotReceive->id
]);
}
}

View File

@ -592,4 +592,26 @@ class RestrictionsTest extends BrowserKitTest
->see('You do not have permission')
->seePageIs('/');
}
public function test_can_create_page_if_chapter_has_permissions_when_book_not_visible()
{
$book = Book::first();
$this->setEntityRestrictions($book, []);
$bookChapter = $book->chapters->first();
$this->setEntityRestrictions($bookChapter, ['view']);
$this->actingAs($this->user)->visit($bookChapter->getUrl())
->dontSee('New Page');
$this->setEntityRestrictions($bookChapter, ['view', 'create']);
$this->actingAs($this->user)->visit($bookChapter->getUrl())
->click('New Page')
->seeStatusCode(200)
->type('test page', 'name')
->type('test content', 'html')
->press('Save Page')
->seePageIs($book->getUrl('/page/test-page'))
->seeStatusCode(200);
}
}

View File

@ -25,7 +25,11 @@ const config = {
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
presets: [[
'@babel/preset-env', {
useBuiltIns: 'usage'
}
]]
}
}
},