mirror of
https://github.com/BookStackApp/BookStack.git
synced 2024-11-23 11:04:17 +08:00
Merge branch 'master' into attachment_drag_drop
This commit is contained in:
commit
e305ba14d9
|
@ -270,4 +270,12 @@ API_DEFAULT_ITEM_COUNT=100
|
|||
API_MAX_ITEM_COUNT=500
|
||||
|
||||
# The number of API requests that can be made per minute by a single user.
|
||||
API_REQUESTS_PER_MIN=180
|
||||
API_REQUESTS_PER_MIN=180
|
||||
|
||||
# Enable the logging of failed email+password logins with the given message
|
||||
# The defaul log channel below uses the php 'error_log' function which commonly
|
||||
# results in messages being output to the webserver error logs.
|
||||
# The message can contain a %u parameter which will be replaced with the login
|
||||
# user identifier (Username or email).
|
||||
LOG_FAILED_LOGIN_MESSAGE=false
|
||||
LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver
|
||||
|
|
|
@ -4,6 +4,7 @@ use BookStack\Auth\Permissions\PermissionService;
|
|||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Entity;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ActivityService
|
||||
{
|
||||
|
@ -49,7 +50,7 @@ class ActivityService
|
|||
protected function newActivityForUser(string $key, ?int $bookId = null): Activity
|
||||
{
|
||||
return $this->activity->newInstance()->forceFill([
|
||||
'key' => strtolower($key),
|
||||
'key' => strtolower($key),
|
||||
'user_id' => $this->user->id,
|
||||
'book_id' => $bookId ?? 0,
|
||||
]);
|
||||
|
@ -64,8 +65,8 @@ class ActivityService
|
|||
{
|
||||
$activities = $entity->activity()->get();
|
||||
$entity->activity()->update([
|
||||
'extra' => $entity->name,
|
||||
'entity_id' => 0,
|
||||
'extra' => $entity->name,
|
||||
'entity_id' => 0,
|
||||
'entity_type' => '',
|
||||
]);
|
||||
return $activities;
|
||||
|
@ -99,7 +100,7 @@ class ActivityService
|
|||
$query = $this->activity->newQuery()->where('entity_type', '=', $entity->getMorphClass())
|
||||
->where('entity_id', '=', $entity->id);
|
||||
}
|
||||
|
||||
|
||||
$activity = $this->permissionService
|
||||
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
|
||||
->orderBy('created_at', 'desc')
|
||||
|
@ -159,4 +160,20 @@ class ActivityService
|
|||
session()->flash('success', $message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log out a failed login attempt, Providing the given username
|
||||
* as part of the message if the '%u' string is used.
|
||||
*/
|
||||
public function logFailedLogin(string $username)
|
||||
{
|
||||
$message = config('logging.failed_login.message');
|
||||
if (!$message) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message = str_replace("%u", $username, $message);
|
||||
$channel = config('logging.failed_login.channel');
|
||||
Log::channel($channel)->warning($message);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ExternalAuthService
|
||||
{
|
||||
|
@ -39,22 +41,14 @@ class ExternalAuthService
|
|||
/**
|
||||
* Match an array of group names to BookStack system roles.
|
||||
* Formats group names to be lower-case and hyphenated.
|
||||
* @param array $groupNames
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function matchGroupsToSystemsRoles(array $groupNames)
|
||||
protected function matchGroupsToSystemsRoles(array $groupNames): Collection
|
||||
{
|
||||
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();
|
||||
|
||||
$roles = Role::query()->get(['id', 'external_auth_id', 'display_name']);
|
||||
$matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
|
||||
return $this->roleMatchesGroupNames($role, $groupNames);
|
||||
});
|
||||
|
|
|
@ -71,15 +71,15 @@ class RegistrationService
|
|||
// Start email confirmation flow if required
|
||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
|
||||
$newUser->save();
|
||||
$message = '';
|
||||
|
||||
try {
|
||||
$this->emailConfirmationService->sendConfirmation($newUser);
|
||||
session()->flash('sent-email-confirmation', true);
|
||||
} catch (Exception $e) {
|
||||
$message = trans('auth.email_confirm_send_error');
|
||||
throw new UserRegistrationException($message, '/register/confirm');
|
||||
}
|
||||
|
||||
throw new UserRegistrationException($message, '/register/confirm');
|
||||
}
|
||||
|
||||
return $newUser;
|
||||
|
|
|
@ -311,7 +311,6 @@ class Saml2Service extends ExternalAuthService
|
|||
|
||||
/**
|
||||
* Get the user from the database for the specified details.
|
||||
* @throws SamlException
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function getOrRegisterUser(array $userDetails): ?User
|
||||
|
|
|
@ -3,25 +3,26 @@
|
|||
use BookStack\Auth\Role;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphOne;
|
||||
|
||||
class JointPermission extends Model
|
||||
{
|
||||
protected $primaryKey = null;
|
||||
public $timestamps = false;
|
||||
|
||||
/**
|
||||
* Get the role that this points to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function role()
|
||||
public function role(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Role::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entity this points to.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
|
||||
*/
|
||||
public function entity()
|
||||
public function entity(): MorphOne
|
||||
{
|
||||
return $this->morphOne(Entity::class, 'entity');
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Permissions;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PermissionsRepo
|
||||
|
@ -16,11 +17,8 @@ class PermissionsRepo
|
|||
|
||||
/**
|
||||
* PermissionsRepo constructor.
|
||||
* @param RolePermission $permission
|
||||
* @param Role $role
|
||||
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(RolePermission $permission, Role $role, Permissions\PermissionService $permissionService)
|
||||
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
|
||||
{
|
||||
$this->permission = $permission;
|
||||
$this->role = $role;
|
||||
|
@ -29,46 +27,34 @@ class PermissionsRepo
|
|||
|
||||
/**
|
||||
* Get all the user roles from the system.
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getAllRoles()
|
||||
public function getAllRoles(): Collection
|
||||
{
|
||||
return $this->role->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the roles except for the provided one.
|
||||
* @param Role $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function getAllRolesExcept(Role $role)
|
||||
public function getAllRolesExcept(Role $role): Collection
|
||||
{
|
||||
return $this->role->where('id', '!=', $role->id)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a role via its ID.
|
||||
* @param $id
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRoleById($id)
|
||||
public function getRoleById($id): Role
|
||||
{
|
||||
return $this->role->findOrFail($id);
|
||||
return $this->role->newQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new role into the system.
|
||||
* @param array $roleData
|
||||
* @return Role
|
||||
*/
|
||||
public function saveNewRole($roleData)
|
||||
public function saveNewRole(array $roleData): Role
|
||||
{
|
||||
$role = $this->role->newInstance($roleData);
|
||||
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
|
||||
// Prevent duplicate names
|
||||
while ($this->role->where('name', '=', $role->name)->count() > 0) {
|
||||
$role->name .= strtolower(Str::random(2));
|
||||
}
|
||||
$role->save();
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
|
@ -80,13 +66,11 @@ class PermissionsRepo
|
|||
/**
|
||||
* Updates an existing role.
|
||||
* Ensure Admin role always have core permissions.
|
||||
* @param $roleId
|
||||
* @param $roleData
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function updateRole($roleId, $roleData)
|
||||
public function updateRole($roleId, array $roleData)
|
||||
{
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
/** @var Role $role */
|
||||
$role = $this->role->newQuery()->findOrFail($roleId);
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
if ($role->system_name === 'admin') {
|
||||
|
@ -108,16 +92,19 @@ class PermissionsRepo
|
|||
|
||||
/**
|
||||
* Assign an list of permission names to an role.
|
||||
* @param Role $role
|
||||
* @param array $permissionNameArray
|
||||
*/
|
||||
public function assignRolePermissions(Role $role, $permissionNameArray = [])
|
||||
public function assignRolePermissions(Role $role, array $permissionNameArray = [])
|
||||
{
|
||||
$permissions = [];
|
||||
$permissionNameArray = array_values($permissionNameArray);
|
||||
if ($permissionNameArray && count($permissionNameArray) > 0) {
|
||||
$permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
|
||||
|
||||
if ($permissionNameArray) {
|
||||
$permissions = $this->permission->newQuery()
|
||||
->whereIn('name', $permissionNameArray)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
$role->permissions()->sync($permissions);
|
||||
}
|
||||
|
||||
|
@ -126,13 +113,13 @@ class PermissionsRepo
|
|||
* Check it's not an admin role or set as default before deleting.
|
||||
* If an migration Role ID is specified the users assign to the current role
|
||||
* will be added to the role of the specified id.
|
||||
* @param $roleId
|
||||
* @param $migrateRoleId
|
||||
* @throws PermissionsException
|
||||
* @throws Exception
|
||||
*/
|
||||
public function deleteRole($roleId, $migrateRoleId)
|
||||
{
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
/** @var Role $role */
|
||||
$role = $this->role->newQuery()->findOrFail($roleId);
|
||||
|
||||
// Prevent deleting admin role or default registration role.
|
||||
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
|
||||
|
@ -142,9 +129,9 @@ class PermissionsRepo
|
|||
}
|
||||
|
||||
if ($migrateRoleId) {
|
||||
$newRole = $this->role->find($migrateRoleId);
|
||||
$newRole = $this->role->newQuery()->find($migrateRoleId);
|
||||
if ($newRole) {
|
||||
$users = $role->users->pluck('id')->toArray();
|
||||
$users = $role->users()->pluck('id')->toArray();
|
||||
$newRole->users()->sync($users);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
use BookStack\Auth\Role;
|
||||
use BookStack\Model;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
*/
|
||||
class RolePermission extends Model
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -3,13 +3,16 @@
|
|||
use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Auth\Permissions\RolePermission;
|
||||
use BookStack\Model;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
/**
|
||||
* Class Role
|
||||
* @property int $id
|
||||
* @property string $display_name
|
||||
* @property string $description
|
||||
* @property string $external_auth_id
|
||||
* @package BookStack\Auth
|
||||
* @property string $system_name
|
||||
*/
|
||||
class Role extends Model
|
||||
{
|
||||
|
@ -26,9 +29,8 @@ class Role extends Model
|
|||
|
||||
/**
|
||||
* Get all related JointPermissions.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
*/
|
||||
public function jointPermissions()
|
||||
public function jointPermissions(): HasMany
|
||||
{
|
||||
return $this->hasMany(JointPermission::class);
|
||||
}
|
||||
|
@ -43,10 +45,8 @@ class Role extends Model
|
|||
|
||||
/**
|
||||
* Check if this role has a permission.
|
||||
* @param $permissionName
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPermission($permissionName)
|
||||
public function hasPermission(string $permissionName): bool
|
||||
{
|
||||
$permissions = $this->getRelationValue('permissions');
|
||||
foreach ($permissions as $permission) {
|
||||
|
@ -59,7 +59,6 @@ class Role extends Model
|
|||
|
||||
/**
|
||||
* Add a permission to this role.
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public function attachPermission(RolePermission $permission)
|
||||
{
|
||||
|
@ -68,7 +67,6 @@ class Role extends Model
|
|||
|
||||
/**
|
||||
* Detach a single permission from this role.
|
||||
* @param RolePermission $permission
|
||||
*/
|
||||
public function detachPermission(RolePermission $permission)
|
||||
{
|
||||
|
@ -76,39 +74,33 @@ class Role extends Model
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the role object for the specified role.
|
||||
* @param $roleName
|
||||
* @return Role
|
||||
* Get the role of the specified display name.
|
||||
*/
|
||||
public static function getRole($roleName)
|
||||
public static function getRole(string $displayName): ?Role
|
||||
{
|
||||
return static::query()->where('name', '=', $roleName)->first();
|
||||
return static::query()->where('display_name', '=', $displayName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the role object for the specified system role.
|
||||
* @param $roleName
|
||||
* @return Role
|
||||
*/
|
||||
public static function getSystemRole($roleName)
|
||||
public static function getSystemRole(string $systemName): ?Role
|
||||
{
|
||||
return static::query()->where('system_name', '=', $roleName)->first();
|
||||
return static::query()->where('system_name', '=', $systemName)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all visible roles
|
||||
* @return mixed
|
||||
*/
|
||||
public static function visible()
|
||||
public static function visible(): Collection
|
||||
{
|
||||
return static::query()->where('hidden', '=', false)->orderBy('name')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the roles that can be restricted.
|
||||
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
public static function restrictable()
|
||||
public static function restrictable(): Collection
|
||||
{
|
||||
return static::query()->where('system_name', '!=', 'admin')->get();
|
||||
}
|
||||
|
|
|
@ -101,12 +101,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||
|
||||
/**
|
||||
* Check if the user has a role.
|
||||
* @param $role
|
||||
* @return mixed
|
||||
*/
|
||||
public function hasRole($role)
|
||||
public function hasRole($roleId): bool
|
||||
{
|
||||
return $this->roles->pluck('name')->contains($role);
|
||||
return $this->roles->pluck('id')->contains($roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -163,7 +161,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
|
|||
|
||||
/**
|
||||
* Attach a role to this user.
|
||||
* @param Role $role
|
||||
*/
|
||||
public function attachRole(Role $role)
|
||||
{
|
||||
|
|
|
@ -238,7 +238,7 @@ class UserRepo
|
|||
*/
|
||||
public function getAllRoles()
|
||||
{
|
||||
return $this->role->newQuery()->orderBy('name', 'asc')->get();
|
||||
return $this->role->newQuery()->orderBy('display_name', 'asc')->get();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Formatter\LineFormatter;
|
||||
use Monolog\Handler\ErrorLogHandler;
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
|
||||
|
@ -73,6 +75,19 @@ return [
|
|||
'level' => 'debug',
|
||||
],
|
||||
|
||||
// Custom errorlog implementation that logs out a plain,
|
||||
// non-formatted message intended for the webserver log.
|
||||
'errorlog_plain_webserver' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => 'debug',
|
||||
'handler' => ErrorLogHandler::class,
|
||||
'handler_with' => [4],
|
||||
'formatter' => LineFormatter::class,
|
||||
'formatter_with' => [
|
||||
'format' => "%message%",
|
||||
],
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
|
@ -86,4 +101,12 @@ return [
|
|||
],
|
||||
],
|
||||
|
||||
|
||||
// Failed Login Message
|
||||
// Allows a configurable message to be logged when a login request fails.
|
||||
'failed_login' => [
|
||||
'message' => env('LOG_FAILED_LOGIN_MESSAGE', null),
|
||||
'channel' => env('LOG_FAILED_LOGIN_CHANNEL', 'errorlog_plain_webserver'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -101,7 +101,7 @@ return [
|
|||
'url' => env('SAML2_IDP_SLO', null),
|
||||
// URL location of the IdP where the SP will send the SLO Response (ResponseLocation)
|
||||
// if not set, url for the SLO Request will be used
|
||||
'responseUrl' => '',
|
||||
'responseUrl' => null,
|
||||
// SAML protocol binding to be used when returning the <Response>
|
||||
// message. Onelogin Toolkit supports for this endpoint the
|
||||
// HTTP-Redirect binding only
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||
use BookStack\Exceptions\LoginAttemptException;
|
||||
|
@ -76,9 +77,13 @@ class LoginController extends Controller
|
|||
]);
|
||||
}
|
||||
|
||||
// Store the previous location for redirect after login
|
||||
$previous = url()->previous('');
|
||||
if (setting('app-public') && $previous && $previous !== url('/login')) {
|
||||
redirect()->setIntendedUrl($previous);
|
||||
if ($previous && $previous !== url('/login') && setting('app-public')) {
|
||||
$isPreviousFromInstance = (strpos($previous, url('/')) === 0);
|
||||
if ($isPreviousFromInstance) {
|
||||
redirect()->setIntendedUrl($previous);
|
||||
}
|
||||
}
|
||||
|
||||
return view('auth.login', [
|
||||
|
@ -98,6 +103,7 @@ class LoginController extends Controller
|
|||
public function login(Request $request)
|
||||
{
|
||||
$this->validateLogin($request);
|
||||
$username = $request->get($this->username());
|
||||
|
||||
// If the class is using the ThrottlesLogins trait, we can automatically throttle
|
||||
// the login attempts for this application. We'll key this by the username and
|
||||
|
@ -106,6 +112,7 @@ class LoginController extends Controller
|
|||
$this->hasTooManyLoginAttempts($request)) {
|
||||
$this->fireLockoutEvent($request);
|
||||
|
||||
Activity::logFailedLogin($username);
|
||||
return $this->sendLockoutResponse($request);
|
||||
}
|
||||
|
||||
|
@ -114,6 +121,7 @@ class LoginController extends Controller
|
|||
return $this->sendLoginResponse($request);
|
||||
}
|
||||
} catch (LoginAttemptException $exception) {
|
||||
Activity::logFailedLogin($username);
|
||||
return $this->sendLoginAttemptExceptionResponse($exception, $request);
|
||||
}
|
||||
|
||||
|
@ -122,6 +130,7 @@ class LoginController extends Controller
|
|||
// user surpasses their maximum number of attempts they will get locked out.
|
||||
$this->incrementLoginAttempts($request);
|
||||
|
||||
Activity::logFailedLogin($username);
|
||||
return $this->sendFailedLoginResponse($request);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ use Illuminate\Foundation\Validation\ValidatesRequests;
|
|||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
|
@ -132,23 +133,6 @@ abstract class Controller extends BaseController
|
|||
return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the response for when a request fails validation.
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param array $errors
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
protected function buildFailedValidationResponse(Request $request, array $errors)
|
||||
{
|
||||
if ($request->expectsJson()) {
|
||||
return response()->json(['validation' => $errors], 422);
|
||||
}
|
||||
|
||||
return redirect()->to($this->getRedirectUrl())
|
||||
->withInput($request->input())
|
||||
->withErrors($errors, $this->errorBag());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a response that forces a download in the browser.
|
||||
* @param string $content
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
use BookStack\Auth\Permissions\PermissionsRepo;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class PermissionController extends Controller
|
||||
{
|
||||
|
@ -11,7 +13,6 @@ class PermissionController extends Controller
|
|||
|
||||
/**
|
||||
* PermissionController constructor.
|
||||
* @param \BookStack\Auth\Permissions\PermissionsRepo $permissionsRepo
|
||||
*/
|
||||
public function __construct(PermissionsRepo $permissionsRepo)
|
||||
{
|
||||
|
@ -31,7 +32,6 @@ class PermissionController extends Controller
|
|||
|
||||
/**
|
||||
* Show the form to create a new role
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function createRole()
|
||||
{
|
||||
|
@ -41,15 +41,13 @@ class PermissionController extends Controller
|
|||
|
||||
/**
|
||||
* Store a new role in the system.
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function storeRole(Request $request)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$this->validate($request, [
|
||||
'display_name' => 'required|min:3|max:200',
|
||||
'description' => 'max:250'
|
||||
'display_name' => 'required|min:3|max:180',
|
||||
'description' => 'max:180'
|
||||
]);
|
||||
|
||||
$this->permissionsRepo->saveNewRole($request->all());
|
||||
|
@ -59,11 +57,9 @@ class PermissionController extends Controller
|
|||
|
||||
/**
|
||||
* Show the form for editing a user role.
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function editRole($id)
|
||||
public function editRole(string $id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
|
@ -75,18 +71,14 @@ class PermissionController extends Controller
|
|||
|
||||
/**
|
||||
* Updates a user role.
|
||||
* @param Request $request
|
||||
* @param $id
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws PermissionsException
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function updateRole(Request $request, $id)
|
||||
public function updateRole(Request $request, string $id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$this->validate($request, [
|
||||
'display_name' => 'required|min:3|max:200',
|
||||
'description' => 'max:250'
|
||||
'display_name' => 'required|min:3|max:180',
|
||||
'description' => 'max:180'
|
||||
]);
|
||||
|
||||
$this->permissionsRepo->updateRole($id, $request->all());
|
||||
|
@ -97,10 +89,8 @@ class PermissionController extends Controller
|
|||
/**
|
||||
* Show the view to delete a role.
|
||||
* Offers the chance to migrate users.
|
||||
* @param $id
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
*/
|
||||
public function showDeleteRole($id)
|
||||
public function showDeleteRole(string $id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
$role = $this->permissionsRepo->getRoleById($id);
|
||||
|
@ -113,11 +103,9 @@ class PermissionController extends Controller
|
|||
/**
|
||||
* Delete a role from the system,
|
||||
* Migrate from a previous role if set.
|
||||
* @param Request $request
|
||||
* @param $id
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws Exception
|
||||
*/
|
||||
public function deleteRole(Request $request, $id)
|
||||
public function deleteRole(Request $request, string $id)
|
||||
{
|
||||
$this->checkPermission('user-roles-manage');
|
||||
|
||||
|
|
|
@ -66,8 +66,8 @@ class UserController extends Controller
|
|||
{
|
||||
$this->checkPermission('users-manage');
|
||||
$validationRules = [
|
||||
'name' => 'required',
|
||||
'email' => 'required|email|unique:users,email'
|
||||
'name' => 'required',
|
||||
'email' => 'required|email|unique:users,email'
|
||||
];
|
||||
|
||||
$authMethod = config('auth.method');
|
||||
|
|
|
@ -44,6 +44,10 @@ class Authenticate
|
|||
], 401);
|
||||
}
|
||||
|
||||
if (session()->get('sent-email-confirmation') === true) {
|
||||
return redirect('/register/confirm');
|
||||
}
|
||||
|
||||
return redirect('/register/confirm/awaiting');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class DropJointPermissionsId extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('joint_permissions', function (Blueprint $table) {
|
||||
$table->dropColumn('id');
|
||||
$table->primary(['role_id', 'entity_type', 'entity_id', 'action'], 'joint_primary');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('joint_permissions', function (Blueprint $table) {
|
||||
$table->dropPrimary(['role_id', 'entity_type', 'entity_id', 'action']);
|
||||
});
|
||||
|
||||
Schema::table('joint_permissions', function (Blueprint $table) {
|
||||
$table->increments('id')->unsigned();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class RemoveRoleNameField extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->dropColumn('name');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('roles', function (Blueprint $table) {
|
||||
$table->string('name')->index();
|
||||
});
|
||||
|
||||
DB::table('roles')->update([
|
||||
"name" => DB::raw("lower(replace(`display_name`, ' ', '-'))"),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -59,4 +59,41 @@ Will result with `this.$opts` being:
|
|||
"delay": "500",
|
||||
"show": ""
|
||||
}
|
||||
```
|
||||
|
||||
#### Global Helpers
|
||||
|
||||
There are various global helper libraries which can be used in components:
|
||||
|
||||
```js
|
||||
// HTTP service
|
||||
window.$http.get(url, params);
|
||||
window.$http.post(url, data);
|
||||
window.$http.put(url, data);
|
||||
window.$http.delete(url, data);
|
||||
window.$http.patch(url, data);
|
||||
|
||||
// Global event system
|
||||
// Emit a global event
|
||||
window.$events.emit(eventName, eventData);
|
||||
// Listen to a global event
|
||||
window.$events.listen(eventName, callback);
|
||||
// Show a success message
|
||||
window.$events.success(message);
|
||||
// Show an error message
|
||||
window.$events.error(message);
|
||||
// Show validation errors, if existing, as an error notification
|
||||
window.$events.showValidationErrors(error);
|
||||
|
||||
// Translator
|
||||
// Take the given plural text and count to decide on what plural option
|
||||
// to use, Similar to laravel's trans_choice function but instead
|
||||
// takes the direction directly instead of a translation key.
|
||||
window.trans_plural(translationString, count, replacements);
|
||||
|
||||
// Component System
|
||||
// Parse and initialise any components from the given root el down.
|
||||
window.components.init(rootEl);
|
||||
// Get the first active component of the given name
|
||||
window.components.first(name);
|
||||
```
|
3670
package-lock.json
generated
3670
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
|
@ -4,9 +4,9 @@
|
|||
"build:css:dev": "sass ./resources/sass:./public/dist",
|
||||
"build:css:watch": "sass ./resources/sass:./public/dist --watch",
|
||||
"build:css:production": "sass ./resources/sass:./public/dist -s compressed",
|
||||
"build:js:dev": "webpack",
|
||||
"build:js:watch": "webpack --watch",
|
||||
"build:js:production": "NODE_ENV=production webpack",
|
||||
"build:js:dev": "esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --target=es2020",
|
||||
"build:js:watch": "chokidar \"./resources/**/*.js\" -c \"npm run build:js:dev\"",
|
||||
"build:js:production": "NODE_ENV=production esbuild --bundle ./resources/js/index.js --outfile=public/dist/app.js --sourcemap --minify",
|
||||
"build": "npm-run-all --parallel build:*:dev",
|
||||
"production": "npm-run-all --parallel build:*:production",
|
||||
"dev": "npm-run-all --parallel watch livereload",
|
||||
|
@ -15,15 +15,16 @@
|
|||
"permissions": "chown -R $USER:$USER bootstrap/cache storage public/uploads"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chokidar-cli": "^2.1.0",
|
||||
"esbuild": "0.6.30",
|
||||
"livereload": "^0.9.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"sass": "^1.26.10",
|
||||
"webpack": "^4.44.0",
|
||||
"webpack-cli": "^3.3.12"
|
||||
"punycode": "^2.1.1",
|
||||
"sass": "^1.26.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"clipboard": "^2.0.6",
|
||||
"codemirror": "^5.56.0",
|
||||
"codemirror": "^5.57.0",
|
||||
"dropzone": "^5.7.2",
|
||||
"markdown-it": "^11.0.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
|
|
|
@ -51,5 +51,7 @@
|
|||
<server name="DEBUGBAR_ENABLED" value="false"/>
|
||||
<server name="SAML2_ENABLED" value="false"/>
|
||||
<server name="API_REQUESTS_PER_MIN" value="180"/>
|
||||
<server name="LOG_FAILED_LOGIN_MESSAGE" value=""/>
|
||||
<server name="LOG_FAILED_LOGIN_CHANNEL" value="testing"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
|
|
|
@ -51,7 +51,7 @@ All development on BookStack is currently done on the master branch. When it's t
|
|||
|
||||
* [Node.js](https://nodejs.org/en/) v10.0+
|
||||
|
||||
This project uses SASS for CSS development and this is built, along with the JavaScript, using webpack. The below npm commands can be used to install the dependencies & run the build tasks:
|
||||
This project uses SASS for CSS development and this is built, along with the JavaScript, using a range of npm scripts. The below npm commands can be used to install the dependencies & run the build tasks:
|
||||
|
||||
``` bash
|
||||
# Install NPM Dependencies
|
||||
|
|
|
@ -1,12 +1,106 @@
|
|||
const componentMapping = {};
|
||||
import addRemoveRows from "./add-remove-rows.js"
|
||||
import ajaxDeleteRow from "./ajax-delete-row.js"
|
||||
import ajaxForm from "./ajax-form.js"
|
||||
import attachments from "./attachments.js"
|
||||
import autoSuggest from "./auto-suggest.js"
|
||||
import backToTop from "./back-to-top.js"
|
||||
import bookSort from "./book-sort.js"
|
||||
import breadcrumbListing from "./breadcrumb-listing.js"
|
||||
import chapterToggle from "./chapter-toggle.js"
|
||||
import codeEditor from "./code-editor.js"
|
||||
import codeHighlighter from "./code-highlighter.js"
|
||||
import collapsible from "./collapsible.js"
|
||||
import customCheckbox from "./custom-checkbox.js"
|
||||
import detailsHighlighter from "./details-highlighter.js"
|
||||
import dropdown from "./dropdown.js"
|
||||
import dropzone from "./dropzone.js"
|
||||
import editorToolbox from "./editor-toolbox.js"
|
||||
import entityPermissionsEditor from "./entity-permissions-editor.js"
|
||||
import entitySearch from "./entity-search.js"
|
||||
import entitySelector from "./entity-selector.js"
|
||||
import entitySelectorPopup from "./entity-selector-popup.js"
|
||||
import eventEmitSelect from "./event-emit-select.js"
|
||||
import expandToggle from "./expand-toggle.js"
|
||||
import headerMobileToggle from "./header-mobile-toggle.js"
|
||||
import homepageControl from "./homepage-control.js"
|
||||
import imageManager from "./image-manager.js"
|
||||
import imagePicker from "./image-picker.js"
|
||||
import index from "./index.js"
|
||||
import listSortControl from "./list-sort-control.js"
|
||||
import markdownEditor from "./markdown-editor.js"
|
||||
import newUserPassword from "./new-user-password.js"
|
||||
import notification from "./notification.js"
|
||||
import optionalInput from "./optional-input.js"
|
||||
import pageComments from "./page-comments.js"
|
||||
import pageDisplay from "./page-display.js"
|
||||
import pageEditor from "./page-editor.js"
|
||||
import pagePicker from "./page-picker.js"
|
||||
import permissionsTable from "./permissions-table.js"
|
||||
import popup from "./popup.js"
|
||||
import settingAppColorPicker from "./setting-app-color-picker.js"
|
||||
import settingColorPicker from "./setting-color-picker.js"
|
||||
import shelfSort from "./shelf-sort.js"
|
||||
import sidebar from "./sidebar.js"
|
||||
import sortableList from "./sortable-list.js"
|
||||
import tabs from "./tabs.js"
|
||||
import tagManager from "./tag-manager.js"
|
||||
import templateManager from "./template-manager.js"
|
||||
import toggleSwitch from "./toggle-switch.js"
|
||||
import triLayout from "./tri-layout.js"
|
||||
import wysiwygEditor from "./wysiwyg-editor.js"
|
||||
|
||||
const definitionFiles = require.context('./', false, /\.js$/);
|
||||
for (const fileName of definitionFiles.keys()) {
|
||||
const name = fileName.replace('./', '').split('.')[0];
|
||||
if (name !== 'index') {
|
||||
componentMapping[name] = definitionFiles(fileName).default;
|
||||
}
|
||||
}
|
||||
const componentMapping = {
|
||||
"add-remove-rows": addRemoveRows,
|
||||
"ajax-delete-row": ajaxDeleteRow,
|
||||
"ajax-form": ajaxForm,
|
||||
"attachments": attachments,
|
||||
"auto-suggest": autoSuggest,
|
||||
"back-to-top": backToTop,
|
||||
"book-sort": bookSort,
|
||||
"breadcrumb-listing": breadcrumbListing,
|
||||
"chapter-toggle": chapterToggle,
|
||||
"code-editor": codeEditor,
|
||||
"code-highlighter": codeHighlighter,
|
||||
"collapsible": collapsible,
|
||||
"custom-checkbox": customCheckbox,
|
||||
"details-highlighter": detailsHighlighter,
|
||||
"dropdown": dropdown,
|
||||
"dropzone": dropzone,
|
||||
"editor-toolbox": editorToolbox,
|
||||
"entity-permissions-editor": entityPermissionsEditor,
|
||||
"entity-search": entitySearch,
|
||||
"entity-selector": entitySelector,
|
||||
"entity-selector-popup": entitySelectorPopup,
|
||||
"event-emit-select": eventEmitSelect,
|
||||
"expand-toggle": expandToggle,
|
||||
"header-mobile-toggle": headerMobileToggle,
|
||||
"homepage-control": homepageControl,
|
||||
"image-manager": imageManager,
|
||||
"image-picker": imagePicker,
|
||||
"index": index,
|
||||
"list-sort-control": listSortControl,
|
||||
"markdown-editor": markdownEditor,
|
||||
"new-user-password": newUserPassword,
|
||||
"notification": notification,
|
||||
"optional-input": optionalInput,
|
||||
"page-comments": pageComments,
|
||||
"page-display": pageDisplay,
|
||||
"page-editor": pageEditor,
|
||||
"page-picker": pagePicker,
|
||||
"permissions-table": permissionsTable,
|
||||
"popup": popup,
|
||||
"setting-app-color-picker": settingAppColorPicker,
|
||||
"setting-color-picker": settingColorPicker,
|
||||
"shelf-sort": shelfSort,
|
||||
"sidebar": sidebar,
|
||||
"sortable-list": sortableList,
|
||||
"tabs": tabs,
|
||||
"tag-manager": tagManager,
|
||||
"template-manager": templateManager,
|
||||
"toggle-switch": toggleSwitch,
|
||||
"tri-layout": triLayout,
|
||||
"wysiwyg-editor": wysiwygEditor,
|
||||
};
|
||||
|
||||
window.components = {};
|
||||
|
||||
|
|
|
@ -1,16 +1,31 @@
|
|||
import {scrollAndHighlightElement} from "../services/util";
|
||||
|
||||
/**
|
||||
* @extends {Component}
|
||||
*/
|
||||
class PageComments {
|
||||
|
||||
constructor(elem) {
|
||||
this.elem = elem;
|
||||
this.pageId = Number(elem.getAttribute('page-id'));
|
||||
setup() {
|
||||
this.elem = this.$el;
|
||||
this.pageId = Number(this.$opts.pageId);
|
||||
|
||||
// Element references
|
||||
this.container = this.$refs.commentContainer;
|
||||
this.formContainer = this.$refs.formContainer;
|
||||
this.commentCountBar = this.$refs.commentCountBar;
|
||||
this.addButtonContainer = this.$refs.addButtonContainer;
|
||||
this.replyToRow = this.$refs.replyToRow;
|
||||
|
||||
// Translations
|
||||
this.updatedText = this.$opts.updatedText;
|
||||
this.deletedText = this.$opts.deletedText;
|
||||
this.createdText = this.$opts.createdText;
|
||||
this.countText = this.$opts.countText;
|
||||
|
||||
// Internal State
|
||||
this.editingComment = null;
|
||||
this.parentId = null;
|
||||
|
||||
this.container = elem.querySelector('[comment-container]');
|
||||
this.formContainer = elem.querySelector('[comment-form-container]');
|
||||
|
||||
if (this.formContainer) {
|
||||
this.form = this.formContainer.querySelector('form');
|
||||
this.formInput = this.form.querySelector('textarea');
|
||||
|
@ -32,13 +47,14 @@ class PageComments {
|
|||
if (actionElem === null) return;
|
||||
event.preventDefault();
|
||||
|
||||
let action = actionElem.getAttribute('action');
|
||||
if (action === 'edit') this.editComment(actionElem.closest('[comment]'));
|
||||
const action = actionElem.getAttribute('action');
|
||||
const comment = actionElem.closest('[comment]');
|
||||
if (action === 'edit') this.editComment(comment);
|
||||
if (action === 'closeUpdateForm') this.closeUpdateForm();
|
||||
if (action === 'delete') this.deleteComment(actionElem.closest('[comment]'));
|
||||
if (action === 'delete') this.deleteComment(comment);
|
||||
if (action === 'addComment') this.showForm();
|
||||
if (action === 'hideForm') this.hideForm();
|
||||
if (action === 'reply') this.setReply(actionElem.closest('[comment]'));
|
||||
if (action === 'reply') this.setReply(comment);
|
||||
if (action === 'remove-reply-to') this.removeReplyTo();
|
||||
}
|
||||
|
||||
|
@ -69,14 +85,15 @@ class PageComments {
|
|||
};
|
||||
this.showLoading(form);
|
||||
let commentId = this.editingComment.getAttribute('comment');
|
||||
window.$http.put(`/ajax/comment/${commentId}`, reqData).then(resp => {
|
||||
window.$http.put(`/comment/${commentId}`, reqData).then(resp => {
|
||||
let newComment = document.createElement('div');
|
||||
newComment.innerHTML = resp.data;
|
||||
this.editingComment.innerHTML = newComment.children[0].innerHTML;
|
||||
window.$events.emit('success', window.trans('entities.comment_updated_success'));
|
||||
window.$events.success(this.updatedText);
|
||||
window.components.init(this.editingComment);
|
||||
this.closeUpdateForm();
|
||||
this.editingComment = null;
|
||||
}).catch(window.$events.showValidationErrors).then(() => {
|
||||
this.hideLoading(form);
|
||||
});
|
||||
}
|
||||
|
@ -84,9 +101,9 @@ class PageComments {
|
|||
deleteComment(commentElem) {
|
||||
let id = commentElem.getAttribute('comment');
|
||||
this.showLoading(commentElem.querySelector('[comment-content]'));
|
||||
window.$http.delete(`/ajax/comment/${id}`).then(resp => {
|
||||
window.$http.delete(`/comment/${id}`).then(resp => {
|
||||
commentElem.parentNode.removeChild(commentElem);
|
||||
window.$events.emit('success', window.trans('entities.comment_deleted_success'));
|
||||
window.$events.success(this.deletedText);
|
||||
this.updateCount();
|
||||
this.hideForm();
|
||||
});
|
||||
|
@ -101,21 +118,24 @@ class PageComments {
|
|||
parent_id: this.parentId || null,
|
||||
};
|
||||
this.showLoading(this.form);
|
||||
window.$http.post(`/ajax/page/${this.pageId}/comment`, reqData).then(resp => {
|
||||
window.$http.post(`/comment/${this.pageId}`, reqData).then(resp => {
|
||||
let newComment = document.createElement('div');
|
||||
newComment.innerHTML = resp.data;
|
||||
let newElem = newComment.children[0];
|
||||
this.container.appendChild(newElem);
|
||||
window.components.init(newElem);
|
||||
window.$events.emit('success', window.trans('entities.comment_created_success'));
|
||||
window.$events.success(this.createdText);
|
||||
this.resetForm();
|
||||
this.updateCount();
|
||||
}).catch(err => {
|
||||
window.$events.showValidationErrors(err);
|
||||
this.hideLoading(this.form);
|
||||
});
|
||||
}
|
||||
|
||||
updateCount() {
|
||||
let count = this.container.children.length;
|
||||
this.elem.querySelector('[comments-title]').textContent = window.trans_choice('entities.comment_count', count, {count});
|
||||
this.elem.querySelector('[comments-title]').textContent = window.trans_plural(this.countText, count, {count});
|
||||
}
|
||||
|
||||
resetForm() {
|
||||
|
@ -129,7 +149,7 @@ class PageComments {
|
|||
showForm() {
|
||||
this.formContainer.style.display = 'block';
|
||||
this.formContainer.parentNode.style.display = 'block';
|
||||
this.elem.querySelector('[comment-add-button-container]').style.display = 'none';
|
||||
this.addButtonContainer.style.display = 'none';
|
||||
this.formInput.focus();
|
||||
this.formInput.scrollIntoView({behavior: "smooth"});
|
||||
}
|
||||
|
@ -137,14 +157,12 @@ class PageComments {
|
|||
hideForm() {
|
||||
this.formContainer.style.display = 'none';
|
||||
this.formContainer.parentNode.style.display = 'none';
|
||||
const addButtonContainer = this.elem.querySelector('[comment-add-button-container]');
|
||||
if (this.getCommentCount() > 0) {
|
||||
this.elem.appendChild(addButtonContainer)
|
||||
this.elem.appendChild(this.addButtonContainer)
|
||||
} else {
|
||||
const countBar = this.elem.querySelector('[comment-count-bar]');
|
||||
countBar.appendChild(addButtonContainer);
|
||||
this.commentCountBar.appendChild(this.addButtonContainer);
|
||||
}
|
||||
addButtonContainer.style.display = 'block';
|
||||
this.addButtonContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
getCommentCount() {
|
||||
|
@ -154,15 +172,15 @@ class PageComments {
|
|||
setReply(commentElem) {
|
||||
this.showForm();
|
||||
this.parentId = Number(commentElem.getAttribute('local-id'));
|
||||
this.elem.querySelector('[comment-form-reply-to]').style.display = 'block';
|
||||
let replyLink = this.elem.querySelector('[comment-form-reply-to] a');
|
||||
this.replyToRow.style.display = 'block';
|
||||
const replyLink = this.replyToRow.querySelector('a');
|
||||
replyLink.textContent = `#${this.parentId}`;
|
||||
replyLink.href = `#comment${this.parentId}`;
|
||||
}
|
||||
|
||||
removeReplyTo() {
|
||||
this.parentId = null;
|
||||
this.elem.querySelector('[comment-form-reply-to]').style.display = 'none';
|
||||
this.replyToRow.style.display = 'none';
|
||||
}
|
||||
|
||||
showLoading(formElem) {
|
||||
|
|
|
@ -7,11 +7,10 @@ window.baseUrl = function(path) {
|
|||
};
|
||||
|
||||
// Set events and http services on window
|
||||
import Events from "./services/events"
|
||||
import events from "./services/events"
|
||||
import httpInstance from "./services/http"
|
||||
const eventManager = new Events();
|
||||
window.$http = httpInstance;
|
||||
window.$events = eventManager;
|
||||
window.$events = events;
|
||||
|
||||
// Translation setup
|
||||
// Creates a global function with name 'trans' to be used in the same way as Laravel's translation system
|
||||
|
@ -19,6 +18,7 @@ import Translations from "./services/translations"
|
|||
const translator = new Translations();
|
||||
window.trans = translator.get.bind(translator);
|
||||
window.trans_choice = translator.getPlural.bind(translator);
|
||||
window.trans_plural = translator.parsePlural.bind(translator);
|
||||
|
||||
// Load Components
|
||||
import components from "./components"
|
||||
|
|
|
@ -1,55 +1,66 @@
|
|||
const listeners = {};
|
||||
const stack = [];
|
||||
|
||||
/**
|
||||
* Simple global events manager
|
||||
* Emit a custom event for any handlers to pick-up.
|
||||
* @param {String} eventName
|
||||
* @param {*} eventData
|
||||
*/
|
||||
class Events {
|
||||
constructor() {
|
||||
this.listeners = {};
|
||||
this.stack = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a custom event for any handlers to pick-up.
|
||||
* @param {String} eventName
|
||||
* @param {*} eventData
|
||||
* @returns {Events}
|
||||
*/
|
||||
emit(eventName, eventData) {
|
||||
this.stack.push({name: eventName, data: eventData});
|
||||
if (typeof this.listeners[eventName] === 'undefined') return this;
|
||||
let eventsToStart = this.listeners[eventName];
|
||||
for (let i = 0; i < eventsToStart.length; i++) {
|
||||
let event = eventsToStart[i];
|
||||
event(eventData);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to a custom event and run the given callback when that event occurs.
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
* @returns {Events}
|
||||
*/
|
||||
listen(eventName, callback) {
|
||||
if (typeof this.listeners[eventName] === 'undefined') this.listeners[eventName] = [];
|
||||
this.listeners[eventName].push(callback);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event for public use.
|
||||
* Sends the event via the native DOM event handling system.
|
||||
* @param {Element} targetElement
|
||||
* @param {String} eventName
|
||||
* @param {Object} eventData
|
||||
*/
|
||||
emitPublic(targetElement, eventName, eventData) {
|
||||
const event = new CustomEvent(eventName, {
|
||||
detail: eventData,
|
||||
bubbles: true
|
||||
});
|
||||
targetElement.dispatchEvent(event);
|
||||
function emit(eventName, eventData) {
|
||||
stack.push({name: eventName, data: eventData});
|
||||
if (typeof listeners[eventName] === 'undefined') return this;
|
||||
let eventsToStart = listeners[eventName];
|
||||
for (let i = 0; i < eventsToStart.length; i++) {
|
||||
let event = eventsToStart[i];
|
||||
event(eventData);
|
||||
}
|
||||
}
|
||||
|
||||
export default Events;
|
||||
/**
|
||||
* Listen to a custom event and run the given callback when that event occurs.
|
||||
* @param {String} eventName
|
||||
* @param {Function} callback
|
||||
* @returns {Events}
|
||||
*/
|
||||
function listen(eventName, callback) {
|
||||
if (typeof listeners[eventName] === 'undefined') listeners[eventName] = [];
|
||||
listeners[eventName].push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event for public use.
|
||||
* Sends the event via the native DOM event handling system.
|
||||
* @param {Element} targetElement
|
||||
* @param {String} eventName
|
||||
* @param {Object} eventData
|
||||
*/
|
||||
function emitPublic(targetElement, eventName, eventData) {
|
||||
const event = new CustomEvent(eventName, {
|
||||
detail: eventData,
|
||||
bubbles: true
|
||||
});
|
||||
targetElement.dispatchEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify of a http error.
|
||||
* Check for standard scenarios such as validation errors and
|
||||
* formats an error notification accordingly.
|
||||
* @param {Error} error
|
||||
*/
|
||||
function showValidationErrors(error) {
|
||||
if (!error.status) return;
|
||||
if (error.status === 422 && error.data) {
|
||||
const message = Object.values(error.data).flat().join('\n');
|
||||
emit('error', message);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
emit,
|
||||
emitPublic,
|
||||
listen,
|
||||
success: (msg) => emit('success', msg),
|
||||
error: (msg) => emit('error', msg),
|
||||
showValidationErrors,
|
||||
}
|
|
@ -69,7 +69,10 @@ async function dataRequest(method, url, data = null) {
|
|||
|
||||
// Send data as JSON if a plain object
|
||||
if (typeof data === 'object' && !(data instanceof FormData)) {
|
||||
options.headers = {'Content-Type': 'application/json'};
|
||||
options.headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,19 @@ class Translator {
|
|||
*/
|
||||
getPlural(key, count, replacements) {
|
||||
const text = this.getTransText(key);
|
||||
const splitText = text.split('|');
|
||||
return this.parsePlural(text, count, replacements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given translation and find the correct plural option
|
||||
* to use. Similar format at laravel's 'trans_choice' helper.
|
||||
* @param {String} translation
|
||||
* @param {Number} count
|
||||
* @param {Object} replacements
|
||||
* @returns {String}
|
||||
*/
|
||||
parsePlural(translation, count, replacements) {
|
||||
const splitText = translation.split('|');
|
||||
const exactCountRegex = /^{([0-9]+)}/;
|
||||
const rangeRegex = /^\[([0-9]+),([0-9*]+)]/;
|
||||
let result = null;
|
||||
|
|
|
@ -106,6 +106,7 @@ return [
|
|||
'role_access_api' => 'Access system API',
|
||||
'role_manage_settings' => 'Manage app settings',
|
||||
'role_asset' => 'Asset Permissions',
|
||||
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
|
||||
'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_admins' => 'Admins are automatically given access to all content but these options may show or hide UI options.',
|
||||
'role_all' => 'All',
|
||||
|
|
|
@ -130,7 +130,7 @@ p, ul, ol, pre, table, blockquote {
|
|||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
@include lightDark(background, #eaeaea, #222);
|
||||
@include lightDark(background, #eaeaea, #555);
|
||||
margin-bottom: $-l;
|
||||
&.faded {
|
||||
background-image: linear-gradient(to right, #FFF, #e3e0e0 20%, #e3e0e0 80%, #FFF);
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
<section page-comments page-id="{{ $page->id }}" class="comments-list" aria-label="{{ trans('entities.comments') }}">
|
||||
<section component="page-comments"
|
||||
option:page-comments:page-id="{{ $page->id }}"
|
||||
option:page-comments:updated-text="{{ trans('entities.comment_updated_success') }}"
|
||||
option:page-comments:deleted-text="{{ trans('entities.comment_deleted_success') }}"
|
||||
option:page-comments:created-text="{{ trans('entities.comment_created_success') }}"
|
||||
option:page-comments:count-text="{{ trans('entities.comment_count') }}"
|
||||
class="comments-list"
|
||||
aria-label="{{ trans('entities.comments') }}">
|
||||
|
||||
@exposeTranslations([
|
||||
'entities.comment_updated_success',
|
||||
'entities.comment_deleted_success',
|
||||
'entities.comment_created_success',
|
||||
'entities.comment_count',
|
||||
])
|
||||
|
||||
<div comment-count-bar class="grid half left-focus v-center no-row-gap">
|
||||
<div refs="page-comments@commentCountBar" class="grid half left-focus v-center no-row-gap">
|
||||
<h5 comments-title>{{ trans_choice('entities.comment_count', count($page->comments), ['count' => count($page->comments)]) }}</h5>
|
||||
@if (count($page->comments) === 0 && userCan('comment-create-all'))
|
||||
<div class="text-m-right" comment-add-button-container>
|
||||
<div class="text-m-right" refs="page-comments@addButtonContainer">
|
||||
<button type="button" action="addComment"
|
||||
class="button outline">{{ trans('entities.comment_add') }}</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="comment-container" comment-container>
|
||||
<div refs="page-comments@commentContainer" class="comment-container">
|
||||
@foreach($page->comments as $comment)
|
||||
@include('comments.comment', ['comment' => $comment])
|
||||
@endforeach
|
||||
|
@ -27,7 +27,7 @@
|
|||
@include('comments.create')
|
||||
|
||||
@if (count($page->comments) > 0)
|
||||
<div class="text-right" comment-add-button-container>
|
||||
<div refs="page-comments@addButtonContainer" class="text-right">
|
||||
<button type="button" action="addComment"
|
||||
class="button outline">{{ trans('entities.comment_add') }}</button>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<div class="comment-box" comment-box style="display:none;">
|
||||
<div class="comment-box" style="display:none;">
|
||||
|
||||
<div class="header p-s">{{ trans('entities.comment_new') }}</div>
|
||||
<div comment-form-reply-to class="reply-row primary-background-light text-muted px-s py-xs mb-s" style="display: none;">
|
||||
<div refs="page-comments@replyToRow" class="reply-row primary-background-light text-muted px-s py-xs mb-s" style="display: none;">
|
||||
<div class="grid left-focus v-center">
|
||||
<div>
|
||||
{!! trans('entities.comment_in_reply_to', ['commentId' => '<a href=""></a>']) !!}
|
||||
|
@ -10,7 +11,8 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content px-s" comment-form-container>
|
||||
|
||||
<div refs="page-comments@formContainer" class="content px-s">
|
||||
<form novalidate>
|
||||
<div class="form-group description-input">
|
||||
<textarea name="markdown" rows="3"
|
||||
|
@ -26,4 +28,5 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -3,10 +3,10 @@
|
|||
@foreach($roles as $role)
|
||||
<div>
|
||||
@include('components.custom-checkbox', [
|
||||
'name' => $name . '[' . str_replace('.', 'DOT', $role->name) . ']',
|
||||
'name' => $name . '[' . strval($role->id) . ']',
|
||||
'label' => $role->display_name,
|
||||
'value' => $role->id,
|
||||
'checked' => old($name . '.' . str_replace('.', 'DOT', $role->name)) || (!old('name') && isset($model) && $model->hasRole($role->name))
|
||||
'checked' => old($name . '.' . strval($role->id)) || (!old('name') && isset($model) && $model->hasRole($role->id))
|
||||
])
|
||||
</div>
|
||||
@endforeach
|
||||
|
|
|
@ -231,7 +231,8 @@
|
|||
<label for="setting-registration-role">{{ trans('settings.reg_default_role') }}</label>
|
||||
<select id="setting-registration-role" name="setting-registration-role" @if($errors->has('setting-registration-role')) class="neg" @endif>
|
||||
@foreach(\BookStack\Auth\Role::all() as $role)
|
||||
<option value="{{$role->id}}" data-role-name="{{ $role->name }}"
|
||||
<option value="{{$role->id}}"
|
||||
data-system-role-name="{{ $role->system_name ?? '' }}"
|
||||
@if(setting('registration-role', \BookStack\Auth\Role::first()->id) == $role->id) selected @endif
|
||||
>
|
||||
{{ $role->display_name }}
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
@if($role->users->count() > 0)
|
||||
<div class="form-group">
|
||||
<p>{{ trans('settings.role_delete_users_assigned', ['userCount' => $role->users->count()]) }}</p>
|
||||
@include('form.role-select', ['options' => $roles, 'name' => 'migration_role_id'])
|
||||
@include('form.role-select', ['options' => $roles, 'name' => 'migrate_role_id'])
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
|
|
@ -28,19 +28,23 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid half" permissions-table>
|
||||
<div>
|
||||
<label class="setting-list-label">{{ trans('settings.role_system') }}</label>
|
||||
<a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||
</div>
|
||||
<div class="toggle-switch-list">
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
|
||||
<div permissions-table>
|
||||
<label class="setting-list-label">{{ trans('settings.role_system') }}</label>
|
||||
<a href="#" permissions-table-toggle-all class="text-small text-primary">{{ trans('common.toggle_all') }}</a>
|
||||
|
||||
<div class="toggle-switch-list grid half mt-m">
|
||||
<div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'restrictions-manage-all', 'label' => trans('settings.role_manage_entity_permissions')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'restrictions-manage-own', 'label' => trans('settings.role_manage_own_entity_permissions')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'templates-manage', 'label' => trans('settings.role_manage_page_templates')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'access-api', 'label' => trans('settings.role_access_api')])</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'settings-manage', 'label' => trans('settings.role_manage_settings')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'users-manage', 'label' => trans('settings.role_manage_users')])</div>
|
||||
<div>@include('settings.roles.checkbox', ['permission' => 'user-roles-manage', 'label' => trans('settings.role_manage_roles')])</div>
|
||||
<p class="text-warn text-small mt-s mb-none">{{ trans('settings.roles_system_warning') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -135,9 +135,9 @@ Route::group(['middleware' => 'auth'], function () {
|
|||
Route::get('/ajax/search/entities', 'SearchController@searchEntitiesAjax');
|
||||
|
||||
// Comments
|
||||
Route::post('/ajax/page/{pageId}/comment', 'CommentController@savePageComment');
|
||||
Route::put('/ajax/comment/{id}', 'CommentController@update');
|
||||
Route::delete('/ajax/comment/{id}', 'CommentController@destroy');
|
||||
Route::post('/comment/{pageId}', 'CommentController@savePageComment');
|
||||
Route::put('/comment/{id}', 'CommentController@update');
|
||||
Route::delete('/comment/{id}', 'CommentController@destroy');
|
||||
|
||||
// Links
|
||||
Route::get('/link/{id}', 'PageController@redirectFromLink');
|
||||
|
|
|
@ -170,6 +170,11 @@ class AuthTest extends BrowserKitTest
|
|||
->seePageIs('/register/confirm')
|
||||
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
|
||||
|
||||
$this->visit('/')
|
||||
->seePageIs('/register/confirm/awaiting');
|
||||
|
||||
auth()->logout();
|
||||
|
||||
$this->visit('/')->seePageIs('/login')
|
||||
->type($user->email, '#email')
|
||||
->type($user->password, '#password')
|
||||
|
@ -202,6 +207,10 @@ class AuthTest extends BrowserKitTest
|
|||
->seePageIs('/register/confirm')
|
||||
->seeInDatabase('users', ['name' => $user->name, 'email' => $user->email, 'email_confirmed' => false]);
|
||||
|
||||
$this->visit('/')
|
||||
->seePageIs('/register/confirm/awaiting');
|
||||
|
||||
auth()->logout();
|
||||
$this->visit('/')->seePageIs('/login')
|
||||
->type($user->email, '#email')
|
||||
->type($user->password, '#password')
|
||||
|
@ -213,13 +222,14 @@ class AuthTest extends BrowserKitTest
|
|||
public function test_user_creation()
|
||||
{
|
||||
$user = factory(User::class)->make();
|
||||
$adminRole = Role::getRole('admin');
|
||||
|
||||
$this->asAdmin()
|
||||
->visit('/settings/users')
|
||||
->click('Add New User')
|
||||
->type($user->name, '#name')
|
||||
->type($user->email, '#email')
|
||||
->check('roles[admin]')
|
||||
->check("roles[{$adminRole->id}]")
|
||||
->type($user->password, '#password')
|
||||
->type($user->password, '#password-confirm')
|
||||
->press('Save')
|
||||
|
@ -381,6 +391,17 @@ class AuthTest extends BrowserKitTest
|
|||
->seePageUrlIs($page->getUrl());
|
||||
}
|
||||
|
||||
public function test_login_intended_redirect_does_not_redirect_to_external_pages()
|
||||
{
|
||||
config()->set('app.url', 'http://localhost');
|
||||
$this->setSettings(['app-public' => true]);
|
||||
|
||||
$this->get('/login', ['referer' => 'https://example.com']);
|
||||
$login = $this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);
|
||||
|
||||
$login->assertRedirectedTo('http://localhost');
|
||||
}
|
||||
|
||||
public function test_login_authenticates_admins_on_all_guards()
|
||||
{
|
||||
$this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);
|
||||
|
@ -401,6 +422,18 @@ class AuthTest extends BrowserKitTest
|
|||
$this->assertFalse(auth('saml2')->check());
|
||||
}
|
||||
|
||||
public function test_failed_logins_are_logged_when_message_configured()
|
||||
{
|
||||
$log = $this->withTestLogger();
|
||||
config()->set(['logging.failed_login.message' => 'Failed login for %u']);
|
||||
|
||||
$this->post('/login', ['email' => 'admin@example.com', 'password' => 'cattreedog']);
|
||||
$this->assertTrue($log->hasWarningThatContains('Failed login for admin@example.com'));
|
||||
|
||||
$this->post('/login', ['email' => 'admin@admin.com', 'password' => 'password']);
|
||||
$this->assertFalse($log->hasWarningThatContains('Failed login for admin@admin.com'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a login
|
||||
*/
|
||||
|
|
|
@ -237,9 +237,9 @@ class LdapTest extends BrowserKitTest
|
|||
|
||||
public function test_login_maps_roles_and_retains_existing_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']);
|
||||
$roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']);
|
||||
$roleToReceive2 = factory(Role::class)->create(['display_name' => 'LdapTester Second']);
|
||||
$existingRole = factory(Role::class)->create(['display_name' => 'ldaptester-existing']);
|
||||
$this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
|
||||
$this->mockUser->attachRole($existingRole);
|
||||
|
||||
|
@ -283,8 +283,8 @@ class LdapTest extends BrowserKitTest
|
|||
|
||||
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']);
|
||||
$roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']);
|
||||
$existingRole = factory(Role::class)->create(['display_name' => 'ldaptester-existing']);
|
||||
$this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
|
||||
$this->mockUser->attachRole($existingRole);
|
||||
|
||||
|
@ -323,15 +323,15 @@ class LdapTest extends BrowserKitTest
|
|||
|
||||
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']);
|
||||
$role = factory(Role::class)->create(['display_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']);
|
||||
$roleToReceive = factory(Role::class)->create(['display_name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
|
||||
$roleToNotReceive = factory(Role::class)->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
|
||||
|
||||
app('config')->set([
|
||||
'services.ldap.user_to_groups' => true,
|
||||
|
@ -368,8 +368,8 @@ class LdapTest extends BrowserKitTest
|
|||
|
||||
public function test_login_group_mapping_does_not_conflict_with_default_role()
|
||||
{
|
||||
$roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
|
||||
$roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
|
||||
$roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']);
|
||||
$roleToReceive2 = factory(Role::class)->create(['display_name' => 'LdapTester Second']);
|
||||
$this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
|
||||
|
||||
setting()->put('registration-role', $roleToReceive->id);
|
||||
|
@ -593,4 +593,59 @@ class LdapTest extends BrowserKitTest
|
|||
|
||||
$this->see('A user with the email tester@example.com already exists but with different credentials');
|
||||
}
|
||||
|
||||
public function test_login_with_email_confirmation_required_maps_groups_but_shows_confirmation_screen()
|
||||
{
|
||||
$roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']);
|
||||
$user = factory(User::class)->make();
|
||||
setting()->put('registration-confirmation', 'true');
|
||||
|
||||
app('config')->set([
|
||||
'services.ldap.user_to_groups' => true,
|
||||
'services.ldap.group_attribute' => 'memberOf',
|
||||
'services.ldap.remove_from_groups' => true,
|
||||
]);
|
||||
|
||||
$this->commonLdapMocks(1, 1, 3, 4, 3, 2);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')
|
||||
->times(3)
|
||||
->andReturn(['count' => 1, 0 => [
|
||||
'uid' => [$user->name],
|
||||
'cn' => [$user->name],
|
||||
'dn' => ['dc=test' . config('services.ldap.base_dn')],
|
||||
'mail' => [$user->email],
|
||||
'memberof' => [
|
||||
'count' => 1,
|
||||
0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
|
||||
]
|
||||
]]);
|
||||
|
||||
$this->mockUserLogin()->seePageIs('/register/confirm');
|
||||
$this->seeInDatabase('users', [
|
||||
'email' => $user->email,
|
||||
'email_confirmed' => false,
|
||||
]);
|
||||
|
||||
$user = User::query()->where('email', '=', $user->email)->first();
|
||||
$this->seeInDatabase('role_user', [
|
||||
'user_id' => $user->id,
|
||||
'role_id' => $roleToReceive->id
|
||||
]);
|
||||
|
||||
$homePage = $this->get('/');
|
||||
$homePage->assertRedirectedTo('/register/confirm/awaiting');
|
||||
}
|
||||
|
||||
public function test_failed_logins_are_logged_when_message_configured()
|
||||
{
|
||||
$log = $this->withTestLogger();
|
||||
config()->set(['logging.failed_login.message' => 'Failed login for %u']);
|
||||
|
||||
$this->commonLdapMocks(1, 1, 1, 1, 1);
|
||||
$this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
|
||||
->andReturn(['count' => 0]);
|
||||
|
||||
$this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
|
||||
$this->assertTrue($log->hasWarningThatContains('Failed login for timmyjenkins'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -290,6 +290,35 @@ class Saml2Test extends TestCase
|
|||
});
|
||||
}
|
||||
|
||||
public function test_group_sync_functions_when_email_confirmation_required()
|
||||
{
|
||||
setting()->put('registration-confirmation', 'true');
|
||||
config()->set([
|
||||
'saml2.onelogin.strict' => false,
|
||||
'saml2.user_to_groups' => true,
|
||||
'saml2.remove_from_groups' => false,
|
||||
]);
|
||||
|
||||
$memberRole = factory(Role::class)->create(['external_auth_id' => 'member']);
|
||||
$adminRole = Role::getSystemRole('admin');
|
||||
|
||||
$this->withPost(['SAMLResponse' => $this->acsPostData], function () use ($memberRole, $adminRole) {
|
||||
$acsPost = $this->followingRedirects()->post('/saml2/acs');
|
||||
|
||||
$this->assertEquals('http://localhost/register/confirm', url()->current());
|
||||
$acsPost->assertSee('Please check your email and click the confirmation button to access BookStack.');
|
||||
$user = User::query()->where('external_auth_id', '=', 'user')->first();
|
||||
|
||||
$userRoleIds = $user->roles()->pluck('id');
|
||||
$this->assertContains($memberRole->id, $userRoleIds, 'User was assigned to member role');
|
||||
$this->assertContains($adminRole->id, $userRoleIds, 'User was assigned to admin role');
|
||||
$this->assertTrue($user->email_confirmed == false, 'User email remains unconfirmed');
|
||||
});
|
||||
|
||||
$homeGet = $this->get('/');
|
||||
$homeGet->assertRedirect('/register/confirm/awaiting');
|
||||
}
|
||||
|
||||
protected function withGet(array $options, callable $callback)
|
||||
{
|
||||
return $this->withGlobal($_GET, $options, $callback);
|
||||
|
|
|
@ -13,7 +13,7 @@ class CommentTest extends TestCase
|
|||
$page = Page::first();
|
||||
|
||||
$comment = factory(Comment::class)->make(['parent_id' => 2]);
|
||||
$resp = $this->postJson("/ajax/page/$page->id/comment", $comment->getAttributes());
|
||||
$resp = $this->postJson("/comment/$page->id", $comment->getAttributes());
|
||||
|
||||
$resp->assertStatus(200);
|
||||
$resp->assertSee($comment->text);
|
||||
|
@ -36,11 +36,11 @@ class CommentTest extends TestCase
|
|||
$page = Page::first();
|
||||
|
||||
$comment = factory(Comment::class)->make();
|
||||
$this->postJson("/ajax/page/$page->id/comment", $comment->getAttributes());
|
||||
$this->postJson("/comment/$page->id", $comment->getAttributes());
|
||||
|
||||
$comment = $page->comments()->first();
|
||||
$newText = 'updated text content';
|
||||
$resp = $this->putJson("/ajax/comment/$comment->id", [
|
||||
$resp = $this->putJson("/comment/$comment->id", [
|
||||
'text' => $newText,
|
||||
]);
|
||||
|
||||
|
@ -60,11 +60,11 @@ class CommentTest extends TestCase
|
|||
$page = Page::first();
|
||||
|
||||
$comment = factory(Comment::class)->make();
|
||||
$this->postJson("/ajax/page/$page->id/comment", $comment->getAttributes());
|
||||
$this->postJson("/comment/$page->id", $comment->getAttributes());
|
||||
|
||||
$comment = $page->comments()->first();
|
||||
|
||||
$resp = $this->delete("/ajax/comment/$comment->id");
|
||||
$resp = $this->delete("/comment/$comment->id");
|
||||
$resp->assertStatus(200);
|
||||
|
||||
$this->assertDatabaseMissing('comments', [
|
||||
|
@ -75,7 +75,7 @@ class CommentTest extends TestCase
|
|||
public function test_comments_converts_markdown_input_to_html()
|
||||
{
|
||||
$page = Page::first();
|
||||
$this->asAdmin()->postJson("/ajax/page/$page->id/comment", [
|
||||
$this->asAdmin()->postJson("/comment/$page->id", [
|
||||
'text' => '# My Title',
|
||||
]);
|
||||
|
||||
|
@ -96,7 +96,7 @@ class CommentTest extends TestCase
|
|||
$page = Page::first();
|
||||
|
||||
$script = '<script>const a = "script";</script>\n\n# sometextinthecomment';
|
||||
$this->postJson("/ajax/page/$page->id/comment", [
|
||||
$this->postJson("/comment/$page->id", [
|
||||
'text' => $script,
|
||||
]);
|
||||
|
||||
|
@ -105,7 +105,7 @@ class CommentTest extends TestCase
|
|||
$pageView->assertSee('sometextinthecomment');
|
||||
|
||||
$comment = $page->comments()->first();
|
||||
$this->putJson("/ajax/comment/$comment->id", [
|
||||
$this->putJson("/comment/$comment->id", [
|
||||
'text' => $script . 'updated',
|
||||
]);
|
||||
|
||||
|
|
|
@ -2,10 +2,8 @@
|
|||
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Auth\Permissions\PermissionsRepo;
|
||||
use BookStack\Auth\Role;
|
||||
use Laravel\BrowserKitTesting\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Tests\BrowserKitTest;
|
||||
|
||||
class RolesTest extends BrowserKitTest
|
||||
|
@ -59,7 +57,7 @@ class RolesTest extends BrowserKitTest
|
|||
->type('Test Role', 'display_name')
|
||||
->type('A little test description', 'description')
|
||||
->press('Save Role')
|
||||
->seeInDatabase('roles', ['display_name' => $testRoleName, 'name' => 'test-role', 'description' => $testRoleDesc])
|
||||
->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc])
|
||||
->seePageIs('/settings/roles');
|
||||
// Updating
|
||||
$this->asAdmin()->visit('/settings/roles')
|
||||
|
@ -67,7 +65,7 @@ class RolesTest extends BrowserKitTest
|
|||
->click($testRoleName)
|
||||
->type($testRoleUpdateName, '#display_name')
|
||||
->press('Save Role')
|
||||
->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'name' => 'test-role', 'description' => $testRoleDesc])
|
||||
->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc])
|
||||
->seePageIs('/settings/roles');
|
||||
// Deleting
|
||||
$this->asAdmin()->visit('/settings/roles')
|
||||
|
@ -101,6 +99,25 @@ class RolesTest extends BrowserKitTest
|
|||
$this->see('This user is the only user assigned to the administrator role');
|
||||
}
|
||||
|
||||
public function test_migrate_users_on_delete_works()
|
||||
{
|
||||
$roleA = Role::query()->create(['display_name' => 'Delete Test A']);
|
||||
$roleB = Role::query()->create(['display_name' => 'Delete Test B']);
|
||||
$this->user->attachRole($roleB);
|
||||
|
||||
$this->assertCount(0, $roleA->users()->get());
|
||||
$this->assertCount(1, $roleB->users()->get());
|
||||
|
||||
$deletePage = $this->asAdmin()->get("/settings/roles/delete/{$roleB->id}");
|
||||
$deletePage->seeElement('select[name=migrate_role_id]');
|
||||
$this->asAdmin()->delete("/settings/roles/delete/{$roleB->id}", [
|
||||
'migrate_role_id' => $roleA->id,
|
||||
]);
|
||||
|
||||
$this->assertCount(1, $roleA->users()->get());
|
||||
$this->assertEquals($this->user->id, $roleA->users()->first()->id);
|
||||
}
|
||||
|
||||
public function test_manage_user_permission()
|
||||
{
|
||||
$this->actingAs($this->user)->visit('/settings/users')
|
||||
|
@ -669,9 +686,11 @@ class RolesTest extends BrowserKitTest
|
|||
public function test_public_role_visible_in_user_edit_screen()
|
||||
{
|
||||
$user = \BookStack\Auth\User::first();
|
||||
$adminRole = Role::getSystemRole('admin');
|
||||
$publicRole = Role::getSystemRole('public');
|
||||
$this->asAdmin()->visit('/settings/users/' . $user->id)
|
||||
->seeElement('[name="roles[admin]"]')
|
||||
->seeElement('[name="roles[public]"]');
|
||||
->seeElement('[name="roles['.$adminRole->id.']"]')
|
||||
->seeElement('[name="roles['.$publicRole->id.']"]');
|
||||
}
|
||||
|
||||
public function test_public_role_visible_in_role_listing()
|
||||
|
@ -684,9 +703,8 @@ class RolesTest extends BrowserKitTest
|
|||
public function test_public_role_visible_in_default_role_setting()
|
||||
{
|
||||
$this->asAdmin()->visit('/settings')
|
||||
->seeElement('[data-role-name="admin"]')
|
||||
->seeElement('[data-role-name="public"]');
|
||||
|
||||
->seeElement('[data-system-role-name="admin"]')
|
||||
->seeElement('[data-system-role-name="public"]');
|
||||
}
|
||||
|
||||
public function test_public_role_not_deleteable()
|
||||
|
@ -852,7 +870,7 @@ class RolesTest extends BrowserKitTest
|
|||
|
||||
private function addComment($page) {
|
||||
$comment = factory(\BookStack\Actions\Comment::class)->make();
|
||||
$url = "/ajax/page/$page->id/comment";
|
||||
$url = "/comment/$page->id";
|
||||
$request = [
|
||||
'text' => $comment->text,
|
||||
'html' => $comment->html
|
||||
|
@ -865,7 +883,7 @@ class RolesTest extends BrowserKitTest
|
|||
|
||||
private function updateComment($commentId) {
|
||||
$comment = factory(\BookStack\Actions\Comment::class)->make();
|
||||
$url = "/ajax/comment/$commentId";
|
||||
$url = "/comment/$commentId";
|
||||
$request = [
|
||||
'text' => $comment->text,
|
||||
'html' => $comment->html
|
||||
|
@ -875,7 +893,7 @@ class RolesTest extends BrowserKitTest
|
|||
}
|
||||
|
||||
private function deleteComment($commentId) {
|
||||
$url = '/ajax/comment/' . $commentId;
|
||||
$url = '/comment/' . $commentId;
|
||||
return $this->json('DELETE', $url);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php namespace Tests\Unit;
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
|
@ -36,6 +37,28 @@ class ConfigTest extends TestCase
|
|||
$this->checkEnvConfigResult('APP_URL', $oldDefault, 'app.url', '');
|
||||
}
|
||||
|
||||
public function test_errorlog_plain_webserver_channel()
|
||||
{
|
||||
// We can't full test this due to it being targeted for the SAPI logging handler
|
||||
// so we just overwrite that component so we can capture the error log output.
|
||||
config()->set([
|
||||
'logging.channels.errorlog_plain_webserver.handler_with' => [0],
|
||||
]);
|
||||
|
||||
$temp = tempnam(sys_get_temp_dir(), 'bs-test');
|
||||
$original = ini_set( 'error_log', $temp);
|
||||
|
||||
Log::channel('errorlog_plain_webserver')->info('Aww, look, a cute puppy');
|
||||
|
||||
ini_set( 'error_log', $original);
|
||||
|
||||
$output = file_get_contents($temp);
|
||||
$this->assertStringContainsString('Aww, look, a cute puppy', $output);
|
||||
$this->assertStringNotContainsString('INFO', $output);
|
||||
$this->assertStringNotContainsString('info', $output);
|
||||
$this->assertStringNotContainsString('testing', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an environment variable of the given name and value
|
||||
* then check the given config key to see if it matches the given result.
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
const path = require('path');
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
const config = {
|
||||
target: 'web',
|
||||
mode: dev? 'development' : 'production',
|
||||
entry: {
|
||||
app: './resources/js/index.js',
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, 'public/dist')
|
||||
},
|
||||
};
|
||||
|
||||
if (dev) {
|
||||
config['devtool'] = 'inline-source-map';
|
||||
}
|
||||
|
||||
module.exports = config;
|
Loading…
Reference in New Issue
Block a user